Merge branch 'MDL-64497-master' of https://github.com/sammarshallou/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_forum
19  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
20  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
21  */
23 defined('MOODLE_INTERNAL') || die();
25 /** Include required files */
26 require_once(__DIR__ . '/deprecatedlib.php');
27 require_once($CFG->libdir.'/filelib.php');
29 /// CONSTANTS ///////////////////////////////////////////////////////////
31 define('FORUM_MODE_FLATOLDEST', 1);
32 define('FORUM_MODE_FLATNEWEST', -1);
33 define('FORUM_MODE_THREADED', 2);
34 define('FORUM_MODE_NESTED', 3);
36 define('FORUM_CHOOSESUBSCRIBE', 0);
37 define('FORUM_FORCESUBSCRIBE', 1);
38 define('FORUM_INITIALSUBSCRIBE', 2);
39 define('FORUM_DISALLOWSUBSCRIBE',3);
41 /**
42  * FORUM_TRACKING_OFF - Tracking is not available for this forum.
43  */
44 define('FORUM_TRACKING_OFF', 0);
46 /**
47  * FORUM_TRACKING_OPTIONAL - Tracking is based on user preference.
48  */
49 define('FORUM_TRACKING_OPTIONAL', 1);
51 /**
52  * FORUM_TRACKING_FORCED - Tracking is on, regardless of user setting.
53  * Treated as FORUM_TRACKING_OPTIONAL if $CFG->forum_allowforcedreadtracking is off.
54  */
55 define('FORUM_TRACKING_FORCED', 2);
57 define('FORUM_MAILED_PENDING', 0);
58 define('FORUM_MAILED_SUCCESS', 1);
59 define('FORUM_MAILED_ERROR', 2);
61 if (!defined('FORUM_CRON_USER_CACHE')) {
62     /** Defines how many full user records are cached in forum cron. */
63     define('FORUM_CRON_USER_CACHE', 5000);
64 }
66 /**
67  * FORUM_POSTS_ALL_USER_GROUPS - All the posts in groups where the user is enrolled.
68  */
69 define('FORUM_POSTS_ALL_USER_GROUPS', -2);
71 define('FORUM_DISCUSSION_PINNED', 1);
72 define('FORUM_DISCUSSION_UNPINNED', 0);
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     forum_grade_item_update($forum);
132     $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null;
133     \core_completion\api::update_completion_date_event($forum->coursemodule, 'forum', $forum->id, $completiontimeexpected);
135     return $forum->id;
138 /**
139  * Handle changes following the creation of a forum instance.
140  * This function is typically called by the course_module_created observer.
141  *
142  * @param object $context the forum context
143  * @param stdClass $forum The forum object
144  * @return void
145  */
146 function forum_instance_created($context, $forum) {
147     if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) {
148         $users = \mod_forum\subscriptions::get_potential_subscribers($context, 0, 'u.id, u.email');
149         foreach ($users as $user) {
150             \mod_forum\subscriptions::subscribe_user($user->id, $forum, $context);
151         }
152     }
155 /**
156  * Given an object containing all the necessary data,
157  * (defined by the form in mod_form.php) this function
158  * will update an existing instance with new data.
159  *
160  * @global object
161  * @param object $forum forum instance (with magic quotes)
162  * @return bool success
163  */
164 function forum_update_instance($forum, $mform) {
165     global $DB, $OUTPUT, $USER;
167     $forum->timemodified = time();
168     $forum->id           = $forum->instance;
170     if (empty($forum->assessed)) {
171         $forum->assessed = 0;
172     }
174     if (empty($forum->ratingtime) or empty($forum->assessed)) {
175         $forum->assesstimestart  = 0;
176         $forum->assesstimefinish = 0;
177     }
179     $oldforum = $DB->get_record('forum', array('id'=>$forum->id));
181     // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum
182     // if  scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
183     // 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
184     if (($oldforum->assessed<>$forum->assessed) or ($oldforum->scale<>$forum->scale)) {
185         forum_update_grades($forum); // recalculate grades for the forum
186     }
188     if ($forum->type == 'single') {  // Update related discussion and post.
189         $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC');
190         if (!empty($discussions)) {
191             if (count($discussions) > 1) {
192                 echo $OUTPUT->notification(get_string('warnformorepost', 'forum'));
193             }
194             $discussion = array_pop($discussions);
195         } else {
196             // try to recover by creating initial discussion - MDL-16262
197             $discussion = new stdClass();
198             $discussion->course          = $forum->course;
199             $discussion->forum           = $forum->id;
200             $discussion->name            = $forum->name;
201             $discussion->assessed        = $forum->assessed;
202             $discussion->message         = $forum->intro;
203             $discussion->messageformat   = $forum->introformat;
204             $discussion->messagetrust    = true;
205             $discussion->mailnow         = false;
206             $discussion->groupid         = -1;
208             $message = '';
210             forum_add_discussion($discussion, null, $message);
212             if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) {
213                 print_error('cannotadd', 'forum');
214             }
215         }
216         if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) {
217             print_error('cannotfindfirstpost', 'forum');
218         }
220         $cm         = get_coursemodule_from_instance('forum', $forum->id);
221         $modcontext = context_module::instance($cm->id, MUST_EXIST);
223         $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
224         $post->subject       = $forum->name;
225         $post->message       = $forum->intro;
226         $post->messageformat = $forum->introformat;
227         $post->messagetrust  = trusttext_trusted($modcontext);
228         $post->modified      = $forum->timemodified;
229         $post->userid        = $USER->id;    // MDL-18599, so that current teacher can take ownership of activities.
231         if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
232             // Ugly hack - we need to copy the files somehow.
233             $options = array('subdirs'=>true); // Use the same options as intro field!
234             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
235         }
237         $DB->update_record('forum_posts', $post);
238         $discussion->name = $forum->name;
239         $DB->update_record('forum_discussions', $discussion);
240     }
242     $DB->update_record('forum', $forum);
244     $modcontext = context_module::instance($forum->coursemodule);
245     if (($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) && ($oldforum->forcesubscribe <> $forum->forcesubscribe)) {
246         $users = \mod_forum\subscriptions::get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
247         foreach ($users as $user) {
248             \mod_forum\subscriptions::subscribe_user($user->id, $forum, $modcontext);
249         }
250     }
252     forum_grade_item_update($forum);
254     $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null;
255     \core_completion\api::update_completion_date_event($forum->coursemodule, 'forum', $forum->id, $completiontimeexpected);
257     return true;
261 /**
262  * Given an ID of an instance of this module,
263  * this function will permanently delete the instance
264  * and any data that depends on it.
265  *
266  * @global object
267  * @param int $id forum instance id
268  * @return bool success
269  */
270 function forum_delete_instance($id) {
271     global $DB;
273     if (!$forum = $DB->get_record('forum', array('id'=>$id))) {
274         return false;
275     }
276     if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
277         return false;
278     }
279     if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
280         return false;
281     }
283     $context = context_module::instance($cm->id);
285     // now get rid of all files
286     $fs = get_file_storage();
287     $fs->delete_area_files($context->id);
289     $result = true;
291     \core_completion\api::update_completion_date_event($cm->id, 'forum', $forum->id, null);
293     // Delete digest and subscription preferences.
294     $DB->delete_records('forum_digests', array('forum' => $forum->id));
295     $DB->delete_records('forum_subscriptions', array('forum'=>$forum->id));
296     $DB->delete_records('forum_discussion_subs', array('forum' => $forum->id));
298     if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) {
299         foreach ($discussions as $discussion) {
300             if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) {
301                 $result = false;
302             }
303         }
304     }
306     forum_tp_delete_read_records(-1, -1, -1, $forum->id);
308     if (!$DB->delete_records('forum', array('id'=>$forum->id))) {
309         $result = false;
310     }
312     forum_grade_item_delete($forum);
314     return $result;
318 /**
319  * Indicates API features that the forum supports.
320  *
321  * @uses FEATURE_GROUPS
322  * @uses FEATURE_GROUPINGS
323  * @uses FEATURE_MOD_INTRO
324  * @uses FEATURE_COMPLETION_TRACKS_VIEWS
325  * @uses FEATURE_COMPLETION_HAS_RULES
326  * @uses FEATURE_GRADE_HAS_GRADE
327  * @uses FEATURE_GRADE_OUTCOMES
328  * @param string $feature
329  * @return mixed True if yes (some features may use other values)
330  */
331 function forum_supports($feature) {
332     switch($feature) {
333         case FEATURE_GROUPS:                  return true;
334         case FEATURE_GROUPINGS:               return true;
335         case FEATURE_MOD_INTRO:               return true;
336         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
337         case FEATURE_COMPLETION_HAS_RULES:    return true;
338         case FEATURE_GRADE_HAS_GRADE:         return true;
339         case FEATURE_GRADE_OUTCOMES:          return true;
340         case FEATURE_RATE:                    return true;
341         case FEATURE_BACKUP_MOODLE2:          return true;
342         case FEATURE_SHOW_DESCRIPTION:        return true;
343         case FEATURE_PLAGIARISM:              return true;
345         default: return null;
346     }
350 /**
351  * Obtains the automatic completion state for this forum based on any conditions
352  * in forum settings.
353  *
354  * @global object
355  * @global object
356  * @param object $course Course
357  * @param object $cm Course-module
358  * @param int $userid User ID
359  * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
360  * @return bool True if completed, false if not. (If no conditions, then return
361  *   value depends on comparison type)
362  */
363 function forum_get_completion_state($course,$cm,$userid,$type) {
364     global $CFG,$DB;
366     // Get forum details
367     if (!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) {
368         throw new Exception("Can't find forum {$cm->instance}");
369     }
371     $result=$type; // Default return value
373     $postcountparams=array('userid'=>$userid,'forumid'=>$forum->id);
374     $postcountsql="
375 SELECT
376     COUNT(1)
377 FROM
378     {forum_posts} fp
379     INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
380 WHERE
381     fp.userid=:userid AND fd.forum=:forumid";
383     if ($forum->completiondiscussions) {
384         $value = $forum->completiondiscussions <=
385                  $DB->count_records('forum_discussions',array('forum'=>$forum->id,'userid'=>$userid));
386         if ($type == COMPLETION_AND) {
387             $result = $result && $value;
388         } else {
389             $result = $result || $value;
390         }
391     }
392     if ($forum->completionreplies) {
393         $value = $forum->completionreplies <=
394                  $DB->get_field_sql( $postcountsql.' AND fp.parent<>0',$postcountparams);
395         if ($type==COMPLETION_AND) {
396             $result = $result && $value;
397         } else {
398             $result = $result || $value;
399         }
400     }
401     if ($forum->completionposts) {
402         $value = $forum->completionposts <= $DB->get_field_sql($postcountsql,$postcountparams);
403         if ($type == COMPLETION_AND) {
404             $result = $result && $value;
405         } else {
406             $result = $result || $value;
407         }
408     }
410     return $result;
413 /**
414  * Create a message-id string to use in the custom headers of forum notification emails
415  *
416  * message-id is used by email clients to identify emails and to nest conversations
417  *
418  * @param int $postid The ID of the forum post we are notifying the user about
419  * @param int $usertoid The ID of the user being notified
420  * @return string A unique message-id
421  */
422 function forum_get_email_message_id($postid, $usertoid) {
423     return generate_email_messageid(hash('sha256', $postid . 'to' . $usertoid));
426 /**
427  * Removes properties from user record that are not necessary
428  * for sending post notifications.
429  * @param stdClass $user
430  * @return void, $user parameter is modified
431  */
432 function forum_cron_minimise_user_record(stdClass $user) {
434     // We store large amount of users in one huge array,
435     // make sure we do not store info there we do not actually need
436     // in mail generation code or messaging.
438     unset($user->institution);
439     unset($user->department);
440     unset($user->address);
441     unset($user->city);
442     unset($user->url);
443     unset($user->currentlogin);
444     unset($user->description);
445     unset($user->descriptionformat);
448 /**
449  * Function to be run periodically according to the scheduled task.
450  *
451  * Finds all posts that have yet to be mailed out, and mails them
452  * out to all subscribers as well as other maintance tasks.
453  *
454  * NOTE: Since 2.7.2 this function is run by scheduled task rather
455  * than standard cron.
456  *
457  * @todo MDL-44734 The function will be split up into seperate tasks.
458  */
459 function forum_cron() {
460     global $CFG, $USER, $DB, $PAGE;
462     $site = get_site();
464     // The main renderers.
465     $htmlout = $PAGE->get_renderer('mod_forum', 'email', 'htmlemail');
466     $textout = $PAGE->get_renderer('mod_forum', 'email', 'textemail');
467     $htmldigestfullout = $PAGE->get_renderer('mod_forum', 'emaildigestfull', 'htmlemail');
468     $textdigestfullout = $PAGE->get_renderer('mod_forum', 'emaildigestfull', 'textemail');
469     $htmldigestbasicout = $PAGE->get_renderer('mod_forum', 'emaildigestbasic', 'htmlemail');
470     $textdigestbasicout = $PAGE->get_renderer('mod_forum', 'emaildigestbasic', 'textemail');
472     // All users that are subscribed to any post that needs sending,
473     // please increase $CFG->extramemorylimit on large sites that
474     // send notifications to a large number of users.
475     $users = array();
476     $userscount = 0; // Cached user counter - count($users) in PHP is horribly slow!!!
478     // Status arrays.
479     $mailcount  = array();
480     $errorcount = array();
482     // caches
483     $discussions        = array();
484     $forums             = array();
485     $courses            = array();
486     $coursemodules      = array();
487     $subscribedusers    = array();
488     $messageinboundhandlers = array();
490     // Posts older than 2 days will not be mailed.  This is to avoid the problem where
491     // cron has not been running for a long time, and then suddenly people are flooded
492     // with mail from the past few weeks or months
493     $timenow   = time();
494     $endtime   = $timenow - $CFG->maxeditingtime;
495     $starttime = $endtime - 48 * 3600;   // Two days earlier
497     // Get the list of forum subscriptions for per-user per-forum maildigest settings.
498     $digestsset = $DB->get_recordset('forum_digests', null, '', 'id, userid, forum, maildigest');
499     $digests = array();
500     foreach ($digestsset as $thisrow) {
501         if (!isset($digests[$thisrow->forum])) {
502             $digests[$thisrow->forum] = array();
503         }
504         $digests[$thisrow->forum][$thisrow->userid] = $thisrow->maildigest;
505     }
506     $digestsset->close();
508     // Create the generic messageinboundgenerator.
509     $messageinboundgenerator = new \core\message\inbound\address_manager();
510     $messageinboundgenerator->set_handler('\mod_forum\message\inbound\reply_handler');
512     if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) {
513         // Mark them all now as being mailed.  It's unlikely but possible there
514         // might be an error later so that a post is NOT actually mailed out,
515         // but since mail isn't crucial, we can accept this risk.  Doing it now
516         // prevents the risk of duplicated mails, which is a worse problem.
518         if (!forum_mark_old_posts_as_mailed($endtime)) {
519             mtrace('Errors occurred while trying to mark some posts as being mailed.');
520             return false;  // Don't continue trying to mail them, in case we are in a cron loop
521         }
523         // checking post validity, and adding users to loop through later
524         foreach ($posts as $pid => $post) {
526             $discussionid = $post->discussion;
527             if (!isset($discussions[$discussionid])) {
528                 if ($discussion = $DB->get_record('forum_discussions', array('id'=> $post->discussion))) {
529                     $discussions[$discussionid] = $discussion;
530                     \mod_forum\subscriptions::fill_subscription_cache($discussion->forum);
531                     \mod_forum\subscriptions::fill_discussion_subscription_cache($discussion->forum);
533                 } else {
534                     mtrace('Could not find discussion ' . $discussionid);
535                     unset($posts[$pid]);
536                     continue;
537                 }
538             }
539             $forumid = $discussions[$discussionid]->forum;
540             if (!isset($forums[$forumid])) {
541                 if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
542                     $forums[$forumid] = $forum;
543                 } else {
544                     mtrace('Could not find forum '.$forumid);
545                     unset($posts[$pid]);
546                     continue;
547                 }
548             }
549             $courseid = $forums[$forumid]->course;
550             if (!isset($courses[$courseid])) {
551                 if ($course = $DB->get_record('course', array('id' => $courseid))) {
552                     $courses[$courseid] = $course;
553                 } else {
554                     mtrace('Could not find course '.$courseid);
555                     unset($posts[$pid]);
556                     continue;
557                 }
558             }
559             if (!isset($coursemodules[$forumid])) {
560                 if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
561                     $coursemodules[$forumid] = $cm;
562                 } else {
563                     mtrace('Could not find course module for forum '.$forumid);
564                     unset($posts[$pid]);
565                     continue;
566                 }
567             }
569             $modcontext = context_module::instance($coursemodules[$forumid]->id);
571             // Save the Inbound Message datakey here to reduce DB queries later.
572             $messageinboundgenerator->set_data($pid);
573             $messageinboundhandlers[$pid] = $messageinboundgenerator->fetch_data_key();
575             // Caching subscribed users of each forum.
576             if (!isset($subscribedusers[$forumid])) {
577                 if ($subusers = \mod_forum\subscriptions::fetch_subscribed_users($forums[$forumid], 0, $modcontext, 'u.*', true)) {
579                     foreach ($subusers as $postuser) {
580                         // this user is subscribed to this forum
581                         $subscribedusers[$forumid][$postuser->id] = $postuser->id;
582                         $userscount++;
583                         if ($userscount > FORUM_CRON_USER_CACHE) {
584                             // Store minimal user info.
585                             $minuser = new stdClass();
586                             $minuser->id = $postuser->id;
587                             $users[$postuser->id] = $minuser;
588                         } else {
589                             // Cache full user record.
590                             forum_cron_minimise_user_record($postuser);
591                             $users[$postuser->id] = $postuser;
592                         }
593                     }
594                     // Release memory.
595                     unset($subusers);
596                     unset($postuser);
597                 }
598             }
599             $mailcount[$pid] = 0;
600             $errorcount[$pid] = 0;
601         }
602     }
604     if ($users && $posts) {
606         foreach ($users as $userto) {
607             // Terminate if processing of any account takes longer than 2 minutes.
608             core_php_time_limit::raise(120);
610             mtrace('Processing user ' . $userto->id);
612             // Init user caches - we keep the cache for one cycle only, otherwise it could consume too much memory.
613             if (isset($userto->username)) {
614                 $userto = clone($userto);
615             } else {
616                 $userto = $DB->get_record('user', array('id' => $userto->id));
617                 forum_cron_minimise_user_record($userto);
618             }
619             $userto->viewfullnames = array();
620             $userto->canpost       = array();
621             $userto->markposts     = array();
623             // Setup this user so that the capabilities are cached, and environment matches receiving user.
624             cron_setup_user($userto);
626             // Reset the caches.
627             foreach ($coursemodules as $forumid => $unused) {
628                 $coursemodules[$forumid]->cache       = new stdClass();
629                 $coursemodules[$forumid]->cache->caps = array();
630                 unset($coursemodules[$forumid]->uservisible);
631             }
633             foreach ($posts as $pid => $post) {
634                 $discussion = $discussions[$post->discussion];
635                 $forum      = $forums[$discussion->forum];
636                 $course     = $courses[$forum->course];
637                 $cm         =& $coursemodules[$forum->id];
639                 // Do some checks to see if we can bail out now.
641                 // Only active enrolled users are in the list of subscribers.
642                 // This does not necessarily mean that the user is subscribed to the forum or to the discussion though.
643                 if (!isset($subscribedusers[$forum->id][$userto->id])) {
644                     // The user does not subscribe to this forum.
645                     continue;
646                 }
648                 if (!\mod_forum\subscriptions::is_subscribed($userto->id, $forum, $post->discussion, $coursemodules[$forum->id])) {
649                     // The user does not subscribe to this forum, or to this specific discussion.
650                     continue;
651                 }
653                 if ($subscriptiontime = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $userto->id)) {
654                     // Skip posts if the user subscribed to the discussion after it was created.
655                     if (isset($subscriptiontime[$post->discussion]) && ($subscriptiontime[$post->discussion] > $post->created)) {
656                         continue;
657                     }
658                 }
660                 $coursecontext = context_course::instance($course->id);
661                 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userto->id)) {
662                     // The course is hidden and the user does not have access to it.
663                     continue;
664                 }
666                 // Don't send email if the forum is Q&A and the user has not posted.
667                 // Initial topics are still mailed.
668                 if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
669                     mtrace('Did not email ' . $userto->id.' because user has not posted in discussion');
670                     continue;
671                 }
673                 // Get info about the sending user.
674                 if (array_key_exists($post->userid, $users)) {
675                     // We might know the user already.
676                     $userfrom = $users[$post->userid];
677                     if (!isset($userfrom->idnumber)) {
678                         // Minimalised user info, fetch full record.
679                         $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
680                         forum_cron_minimise_user_record($userfrom);
681                     }
683                 } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
684                     forum_cron_minimise_user_record($userfrom);
685                     // Fetch only once if possible, we can add it to user list, it will be skipped anyway.
686                     if ($userscount <= FORUM_CRON_USER_CACHE) {
687                         $userscount++;
688                         $users[$userfrom->id] = $userfrom;
689                     }
690                 } else {
691                     mtrace('Could not find user ' . $post->userid . ', author of post ' . $post->id . '. Unable to send message.');
692                     continue;
693                 }
695                 // Note: If we want to check that userto and userfrom are not the same person this is probably the spot to do it.
697                 // Setup global $COURSE properly - needed for roles and languages.
698                 cron_setup_user($userto, $course);
700                 // Fill caches.
701                 if (!isset($userto->viewfullnames[$forum->id])) {
702                     $modcontext = context_module::instance($cm->id);
703                     $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
704                 }
705                 if (!isset($userto->canpost[$discussion->id])) {
706                     $modcontext = context_module::instance($cm->id);
707                     $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
708                 }
709                 if (!isset($userfrom->groups[$forum->id])) {
710                     if (!isset($userfrom->groups)) {
711                         $userfrom->groups = array();
712                         if (isset($users[$userfrom->id])) {
713                             $users[$userfrom->id]->groups = array();
714                         }
715                     }
716                     $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
717                     if (isset($users[$userfrom->id])) {
718                         $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
719                     }
720                 }
722                 // Make sure groups allow this user to see this email.
723                 if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {
724                     // Groups are being used.
725                     if (!groups_group_exists($discussion->groupid)) {
726                         // Can't find group - be safe and don't this message.
727                         continue;
728                     }
730                     if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) {
731                         // Do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS.
732                         continue;
733                     }
734                 }
736                 // Make sure we're allowed to see the post.
737                 if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm)) {
738                     mtrace('User ' . $userto->id .' can not see ' . $post->id . '. Not sending message.');
739                     continue;
740                 }
742                 // OK so we need to send the email.
744                 // Does the user want this post in a digest?  If so postpone it for now.
745                 $maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
747                 if ($maildigest > 0) {
748                     // This user wants the mails to be in digest form.
749                     $queue = new stdClass();
750                     $queue->userid       = $userto->id;
751                     $queue->discussionid = $discussion->id;
752                     $queue->postid       = $post->id;
753                     $queue->timemodified = $post->created;
754                     $DB->insert_record('forum_queue', $queue);
755                     continue;
756                 }
758                 // Prepare to actually send the post now, and build up the content.
760                 $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
762                 $userfrom->customheaders = array (
763                     // Headers to make emails easier to track.
764                     'List-Id: "'        . $cleanforumname . '" ' . generate_email_messageid('moodleforum' . $forum->id),
765                     'List-Help: '       . $CFG->wwwroot . '/mod/forum/view.php?f=' . $forum->id,
766                     'Message-ID: '      . forum_get_email_message_id($post->id, $userto->id),
767                     'X-Course-Id: '     . $course->id,
768                     'X-Course-Name: '   . format_string($course->fullname, true),
770                     // Headers to help prevent auto-responders.
771                     'Precedence: Bulk',
772                     'X-Auto-Response-Suppress: All',
773                     'Auto-Submitted: auto-generated',
774                 );
776                 $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
778                 // Generate a reply-to address from using the Inbound Message handler.
779                 $replyaddress = null;
780                 if ($userto->canpost[$discussion->id] && array_key_exists($post->id, $messageinboundhandlers)) {
781                     $messageinboundgenerator->set_data($post->id, $messageinboundhandlers[$post->id]);
782                     $replyaddress = $messageinboundgenerator->generate($userto->id);
783                 }
785                 if (!isset($userto->canpost[$discussion->id])) {
786                     $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
787                 } else {
788                     $canreply = $userto->canpost[$discussion->id];
789                 }
791                 $data = new \mod_forum\output\forum_post_email(
792                         $course,
793                         $cm,
794                         $forum,
795                         $discussion,
796                         $post,
797                         $userfrom,
798                         $userto,
799                         $canreply
800                     );
802                 $userfrom->customheaders[] = sprintf('List-Unsubscribe: <%s>',
803                     $data->get_unsubscribediscussionlink());
805                 if (!isset($userto->viewfullnames[$forum->id])) {
806                     $data->viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
807                 } else {
808                     $data->viewfullnames = $userto->viewfullnames[$forum->id];
809                 }
811                 // Not all of these variables are used in the default language
812                 // string but are made available to support custom subjects.
813                 $a = new stdClass();
814                 $a->subject = $data->get_subject();
815                 $a->forumname = $cleanforumname;
816                 $a->sitefullname = format_string($site->fullname);
817                 $a->siteshortname = format_string($site->shortname);
818                 $a->courseidnumber = $data->get_courseidnumber();
819                 $a->coursefullname = $data->get_coursefullname();
820                 $a->courseshortname = $data->get_coursename();
821                 $postsubject = html_to_text(get_string('postmailsubject', 'forum', $a), 0);
823                 $rootid = forum_get_email_message_id($discussion->firstpost, $userto->id);
825                 if ($post->parent) {
826                     // This post is a reply, so add reply header (RFC 2822).
827                     $parentid = forum_get_email_message_id($post->parent, $userto->id);
828                     $userfrom->customheaders[] = "In-Reply-To: $parentid";
830                     // If the post is deeply nested we also reference the parent message id and
831                     // the root message id (if different) to aid threading when parts of the email
832                     // conversation have been deleted (RFC1036).
833                     if ($post->parent != $discussion->firstpost) {
834                         $userfrom->customheaders[] = "References: $rootid $parentid";
835                     } else {
836                         $userfrom->customheaders[] = "References: $parentid";
837                     }
838                 }
840                 // MS Outlook / Office uses poorly documented and non standard headers, including
841                 // Thread-Topic which overrides the Subject and shouldn't contain Re: or Fwd: etc.
842                 $a->subject = $discussion->name;
843                 $threadtopic = html_to_text(get_string('postmailsubject', 'forum', $a), 0);
844                 $userfrom->customheaders[] = "Thread-Topic: $threadtopic";
845                 $userfrom->customheaders[] = "Thread-Index: " . substr($rootid, 1, 28);
847                 // Send the post now!
848                 mtrace('Sending ', '');
850                 $eventdata = new \core\message\message();
851                 $eventdata->courseid            = $course->id;
852                 $eventdata->component           = 'mod_forum';
853                 $eventdata->name                = 'posts';
854                 $eventdata->userfrom            = $userfrom;
855                 $eventdata->userto              = $userto;
856                 $eventdata->subject             = $postsubject;
857                 $eventdata->fullmessage         = $textout->render($data);
858                 $eventdata->fullmessageformat   = FORMAT_PLAIN;
859                 $eventdata->fullmessagehtml     = $htmlout->render($data);
860                 $eventdata->notification        = 1;
861                 $eventdata->replyto             = $replyaddress;
862                 if (!empty($replyaddress)) {
863                     // Add extra text to email messages if they can reply back.
864                     $textfooter = "\n\n" . get_string('replytopostbyemail', 'mod_forum');
865                     $htmlfooter = html_writer::tag('p', get_string('replytopostbyemail', 'mod_forum'));
866                     $additionalcontent = array('fullmessage' => array('footer' => $textfooter),
867                                      'fullmessagehtml' => array('footer' => $htmlfooter));
868                     $eventdata->set_additional_content('email', $additionalcontent);
869                 }
871                 $smallmessagestrings = new stdClass();
872                 $smallmessagestrings->user          = fullname($userfrom);
873                 $smallmessagestrings->forumname     = "$shortname: " . format_string($forum->name, true) . ": " . $discussion->name;
874                 $smallmessagestrings->message       = $post->message;
876                 // Make sure strings are in message recipients language.
877                 $eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'forum', $smallmessagestrings, $userto->lang);
879                 $contexturl = new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id), 'p' . $post->id);
880                 $eventdata->contexturl = $contexturl->out();
881                 $eventdata->contexturlname = $discussion->name;
883                 $mailresult = message_send($eventdata);
884                 if (!$mailresult) {
885                     mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id $post->id to user $userto->id".
886                             " ($userto->email) .. not trying again.");
887                     $errorcount[$post->id]++;
888                 } else {
889                     $mailcount[$post->id]++;
891                     // Mark post as read if forum_usermarksread is set off.
892                     if (!$CFG->forum_usermarksread) {
893                         $userto->markposts[$post->id] = $post->id;
894                     }
895                 }
897                 mtrace('post ' . $post->id . ': ' . $post->subject);
898             }
900             // Mark processed posts as read.
901             if (get_user_preferences('forum_markasreadonnotification', 1, $userto->id) == 1) {
902                 forum_tp_mark_posts_read($userto, $userto->markposts);
903             }
905             unset($userto);
906         }
907     }
909     if ($posts) {
910         foreach ($posts as $post) {
911             mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'");
912             if ($errorcount[$post->id]) {
913                 $DB->set_field('forum_posts', 'mailed', FORUM_MAILED_ERROR, array('id' => $post->id));
914             }
915         }
916     }
918     // release some memory
919     unset($subscribedusers);
920     unset($mailcount);
921     unset($errorcount);
923     cron_setup_user();
925     $sitetimezone = core_date::get_server_timezone();
927     // Now see if there are any digest mails waiting to be sent, and if we should send them
929     mtrace('Starting digest processing...');
931     core_php_time_limit::raise(300); // terminate if not able to fetch all digests in 5 minutes
933     if (!isset($CFG->digestmailtimelast)) {    // To catch the first time
934         set_config('digestmailtimelast', 0);
935     }
937     $timenow = time();
938     $digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
940     // Delete any really old ones (normally there shouldn't be any)
941     $weekago = $timenow - (7 * 24 * 3600);
942     $DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
943     mtrace ('Cleaned old digest records');
945     if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) {
947         mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone));
949         $digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime));
951         if ($digestposts_rs->valid()) {
953             // We have work to do
954             $usermailcount = 0;
956             //caches - reuse the those filled before too
957             $discussionposts = array();
958             $userdiscussions = array();
960             foreach ($digestposts_rs as $digestpost) {
961                 if (!isset($posts[$digestpost->postid])) {
962                     if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) {
963                         $posts[$digestpost->postid] = $post;
964                     } else {
965                         continue;
966                     }
967                 }
968                 $discussionid = $digestpost->discussionid;
969                 if (!isset($discussions[$discussionid])) {
970                     if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) {
971                         $discussions[$discussionid] = $discussion;
972                     } else {
973                         continue;
974                     }
975                 }
976                 $forumid = $discussions[$discussionid]->forum;
977                 if (!isset($forums[$forumid])) {
978                     if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
979                         $forums[$forumid] = $forum;
980                     } else {
981                         continue;
982                     }
983                 }
985                 $courseid = $forums[$forumid]->course;
986                 if (!isset($courses[$courseid])) {
987                     if ($course = $DB->get_record('course', array('id' => $courseid))) {
988                         $courses[$courseid] = $course;
989                     } else {
990                         continue;
991                     }
992                 }
994                 if (!isset($coursemodules[$forumid])) {
995                     if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
996                         $coursemodules[$forumid] = $cm;
997                     } else {
998                         continue;
999                     }
1000                 }
1001                 $userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid;
1002                 $discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid;
1003             }
1004             $digestposts_rs->close(); /// Finished iteration, let's close the resultset
1006             // Data collected, start sending out emails to each user
1007             foreach ($userdiscussions as $userid => $thesediscussions) {
1009                 core_php_time_limit::raise(120); // terminate if processing of any account takes longer than 2 minutes
1011                 cron_setup_user();
1013                 mtrace(get_string('processingdigest', 'forum', $userid), '... ');
1015                 // First of all delete all the queue entries for this user
1016                 $DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime));
1018                 // Init user caches - we keep the cache for one cycle only,
1019                 // otherwise it would unnecessarily consume memory.
1020                 if (array_key_exists($userid, $users) and isset($users[$userid]->username)) {
1021                     $userto = clone($users[$userid]);
1022                 } else {
1023                     $userto = $DB->get_record('user', array('id' => $userid));
1024                     forum_cron_minimise_user_record($userto);
1025                 }
1026                 $userto->viewfullnames = array();
1027                 $userto->canpost       = array();
1028                 $userto->markposts     = array();
1030                 // Override the language and timezone of the "current" user, so that
1031                 // mail is customised for the receiver.
1032                 cron_setup_user($userto);
1034                 $postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
1036                 $headerdata = new stdClass();
1037                 $headerdata->sitename = format_string($site->fullname, true);
1038                 $headerdata->userprefs = $CFG->wwwroot.'/user/forum.php?id='.$userid.'&amp;course='.$site->id;
1040                 $posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
1041                 $headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
1043                 $posthtml = '<p>'.get_string('digestmailheader', 'forum', $headerdata).'</p>'
1044                     . '<br /><hr size="1" noshade="noshade" />';
1046                 foreach ($thesediscussions as $discussionid) {
1048                     core_php_time_limit::raise(120);   // to be reset for each post
1050                     $discussion = $discussions[$discussionid];
1051                     $forum      = $forums[$discussion->forum];
1052                     $course     = $courses[$forum->course];
1053                     $cm         = $coursemodules[$forum->id];
1055                     //override language
1056                     cron_setup_user($userto, $course);
1058                     // Fill caches
1059                     if (!isset($userto->viewfullnames[$forum->id])) {
1060                         $modcontext = context_module::instance($cm->id);
1061                         $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
1062                     }
1063                     if (!isset($userto->canpost[$discussion->id])) {
1064                         $modcontext = context_module::instance($cm->id);
1065                         $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
1066                     }
1068                     $strforums      = get_string('forums', 'forum');
1069                     $canunsubscribe = ! \mod_forum\subscriptions::is_forcesubscribed($forum);
1070                     $canreply       = $userto->canpost[$discussion->id];
1071                     $shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
1073                     $posttext .= "\n \n";
1074                     $posttext .= '=====================================================================';
1075                     $posttext .= "\n \n";
1076                     $posttext .= "$shortname -> $strforums -> ".format_string($forum->name,true);
1077                     if ($discussion->name != $forum->name) {
1078                         $posttext  .= " -> ".format_string($discussion->name,true);
1079                     }
1080                     $posttext .= "\n";
1081                     $posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
1082                     $posttext .= "\n";
1084                     $posthtml .= "<p><font face=\"sans-serif\">".
1085                     "<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
1086                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
1087                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
1088                     if ($discussion->name == $forum->name) {
1089                         $posthtml .= "</font></p>";
1090                     } else {
1091                         $posthtml .= " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a></font></p>";
1092                     }
1093                     $posthtml .= '<p>';
1095                     $postsarray = $discussionposts[$discussionid];
1096                     sort($postsarray);
1097                     $sentcount = 0;
1099                     foreach ($postsarray as $postid) {
1100                         $post = $posts[$postid];
1102                         if (array_key_exists($post->userid, $users)) { // we might know him/her already
1103                             $userfrom = $users[$post->userid];
1104                             if (!isset($userfrom->idnumber)) {
1105                                 $userfrom = $DB->get_record('user', array('id' => $userfrom->id));
1106                                 forum_cron_minimise_user_record($userfrom);
1107                             }
1109                         } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
1110                             forum_cron_minimise_user_record($userfrom);
1111                             if ($userscount <= FORUM_CRON_USER_CACHE) {
1112                                 $userscount++;
1113                                 $users[$userfrom->id] = $userfrom;
1114                             }
1116                         } else {
1117                             mtrace('Could not find user '.$post->userid);
1118                             continue;
1119                         }
1121                         if (!isset($userfrom->groups[$forum->id])) {
1122                             if (!isset($userfrom->groups)) {
1123                                 $userfrom->groups = array();
1124                                 if (isset($users[$userfrom->id])) {
1125                                     $users[$userfrom->id]->groups = array();
1126                                 }
1127                             }
1128                             $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
1129                             if (isset($users[$userfrom->id])) {
1130                                 $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
1131                             }
1132                         }
1134                         // Headers to help prevent auto-responders.
1135                         $userfrom->customheaders = array(
1136                                 "Precedence: Bulk",
1137                                 'X-Auto-Response-Suppress: All',
1138                                 'Auto-Submitted: auto-generated',
1139                             );
1141                         $maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
1142                         if (!isset($userto->canpost[$discussion->id])) {
1143                             $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
1144                         } else {
1145                             $canreply = $userto->canpost[$discussion->id];
1146                         }
1148                         $data = new \mod_forum\output\forum_post_email(
1149                                 $course,
1150                                 $cm,
1151                                 $forum,
1152                                 $discussion,
1153                                 $post,
1154                                 $userfrom,
1155                                 $userto,
1156                                 $canreply
1157                             );
1159                         if (!isset($userto->viewfullnames[$forum->id])) {
1160                             $data->viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
1161                         } else {
1162                             $data->viewfullnames = $userto->viewfullnames[$forum->id];
1163                         }
1165                         if ($maildigest == 2) {
1166                             // Subjects and link only.
1167                             $posttext .= $textdigestbasicout->render($data);
1168                             $posthtml .= $htmldigestbasicout->render($data);
1169                         } else {
1170                             // The full treatment.
1171                             $posttext .= $textdigestfullout->render($data);
1172                             $posthtml .= $htmldigestfullout->render($data);
1174                             // Create an array of postid's for this user to mark as read.
1175                             if (!$CFG->forum_usermarksread) {
1176                                 $userto->markposts[$post->id] = $post->id;
1177                             }
1178                         }
1179                         $sentcount++;
1180                     }
1181                     $footerlinks = array();
1182                     if ($canunsubscribe) {
1183                         $footerlinks[] = "<a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">" . get_string("unsubscribe", "forum") . "</a>";
1184                     } else {
1185                         $footerlinks[] = get_string("everyoneissubscribed", "forum");
1186                     }
1187                     $footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . get_string("digestmailpost", "forum") . '</a>';
1188                     $posthtml .= "\n<div class='mdl-right'><font size=\"1\">" . implode('&nbsp;', $footerlinks) . '</font></div>';
1189                     $posthtml .= '<hr size="1" noshade="noshade" /></p>';
1190                 }
1192                 if (empty($userto->mailformat) || $userto->mailformat != 1) {
1193                     // This user DOESN'T want to receive HTML
1194                     $posthtml = '';
1195                 }
1197                 $eventdata = new \core\message\message();
1198                 $eventdata->courseid            = SITEID;
1199                 $eventdata->component           = 'mod_forum';
1200                 $eventdata->name                = 'digests';
1201                 $eventdata->userfrom            = core_user::get_noreply_user();
1202                 $eventdata->userto              = $userto;
1203                 $eventdata->subject             = $postsubject;
1204                 $eventdata->fullmessage         = $posttext;
1205                 $eventdata->fullmessageformat   = FORMAT_PLAIN;
1206                 $eventdata->fullmessagehtml     = $posthtml;
1207                 $eventdata->notification        = 1;
1208                 $eventdata->smallmessage        = get_string('smallmessagedigest', 'forum', $sentcount);
1209                 $mailresult = message_send($eventdata);
1211                 if (!$mailresult) {
1212                     mtrace("ERROR: mod/forum/cron.php: Could not send out digest mail to user $userto->id ".
1213                         "($userto->email)... not trying again.");
1214                 } else {
1215                     mtrace("success.");
1216                     $usermailcount++;
1218                     // Mark post as read if forum_usermarksread is set off
1219                     if (get_user_preferences('forum_markasreadonnotification', 1, $userto->id) == 1) {
1220                         forum_tp_mark_posts_read($userto, $userto->markposts);
1221                     }
1222                 }
1223             }
1224         }
1225     /// We have finishied all digest emails, update $CFG->digestmailtimelast
1226         set_config('digestmailtimelast', $timenow);
1227     }
1229     cron_setup_user();
1231     if (!empty($usermailcount)) {
1232         mtrace(get_string('digestsentusers', 'forum', $usermailcount));
1233     }
1235     if (!empty($CFG->forum_lastreadclean)) {
1236         $timenow = time();
1237         if ($CFG->forum_lastreadclean + (24*3600) < $timenow) {
1238             set_config('forum_lastreadclean', $timenow);
1239             mtrace('Removing old forum read tracking info...');
1240             forum_tp_clean_read_records();
1241         }
1242     } else {
1243         set_config('forum_lastreadclean', time());
1244     }
1246     return true;
1249 /**
1250  *
1251  * @param object $course
1252  * @param object $user
1253  * @param object $mod TODO this is not used in this function, refactor
1254  * @param object $forum
1255  * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
1256  */
1257 function forum_user_outline($course, $user, $mod, $forum) {
1258     global $CFG;
1259     require_once("$CFG->libdir/gradelib.php");
1260     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1261     if (empty($grades->items[0]->grades)) {
1262         $grade = false;
1263     } else {
1264         $grade = reset($grades->items[0]->grades);
1265     }
1267     $count = forum_count_user_posts($forum->id, $user->id);
1269     if ($count && $count->postcount > 0) {
1270         $result = new stdClass();
1271         $result->info = get_string("numposts", "forum", $count->postcount);
1272         $result->time = $count->lastpost;
1273         if ($grade) {
1274             $result->info .= ', ' . get_string('grade') . ': ' . $grade->str_long_grade;
1275         }
1276         return $result;
1277     } else if ($grade) {
1278         $result = new stdClass();
1279         $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
1281         //datesubmitted == time created. dategraded == time modified or time overridden
1282         //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
1283         //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
1284         if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1285             $result->time = $grade->dategraded;
1286         } else {
1287             $result->time = $grade->datesubmitted;
1288         }
1290         return $result;
1291     }
1292     return NULL;
1296 /**
1297  * @global object
1298  * @global object
1299  * @param object $coure
1300  * @param object $user
1301  * @param object $mod
1302  * @param object $forum
1303  */
1304 function forum_user_complete($course, $user, $mod, $forum) {
1305     global $CFG,$USER, $OUTPUT;
1306     require_once("$CFG->libdir/gradelib.php");
1308     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1309     if (!empty($grades->items[0]->grades)) {
1310         $grade = reset($grades->items[0]->grades);
1311         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
1312         if ($grade->str_feedback) {
1313             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
1314         }
1315     }
1317     if ($posts = forum_get_user_posts($forum->id, $user->id)) {
1319         if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
1320             print_error('invalidcoursemodule');
1321         }
1322         $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
1324         foreach ($posts as $post) {
1325             if (!isset($discussions[$post->discussion])) {
1326                 continue;
1327             }
1328             $discussion = $discussions[$post->discussion];
1330             forum_print_post_start($post);
1331             forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
1332             forum_print_post_end($post);
1333         }
1334     } else {
1335         echo "<p>".get_string("noposts", "forum")."</p>";
1336     }
1339 /**
1340  * Filters the forum discussions according to groups membership and config.
1341  *
1342  * @deprecated since 3.3
1343  * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
1344  * @since  Moodle 2.8, 2.7.1, 2.6.4
1345  * @param  array $discussions Discussions with new posts array
1346  * @return array Forums with the number of new posts
1347  */
1348 function forum_filter_user_groups_discussions($discussions) {
1350     debugging('The function forum_filter_user_groups_discussions() is now deprecated.', DEBUG_DEVELOPER);
1352     // Group the remaining discussions posts by their forumid.
1353     $filteredforums = array();
1355     // Discard not visible groups.
1356     foreach ($discussions as $discussion) {
1358         // Course data is already cached.
1359         $instances = get_fast_modinfo($discussion->course)->get_instances();
1360         $forum = $instances['forum'][$discussion->forum];
1362         // Continue if the user should not see this discussion.
1363         if (!forum_is_user_group_discussion($forum, $discussion->groupid)) {
1364             continue;
1365         }
1367         // Grouping results by forum.
1368         if (empty($filteredforums[$forum->instance])) {
1369             $filteredforums[$forum->instance] = new stdClass();
1370             $filteredforums[$forum->instance]->id = $forum->id;
1371             $filteredforums[$forum->instance]->count = 0;
1372         }
1373         $filteredforums[$forum->instance]->count += $discussion->count;
1375     }
1377     return $filteredforums;
1380 /**
1381  * Returns whether the discussion group is visible by the current user or not.
1382  *
1383  * @since Moodle 2.8, 2.7.1, 2.6.4
1384  * @param cm_info $cm The discussion course module
1385  * @param int $discussiongroupid The discussion groupid
1386  * @return bool
1387  */
1388 function forum_is_user_group_discussion(cm_info $cm, $discussiongroupid) {
1390     if ($discussiongroupid == -1 || $cm->effectivegroupmode != SEPARATEGROUPS) {
1391         return true;
1392     }
1394     if (isguestuser()) {
1395         return false;
1396     }
1398     if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id)) ||
1399             in_array($discussiongroupid, $cm->get_modinfo()->get_groups($cm->groupingid))) {
1400         return true;
1401     }
1403     return false;
1406 /**
1407  * @deprecated since 3.3
1408  * @todo The final deprecation of this function will take place in Moodle 3.7 - see MDL-57487.
1409  * @global object
1410  * @global object
1411  * @global object
1412  * @param array $courses
1413  * @param array $htmlarray
1414  */
1415 function forum_print_overview($courses,&$htmlarray) {
1416     global $USER, $CFG, $DB, $SESSION;
1418     debugging('The function forum_print_overview() is now deprecated.', DEBUG_DEVELOPER);
1420     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1421         return array();
1422     }
1424     if (!$forums = get_all_instances_in_courses('forum',$courses)) {
1425         return;
1426     }
1428     // Courses to search for new posts
1429     $coursessqls = array();
1430     $params = array();
1431     foreach ($courses as $course) {
1433         // If the user has never entered into the course all posts are pending
1434         if ($course->lastaccess == 0) {
1435             $coursessqls[] = '(d.course = ?)';
1436             $params[] = $course->id;
1438         // Only posts created after the course last access
1439         } else {
1440             $coursessqls[] = '(d.course = ? AND p.created > ?)';
1441             $params[] = $course->id;
1442             $params[] = $course->lastaccess;
1443         }
1444     }
1445     $params[] = $USER->id;
1446     $coursessql = implode(' OR ', $coursessqls);
1448     $sql = "SELECT d.id, d.forum, d.course, d.groupid, COUNT(*) as count "
1449                 .'FROM {forum_discussions} d '
1450                 .'JOIN {forum_posts} p ON p.discussion = d.id '
1451                 ."WHERE ($coursessql) "
1452                 .'AND p.deleted <> 1 '
1453                 .'AND p.userid != ? '
1454                 .'AND (d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?)) '
1455                 .'GROUP BY d.id, d.forum, d.course, d.groupid '
1456                 .'ORDER BY d.course, d.forum';
1457     $params[] = time();
1458     $params[] = time();
1460     // Avoid warnings.
1461     if (!$discussions = $DB->get_records_sql($sql, $params)) {
1462         $discussions = array();
1463     }
1465     $forumsnewposts = forum_filter_user_groups_discussions($discussions);
1467     // also get all forum tracking stuff ONCE.
1468     $trackingforums = array();
1469     foreach ($forums as $forum) {
1470         if (forum_tp_can_track_forums($forum)) {
1471             $trackingforums[$forum->id] = $forum;
1472         }
1473     }
1475     if (count($trackingforums) > 0) {
1476         $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0;
1477         $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
1478             ' FROM {forum_posts} p '.
1479             ' JOIN {forum_discussions} d ON p.discussion = d.id '.
1480             ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE p.deleted <> 1 AND (';
1481         $params = array($USER->id);
1483         foreach ($trackingforums as $track) {
1484             $sql .= '(d.forum = ? AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = ?)) OR ';
1485             $params[] = $track->id;
1486             if (isset($SESSION->currentgroup[$track->course])) {
1487                 $groupid =  $SESSION->currentgroup[$track->course];
1488             } else {
1489                 // get first groupid
1490                 $groupids = groups_get_all_groups($track->course, $USER->id);
1491                 if ($groupids) {
1492                     reset($groupids);
1493                     $groupid = key($groupids);
1494                     $SESSION->currentgroup[$track->course] = $groupid;
1495                 } else {
1496                     $groupid = 0;
1497                 }
1498                 unset($groupids);
1499             }
1500             $params[] = $groupid;
1501         }
1502         $sql = substr($sql,0,-3); // take off the last OR
1503         $sql .= ') AND p.modified >= ? AND r.id is NULL ';
1504         $sql .= 'AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)) ';
1505         $sql .= 'GROUP BY d.forum,d.course';
1506         $params[] = $cutoffdate;
1507         $params[] = time();
1508         $params[] = time();
1510         if (!$unread = $DB->get_records_sql($sql, $params)) {
1511             $unread = array();
1512         }
1513     } else {
1514         $unread = array();
1515     }
1517     if (empty($unread) and empty($forumsnewposts)) {
1518         return;
1519     }
1521     $strforum = get_string('modulename','forum');
1523     foreach ($forums as $forum) {
1524         $str = '';
1525         $count = 0;
1526         $thisunread = 0;
1527         $showunread = false;
1528         // either we have something from logs, or trackposts, or nothing.
1529         if (array_key_exists($forum->id, $forumsnewposts) && !empty($forumsnewposts[$forum->id])) {
1530             $count = $forumsnewposts[$forum->id]->count;
1531         }
1532         if (array_key_exists($forum->id,$unread)) {
1533             $thisunread = $unread[$forum->id]->count;
1534             $showunread = true;
1535         }
1536         if ($count > 0 || $thisunread > 0) {
1537             $str .= '<div class="overview forum"><div class="name">'.$strforum.': <a title="'.$strforum.'" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.
1538                 $forum->name.'</a></div>';
1539             $str .= '<div class="info"><span class="postsincelogin">';
1540             $str .= get_string('overviewnumpostssince', 'forum', $count)."</span>";
1541             if (!empty($showunread)) {
1542                 $str .= '<div class="unreadposts">'.get_string('overviewnumunread', 'forum', $thisunread).'</div>';
1543             }
1544             $str .= '</div></div>';
1545         }
1546         if (!empty($str)) {
1547             if (!array_key_exists($forum->course,$htmlarray)) {
1548                 $htmlarray[$forum->course] = array();
1549             }
1550             if (!array_key_exists('forum',$htmlarray[$forum->course])) {
1551                 $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings
1552             }
1553             $htmlarray[$forum->course]['forum'] .= $str;
1554         }
1555     }
1558 /**
1559  * Given a course and a date, prints a summary of all the new
1560  * messages posted in the course since that date
1561  *
1562  * @global object
1563  * @global object
1564  * @global object
1565  * @uses CONTEXT_MODULE
1566  * @uses VISIBLEGROUPS
1567  * @param object $course
1568  * @param bool $viewfullnames capability
1569  * @param int $timestart
1570  * @return bool success
1571  */
1572 function forum_print_recent_activity($course, $viewfullnames, $timestart) {
1573     global $CFG, $USER, $DB, $OUTPUT;
1575     // do not use log table if possible, it may be huge and is expensive to join with other tables
1577     $allnamefields = user_picture::fields('u', null, 'duserid');
1578     if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
1579                                               d.timestart, d.timeend, $allnamefields
1580                                          FROM {forum_posts} p
1581                                               JOIN {forum_discussions} d ON d.id = p.discussion
1582                                               JOIN {forum} f             ON f.id = d.forum
1583                                               JOIN {user} u              ON u.id = p.userid
1584                                         WHERE p.created > ? AND f.course = ? AND p.deleted <> 1
1585                                      ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
1586          return false;
1587     }
1589     $modinfo = get_fast_modinfo($course);
1591     $groupmodes = array();
1592     $cms    = array();
1594     $strftimerecent = get_string('strftimerecent');
1596     $printposts = array();
1597     foreach ($posts as $post) {
1598         if (!isset($modinfo->instances['forum'][$post->forum])) {
1599             // not visible
1600             continue;
1601         }
1602         $cm = $modinfo->instances['forum'][$post->forum];
1603         if (!$cm->uservisible) {
1604             continue;
1605         }
1606         $context = context_module::instance($cm->id);
1608         if (!has_capability('mod/forum:viewdiscussion', $context)) {
1609             continue;
1610         }
1612         if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
1613           and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
1614             if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1615                 continue;
1616             }
1617         }
1619         // Check that the user can see the discussion.
1620         if (forum_is_user_group_discussion($cm, $post->groupid)) {
1621             $printposts[] = $post;
1622         }
1624     }
1625     unset($posts);
1627     if (!$printposts) {
1628         return false;
1629     }
1631     echo $OUTPUT->heading(get_string('newforumposts', 'forum').':', 3);
1632     $list = html_writer::start_tag('ul', ['class' => 'unlist']);
1634     foreach ($printposts as $post) {
1635         $subjectclass = empty($post->parent) ? ' bold' : '';
1636         $authorhidden = forum_is_author_hidden($post, (object) ['type' => $post->forumtype]);
1638         $list .= html_writer::start_tag('li');
1639         $list .= html_writer::start_div('head');
1640         $list .= html_writer::div(userdate_htmltime($post->modified, $strftimerecent), 'date');
1641         if (!$authorhidden) {
1642             $list .= html_writer::div(fullname($post, $viewfullnames), 'name');
1643         }
1644         $list .= html_writer::end_div(); // Head.
1646         $list .= html_writer::start_div('info' . $subjectclass);
1647         $discussionurl = new moodle_url('/mod/forum/discuss.php', ['d' => $post->discussion]);
1648         if (!empty($post->parent)) {
1649             $discussionurl->param('parent', $post->parent);
1650             $discussionurl->set_anchor('p'. $post->id);
1651         }
1652         $post->subject = break_up_long_words(format_string($post->subject, true));
1653         $list .= html_writer::link($discussionurl, $post->subject, ['rel' => 'bookmark']);
1654         $list .= html_writer::end_div(); // Info.
1655         $list .= html_writer::end_tag('li');
1656     }
1658     $list .= html_writer::end_tag('ul');
1659     echo $list;
1661     return true;
1664 /**
1665  * Return grade for given user or all users.
1666  *
1667  * @global object
1668  * @global object
1669  * @param object $forum
1670  * @param int $userid optional user id, 0 means all users
1671  * @return array array of grades, false if none
1672  */
1673 function forum_get_user_grades($forum, $userid = 0) {
1674     global $CFG;
1676     require_once($CFG->dirroot.'/rating/lib.php');
1678     $ratingoptions = new stdClass;
1679     $ratingoptions->component = 'mod_forum';
1680     $ratingoptions->ratingarea = 'post';
1682     //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance?
1683     $ratingoptions->modulename = 'forum';
1684     $ratingoptions->moduleid   = $forum->id;
1685     $ratingoptions->userid = $userid;
1686     $ratingoptions->aggregationmethod = $forum->assessed;
1687     $ratingoptions->scaleid = $forum->scale;
1688     $ratingoptions->itemtable = 'forum_posts';
1689     $ratingoptions->itemtableusercolumn = 'userid';
1691     $rm = new rating_manager();
1692     return $rm->get_user_grades($ratingoptions);
1695 /**
1696  * Update activity grades
1697  *
1698  * @category grade
1699  * @param object $forum
1700  * @param int $userid specific user only, 0 means all
1701  * @param boolean $nullifnone return null if grade does not exist
1702  * @return void
1703  */
1704 function forum_update_grades($forum, $userid=0, $nullifnone=true) {
1705     global $CFG, $DB;
1706     require_once($CFG->libdir.'/gradelib.php');
1708     if (!$forum->assessed) {
1709         forum_grade_item_update($forum);
1711     } else if ($grades = forum_get_user_grades($forum, $userid)) {
1712         forum_grade_item_update($forum, $grades);
1714     } else if ($userid and $nullifnone) {
1715         $grade = new stdClass();
1716         $grade->userid   = $userid;
1717         $grade->rawgrade = NULL;
1718         forum_grade_item_update($forum, $grade);
1720     } else {
1721         forum_grade_item_update($forum);
1722     }
1725 /**
1726  * Create/update grade item for given forum
1727  *
1728  * @category grade
1729  * @uses GRADE_TYPE_NONE
1730  * @uses GRADE_TYPE_VALUE
1731  * @uses GRADE_TYPE_SCALE
1732  * @param stdClass $forum Forum object with extra cmidnumber
1733  * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
1734  * @return int 0 if ok
1735  */
1736 function forum_grade_item_update($forum, $grades=NULL) {
1737     global $CFG;
1738     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
1739         require_once($CFG->libdir.'/gradelib.php');
1740     }
1742     $params = array('itemname'=>$forum->name, 'idnumber'=>$forum->cmidnumber);
1744     if (!$forum->assessed or $forum->scale == 0) {
1745         $params['gradetype'] = GRADE_TYPE_NONE;
1747     } else if ($forum->scale > 0) {
1748         $params['gradetype'] = GRADE_TYPE_VALUE;
1749         $params['grademax']  = $forum->scale;
1750         $params['grademin']  = 0;
1752     } else if ($forum->scale < 0) {
1753         $params['gradetype'] = GRADE_TYPE_SCALE;
1754         $params['scaleid']   = -$forum->scale;
1755     }
1757     if ($grades  === 'reset') {
1758         $params['reset'] = true;
1759         $grades = NULL;
1760     }
1762     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $grades, $params);
1765 /**
1766  * Delete grade item for given forum
1767  *
1768  * @category grade
1769  * @param stdClass $forum Forum object
1770  * @return grade_item
1771  */
1772 function forum_grade_item_delete($forum) {
1773     global $CFG;
1774     require_once($CFG->libdir.'/gradelib.php');
1776     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1));
1780 /**
1781  * This function returns if a scale is being used by one forum
1782  *
1783  * @global object
1784  * @param int $forumid
1785  * @param int $scaleid negative number
1786  * @return bool
1787  */
1788 function forum_scale_used ($forumid,$scaleid) {
1789     global $DB;
1790     $return = false;
1792     $rec = $DB->get_record("forum",array("id" => "$forumid","scale" => "-$scaleid"));
1794     if (!empty($rec) && !empty($scaleid)) {
1795         $return = true;
1796     }
1798     return $return;
1801 /**
1802  * Checks if scale is being used by any instance of forum
1803  *
1804  * This is used to find out if scale used anywhere
1805  *
1806  * @global object
1807  * @param $scaleid int
1808  * @return boolean True if the scale is used by any forum
1809  */
1810 function forum_scale_used_anywhere($scaleid) {
1811     global $DB;
1812     if ($scaleid and $DB->record_exists('forum', array('scale' => -$scaleid))) {
1813         return true;
1814     } else {
1815         return false;
1816     }
1819 // SQL FUNCTIONS ///////////////////////////////////////////////////////////
1821 /**
1822  * Gets a post with all info ready for forum_print_post
1823  * Most of these joins are just to get the forum id
1824  *
1825  * @global object
1826  * @global object
1827  * @param int $postid
1828  * @return mixed array of posts or false
1829  */
1830 function forum_get_post_full($postid) {
1831     global $CFG, $DB;
1833     $allnames = get_all_user_name_fields(true, 'u');
1834     return $DB->get_record_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
1835                              FROM {forum_posts} p
1836                                   JOIN {forum_discussions} d ON p.discussion = d.id
1837                                   LEFT JOIN {user} u ON p.userid = u.id
1838                             WHERE p.id = ?", array($postid));
1841 /**
1842  * Gets all posts in discussion including top parent.
1843  *
1844  * @global object
1845  * @global object
1846  * @global object
1847  * @param int $discussionid
1848  * @param string $sort
1849  * @param bool $tracking does user track the forum?
1850  * @return array of posts
1851  */
1852 function forum_get_all_discussion_posts($discussionid, $sort, $tracking=false) {
1853     global $CFG, $DB, $USER;
1855     $tr_sel  = "";
1856     $tr_join = "";
1857     $params = array();
1859     if ($tracking) {
1860         $tr_sel  = ", fr.id AS postread";
1861         $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
1862         $params[] = $USER->id;
1863     }
1865     $allnames = get_all_user_name_fields(true, 'u');
1866     $params[] = $discussionid;
1867     if (!$posts = $DB->get_records_sql("SELECT p.*, $allnames, u.email, u.picture, u.imagealt $tr_sel
1868                                      FROM {forum_posts} p
1869                                           LEFT JOIN {user} u ON p.userid = u.id
1870                                           $tr_join
1871                                     WHERE p.discussion = ?
1872                                  ORDER BY $sort", $params)) {
1873         return array();
1874     }
1876     foreach ($posts as $pid=>$p) {
1877         if ($tracking) {
1878             if (forum_tp_is_post_old($p)) {
1879                  $posts[$pid]->postread = true;
1880             }
1881         }
1882         if (!$p->parent) {
1883             continue;
1884         }
1885         if (!isset($posts[$p->parent])) {
1886             continue; // parent does not exist??
1887         }
1888         if (!isset($posts[$p->parent]->children)) {
1889             $posts[$p->parent]->children = array();
1890         }
1891         $posts[$p->parent]->children[$pid] =& $posts[$pid];
1892     }
1894     // Start with the last child of the first post.
1895     $post = &$posts[reset($posts)->id];
1897     $lastpost = false;
1898     while (!$lastpost) {
1899         if (!isset($post->children)) {
1900             $post->lastpost = true;
1901             $lastpost = true;
1902         } else {
1903              // Go to the last child of this post.
1904             $post = &$posts[end($post->children)->id];
1905         }
1906     }
1908     return $posts;
1911 /**
1912  * An array of forum objects that the user is allowed to read/search through.
1913  *
1914  * @global object
1915  * @global object
1916  * @global object
1917  * @param int $userid
1918  * @param int $courseid if 0, we look for forums throughout the whole site.
1919  * @return array of forum objects, or false if no matches
1920  *         Forum objects have the following attributes:
1921  *         id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
1922  *         viewhiddentimedposts
1923  */
1924 function forum_get_readable_forums($userid, $courseid=0) {
1926     global $CFG, $DB, $USER;
1927     require_once($CFG->dirroot.'/course/lib.php');
1929     if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
1930         print_error('notinstalled', 'forum');
1931     }
1933     if ($courseid) {
1934         $courses = $DB->get_records('course', array('id' => $courseid));
1935     } else {
1936         // If no course is specified, then the user can see SITE + his courses.
1937         $courses1 = $DB->get_records('course', array('id' => SITEID));
1938         $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
1939         $courses = array_merge($courses1, $courses2);
1940     }
1941     if (!$courses) {
1942         return array();
1943     }
1945     $readableforums = array();
1947     foreach ($courses as $course) {
1949         $modinfo = get_fast_modinfo($course);
1951         if (empty($modinfo->instances['forum'])) {
1952             // hmm, no forums?
1953             continue;
1954         }
1956         $courseforums = $DB->get_records('forum', array('course' => $course->id));
1958         foreach ($modinfo->instances['forum'] as $forumid => $cm) {
1959             if (!$cm->uservisible or !isset($courseforums[$forumid])) {
1960                 continue;
1961             }
1962             $context = context_module::instance($cm->id);
1963             $forum = $courseforums[$forumid];
1964             $forum->context = $context;
1965             $forum->cm = $cm;
1967             if (!has_capability('mod/forum:viewdiscussion', $context)) {
1968                 continue;
1969             }
1971          /// group access
1972             if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
1974                 $forum->onlygroups = $modinfo->get_groups($cm->groupingid);
1975                 $forum->onlygroups[] = -1;
1976             }
1978         /// hidden timed discussions
1979             $forum->viewhiddentimedposts = true;
1980             if (!empty($CFG->forum_enabletimedposts)) {
1981                 if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1982                     $forum->viewhiddentimedposts = false;
1983                 }
1984             }
1986         /// qanda access
1987             if ($forum->type == 'qanda'
1988                     && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
1990                 // We need to check whether the user has posted in the qanda forum.
1991                 $forum->onlydiscussions = array();  // Holds discussion ids for the discussions
1992                                                     // the user is allowed to see in this forum.
1993                 if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
1994                     foreach ($discussionspostedin as $d) {
1995                         $forum->onlydiscussions[] = $d->id;
1996                     }
1997                 }
1998             }
2000             $readableforums[$forum->id] = $forum;
2001         }
2003         unset($modinfo);
2005     } // End foreach $courses
2007     return $readableforums;
2010 /**
2011  * Returns a list of posts found using an array of search terms.
2012  *
2013  * @global object
2014  * @global object
2015  * @global object
2016  * @param array $searchterms array of search terms, e.g. word +word -word
2017  * @param int $courseid if 0, we search through the whole site
2018  * @param int $limitfrom
2019  * @param int $limitnum
2020  * @param int &$totalcount
2021  * @param string $extrasql
2022  * @return array|bool Array of posts found or false
2023  */
2024 function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=50,
2025                             &$totalcount, $extrasql='') {
2026     global $CFG, $DB, $USER;
2027     require_once($CFG->libdir.'/searchlib.php');
2029     $forums = forum_get_readable_forums($USER->id, $courseid);
2031     if (count($forums) == 0) {
2032         $totalcount = 0;
2033         return false;
2034     }
2036     $now = floor(time() / 60) * 60; // DB Cache Friendly.
2038     $fullaccess = array();
2039     $where = array();
2040     $params = array();
2042     foreach ($forums as $forumid => $forum) {
2043         $select = array();
2045         if (!$forum->viewhiddentimedposts) {
2046             $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
2047             $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
2048         }
2050         $cm = $forum->cm;
2051         $context = $forum->context;
2053         if ($forum->type == 'qanda'
2054             && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
2055             if (!empty($forum->onlydiscussions)) {
2056                 list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
2057                 $params = array_merge($params, $discussionid_params);
2058                 $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
2059             } else {
2060                 $select[] = "p.parent = 0";
2061             }
2062         }
2064         if (!empty($forum->onlygroups)) {
2065             list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
2066             $params = array_merge($params, $groupid_params);
2067             $select[] = "d.groupid $groupid_sql";
2068         }
2070         if ($select) {
2071             $selects = implode(" AND ", $select);
2072             $where[] = "(d.forum = :forum{$forumid} AND $selects)";
2073             $params['forum'.$forumid] = $forumid;
2074         } else {
2075             $fullaccess[] = $forumid;
2076         }
2077     }
2079     if ($fullaccess) {
2080         list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
2081         $params = array_merge($params, $fullid_params);
2082         $where[] = "(d.forum $fullid_sql)";
2083     }
2085     $selectdiscussion = "(".implode(" OR ", $where).")";
2087     $messagesearch = '';
2088     $searchstring = '';
2090     // Need to concat these back together for parser to work.
2091     foreach($searchterms as $searchterm){
2092         if ($searchstring != '') {
2093             $searchstring .= ' ';
2094         }
2095         $searchstring .= $searchterm;
2096     }
2098     // We need to allow quoted strings for the search. The quotes *should* be stripped
2099     // by the parser, but this should be examined carefully for security implications.
2100     $searchstring = str_replace("\\\"","\"",$searchstring);
2101     $parser = new search_parser();
2102     $lexer = new search_lexer($parser);
2104     if ($lexer->parse($searchstring)) {
2105         $parsearray = $parser->get_parsed_array();
2107         $tagjoins = '';
2108         $tagfields = [];
2109         $tagfieldcount = 0;
2110         foreach ($parsearray as $token) {
2111             if ($token->getType() == TOKEN_TAGS) {
2112                 for ($i = 0; $i <= substr_count($token->getValue(), ','); $i++) {
2113                     // Queries can only have a limited number of joins so set a limit sensible users won't exceed.
2114                     if ($tagfieldcount > 10) {
2115                         continue;
2116                     }
2117                     $tagjoins .= " LEFT JOIN {tag_instance} ti_$tagfieldcount
2118                                         ON p.id = ti_$tagfieldcount.itemid
2119                                             AND ti_$tagfieldcount.component = 'mod_forum'
2120                                             AND ti_$tagfieldcount.itemtype = 'forum_posts'";
2121                     $tagjoins .= " LEFT JOIN {tag} t_$tagfieldcount ON t_$tagfieldcount.id = ti_$tagfieldcount.tagid";
2122                     $tagfields[] = "t_$tagfieldcount.rawname";
2123                     $tagfieldcount++;
2124                 }
2125             }
2126         }
2127         list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
2128                                                               'p.userid', 'u.id', 'u.firstname',
2129                                                               'u.lastname', 'p.modified', 'd.forum',
2130                                                               $tagfields);
2131         $params = array_merge($params, $msparams);
2132     }
2134     $fromsql = "{forum_posts} p
2135                   INNER JOIN {forum_discussions} d ON d.id = p.discussion
2136                   INNER JOIN {user} u ON u.id = p.userid $tagjoins";
2138     $selectsql = " $messagesearch
2139                AND p.discussion = d.id
2140                AND p.userid = u.id
2141                AND $selectdiscussion
2142                    $extrasql";
2144     $countsql = "SELECT COUNT(*)
2145                    FROM $fromsql
2146                   WHERE $selectsql";
2148     $allnames = get_all_user_name_fields(true, 'u');
2149     $searchsql = "SELECT p.*,
2150                          d.forum,
2151                          $allnames,
2152                          u.email,
2153                          u.picture,
2154                          u.imagealt
2155                     FROM $fromsql
2156                    WHERE $selectsql
2157                 ORDER BY p.modified DESC";
2159     $totalcount = $DB->count_records_sql($countsql, $params);
2161     return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
2164 /**
2165  * Returns a list of all new posts that have not been mailed yet
2166  *
2167  * @param int $starttime posts created after this time
2168  * @param int $endtime posts created before this
2169  * @param int $now used for timed discussions only
2170  * @return array
2171  */
2172 function forum_get_unmailed_posts($starttime, $endtime, $now=null) {
2173     global $CFG, $DB;
2175     $params = array();
2176     $params['mailed'] = FORUM_MAILED_PENDING;
2177     $params['ptimestart'] = $starttime;
2178     $params['ptimeend'] = $endtime;
2179     $params['mailnow'] = 1;
2181     if (!empty($CFG->forum_enabletimedposts)) {
2182         if (empty($now)) {
2183             $now = time();
2184         }
2185         $selectsql = "AND (p.created >= :ptimestart OR d.timestart >= :pptimestart)";
2186         $params['pptimestart'] = $starttime;
2187         $timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))";
2188         $params['dtimestart'] = $now;
2189         $params['dtimeend'] = $now;
2190     } else {
2191         $timedsql = "";
2192         $selectsql = "AND p.created >= :ptimestart";
2193     }
2195     return $DB->get_records_sql("SELECT p.*, d.course, d.forum
2196                                  FROM {forum_posts} p
2197                                  JOIN {forum_discussions} d ON d.id = p.discussion
2198                                  WHERE p.mailed = :mailed
2199                                  $selectsql
2200                                  AND (p.created < :ptimeend OR p.mailnow = :mailnow)
2201                                  $timedsql
2202                                  ORDER BY p.modified ASC", $params);
2205 /**
2206  * Marks posts before a certain time as being mailed already
2207  *
2208  * @global object
2209  * @global object
2210  * @param int $endtime
2211  * @param int $now Defaults to time()
2212  * @return bool
2213  */
2214 function forum_mark_old_posts_as_mailed($endtime, $now=null) {
2215     global $CFG, $DB;
2217     if (empty($now)) {
2218         $now = time();
2219     }
2221     $params = array();
2222     $params['mailedsuccess'] = FORUM_MAILED_SUCCESS;
2223     $params['now'] = $now;
2224     $params['endtime'] = $endtime;
2225     $params['mailnow'] = 1;
2226     $params['mailedpending'] = FORUM_MAILED_PENDING;
2228     if (empty($CFG->forum_enabletimedposts)) {
2229         return $DB->execute("UPDATE {forum_posts}
2230                              SET mailed = :mailedsuccess
2231                              WHERE (created < :endtime OR mailnow = :mailnow)
2232                              AND mailed = :mailedpending", $params);
2233     } else {
2234         return $DB->execute("UPDATE {forum_posts}
2235                              SET mailed = :mailedsuccess
2236                              WHERE discussion NOT IN (SELECT d.id
2237                                                       FROM {forum_discussions} d
2238                                                       WHERE d.timestart > :now)
2239                              AND (created < :endtime OR mailnow = :mailnow)
2240                              AND mailed = :mailedpending", $params);
2241     }
2244 /**
2245  * Get all the posts for a user in a forum suitable for forum_print_post
2246  *
2247  * @global object
2248  * @global object
2249  * @uses CONTEXT_MODULE
2250  * @return array
2251  */
2252 function forum_get_user_posts($forumid, $userid) {
2253     global $CFG, $DB;
2255     $timedsql = "";
2256     $params = array($forumid, $userid);
2258     if (!empty($CFG->forum_enabletimedposts)) {
2259         $cm = get_coursemodule_from_instance('forum', $forumid);
2260         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2261             $now = time();
2262             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2263             $params[] = $now;
2264             $params[] = $now;
2265         }
2266     }
2268     $allnames = get_all_user_name_fields(true, 'u');
2269     return $DB->get_records_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
2270                               FROM {forum} f
2271                                    JOIN {forum_discussions} d ON d.forum = f.id
2272                                    JOIN {forum_posts} p       ON p.discussion = d.id
2273                                    JOIN {user} u              ON u.id = p.userid
2274                              WHERE f.id = ?
2275                                    AND p.userid = ?
2276                                    $timedsql
2277                           ORDER BY p.modified ASC", $params);
2280 /**
2281  * Get all the discussions user participated in
2282  *
2283  * @global object
2284  * @global object
2285  * @uses CONTEXT_MODULE
2286  * @param int $forumid
2287  * @param int $userid
2288  * @return array Array or false
2289  */
2290 function forum_get_user_involved_discussions($forumid, $userid) {
2291     global $CFG, $DB;
2293     $timedsql = "";
2294     $params = array($forumid, $userid);
2295     if (!empty($CFG->forum_enabletimedposts)) {
2296         $cm = get_coursemodule_from_instance('forum', $forumid);
2297         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2298             $now = time();
2299             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2300             $params[] = $now;
2301             $params[] = $now;
2302         }
2303     }
2305     return $DB->get_records_sql("SELECT DISTINCT d.*
2306                               FROM {forum} f
2307                                    JOIN {forum_discussions} d ON d.forum = f.id
2308                                    JOIN {forum_posts} p       ON p.discussion = d.id
2309                              WHERE f.id = ?
2310                                    AND p.userid = ?
2311                                    $timedsql", $params);
2314 /**
2315  * Get all the posts for a user in a forum suitable for forum_print_post
2316  *
2317  * @global object
2318  * @global object
2319  * @param int $forumid
2320  * @param int $userid
2321  * @return array of counts or false
2322  */
2323 function forum_count_user_posts($forumid, $userid) {
2324     global $CFG, $DB;
2326     $timedsql = "";
2327     $params = array($forumid, $userid);
2328     if (!empty($CFG->forum_enabletimedposts)) {
2329         $cm = get_coursemodule_from_instance('forum', $forumid);
2330         if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
2331             $now = time();
2332             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2333             $params[] = $now;
2334             $params[] = $now;
2335         }
2336     }
2338     return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
2339                              FROM {forum} f
2340                                   JOIN {forum_discussions} d ON d.forum = f.id
2341                                   JOIN {forum_posts} p       ON p.discussion = d.id
2342                                   JOIN {user} u              ON u.id = p.userid
2343                             WHERE f.id = ?
2344                                   AND p.userid = ?
2345                                   $timedsql", $params);
2348 /**
2349  * Given a log entry, return the forum post details for it.
2350  *
2351  * @global object
2352  * @global object
2353  * @param object $log
2354  * @return array|null
2355  */
2356 function forum_get_post_from_log($log) {
2357     global $CFG, $DB;
2359     $allnames = get_all_user_name_fields(true, 'u');
2360     if ($log->action == "add post") {
2362         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
2363                                  FROM {forum_discussions} d,
2364                                       {forum_posts} p,
2365                                       {forum} f,
2366                                       {user} u
2367                                 WHERE p.id = ?
2368                                   AND d.id = p.discussion
2369                                   AND p.userid = u.id
2370                                   AND u.deleted <> '1'
2371                                   AND f.id = d.forum", array($log->info));
2374     } else if ($log->action == "add discussion") {
2376         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
2377                                  FROM {forum_discussions} d,
2378                                       {forum_posts} p,
2379                                       {forum} f,
2380                                       {user} u
2381                                 WHERE d.id = ?
2382                                   AND d.firstpost = p.id
2383                                   AND p.userid = u.id
2384                                   AND u.deleted <> '1'
2385                                   AND f.id = d.forum", array($log->info));
2386     }
2387     return NULL;
2390 /**
2391  * Given a discussion id, return the first post from the discussion
2392  *
2393  * @global object
2394  * @global object
2395  * @param int $dicsussionid
2396  * @return array
2397  */
2398 function forum_get_firstpost_from_discussion($discussionid) {
2399     global $CFG, $DB;
2401     return $DB->get_record_sql("SELECT p.*
2402                              FROM {forum_discussions} d,
2403                                   {forum_posts} p
2404                             WHERE d.id = ?
2405                               AND d.firstpost = p.id ", array($discussionid));
2408 /**
2409  * Returns an array of counts of replies to each discussion
2410  *
2411  * @global object
2412  * @global object
2413  * @param int $forumid
2414  * @param string $forumsort
2415  * @param int $limit
2416  * @param int $page
2417  * @param int $perpage
2418  * @return array
2419  */
2420 function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $page=-1, $perpage=0) {
2421     global $CFG, $DB;
2423     if ($limit > 0) {
2424         $limitfrom = 0;
2425         $limitnum  = $limit;
2426     } else if ($page != -1) {
2427         $limitfrom = $page*$perpage;
2428         $limitnum  = $perpage;
2429     } else {
2430         $limitfrom = 0;
2431         $limitnum  = 0;
2432     }
2434     if ($forumsort == "") {
2435         $orderby = "";
2436         $groupby = "";
2438     } else {
2439         $orderby = "ORDER BY $forumsort";
2440         $groupby = ", ".strtolower($forumsort);
2441         $groupby = str_replace('desc', '', $groupby);
2442         $groupby = str_replace('asc', '', $groupby);
2443     }
2445     if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
2446         $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
2447                   FROM {forum_posts} p
2448                        JOIN {forum_discussions} d ON p.discussion = d.id
2449                  WHERE p.parent > 0 AND d.forum = ?
2450               GROUP BY p.discussion";
2451         return $DB->get_records_sql($sql, array($forumid));
2453     } else {
2454         $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
2455                   FROM {forum_posts} p
2456                        JOIN {forum_discussions} d ON p.discussion = d.id
2457                  WHERE d.forum = ?
2458               GROUP BY p.discussion $groupby $orderby";
2459         return $DB->get_records_sql($sql, array($forumid), $limitfrom, $limitnum);
2460     }
2463 /**
2464  * @global object
2465  * @global object
2466  * @global object
2467  * @staticvar array $cache
2468  * @param object $forum
2469  * @param object $cm
2470  * @param object $course
2471  * @return mixed
2472  */
2473 function forum_count_discussions($forum, $cm, $course) {
2474     global $CFG, $DB, $USER;
2476     static $cache = array();
2478     $now = floor(time() / 60) * 60; // DB Cache Friendly.
2480     $params = array($course->id);
2482     if (!isset($cache[$course->id])) {
2483         if (!empty($CFG->forum_enabletimedposts)) {
2484             $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
2485             $params[] = $now;
2486             $params[] = $now;
2487         } else {
2488             $timedsql = "";
2489         }
2491         $sql = "SELECT f.id, COUNT(d.id) as dcount
2492                   FROM {forum} f
2493                        JOIN {forum_discussions} d ON d.forum = f.id
2494                  WHERE f.course = ?
2495                        $timedsql
2496               GROUP BY f.id";
2498         if ($counts = $DB->get_records_sql($sql, $params)) {
2499             foreach ($counts as $count) {
2500                 $counts[$count->id] = $count->dcount;
2501             }
2502             $cache[$course->id] = $counts;
2503         } else {
2504             $cache[$course->id] = array();
2505         }
2506     }
2508     if (empty($cache[$course->id][$forum->id])) {
2509         return 0;
2510     }
2512     $groupmode = groups_get_activity_groupmode($cm, $course);
2514     if ($groupmode != SEPARATEGROUPS) {
2515         return $cache[$course->id][$forum->id];
2516     }
2518     if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) {
2519         return $cache[$course->id][$forum->id];
2520     }
2522     require_once($CFG->dirroot.'/course/lib.php');
2524     $modinfo = get_fast_modinfo($course);
2526     $mygroups = $modinfo->get_groups($cm->groupingid);
2528     // add all groups posts
2529     $mygroups[-1] = -1;
2531     list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
2532     $params[] = $forum->id;
2534     if (!empty($CFG->forum_enabletimedposts)) {
2535         $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
2536         $params[] = $now;
2537         $params[] = $now;
2538     } else {
2539         $timedsql = "";
2540     }
2542     $sql = "SELECT COUNT(d.id)
2543               FROM {forum_discussions} d
2544              WHERE d.groupid $mygroups_sql AND d.forum = ?
2545                    $timedsql";
2547     return $DB->get_field_sql($sql, $params);
2550 /**
2551  * Get all discussions in a forum
2552  *
2553  * @global object
2554  * @global object
2555  * @global object
2556  * @uses CONTEXT_MODULE
2557  * @uses VISIBLEGROUPS
2558  * @param object $cm
2559  * @param string $forumsort
2560  * @param bool $fullpost
2561  * @param int $unused
2562  * @param int $limit
2563  * @param bool $userlastmodified
2564  * @param int $page
2565  * @param int $perpage
2566  * @param int $groupid if groups enabled, get discussions for this group overriding the current group.
2567  *                     Use FORUM_POSTS_ALL_USER_GROUPS for all the user groups
2568  * @param int $updatedsince retrieve only discussions updated since the given time
2569  * @return array
2570  */
2571 function forum_get_discussions($cm, $forumsort="", $fullpost=true, $unused=-1, $limit=-1,
2572                                 $userlastmodified=false, $page=-1, $perpage=0, $groupid = -1,
2573                                 $updatedsince = 0) {
2574     global $CFG, $DB, $USER;
2576     $timelimit = '';
2578     $now = floor(time() / 60) * 60;
2579     $params = array($cm->instance);
2581     $modcontext = context_module::instance($cm->id);
2583     if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
2584         return array();
2585     }
2587     if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
2589         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2590             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2591             $params[] = $now;
2592             $params[] = $now;
2593             if (isloggedin()) {
2594                 $timelimit .= " OR d.userid = ?";
2595                 $params[] = $USER->id;
2596             }
2597             $timelimit .= ")";
2598         }
2599     }
2601     if ($limit > 0) {
2602         $limitfrom = 0;
2603         $limitnum  = $limit;
2604     } else if ($page != -1) {
2605         $limitfrom = $page*$perpage;
2606         $limitnum  = $perpage;
2607     } else {
2608         $limitfrom = 0;
2609         $limitnum  = 0;
2610     }
2612     $groupmode    = groups_get_activity_groupmode($cm);
2614     if ($groupmode) {
2616         if (empty($modcontext)) {
2617             $modcontext = context_module::instance($cm->id);
2618         }
2620         // Special case, we received a groupid to override currentgroup.
2621         if ($groupid > 0) {
2622             $course = get_course($cm->course);
2623             if (!groups_group_visible($groupid, $course, $cm)) {
2624                 // User doesn't belong to this group, return nothing.
2625                 return array();
2626             }
2627             $currentgroup = $groupid;
2628         } else if ($groupid === -1) {
2629             $currentgroup = groups_get_activity_group($cm);
2630         } else {
2631             // Get discussions for all groups current user can see.
2632             $currentgroup = null;
2633         }
2635         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2636             if ($currentgroup) {
2637                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2638                 $params[] = $currentgroup;
2639             } else {
2640                 $groupselect = "";
2641             }
2643         } else {
2644             // Separate groups.
2646             // Get discussions for all groups current user can see.
2647             if ($currentgroup === null) {
2648                 $mygroups = array_keys(groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.id'));
2649                 if (empty($mygroups)) {
2650                      $groupselect = "AND d.groupid = -1";
2651                 } else {
2652                     list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($mygroups);
2653                     $groupselect = "AND (d.groupid = -1 OR d.groupid $insqlgroups)";
2654                     $params = array_merge($params, $inparamsgroups);
2655                 }
2656             } else if ($currentgroup) {
2657                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2658                 $params[] = $currentgroup;
2659             } else {
2660                 $groupselect = "AND d.groupid = -1";
2661             }
2662         }
2663     } else {
2664         $groupselect = "";
2665     }
2666     if (empty($forumsort)) {
2667         $forumsort = forum_get_default_sort_order();
2668     }
2669     if (empty($fullpost)) {
2670         $postdata = "p.id, p.subject, p.modified, p.discussion, p.userid, p.created";
2671     } else {
2672         $postdata = "p.*";
2673     }
2675     if (empty($userlastmodified)) {  // We don't need to know this
2676         $umfields = "";
2677         $umtable  = "";
2678     } else {
2679         $umfields = ', ' . get_all_user_name_fields(true, 'um', null, 'um') . ', um.email AS umemail, um.picture AS umpicture,
2680                         um.imagealt AS umimagealt';
2681         $umtable  = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
2682     }
2684     $updatedsincesql = '';
2685     if (!empty($updatedsince)) {
2686         $updatedsincesql = 'AND d.timemodified > ?';
2687         $params[] = $updatedsince;
2688     }
2690     $allnames = get_all_user_name_fields(true, 'u');
2691     $sql = "SELECT $postdata, d.name, d.timemodified, d.usermodified, d.groupid, d.timestart, d.timeend, d.pinned,
2692                    $allnames, u.email, u.picture, u.imagealt $umfields
2693               FROM {forum_discussions} d
2694                    JOIN {forum_posts} p ON p.discussion = d.id
2695                    JOIN {user} u ON p.userid = u.id
2696                    $umtable
2697              WHERE d.forum = ? AND p.parent = 0
2698                    $timelimit $groupselect $updatedsincesql
2699           ORDER BY $forumsort, d.id DESC";
2701     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
2704 /**
2705  * Gets the neighbours (previous and next) of a discussion.
2706  *
2707  * The calculation is based on the timemodified when time modified or time created is identical
2708  * It will revert to using the ID to sort consistently. This is better tha skipping a discussion.
2709  *
2710  * For blog-style forums, the calculation is based on the original creation time of the
2711  * blog post.
2712  *
2713  * Please note that this does not check whether or not the discussion passed is accessible
2714  * by the user, it simply uses it as a reference to find the neighbours. On the other hand,
2715  * the returned neighbours are checked and are accessible to the current user.
2716  *
2717  * @param object $cm The CM record.
2718  * @param object $discussion The discussion record.
2719  * @param object $forum The forum instance record.
2720  * @return array That always contains the keys 'prev' and 'next'. When there is a result
2721  *               they contain the record with minimal information such as 'id' and 'name'.
2722  *               When the neighbour is not found the value is false.
2723  */
2724 function forum_get_discussion_neighbours($cm, $discussion, $forum) {
2725     global $CFG, $DB, $USER;
2727     if ($cm->instance != $discussion->forum or $discussion->forum != $forum->id or $forum->id != $cm->instance) {
2728         throw new coding_exception('Discussion is not part of the same forum.');
2729     }
2731     $neighbours = array('prev' => false, 'next' => false);
2732     $now = floor(time() / 60) * 60;
2733     $params = array();
2735     $modcontext = context_module::instance($cm->id);
2736     $groupmode    = groups_get_activity_groupmode($cm);
2737     $currentgroup = groups_get_activity_group($cm);
2739     // Users must fulfill timed posts.
2740     $timelimit = '';
2741     if (!empty($CFG->forum_enabletimedposts)) {
2742         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2743             $timelimit = ' AND ((d.timestart <= :tltimestart AND (d.timeend = 0 OR d.timeend > :tltimeend))';
2744             $params['tltimestart'] = $now;
2745             $params['tltimeend'] = $now;
2746             if (isloggedin()) {
2747                 $timelimit .= ' OR d.userid = :tluserid';
2748                 $params['tluserid'] = $USER->id;
2749             }
2750             $timelimit .= ')';
2751         }
2752     }
2754     // Limiting to posts accessible according to groups.
2755     $groupselect = '';
2756     if ($groupmode) {
2757         if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $modcontext)) {
2758             if ($currentgroup) {
2759                 $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
2760                 $params['groupid'] = $currentgroup;
2761             }
2762         } else {
2763             if ($currentgroup) {
2764                 $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
2765                 $params['groupid'] = $currentgroup;
2766             } else {
2767                 $groupselect = 'AND d.groupid = -1';
2768             }
2769         }
2770     }
2772     $params['forumid'] = $cm->instance;
2773     $params['discid1'] = $discussion->id;
2774     $params['discid2'] = $discussion->id;
2775     $params['discid3'] = $discussion->id;
2776     $params['discid4'] = $discussion->id;
2777     $params['disctimecompare1'] = $discussion->timemodified;
2778     $params['disctimecompare2'] = $discussion->timemodified;
2779     $params['pinnedstate1'] = (int) $discussion->pinned;
2780     $params['pinnedstate2'] = (int) $discussion->pinned;
2781     $params['pinnedstate3'] = (int) $discussion->pinned;
2782     $params['pinnedstate4'] = (int) $discussion->pinned;
2784     $sql = "SELECT d.id, d.name, d.timemodified, d.groupid, d.timestart, d.timeend
2785               FROM {forum_discussions} d
2786               JOIN {forum_posts} p ON d.firstpost = p.id
2787              WHERE d.forum = :forumid
2788                AND d.id <> :discid1
2789                    $timelimit
2790                    $groupselect";
2791     $comparefield = "d.timemodified";
2792     $comparevalue = ":disctimecompare1";
2793     $comparevalue2  = ":disctimecompare2";
2794     if (!empty($CFG->forum_enabletimedposts)) {
2795         // Here we need to take into account the release time (timestart)
2796         // if one is set, of the neighbouring posts and compare it to the
2797         // timestart or timemodified of *this* post depending on if the
2798         // release date of this post is in the future or not.
2799         // This stops discussions that appear later because of the
2800         // timestart value from being buried under discussions that were
2801         // made afterwards.
2802         $comparefield = "CASE WHEN d.timemodified < d.timestart
2803                                 THEN d.timestart ELSE d.timemodified END";
2804         if ($discussion->timemodified < $discussion->timestart) {
2805             // Normally we would just use the timemodified for sorting
2806             // discussion posts. However, when timed discussions are enabled,
2807             // then posts need to be sorted base on the later of timemodified
2808             // or the release date of the post (timestart).
2809             $params['disctimecompare1'] = $discussion->timestart;
2810             $params['disctimecompare2'] = $discussion->timestart;
2811         }
2812     }
2813     $orderbydesc = forum_get_default_sort_order(true, $comparefield, 'd', false);
2814     $orderbyasc = forum_get_default_sort_order(false, $comparefield, 'd', false);
2816     if ($forum->type === 'blog') {
2817          $subselect = "SELECT pp.created
2818                    FROM {forum_discussions} dd
2819                    JOIN {forum_posts} pp ON dd.firstpost = pp.id ";
2821          $subselectwhere1 = " WHERE dd.id = :discid3";
2822          $subselectwhere2 = " WHERE dd.id = :discid4";
2824          $comparefield = "p.created";
2826          $sub1 = $subselect.$subselectwhere1;
2827          $comparevalue = "($sub1)";
2829          $sub2 = $subselect.$subselectwhere2;
2830          $comparevalue2 = "($sub2)";
2832          $orderbydesc = "d.pinned, p.created DESC";
2833          $orderbyasc = "d.pinned, p.created ASC";
2834     }
2836     $prevsql = $sql . " AND ( (($comparefield < $comparevalue) AND :pinnedstate1 = d.pinned)
2837                          OR ($comparefield = $comparevalue2 AND (d.pinned = 0 OR d.pinned = :pinnedstate4) AND d.id < :discid2)
2838                          OR (d.pinned = 0 AND d.pinned <> :pinnedstate2))
2839                    ORDER BY CASE WHEN d.pinned = :pinnedstate3 THEN 1 ELSE 0 END DESC, $orderbydesc, d.id DESC";
2841     $nextsql = $sql . " AND ( (($comparefield > $comparevalue) AND :pinnedstate1 = d.pinned)
2842                          OR ($comparefield = $comparevalue2 AND (d.pinned = 1 OR d.pinned = :pinnedstate4) AND d.id > :discid2)
2843                          OR (d.pinned = 1 AND d.pinned <> :pinnedstate2))
2844                    ORDER BY CASE WHEN d.pinned = :pinnedstate3 THEN 1 ELSE 0 END DESC, $orderbyasc, d.id ASC";
2846     $neighbours['prev'] = $DB->get_record_sql($prevsql, $params, IGNORE_MULTIPLE);
2847     $neighbours['next'] = $DB->get_record_sql($nextsql, $params, IGNORE_MULTIPLE);
2848     return $neighbours;
2851 /**
2852  * Get the sql to use in the ORDER BY clause for forum discussions.
2853  *
2854  * This has the ordering take timed discussion windows into account.
2855  *
2856  * @param bool $desc True for DESC, False for ASC.
2857  * @param string $compare The field in the SQL to compare to normally sort by.
2858  * @param string $prefix The prefix being used for the discussion table.
2859  * @param bool $pinned sort pinned posts to the top
2860  * @return string
2861  */
2862 function forum_get_default_sort_order($desc = true, $compare = 'd.timemodified', $prefix = 'd', $pinned = true) {
2863     global $CFG;
2865     if (!empty($prefix)) {
2866         $prefix .= '.';
2867     }
2869     $dir = $desc ? 'DESC' : 'ASC';
2871     if ($pinned == true) {
2872         $pinned = "{$prefix}pinned DESC,";
2873     } else {
2874         $pinned = '';
2875     }
2877     $sort = "{$prefix}timemodified";
2878     if (!empty($CFG->forum_enabletimedposts)) {
2879         $sort = "CASE WHEN {$compare} < {$prefix}timestart
2880                  THEN {$prefix}timestart
2881                  ELSE {$compare}
2882                  END";
2883     }
2884     return "$pinned $sort $dir";
2887 /**
2888  *
2889  * @global object
2890  * @global object
2891  * @global object
2892  * @uses CONTEXT_MODULE
2893  * @uses VISIBLEGROUPS
2894  * @param object $cm
2895  * @return array
2896  */
2897 function forum_get_discussions_unread($cm) {
2898     global $CFG, $DB, $USER;
2900     $now = floor(time() / 60) * 60;
2901     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2903     $params = array();
2904     $groupmode    = groups_get_activity_groupmode($cm);
2905     $currentgroup = groups_get_activity_group($cm);
2907     if ($groupmode) {
2908         $modcontext = context_module::instance($cm->id);
2910         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2911             if ($currentgroup) {
2912                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2913                 $params['currentgroup'] = $currentgroup;
2914             } else {
2915                 $groupselect = "";
2916             }
2918         } else {
2919             //separate groups without access all
2920             if ($currentgroup) {
2921                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2922                 $params['currentgroup'] = $currentgroup;
2923             } else {
2924                 $groupselect = "AND d.groupid = -1";
2925             }
2926         }
2927     } else {
2928         $groupselect = "";
2929     }
2931     if (!empty($CFG->forum_enabletimedposts)) {
2932         $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
2933         $params['now1'] = $now;
2934         $params['now2'] = $now;
2935     } else {
2936         $timedsql = "";
2937     }
2939     $sql = "SELECT d.id, COUNT(p.id) AS unread
2940               FROM {forum_discussions} d
2941                    JOIN {forum_posts} p     ON p.discussion = d.id
2942                    LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
2943              WHERE d.forum = {$cm->instance}
2944                    AND p.modified >= :cutoffdate AND r.id is NULL
2945                    $groupselect
2946                    $timedsql
2947           GROUP BY d.id";
2948     $params['cutoffdate'] = $cutoffdate;
2950     if ($unreads = $DB->get_records_sql($sql, $params)) {
2951         foreach ($unreads as $unread) {
2952             $unreads[$unread->id] = $unread->unread;
2953         }
2954         return $unreads;
2955     } else {
2956         return array();
2957     }
2960 /**
2961  * @global object
2962  * @global object
2963  * @global object
2964  * @uses CONEXT_MODULE
2965  * @uses VISIBLEGROUPS
2966  * @param object $cm
2967  * @return array
2968  */
2969 function forum_get_discussions_count($cm) {
2970     global $CFG, $DB, $USER;
2972     $now = floor(time() / 60) * 60;
2973     $params = array($cm->instance);
2974     $groupmode    = groups_get_activity_groupmode($cm);
2975     $currentgroup = groups_get_activity_group($cm);
2977     if ($groupmode) {
2978         $modcontext = context_module::instance($cm->id);
2980         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2981             if ($currentgroup) {
2982                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2983                 $params[] = $currentgroup;
2984             } else {
2985                 $groupselect = "";
2986             }
2988         } else {
2989             //seprate groups without access all
2990             if ($currentgroup) {
2991                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2992                 $params[] = $currentgroup;
2993             } else {
2994                 $groupselect = "AND d.groupid = -1";
2995             }
2996         }
2997     } else {
2998         $groupselect = "";
2999     }
3001     $timelimit = "";
3003     if (!empty($CFG->forum_enabletimedposts)) {
3005         $modcontext = context_module::instance($cm->id);
3007         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
3008             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
3009             $params[] = $now;
3010             $params[] = $now;
3011             if (isloggedin()) {
3012                 $timelimit .= " OR d.userid = ?";
3013                 $params[] = $USER->id;
3014             }
3015             $timelimit .= ")";
3016         }
3017     }
3019     $sql = "SELECT COUNT(d.id)
3020               FROM {forum_discussions} d
3021                    JOIN {forum_posts} p ON p.discussion = d.id
3022              WHERE d.forum = ? AND p.parent = 0
3023                    $groupselect $timelimit";
3025     return $DB->get_field_sql($sql, $params);
3029 // OTHER FUNCTIONS ///////////////////////////////////////////////////////////
3032 /**
3033  * @global object
3034  * @global object
3035  * @param int $courseid
3036  * @param string $type
3037  */
3038 function forum_get_course_forum($courseid, $type) {
3039 // How to set up special 1-per-course forums
3040     global $CFG, $DB, $OUTPUT, $USER;
3042     if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
3043         // There should always only be ONE, but with the right combination of
3044         // errors there might be more.  In this case, just return the oldest one (lowest ID).
3045         foreach ($forums as $forum) {
3046             return $forum;   // ie the first one
3047         }
3048     }
3050     // Doesn't exist, so create one now.
3051     $forum = new stdClass();
3052     $forum->course = $courseid;
3053     $forum->type = "$type";
3054     if (!empty($USER->htmleditor)) {
3055         $forum->introformat = $USER->htmleditor;
3056     }
3057     switch ($forum->type) {
3058         case "news":
3059             $forum->name  = get_string("namenews", "forum");
3060             $forum->intro = get_string("intronews", "forum");
3061             $forum->introformat = FORMAT_HTML;
3062             $forum->forcesubscribe = FORUM_FORCESUBSCRIBE;
3063             $forum->assessed = 0;
3064             if ($courseid == SITEID) {
3065                 $forum->name  = get_string("sitenews");
3066                 $forum->forcesubscribe = 0;
3067             }
3068             break;
3069         case "social":
3070             $forum->name  = get_string("namesocial", "forum");
3071             $forum->intro = get_string("introsocial", "forum");
3072             $forum->introformat = FORMAT_HTML;
3073             $forum->assessed = 0;
3074             $forum->forcesubscribe = 0;
3075             break;
3076         case "blog":
3077             $forum->name = get_string('blogforum', 'forum');
3078             $forum->intro = get_string('introblog', 'forum');
3079             $forum->introformat = FORMAT_HTML;
3080             $forum->assessed = 0;
3081             $forum->forcesubscribe = 0;
3082             break;
3083         default:
3084             echo $OUTPUT->notification("That forum type doesn't exist!");
3085             return false;
3086             break;
3087     }
3089     $forum->timemodified = time();
3090     $forum->id = $DB->insert_record("forum", $forum);
3092     if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
3093         echo $OUTPUT->notification("Could not find forum module!!");
3094         return false;
3095     }
3096     $mod = new stdClass();
3097     $mod->course = $courseid;
3098     $mod->module = $module->id;
3099     $mod->instance = $forum->id;
3100     $mod->section = 0;
3101     include_once("$CFG->dirroot/course/lib.php");
3102     if (! $mod->coursemodule = add_course_module($mod) ) {
3103         echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
3104         return false;
3105     }
3106     $sectionid = course_add_cm_to_section($courseid, $mod->coursemodule, 0);
3107     return $DB->get_record("forum", array("id" => "$forum->id"));
3110 /**
3111  * Return a static array of posts that are open.
3112  *
3113  * @return array
3114  */
3115 function forum_post_nesting_cache() {
3116     static $nesting = array();
3117     return $nesting;
3120 /**
3121  * Return true for the first time this post was started
3122  *
3123  * @param int $id The id of the post to start
3124  * @return bool
3125  */
3126 function forum_should_start_post_nesting($id) {
3127     $cache = forum_post_nesting_cache();
3128     if (!array_key_exists($id, $cache)) {
3129         $cache[$id] = 1;
3130         return true;
3131     } else {
3132         $cache[$id]++;
3133         return false;
3134     }
3137 /**
3138  * Return true when all the opens are nested with a close.
3139  *
3140  * @param int $id The id of the post to end
3141  * @return bool
3142  */
3143 function forum_should_end_post_nesting($id) {
3144     $cache = forum_post_nesting_cache();
3145     if (!array_key_exists($id, $cache)) {
3146         return true;
3147     } else {
3148         $cache[$id]--;
3149         if ($cache[$id] == 0) {
3150             unset($cache[$id]);
3151             return true;
3152         }
3153     }
3154     return false;
3157 /**
3158  * Start a forum post container
3159  *
3160  * @param object $post The post to print.
3161  * @param bool $return Return the string or print it
3162  * @return string
3163  */
3164 function forum_print_post_start($post, $return = false) {
3165     $output = '';
3167     if (forum_should_start_post_nesting($post->id)) {
3168         $attributes = [
3169             'id' => 'p'.$post->id,
3170             'tabindex' => -1,
3171             'class' => 'relativelink'
3172         ];
3173         $output .= html_writer::start_tag('article', $attributes);
3174     }
3175     if ($return) {
3176         return $output;
3177     }
3178     echo $output;
3179     return;
3182 /**
3183  * End a forum post container
3184  *
3185  * @param object $post The post to print.
3186  * @param bool $return Return the string or print it
3187  * @return string
3188  */
3189 function forum_print_post_end($post, $return = false) {
3190     $output = '';
3192     if (forum_should_end_post_nesting($post->id)) {
3193         $output .= html_writer::end_tag('article');
3194     }
3195     if ($return) {
3196         return $output;
3197     }
3198     echo $output;
3199     return;
3202 /**
3203  * Print a forum post
3204  * This function should always be surrounded with calls to forum_print_post_start
3205  * and forum_print_post_end to create the surrounding container for the post.
3206  * Replies can be nested before forum_print_post_end and should reflect the structure of
3207  * thread.
3208  *
3209  * @global object
3210  * @global object
3211  * @uses FORUM_MODE_THREADED
3212  * @uses PORTFOLIO_FORMAT_PLAINHTML
3213  * @uses PORTFOLIO_FORMAT_FILE
3214  * @uses PORTFOLIO_FORMAT_RICHHTML
3215  * @uses PORTFOLIO_ADD_TEXT_LINK
3216  * @uses CONTEXT_MODULE
3217  * @param object $post The post to print.
3218  * @param object $discussion
3219  * @param object $forum
3220  * @param object $cm
3221  * @param object $course
3222  * @param boolean $ownpost Whether this post belongs to the current user.
3223  * @param boolean $reply Whether to print a 'reply' link at the bottom of the message.
3224  * @param boolean $link Just print a shortened version of the post as a link to the full post.
3225  * @param string $footer Extra stuff to print after the message.
3226  * @param string $highlight Space-separated list of terms to highlight.
3227  * @param int $post_read true, false or -99. If we already know whether this user
3228  *          has read this post, pass that in, otherwise, pass in -99, and this
3229  *          function will work it out.
3230  * @param boolean $dummyifcantsee When forum_user_can_see_post says that
3231  *          the current user can't see this post, if this argument is true
3232  *          (the default) then print a dummy 'you can't see this post' post.
3233  *          If false, don't output anything at all.
3234  * @param bool|null $istracked
3235  * @return void
3236  */
3237 function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=false, $reply=false, $link=false,
3238                           $footer="", $highlight="", $postisread=null, $dummyifcantsee=true, $istracked=null, $return=false) {
3239     global $USER, $CFG, $OUTPUT;
3241     require_once($CFG->libdir . '/filelib.php');
3243     // String cache
3244     static $str;
3245     // This is an extremely hacky way to ensure we only print the 'unread' anchor
3246     // the first time we encounter an unread post on a page. Ideally this would
3247     // be moved into the caller somehow, and be better testable. But at the time
3248     // of dealing with this bug, this static workaround was the most surgical and
3249     // it fits together with only printing th unread anchor id once on a given page.
3250     static $firstunreadanchorprinted = false;
3252     $modcontext = context_module::instance($cm->id);
3254     $post->course = $course->id;
3255     $post->forum  = $forum->id;
3256     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3257     if (!empty($CFG->enableplagiarism)) {
3258         require_once($CFG->libdir.'/plagiarismlib.php');
3259         $post->message .= plagiarism_get_links(array('userid' => $post->userid,
3260             'content' => $post->message,
3261             'cmid' => $cm->id,
3262             'course' => $post->course,
3263             'forum' => $post->forum));
3264     }
3266     // caching
3267     if (!isset($cm->cache)) {
3268         $cm->cache = new stdClass;
3269     }
3271     if (!isset($cm->cache->caps)) {
3272         $cm->cache->caps = array();
3273         $cm->cache->caps['mod/forum:viewdiscussion']   = has_capability('mod/forum:viewdiscussion', $modcontext);
3274         $cm->cache->caps['moodle/site:viewfullnames']  = has_capability('moodle/site:viewfullnames', $modcontext);
3275         $cm->cache->caps['mod/forum:editanypost']      = has_capability('mod/forum:editanypost', $modcontext);
3276         $cm->cache->caps['mod/forum:splitdiscussions'] = has_capability('mod/forum:splitdiscussions', $modcontext);
3277         $cm->cache->caps['mod/forum:deleteownpost']    = has_capability('mod/forum:deleteownpost', $modcontext);
3278         $cm->cache->caps['mod/forum:deleteanypost']    = has_capability('mod/forum:deleteanypost', $modcontext);
3279         $cm->cache->caps['mod/forum:viewanyrating']    = has_capability('mod/forum:viewanyrating', $modcontext);
3280         $cm->cache->caps['mod/forum:exportpost']       = has_capability('mod/forum:exportpost', $modcontext);
3281         $cm->cache->caps['mod/forum:exportownpost']    = has_capability('mod/forum:exportownpost', $modcontext);
3282     }
3284     if (!isset($cm->uservisible)) {
3285         $cm->uservisible = \core_availability\info_module::is_user_visible($cm, 0, false);
3286     }
3288     if ($istracked && is_null($postisread)) {
3289         $postisread = forum_tp_is_post_read($USER->id, $post);
3290     }
3292     if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm, false)) {
3293         // Do _not_ check the deleted flag - we need to display a different UI.
3294         $output = '';
3295         if (!$dummyifcantsee) {
3296             if ($return) {
3297                 return $output;
3298             }
3299             echo $output;
3300             return;
3301         }
3303         $output .= html_writer::start_tag('div', array('class' => 'forumpost clearfix',
3304                                                        'aria-label' => get_string('hiddenforumpost', 'forum')));
3305         $output .= html_writer::start_tag('header', array('class' => 'row header'));
3306         $output .= html_writer::tag('div', '', array('class' => 'left picture', 'role' => 'presentation')); // Picture.
3307         if ($post->parent) {
3308             $output .= html_writer::start_tag('div', array('class' => 'topic'));
3309         } else {
3310             $output .= html_writer::start_tag('div', array('class' => 'topic starter'));
3311         }
3312         $output .= html_writer::tag('div', get_string('forumsubjecthidden','forum'), array('class' => 'subject',
3313                                                                                            'role' => 'header',
3314                                                                                            'id' => ('headp' . $post->id))); // Subject.
3315         $authorclasses = array('class' => 'author');
3316         $output .= html_writer::tag('address', get_string('forumauthorhidden', 'forum'), $authorclasses); // Author.
3317         $output .= html_writer::end_tag('div');
3318         $output .= html_writer::end_tag('header'); // Header.
3319         $output .= html_writer::start_tag('div', array('class'=>'row'));
3320         $output .= html_writer::tag('div', '&nbsp;', array('class'=>'left side')); // Groups
3321         $output .= html_writer::tag('div', get_string('forumbodyhidden','forum'), array('class'=>'content')); // Content
3322         $output .= html_writer::end_tag('div'); // row
3323         $output .= html_writer::end_tag('div'); // forumpost
3325         if ($return) {
3326             return $output;
3327         }
3328         echo $output;
3329         return;
3330     }
3332     if (!empty($post->deleted)) {
3333         // Note: Posts marked as deleted are still returned by the above forum_user_can_post because it is required for
3334         // nesting of posts.
3335         $output = '';
3336         if (!$dummyifcantsee) {
3337             if ($return) {
3338                 return $output;
3339             }
3340             echo $output;
3341             return;
3342         }
3343         $output .= html_writer::start_tag('div', [
3344                 'class' => 'forumpost clearfix',
3345                 'aria-label' => get_string('forumbodydeleted', 'forum'),
3346             ]);
3348         $output .= html_writer::start_tag('header', array('class' => 'row header'));
3349         $output .= html_writer::tag('div', '', array('class' => 'left picture', 'role' => 'presentation'));
3351         $classes = ['topic'];
3352         if (!empty($post->parent)) {
3353             $classes[] = 'starter';
3354         }
3355         $output .= html_writer::start_tag('div', ['class' => implode(' ', $classes)]);
3357         // Subject.
3358         $output .= html_writer::tag('div', get_string('forumsubjectdeleted', 'forum'), [
3359                 'class' => 'subject',
3360                 'role' => 'header',
3361                 'id' => ('headp' . $post->id)
3362             ]);
3364         // Author.
3365         $output .= html_writer::tag('address', '', ['class' => 'author']);
3367         $output .= html_writer::end_tag('div');
3368         $output .= html_writer::end_tag('header'); // End header.
3369         $output .= html_writer::start_tag('div', ['class' => 'row']);
3370         $output .= html_writer::tag('div', '&nbsp;', ['class' => 'left side']); // Groups.
3371         $output .= html_writer::tag('div', get_string('forumbodydeleted', 'forum'), ['class' => 'content']); // Content.
3372         $output .= html_writer::end_tag('div'); // End row.
3373         $output .= html_writer::end_tag('div'); // End forumpost.
3375         if ($return) {
3376             return $output;
3377         }
3378         echo $output;
3379         return;
3380     }
3382     if (empty($str)) {
3383         $str = new stdClass;
3384         $str->edit         = get_string('edit', 'forum');
3385         $str->delete       = get_string('delete', 'forum');
3386         $str->reply        = get_string('reply', 'forum');
3387         $str->parent       = get_string('parent', 'forum');
3388         $str->pruneheading = get_string('pruneheading', 'forum');
3389         $str->prune        = get_string('prune', 'forum');
3390         $str->displaymode     = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
3391         $str->markread     = get_string('markread', 'forum');
3392         $str->markunread   = get_string('markunread', 'forum');
3393     }
3395     $discussionlink = new moodle_url('/mod/forum/discuss.php', array('d'=>$post->discussion));
3397     // Build an object that represents the posting user
3398     $postuser = new stdClass;
3399     $postuserfields = explode(',', user_picture::fields());
3400     $postuser = username_load_fields_from_object($postuser, $post, null, $postuserfields);
3401     $postuser->id = $post->userid;
3402     $postuser->fullname    = fullname($postuser, $cm->cache->caps['moodle/site:viewfullnames']);
3403     $postuser->profilelink = new moodle_url('/user/view.php', array('id'=>$post->userid, 'course'=>$course->id));
3405     // Prepare the groups the posting user belongs to
3406     if (isset($cm->cache->usersgroups)) {
3407         $groups = array();
3408         if (isset($cm->cache->usersgroups[$post->userid])) {
3409             foreach ($cm->cache->usersgroups[$post->userid] as $gid) {
3410                 $groups[$gid] = $cm->cache->groups[$gid];
3411             }
3412         }
3413     } else {
3414         $groups = groups_get_all_groups($course->id, $post->userid, $cm->groupingid);
3415     }
3417     // Prepare the attachements for the post, files then images
3418     list($attachments, $attachedimages) = forum_print_attachments($post, $cm, 'separateimages');
3420     // Determine if we need to shorten this post
3421     $shortenpost = ($link && (strlen(strip_tags($post->message)) > $CFG->forum_longpost));
3423     // Prepare an array of commands
3424     $commands = array();
3426     // Add a permalink.
3427     $permalink = new moodle_url($discussionlink);
3428     $permalink->set_anchor('p' . $post->id);
3429     $commands[] = array('url' => $permalink, 'text' => get_string('permalink', 'forum'), 'attributes' => ['rel' => 'bookmark']);
3431     // SPECIAL CASE: The front page can display a news item post to non-logged in users.
3432     // Don't display the mark read / unread controls in this case.
3433     if ($istracked && $CFG->forum_usermarksread && isloggedin()) {
3434         $url = new moodle_url($discussionlink, array('postid'=>$post->id, 'mark'=>'unread'));
3435         $text = $str->markunread;
3436         if (!$postisread) {
3437             $url->param('mark', 'read');
3438             $text = $str->markread;
3439         }
3440         if ($str->displaymode == FORUM_MODE_THREADED) {
3441             $url->param('parent', $post->parent);
3442         } else {
3443       &nb