d3b609e6588daf3bac95d0f0bdb1c08bae793c69
[moodle.git] / mod / forum / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * @package    mod
19  * @subpackage forum
20  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 defined('MOODLE_INTERNAL') || die();
26 /** Include required files */
27 require_once($CFG->libdir.'/filelib.php');
28 require_once($CFG->libdir.'/eventslib.php');
29 require_once($CFG->dirroot.'/user/selector/lib.php');
31 /// CONSTANTS ///////////////////////////////////////////////////////////
33 define('FORUM_MODE_FLATOLDEST', 1);
34 define('FORUM_MODE_FLATNEWEST', -1);
35 define('FORUM_MODE_THREADED', 2);
36 define('FORUM_MODE_NESTED', 3);
38 define('FORUM_CHOOSESUBSCRIBE', 0);
39 define('FORUM_FORCESUBSCRIBE', 1);
40 define('FORUM_INITIALSUBSCRIBE', 2);
41 define('FORUM_DISALLOWSUBSCRIBE',3);
43 define('FORUM_TRACKING_OFF', 0);
44 define('FORUM_TRACKING_OPTIONAL', 1);
45 define('FORUM_TRACKING_ON', 2);
47 /// STANDARD FUNCTIONS ///////////////////////////////////////////////////////////
49 /**
50  * Given an object containing all the necessary data,
51  * (defined by the form in mod_form.php) this function
52  * will create a new instance and return the id number
53  * of the new instance.
54  *
55  * @param stdClass $forum add forum instance
56  * @param mod_forum_mod_form $mform
57  * @return int intance id
58  */
59 function forum_add_instance($forum, $mform = null) {
60     global $CFG, $DB;
62     $forum->timemodified = time();
64     if (empty($forum->assessed)) {
65         $forum->assessed = 0;
66     }
68     if (empty($forum->ratingtime) or empty($forum->assessed)) {
69         $forum->assesstimestart  = 0;
70         $forum->assesstimefinish = 0;
71     }
73     $forum->id = $DB->insert_record('forum', $forum);
74     $modcontext = get_context_instance(CONTEXT_MODULE, $forum->coursemodule);
76     if ($forum->type == 'single') {  // Create related discussion.
77         $discussion = new stdClass();
78         $discussion->course        = $forum->course;
79         $discussion->forum         = $forum->id;
80         $discussion->name          = $forum->name;
81         $discussion->assessed      = $forum->assessed;
82         $discussion->message       = $forum->intro;
83         $discussion->messageformat = $forum->introformat;
84         $discussion->messagetrust  = trusttext_trusted(get_context_instance(CONTEXT_COURSE, $forum->course));
85         $discussion->mailnow       = false;
86         $discussion->groupid       = -1;
88         $message = '';
90         $discussion->id = forum_add_discussion($discussion, null, $message);
92         if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
93             // ugly hack - we need to copy the files somehow
94             $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST);
95             $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
97             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, array('subdirs'=>true), $post->message);
98             $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
99         }
100     }
102     if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) {
103     /// all users should be subscribed initially
104     /// Note: forum_get_potential_subscribers should take the forum context,
105     /// but that does not exist yet, becuase the forum is only half build at this
106     /// stage. However, because the forum is brand new, we know that there are
107     /// no role assignments or overrides in the forum context, so using the
108     /// course context gives the same list of users.
109         $users = forum_get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
110         foreach ($users as $user) {
111             forum_subscribe($user->id, $forum->id);
112         }
113     }
115     forum_grade_item_update($forum);
117     return $forum->id;
121 /**
122  * Given an object containing all the necessary data,
123  * (defined by the form in mod_form.php) this function
124  * will update an existing instance with new data.
125  *
126  * @global object
127  * @param object $forum forum instance (with magic quotes)
128  * @return bool success
129  */
130 function forum_update_instance($forum, $mform) {
131     global $DB, $OUTPUT, $USER;
133     $forum->timemodified = time();
134     $forum->id           = $forum->instance;
136     if (empty($forum->assessed)) {
137         $forum->assessed = 0;
138     }
140     if (empty($forum->ratingtime) or empty($forum->assessed)) {
141         $forum->assesstimestart  = 0;
142         $forum->assesstimefinish = 0;
143     }
145     $oldforum = $DB->get_record('forum', array('id'=>$forum->id));
147     // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum
148     // if  scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
149     // 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
150     if (($oldforum->assessed<>$forum->assessed) or ($oldforum->scale<>$forum->scale)) {
151         forum_update_grades($forum); // recalculate grades for the forum
152     }
154     if ($forum->type == 'single') {  // Update related discussion and post.
155         $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC');
156         if (!empty($discussions)) {
157             if (count($discussions) > 1) {
158                 echo $OUTPUT->notification(get_string('warnformorepost', 'forum'));
159             }
160             $discussion = array_pop($discussions);
161         } else {
162             // try to recover by creating initial discussion - MDL-16262
163             $discussion = new stdClass();
164             $discussion->course          = $forum->course;
165             $discussion->forum           = $forum->id;
166             $discussion->name            = $forum->name;
167             $discussion->assessed        = $forum->assessed;
168             $discussion->message         = $forum->intro;
169             $discussion->messageformat   = $forum->introformat;
170             $discussion->messagetrust    = true;
171             $discussion->mailnow         = false;
172             $discussion->groupid         = -1;
174             $message = '';
176             forum_add_discussion($discussion, null, $message);
178             if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) {
179                 print_error('cannotadd', 'forum');
180             }
181         }
182         if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) {
183             print_error('cannotfindfirstpost', 'forum');
184         }
186         $cm         = get_coursemodule_from_instance('forum', $forum->id);
187         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id, MUST_EXIST);
189         if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
190             // ugly hack - we need to copy the files somehow
191             $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST);
192             $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
194             $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, array('subdirs'=>true), $post->message);
195         }
197         $post->subject       = $forum->name;
198         $post->message       = $forum->intro;
199         $post->messageformat = $forum->introformat;
200         $post->messagetrust  = trusttext_trusted($modcontext);
201         $post->modified      = $forum->timemodified;
202         $post->userid        = $USER->id;    // MDL-18599, so that current teacher can take ownership of activities
204         $DB->update_record('forum_posts', $post);
205         $discussion->name = $forum->name;
206         $DB->update_record('forum_discussions', $discussion);
207     }
209     $DB->update_record('forum', $forum);
211     forum_grade_item_update($forum);
213     return true;
217 /**
218  * Given an ID of an instance of this module,
219  * this function will permanently delete the instance
220  * and any data that depends on it.
221  *
222  * @global object
223  * @param int $id forum instance id
224  * @return bool success
225  */
226 function forum_delete_instance($id) {
227     global $DB;
229     if (!$forum = $DB->get_record('forum', array('id'=>$id))) {
230         return false;
231     }
232     if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
233         return false;
234     }
235     if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
236         return false;
237     }
239     $context = get_context_instance(CONTEXT_MODULE, $cm->id);
241     // now get rid of all files
242     $fs = get_file_storage();
243     $fs->delete_area_files($context->id);
245     $result = true;
247     if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) {
248         foreach ($discussions as $discussion) {
249             if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) {
250                 $result = false;
251             }
252         }
253     }
255     if (!$DB->delete_records('forum_subscriptions', array('forum'=>$forum->id))) {
256         $result = false;
257     }
259     forum_tp_delete_read_records(-1, -1, -1, $forum->id);
261     if (!$DB->delete_records('forum', array('id'=>$forum->id))) {
262         $result = false;
263     }
265     forum_grade_item_delete($forum);
267     return $result;
271 /**
272  * Indicates API features that the forum supports.
273  *
274  * @uses FEATURE_GROUPS
275  * @uses FEATURE_GROUPINGS
276  * @uses FEATURE_GROUPMEMBERSONLY
277  * @uses FEATURE_MOD_INTRO
278  * @uses FEATURE_COMPLETION_TRACKS_VIEWS
279  * @uses FEATURE_COMPLETION_HAS_RULES
280  * @uses FEATURE_GRADE_HAS_GRADE
281  * @uses FEATURE_GRADE_OUTCOMES
282  * @param string $feature
283  * @return mixed True if yes (some features may use other values)
284  */
285 function forum_supports($feature) {
286     switch($feature) {
287         case FEATURE_GROUPS:                  return true;
288         case FEATURE_GROUPINGS:               return true;
289         case FEATURE_GROUPMEMBERSONLY:        return true;
290         case FEATURE_MOD_INTRO:               return true;
291         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
292         case FEATURE_COMPLETION_HAS_RULES:    return true;
293         case FEATURE_GRADE_HAS_GRADE:         return true;
294         case FEATURE_GRADE_OUTCOMES:          return true;
295         case FEATURE_RATE:                    return true;
296         case FEATURE_BACKUP_MOODLE2:          return true;
297         case FEATURE_SHOW_DESCRIPTION:        return true;
299         default: return null;
300     }
304 /**
305  * Obtains the automatic completion state for this forum based on any conditions
306  * in forum settings.
307  *
308  * @global object
309  * @global object
310  * @param object $course Course
311  * @param object $cm Course-module
312  * @param int $userid User ID
313  * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
314  * @return bool True if completed, false if not. (If no conditions, then return
315  *   value depends on comparison type)
316  */
317 function forum_get_completion_state($course,$cm,$userid,$type) {
318     global $CFG,$DB;
320     // Get forum details
321     if (!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) {
322         throw new Exception("Can't find forum {$cm->instance}");
323     }
325     $result=$type; // Default return value
327     $postcountparams=array('userid'=>$userid,'forumid'=>$forum->id);
328     $postcountsql="
329 SELECT
330     COUNT(1)
331 FROM
332     {forum_posts} fp
333     INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
334 WHERE
335     fp.userid=:userid AND fd.forum=:forumid";
337     if ($forum->completiondiscussions) {
338         $value = $forum->completiondiscussions <=
339                  $DB->count_records('forum_discussions',array('forum'=>$forum->id,'userid'=>$userid));
340         if ($type == COMPLETION_AND) {
341             $result = $result && $value;
342         } else {
343             $result = $result || $value;
344         }
345     }
346     if ($forum->completionreplies) {
347         $value = $forum->completionreplies <=
348                  $DB->get_field_sql( $postcountsql.' AND fp.parent<>0',$postcountparams);
349         if ($type==COMPLETION_AND) {
350             $result = $result && $value;
351         } else {
352             $result = $result || $value;
353         }
354     }
355     if ($forum->completionposts) {
356         $value = $forum->completionposts <= $DB->get_field_sql($postcountsql,$postcountparams);
357         if ($type == COMPLETION_AND) {
358             $result = $result && $value;
359         } else {
360             $result = $result || $value;
361         }
362     }
364     return $result;
367 /**
368  * Create a message-id string to use in the custom headers of forum notification emails
369  *
370  * message-id is used by email clients to identify emails and to nest conversations
371  *
372  * @param int $postid The ID of the forum post we are notifying the user about
373  * @param int $usertoid The ID of the user being notified
374  * @param string $hostname The server's hostname
375  * @return string A unique message-id
376  */
377 function forum_get_email_message_id($postid, $usertoid, $hostname) {
378     return '<'.hash('sha256',$postid.'to'.$usertoid.'@'.$hostname).'>';
381 /**
382  * Function to be run periodically according to the moodle cron
383  * Finds all posts that have yet to be mailed out, and mails them
384  * out to all subscribers
385  *
386  * @global object
387  * @global object
388  * @global object
389  * @uses CONTEXT_MODULE
390  * @uses CONTEXT_COURSE
391  * @uses SITEID
392  * @uses FORMAT_PLAIN
393  * @return void
394  */
395 function forum_cron() {
396     global $CFG, $USER, $DB;
398     $site = get_site();
400     // all users that are subscribed to any post that needs sending
401     $users = array();
403     // status arrays
404     $mailcount  = array();
405     $errorcount = array();
407     // caches
408     $discussions     = array();
409     $forums          = array();
410     $courses         = array();
411     $coursemodules   = array();
412     $subscribedusers = array();
415     // Posts older than 2 days will not be mailed.  This is to avoid the problem where
416     // cron has not been running for a long time, and then suddenly people are flooded
417     // with mail from the past few weeks or months
418     $timenow   = time();
419     $endtime   = $timenow - $CFG->maxeditingtime;
420     $starttime = $endtime - 48 * 3600;   // Two days earlier
422     if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) {
423         // Mark them all now as being mailed.  It's unlikely but possible there
424         // might be an error later so that a post is NOT actually mailed out,
425         // but since mail isn't crucial, we can accept this risk.  Doing it now
426         // prevents the risk of duplicated mails, which is a worse problem.
428         if (!forum_mark_old_posts_as_mailed($endtime)) {
429             mtrace('Errors occurred while trying to mark some posts as being mailed.');
430             return false;  // Don't continue trying to mail them, in case we are in a cron loop
431         }
433         // checking post validity, and adding users to loop through later
434         foreach ($posts as $pid => $post) {
436             $discussionid = $post->discussion;
437             if (!isset($discussions[$discussionid])) {
438                 if ($discussion = $DB->get_record('forum_discussions', array('id'=> $post->discussion))) {
439                     $discussions[$discussionid] = $discussion;
440                 } else {
441                     mtrace('Could not find discussion '.$discussionid);
442                     unset($posts[$pid]);
443                     continue;
444                 }
445             }
446             $forumid = $discussions[$discussionid]->forum;
447             if (!isset($forums[$forumid])) {
448                 if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
449                     $forums[$forumid] = $forum;
450                 } else {
451                     mtrace('Could not find forum '.$forumid);
452                     unset($posts[$pid]);
453                     continue;
454                 }
455             }
456             $courseid = $forums[$forumid]->course;
457             if (!isset($courses[$courseid])) {
458                 if ($course = $DB->get_record('course', array('id' => $courseid))) {
459                     $courses[$courseid] = $course;
460                 } else {
461                     mtrace('Could not find course '.$courseid);
462                     unset($posts[$pid]);
463                     continue;
464                 }
465             }
466             if (!isset($coursemodules[$forumid])) {
467                 if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
468                     $coursemodules[$forumid] = $cm;
469                 } else {
470                     mtrace('Could not find course module for forum '.$forumid);
471                     unset($posts[$pid]);
472                     continue;
473                 }
474             }
477             // caching subscribed users of each forum
478             if (!isset($subscribedusers[$forumid])) {
479                 $modcontext = get_context_instance(CONTEXT_MODULE, $coursemodules[$forumid]->id);
480                 if ($subusers = forum_subscribed_users($courses[$courseid], $forums[$forumid], 0, $modcontext, "u.*")) {
481                     foreach ($subusers as $postuser) {
482                         unset($postuser->description); // not necessary
483                         // this user is subscribed to this forum
484                         $subscribedusers[$forumid][$postuser->id] = $postuser->id;
485                         // this user is a user we have to process later
486                         $users[$postuser->id] = $postuser;
487                     }
488                     unset($subusers); // release memory
489                 }
490             }
492             $mailcount[$pid] = 0;
493             $errorcount[$pid] = 0;
494         }
495     }
497     if ($users && $posts) {
499         $urlinfo = parse_url($CFG->wwwroot);
500         $hostname = $urlinfo['host'];
502         foreach ($users as $userto) {
504             @set_time_limit(120); // terminate if processing of any account takes longer than 2 minutes
506             // set this so that the capabilities are cached, and environment matches receiving user
507             cron_setup_user($userto);
509             mtrace('Processing user '.$userto->id);
511             // init caches
512             $userto->viewfullnames = array();
513             $userto->canpost       = array();
514             $userto->markposts     = array();
516             // reset the caches
517             foreach ($coursemodules as $forumid=>$unused) {
518                 $coursemodules[$forumid]->cache       = new stdClass();
519                 $coursemodules[$forumid]->cache->caps = array();
520                 unset($coursemodules[$forumid]->uservisible);
521             }
523             foreach ($posts as $pid => $post) {
525                 // Set up the environment for the post, discussion, forum, course
526                 $discussion = $discussions[$post->discussion];
527                 $forum      = $forums[$discussion->forum];
528                 $course     = $courses[$forum->course];
529                 $cm         =& $coursemodules[$forum->id];
531                 // Do some checks  to see if we can bail out now
532                 // Only active enrolled users are in the list of subscribers
533                 if (!isset($subscribedusers[$forum->id][$userto->id])) {
534                     continue; // user does not subscribe to this forum
535                 }
537                 // Don't send email if the forum is Q&A and the user has not posted
538                 // Initial topics are still mailed
539                 if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
540                     mtrace('Did not email '.$userto->id.' because user has not posted in discussion');
541                     continue;
542                 }
544                 // Get info about the sending user
545                 if (array_key_exists($post->userid, $users)) { // we might know him/her already
546                     $userfrom = $users[$post->userid];
547                 } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
548                     unset($userfrom->description); // not necessary
549                     $users[$userfrom->id] = $userfrom; // fetch only once, we can add it to user list, it will be skipped anyway
550                 } else {
551                     mtrace('Could not find user '.$post->userid);
552                     continue;
553                 }
555                 //if we want to check that userto and userfrom are not the same person this is probably the spot to do it
557                 // setup global $COURSE properly - needed for roles and languages
558                 cron_setup_user($userto, $course);
560                 // Fill caches
561                 if (!isset($userto->viewfullnames[$forum->id])) {
562                     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
563                     $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
564                 }
565                 if (!isset($userto->canpost[$discussion->id])) {
566                     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
567                     $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
568                 }
569                 if (!isset($userfrom->groups[$forum->id])) {
570                     if (!isset($userfrom->groups)) {
571                         $userfrom->groups = array();
572                         $users[$userfrom->id]->groups = array();
573                     }
574                     $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
575                     $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
576                 }
578                 // Make sure groups allow this user to see this email
579                 if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {   // Groups are being used
580                     if (!groups_group_exists($discussion->groupid)) { // Can't find group
581                         continue;                           // Be safe and don't send it to anyone
582                     }
584                     if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) {
585                         // do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS
586                         continue;
587                     }
588                 }
590                 // Make sure we're allowed to see it...
591                 if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
592                     mtrace('user '.$userto->id. ' can not see '.$post->id);
593                     continue;
594                 }
596                 // OK so we need to send the email.
598                 // Does the user want this post in a digest?  If so postpone it for now.
599                 if ($userto->maildigest > 0) {
600                     // This user wants the mails to be in digest form
601                     $queue = new stdClass();
602                     $queue->userid       = $userto->id;
603                     $queue->discussionid = $discussion->id;
604                     $queue->postid       = $post->id;
605                     $queue->timemodified = $post->created;
606                     $DB->insert_record('forum_queue', $queue);
607                     continue;
608                 }
611                 // Prepare to actually send the post now, and build up the content
613                 $cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
615                 $userfrom->customheaders = array (  // Headers to make emails easier to track
616                            'Precedence: Bulk',
617                            'List-Id: "'.$cleanforumname.'" <moodleforum'.$forum->id.'@'.$hostname.'>',
618                            'List-Help: '.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id,
619                            'Message-ID: '.forum_get_email_message_id($post->id, $userto->id, $hostname),
620                            'X-Course-Id: '.$course->id,
621                            'X-Course-Name: '.format_string($course->fullname, true)
622                 );
624                 if ($post->parent) {  // This post is a reply, so add headers for threading (see MDL-22551)
625                     $userfrom->customheaders[] = 'In-Reply-To: '.forum_get_email_message_id($post->parent, $userto->id, $hostname);
626                     $userfrom->customheaders[] = 'References: '.forum_get_email_message_id($post->parent, $userto->id, $hostname);
627                 }
629                 $shortname = format_string($course->shortname, true, array('context' => get_context_instance(CONTEXT_COURSE, $course->id)));
631                 $postsubject = html_to_text("$shortname: ".format_string($post->subject, true));
632                 $posttext = forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto);
633                 $posthtml = forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto);
635                 // Send the post now!
637                 mtrace('Sending ', '');
639                 $eventdata = new stdClass();
640                 $eventdata->component        = 'mod_forum';
641                 $eventdata->name             = 'posts';
642                 $eventdata->userfrom         = $userfrom;
643                 $eventdata->userto           = $userto;
644                 $eventdata->subject          = $postsubject;
645                 $eventdata->fullmessage      = $posttext;
646                 $eventdata->fullmessageformat = FORMAT_PLAIN;
647                 $eventdata->fullmessagehtml  = $posthtml;
648                 $eventdata->notification = 1;
650                 $smallmessagestrings = new stdClass();
651                 $smallmessagestrings->user = fullname($userfrom);
652                 $smallmessagestrings->forumname = "$shortname: ".format_string($forum->name,true).": ".$discussion->name;
653                 $smallmessagestrings->message = $post->message;
654                 //make sure strings are in message recipients language
655                 $eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'forum', $smallmessagestrings, $userto->lang);
657                 $eventdata->contexturl = "{$CFG->wwwroot}/mod/forum/discuss.php?d={$discussion->id}#p{$post->id}";
658                 $eventdata->contexturlname = $discussion->name;
660                 $mailresult = message_send($eventdata);
661                 if (!$mailresult){
662                     mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id $post->id to user $userto->id".
663                          " ($userto->email) .. not trying again.");
664                     add_to_log($course->id, 'forum', 'mail error', "discuss.php?d=$discussion->id#p$post->id",
665                                substr(format_string($post->subject,true),0,30), $cm->id, $userto->id);
666                     $errorcount[$post->id]++;
667                 } else {
668                     $mailcount[$post->id]++;
670                 // Mark post as read if forum_usermarksread is set off
671                     if (!$CFG->forum_usermarksread) {
672                         $userto->markposts[$post->id] = $post->id;
673                     }
674                 }
676                 mtrace('post '.$post->id. ': '.$post->subject);
677             }
679             // mark processed posts as read
680             forum_tp_mark_posts_read($userto, $userto->markposts);
681         }
682     }
684     if ($posts) {
685         foreach ($posts as $post) {
686             mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'");
687             if ($errorcount[$post->id]) {
688                 $DB->set_field("forum_posts", "mailed", "2", array("id" => "$post->id"));
689             }
690         }
691     }
693     // release some memory
694     unset($subscribedusers);
695     unset($mailcount);
696     unset($errorcount);
698     cron_setup_user();
700     $sitetimezone = $CFG->timezone;
702     // Now see if there are any digest mails waiting to be sent, and if we should send them
704     mtrace('Starting digest processing...');
706     @set_time_limit(300); // terminate if not able to fetch all digests in 5 minutes
708     if (!isset($CFG->digestmailtimelast)) {    // To catch the first time
709         set_config('digestmailtimelast', 0);
710     }
712     $timenow = time();
713     $digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
715     // Delete any really old ones (normally there shouldn't be any)
716     $weekago = $timenow - (7 * 24 * 3600);
717     $DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
718     mtrace ('Cleaned old digest records');
720     if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) {
722         mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone));
724         $digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime));
726         if ($digestposts_rs->valid()) {
728             // We have work to do
729             $usermailcount = 0;
731             //caches - reuse the those filled before too
732             $discussionposts = array();
733             $userdiscussions = array();
735             foreach ($digestposts_rs as $digestpost) {
736                 if (!isset($users[$digestpost->userid])) {
737                     if ($user = $DB->get_record('user', array('id' => $digestpost->userid))) {
738                         $users[$digestpost->userid] = $user;
739                     } else {
740                         continue;
741                     }
742                 }
743                 $postuser = $users[$digestpost->userid];
745                 if (!isset($posts[$digestpost->postid])) {
746                     if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) {
747                         $posts[$digestpost->postid] = $post;
748                     } else {
749                         continue;
750                     }
751                 }
752                 $discussionid = $digestpost->discussionid;
753                 if (!isset($discussions[$discussionid])) {
754                     if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) {
755                         $discussions[$discussionid] = $discussion;
756                     } else {
757                         continue;
758                     }
759                 }
760                 $forumid = $discussions[$discussionid]->forum;
761                 if (!isset($forums[$forumid])) {
762                     if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
763                         $forums[$forumid] = $forum;
764                     } else {
765                         continue;
766                     }
767                 }
769                 $courseid = $forums[$forumid]->course;
770                 if (!isset($courses[$courseid])) {
771                     if ($course = $DB->get_record('course', array('id' => $courseid))) {
772                         $courses[$courseid] = $course;
773                     } else {
774                         continue;
775                     }
776                 }
778                 if (!isset($coursemodules[$forumid])) {
779                     if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
780                         $coursemodules[$forumid] = $cm;
781                     } else {
782                         continue;
783                     }
784                 }
785                 $userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid;
786                 $discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid;
787             }
788             $digestposts_rs->close(); /// Finished iteration, let's close the resultset
790             // Data collected, start sending out emails to each user
791             foreach ($userdiscussions as $userid => $thesediscussions) {
793                 @set_time_limit(120); // terminate if processing of any account takes longer than 2 minutes
795                 cron_setup_user();
797                 mtrace(get_string('processingdigest', 'forum', $userid), '... ');
799                 // First of all delete all the queue entries for this user
800                 $DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime));
801                 $userto = $users[$userid];
803                 // Override the language and timezone of the "current" user, so that
804                 // mail is customised for the receiver.
805                 cron_setup_user($userto);
807                 // init caches
808                 $userto->viewfullnames = array();
809                 $userto->canpost       = array();
810                 $userto->markposts     = array();
812                 $postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
814                 $headerdata = new stdClass();
815                 $headerdata->sitename = format_string($site->fullname, true);
816                 $headerdata->userprefs = $CFG->wwwroot.'/user/edit.php?id='.$userid.'&amp;course='.$site->id;
818                 $posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
819                 $headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
821                 $posthtml = "<head>";
822 /*                foreach ($CFG->stylesheets as $stylesheet) {
823                     //TODO: MDL-21120
824                     $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
825                 }*/
826                 $posthtml .= "</head>\n<body id=\"email\">\n";
827                 $posthtml .= '<p>'.get_string('digestmailheader', 'forum', $headerdata).'</p><br /><hr size="1" noshade="noshade" />';
829                 foreach ($thesediscussions as $discussionid) {
831                     @set_time_limit(120);   // to be reset for each post
833                     $discussion = $discussions[$discussionid];
834                     $forum      = $forums[$discussion->forum];
835                     $course     = $courses[$forum->course];
836                     $cm         = $coursemodules[$forum->id];
838                     //override language
839                     cron_setup_user($userto, $course);
841                     // Fill caches
842                     if (!isset($userto->viewfullnames[$forum->id])) {
843                         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
844                         $userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
845                     }
846                     if (!isset($userto->canpost[$discussion->id])) {
847                         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
848                         $userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
849                     }
851                     $strforums      = get_string('forums', 'forum');
852                     $canunsubscribe = ! forum_is_forcesubscribed($forum);
853                     $canreply       = $userto->canpost[$discussion->id];
854                     $shortname = format_string($course->shortname, true, array('context' => get_context_instance(CONTEXT_COURSE, $course->id)));
856                     $posttext .= "\n \n";
857                     $posttext .= '=====================================================================';
858                     $posttext .= "\n \n";
859                     $posttext .= "$shortname -> $strforums -> ".format_string($forum->name,true);
860                     if ($discussion->name != $forum->name) {
861                         $posttext  .= " -> ".format_string($discussion->name,true);
862                     }
863                     $posttext .= "\n";
865                     $posthtml .= "<p><font face=\"sans-serif\">".
866                     "<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
867                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
868                     "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
869                     if ($discussion->name == $forum->name) {
870                         $posthtml .= "</font></p>";
871                     } else {
872                         $posthtml .= " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a></font></p>";
873                     }
874                     $posthtml .= '<p>';
876                     $postsarray = $discussionposts[$discussionid];
877                     sort($postsarray);
879                     foreach ($postsarray as $postid) {
880                         $post = $posts[$postid];
882                         if (array_key_exists($post->userid, $users)) { // we might know him/her already
883                             $userfrom = $users[$post->userid];
884                         } else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
885                             $users[$userfrom->id] = $userfrom; // fetch only once, we can add it to user list, it will be skipped anyway
886                         } else {
887                             mtrace('Could not find user '.$post->userid);
888                             continue;
889                         }
891                         if (!isset($userfrom->groups[$forum->id])) {
892                             if (!isset($userfrom->groups)) {
893                                 $userfrom->groups = array();
894                                 $users[$userfrom->id]->groups = array();
895                             }
896                             $userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
897                             $users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
898                         }
900                         $userfrom->customheaders = array ("Precedence: Bulk");
902                         if ($userto->maildigest == 2) {
903                             // Subjects only
904                             $by = new stdClass();
905                             $by->name = fullname($userfrom);
906                             $by->date = userdate($post->modified);
907                             $posttext .= "\n".format_string($post->subject,true).' '.get_string("bynameondate", "forum", $by);
908                             $posttext .= "\n---------------------------------------------------------------------";
910                             $by->name = "<a target=\"_blank\" href=\"$CFG->wwwroot/user/view.php?id=$userfrom->id&amp;course=$course->id\">$by->name</a>";
911                             $posthtml .= '<div><a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id.'#p'.$post->id.'">'.format_string($post->subject,true).'</a> '.get_string("bynameondate", "forum", $by).'</div>';
913                         } else {
914                             // The full treatment
915                             $posttext .= forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, true);
916                             $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
918                         // Create an array of postid's for this user to mark as read.
919                             if (!$CFG->forum_usermarksread) {
920                                 $userto->markposts[$post->id] = $post->id;
921                             }
922                         }
923                     }
924                     if ($canunsubscribe) {
925                         $posthtml .= "\n<div class='mdl-right'><font size=\"1\"><a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">".get_string("unsubscribe", "forum")."</a></font></div>";
926                     } else {
927                         $posthtml .= "\n<div class='mdl-right'><font size=\"1\">".get_string("everyoneissubscribed", "forum")."</font></div>";
928                     }
929                     $posthtml .= '<hr size="1" noshade="noshade" /></p>';
930                 }
931                 $posthtml .= '</body>';
933                 if (empty($userto->mailformat) || $userto->mailformat != 1) {
934                     // This user DOESN'T want to receive HTML
935                     $posthtml = '';
936                 }
938                 $attachment = $attachname='';
939                 $usetrueaddress = true;
940                 //directly email forum digests rather than sending them via messaging
941                 $mailresult = email_to_user($userto, $site->shortname, $postsubject, $posttext, $posthtml, $attachment, $attachname, $usetrueaddress, $CFG->forum_replytouser);
943                 if (!$mailresult) {
944                     mtrace("ERROR!");
945                     echo "Error: mod/forum/cron.php: Could not send out digest mail to user $userto->id ($userto->email)... not trying again.\n";
946                     add_to_log($course->id, 'forum', 'mail digest error', '', '', $cm->id, $userto->id);
947                 } else {
948                     mtrace("success.");
949                     $usermailcount++;
951                     // Mark post as read if forum_usermarksread is set off
952                     forum_tp_mark_posts_read($userto, $userto->markposts);
953                 }
954             }
955         }
956     /// We have finishied all digest emails, update $CFG->digestmailtimelast
957         set_config('digestmailtimelast', $timenow);
958     }
960     cron_setup_user();
962     if (!empty($usermailcount)) {
963         mtrace(get_string('digestsentusers', 'forum', $usermailcount));
964     }
966     if (!empty($CFG->forum_lastreadclean)) {
967         $timenow = time();
968         if ($CFG->forum_lastreadclean + (24*3600) < $timenow) {
969             set_config('forum_lastreadclean', $timenow);
970             mtrace('Removing old forum read tracking info...');
971             forum_tp_clean_read_records();
972         }
973     } else {
974         set_config('forum_lastreadclean', time());
975     }
978     return true;
981 /**
982  * Builds and returns the body of the email notification in plain text.
983  *
984  * @global object
985  * @global object
986  * @uses CONTEXT_MODULE
987  * @param object $course
988  * @param object $cm
989  * @param object $forum
990  * @param object $discussion
991  * @param object $post
992  * @param object $userfrom
993  * @param object $userto
994  * @param boolean $bare
995  * @return string The email body in plain text format.
996  */
997 function forum_make_mail_text($course, $cm, $forum, $discussion, $post, $userfrom, $userto, $bare = false) {
998     global $CFG, $USER;
1000     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
1002     if (!isset($userto->viewfullnames[$forum->id])) {
1003         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
1004     } else {
1005         $viewfullnames = $userto->viewfullnames[$forum->id];
1006     }
1008     if (!isset($userto->canpost[$discussion->id])) {
1009         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
1010     } else {
1011         $canreply = $userto->canpost[$discussion->id];
1012     }
1014     $by = New stdClass;
1015     $by->name = fullname($userfrom, $viewfullnames);
1016     $by->date = userdate($post->modified, "", $userto->timezone);
1018     $strbynameondate = get_string('bynameondate', 'forum', $by);
1020     $strforums = get_string('forums', 'forum');
1022     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1024     $posttext = '';
1026     if (!$bare) {
1027         $shortname = format_string($course->shortname, true, array('context' => get_context_instance(CONTEXT_COURSE, $course->id)));
1028         $posttext  = "$shortname -> $strforums -> ".format_string($forum->name,true);
1030         if ($discussion->name != $forum->name) {
1031             $posttext  .= " -> ".format_string($discussion->name,true);
1032         }
1033     }
1035     // add absolute file links
1036     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
1038     $posttext .= "\n---------------------------------------------------------------------\n";
1039     $posttext .= format_string($post->subject,true);
1040     if ($bare) {
1041         $posttext .= " ($CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id#p$post->id)";
1042     }
1043     $posttext .= "\n".$strbynameondate."\n";
1044     $posttext .= "---------------------------------------------------------------------\n";
1045     $posttext .= format_text_email($post->message, $post->messageformat);
1046     $posttext .= "\n\n";
1047     $posttext .= forum_print_attachments($post, $cm, "text");
1049     if (!$bare && $canreply) {
1050         $posttext .= "---------------------------------------------------------------------\n";
1051         $posttext .= get_string("postmailinfo", "forum", $shortname)."\n";
1052         $posttext .= "$CFG->wwwroot/mod/forum/post.php?reply=$post->id\n";
1053     }
1054     if (!$bare && $canunsubscribe) {
1055         $posttext .= "\n---------------------------------------------------------------------\n";
1056         $posttext .= get_string("unsubscribe", "forum");
1057         $posttext .= ": $CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\n";
1058     }
1060     return $posttext;
1063 /**
1064  * Builds and returns the body of the email notification in html format.
1065  *
1066  * @global object
1067  * @param object $course
1068  * @param object $cm
1069  * @param object $forum
1070  * @param object $discussion
1071  * @param object $post
1072  * @param object $userfrom
1073  * @param object $userto
1074  * @return string The email text in HTML format
1075  */
1076 function forum_make_mail_html($course, $cm, $forum, $discussion, $post, $userfrom, $userto) {
1077     global $CFG;
1079     if ($userto->mailformat != 1) {  // Needs to be HTML
1080         return '';
1081     }
1083     if (!isset($userto->canpost[$discussion->id])) {
1084         $canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course);
1085     } else {
1086         $canreply = $userto->canpost[$discussion->id];
1087     }
1089     $strforums = get_string('forums', 'forum');
1090     $canunsubscribe = ! forum_is_forcesubscribed($forum);
1091     $shortname = format_string($course->shortname, true, array('context' => get_context_instance(CONTEXT_COURSE, $course->id)));
1093     $posthtml = '<head>';
1094 /*    foreach ($CFG->stylesheets as $stylesheet) {
1095         //TODO: MDL-21120
1096         $posthtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
1097     }*/
1098     $posthtml .= '</head>';
1099     $posthtml .= "\n<body id=\"email\">\n\n";
1101     $posthtml .= '<div class="navbar">'.
1102     '<a target="_blank" href="'.$CFG->wwwroot.'/course/view.php?id='.$course->id.'">'.$shortname.'</a> &raquo; '.
1103     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/index.php?id='.$course->id.'">'.$strforums.'</a> &raquo; '.
1104     '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.format_string($forum->name,true).'</a>';
1105     if ($discussion->name == $forum->name) {
1106         $posthtml .= '</div>';
1107     } else {
1108         $posthtml .= ' &raquo; <a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id.'">'.
1109                      format_string($discussion->name,true).'</a></div>';
1110     }
1111     $posthtml .= forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto, false, $canreply, true, false);
1113     if ($canunsubscribe) {
1114         $posthtml .= '<hr /><div class="mdl-align unsubscribelink">
1115                       <a href="'.$CFG->wwwroot.'/mod/forum/subscribe.php?id='.$forum->id.'">'.get_string('unsubscribe', 'forum').'</a>&nbsp;
1116                       <a href="'.$CFG->wwwroot.'/mod/forum/unsubscribeall.php">'.get_string('unsubscribeall', 'forum').'</a></div>';
1117     }
1119     $posthtml .= '</body>';
1121     return $posthtml;
1125 /**
1126  *
1127  * @param object $course
1128  * @param object $user
1129  * @param object $mod TODO this is not used in this function, refactor
1130  * @param object $forum
1131  * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
1132  */
1133 function forum_user_outline($course, $user, $mod, $forum) {
1134     global $CFG;
1135     require_once("$CFG->libdir/gradelib.php");
1136     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1137     if (empty($grades->items[0]->grades)) {
1138         $grade = false;
1139     } else {
1140         $grade = reset($grades->items[0]->grades);
1141     }
1143     $count = forum_count_user_posts($forum->id, $user->id);
1145     if ($count && $count->postcount > 0) {
1146         $result = new stdClass();
1147         $result->info = get_string("numposts", "forum", $count->postcount);
1148         $result->time = $count->lastpost;
1149         if ($grade) {
1150             $result->info .= ', ' . get_string('grade') . ': ' . $grade->str_long_grade;
1151         }
1152         return $result;
1153     } else if ($grade) {
1154         $result = new stdClass();
1155         $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
1157         //datesubmitted == time created. dategraded == time modified or time overridden
1158         //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
1159         //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
1160         if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1161             $result->time = $grade->dategraded;
1162         } else {
1163             $result->time = $grade->datesubmitted;
1164         }
1166         return $result;
1167     }
1168     return NULL;
1172 /**
1173  * @global object
1174  * @global object
1175  * @param object $coure
1176  * @param object $user
1177  * @param object $mod
1178  * @param object $forum
1179  */
1180 function forum_user_complete($course, $user, $mod, $forum) {
1181     global $CFG,$USER, $OUTPUT;
1182     require_once("$CFG->libdir/gradelib.php");
1184     $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
1185     if (!empty($grades->items[0]->grades)) {
1186         $grade = reset($grades->items[0]->grades);
1187         echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
1188         if ($grade->str_feedback) {
1189             echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
1190         }
1191     }
1193     if ($posts = forum_get_user_posts($forum->id, $user->id)) {
1195         if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
1196             print_error('invalidcoursemodule');
1197         }
1198         $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
1200         foreach ($posts as $post) {
1201             if (!isset($discussions[$post->discussion])) {
1202                 continue;
1203             }
1204             $discussion = $discussions[$post->discussion];
1206             forum_print_post($post, $discussion, $forum, $cm, $course, false, false, false);
1207         }
1208     } else {
1209         echo "<p>".get_string("noposts", "forum")."</p>";
1210     }
1218 /**
1219  * @global object
1220  * @global object
1221  * @global object
1222  * @param array $courses
1223  * @param array $htmlarray
1224  */
1225 function forum_print_overview($courses,&$htmlarray) {
1226     global $USER, $CFG, $DB, $SESSION;
1228     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1229         return array();
1230     }
1232     if (!$forums = get_all_instances_in_courses('forum',$courses)) {
1233         return;
1234     }
1237     // get all forum logs in ONE query (much better!)
1238     $params = array();
1239     $sql = "SELECT instance,cmid,l.course,COUNT(l.id) as count FROM {log} l "
1240         ." JOIN {course_modules} cm ON cm.id = cmid "
1241         ." WHERE (";
1242     foreach ($courses as $course) {
1243         $sql .= '(l.course = ? AND l.time > ?) OR ';
1244         $params[] = $course->id;
1245         $params[] = $course->lastaccess;
1246     }
1247     $sql = substr($sql,0,-3); // take off the last OR
1249     $sql .= ") AND l.module = 'forum' AND action = 'add post' "
1250         ." AND userid != ? GROUP BY cmid,l.course,instance";
1252     $params[] = $USER->id;
1254     if (!$new = $DB->get_records_sql($sql, $params)) {
1255         $new = array(); // avoid warnings
1256     }
1258     // also get all forum tracking stuff ONCE.
1259     $trackingforums = array();
1260     foreach ($forums as $forum) {
1261         if (forum_tp_can_track_forums($forum)) {
1262             $trackingforums[$forum->id] = $forum;
1263         }
1264     }
1266     if (count($trackingforums) > 0) {
1267         $cutoffdate = isset($CFG->forum_oldpostdays) ? (time() - ($CFG->forum_oldpostdays*24*60*60)) : 0;
1268         $sql = 'SELECT d.forum,d.course,COUNT(p.id) AS count '.
1269             ' FROM {forum_posts} p '.
1270             ' JOIN {forum_discussions} d ON p.discussion = d.id '.
1271             ' LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = ? WHERE (';
1272         $params = array($USER->id);
1274         foreach ($trackingforums as $track) {
1275             $sql .= '(d.forum = ? AND (d.groupid = -1 OR d.groupid = 0 OR d.groupid = ?)) OR ';
1276             $params[] = $track->id;
1277             if (isset($SESSION->currentgroup[$track->course])) {
1278                 $groupid =  $SESSION->currentgroup[$track->course];
1279             } else {
1280                 // get first groupid
1281                 $groupids = groups_get_all_groups($track->course, $USER->id);
1282                 if ($groupids) {
1283                     reset($groupids);
1284                     $groupid = key($groupids);
1285                     $SESSION->currentgroup[$track->course] = $groupid;
1286                 } else {
1287                     $groupid = 0;
1288                 }
1289                 unset($groupids);
1290             }
1291             $params[] = $groupid;
1292         }
1293         $sql = substr($sql,0,-3); // take off the last OR
1294         $sql .= ') AND p.modified >= ? AND r.id is NULL GROUP BY d.forum,d.course';
1295         $params[] = $cutoffdate;
1297         if (!$unread = $DB->get_records_sql($sql, $params)) {
1298             $unread = array();
1299         }
1300     } else {
1301         $unread = array();
1302     }
1304     if (empty($unread) and empty($new)) {
1305         return;
1306     }
1308     $strforum = get_string('modulename','forum');
1309     $strnumunread = get_string('overviewnumunread','forum');
1310     $strnumpostssince = get_string('overviewnumpostssince','forum');
1312     foreach ($forums as $forum) {
1313         $str = '';
1314         $count = 0;
1315         $thisunread = 0;
1316         $showunread = false;
1317         // either we have something from logs, or trackposts, or nothing.
1318         if (array_key_exists($forum->id, $new) && !empty($new[$forum->id])) {
1319             $count = $new[$forum->id]->count;
1320         }
1321         if (array_key_exists($forum->id,$unread)) {
1322             $thisunread = $unread[$forum->id]->count;
1323             $showunread = true;
1324         }
1325         if ($count > 0 || $thisunread > 0) {
1326             $str .= '<div class="overview forum"><div class="name">'.$strforum.': <a title="'.$strforum.'" href="'.$CFG->wwwroot.'/mod/forum/view.php?f='.$forum->id.'">'.
1327                 $forum->name.'</a></div>';
1328             $str .= '<div class="info"><span class="postsincelogin">';
1329             $str .= $count.' '.$strnumpostssince."</span>";
1330             if (!empty($showunread)) {
1331                 $str .= '<div class="unreadposts">'.$thisunread .' '.$strnumunread.'</div>';
1332             }
1333             $str .= '</div></div>';
1334         }
1335         if (!empty($str)) {
1336             if (!array_key_exists($forum->course,$htmlarray)) {
1337                 $htmlarray[$forum->course] = array();
1338             }
1339             if (!array_key_exists('forum',$htmlarray[$forum->course])) {
1340                 $htmlarray[$forum->course]['forum'] = ''; // initialize, avoid warnings
1341             }
1342             $htmlarray[$forum->course]['forum'] .= $str;
1343         }
1344     }
1347 /**
1348  * Given a course and a date, prints a summary of all the new
1349  * messages posted in the course since that date
1350  *
1351  * @global object
1352  * @global object
1353  * @global object
1354  * @uses CONTEXT_MODULE
1355  * @uses VISIBLEGROUPS
1356  * @param object $course
1357  * @param bool $viewfullnames capability
1358  * @param int $timestart
1359  * @return bool success
1360  */
1361 function forum_print_recent_activity($course, $viewfullnames, $timestart) {
1362     global $CFG, $USER, $DB, $OUTPUT;
1364     // do not use log table if possible, it may be huge and is expensive to join with other tables
1366     if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
1367                                               d.timestart, d.timeend, d.userid AS duserid,
1368                                               u.firstname, u.lastname, u.email, u.picture
1369                                          FROM {forum_posts} p
1370                                               JOIN {forum_discussions} d ON d.id = p.discussion
1371                                               JOIN {forum} f             ON f.id = d.forum
1372                                               JOIN {user} u              ON u.id = p.userid
1373                                         WHERE p.created > ? AND f.course = ?
1374                                      ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
1375          return false;
1376     }
1378     $modinfo = get_fast_modinfo($course);
1380     $groupmodes = array();
1381     $cms    = array();
1383     $strftimerecent = get_string('strftimerecent');
1385     $printposts = array();
1386     foreach ($posts as $post) {
1387         if (!isset($modinfo->instances['forum'][$post->forum])) {
1388             // not visible
1389             continue;
1390         }
1391         $cm = $modinfo->instances['forum'][$post->forum];
1392         if (!$cm->uservisible) {
1393             continue;
1394         }
1395         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1397         if (!has_capability('mod/forum:viewdiscussion', $context)) {
1398             continue;
1399         }
1401         if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
1402           and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
1403             if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1404                 continue;
1405             }
1406         }
1408         $groupmode = groups_get_activity_groupmode($cm, $course);
1410         if ($groupmode) {
1411             if ($post->groupid == -1 or $groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context)) {
1412                 // oki (Open discussions have groupid -1)
1413             } else {
1414                 // separate mode
1415                 if (isguestuser()) {
1416                     // shortcut
1417                     continue;
1418                 }
1420                 if (is_null($modinfo->groups)) {
1421                     $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
1422                 }
1424                 if (!array_key_exists($post->groupid, $modinfo->groups[0])) {
1425                     continue;
1426                 }
1427             }
1428         }
1430         $printposts[] = $post;
1431     }
1432     unset($posts);
1434     if (!$printposts) {
1435         return false;
1436     }
1438     echo $OUTPUT->heading(get_string('newforumposts', 'forum').':', 3);
1439     echo "\n<ul class='unlist'>\n";
1441     foreach ($printposts as $post) {
1442         $subjectclass = empty($post->parent) ? ' bold' : '';
1444         echo '<li><div class="head">'.
1445                '<div class="date">'.userdate($post->modified, $strftimerecent).'</div>'.
1446                '<div class="name">'.fullname($post, $viewfullnames).'</div>'.
1447              '</div>';
1448         echo '<div class="info'.$subjectclass.'">';
1449         if (empty($post->parent)) {
1450             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">';
1451         } else {
1452             echo '"<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'&amp;parent='.$post->parent.'#p'.$post->id.'">';
1453         }
1454         $post->subject = break_up_long_words(format_string($post->subject, true));
1455         echo $post->subject;
1456         echo "</a>\"</div></li>\n";
1457     }
1459     echo "</ul>\n";
1461     return true;
1464 /**
1465  * Return grade for given user or all users.
1466  *
1467  * @global object
1468  * @global object
1469  * @param object $forum
1470  * @param int $userid optional user id, 0 means all users
1471  * @return array array of grades, false if none
1472  */
1473 function forum_get_user_grades($forum, $userid = 0) {
1474     global $CFG;
1476     require_once($CFG->dirroot.'/rating/lib.php');
1478     $ratingoptions = new stdClass;
1479     $ratingoptions->component = 'mod_forum';
1480     $ratingoptions->ratingarea = 'post';
1482     //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance?
1483     $ratingoptions->modulename = 'forum';
1484     $ratingoptions->moduleid   = $forum->id;
1485     $ratingoptions->userid = $userid;
1486     $ratingoptions->aggregationmethod = $forum->assessed;
1487     $ratingoptions->scaleid = $forum->scale;
1488     $ratingoptions->itemtable = 'forum_posts';
1489     $ratingoptions->itemtableusercolumn = 'userid';
1491     $rm = new rating_manager();
1492     return $rm->get_user_grades($ratingoptions);
1495 /**
1496  * Update activity grades
1497  *
1498  * @category grade
1499  * @param object $forum
1500  * @param int $userid specific user only, 0 means all
1501  * @param boolean $nullifnone return null if grade does not exist
1502  * @return void
1503  */
1504 function forum_update_grades($forum, $userid=0, $nullifnone=true) {
1505     global $CFG, $DB;
1506     require_once($CFG->libdir.'/gradelib.php');
1508     if (!$forum->assessed) {
1509         forum_grade_item_update($forum);
1511     } else if ($grades = forum_get_user_grades($forum, $userid)) {
1512         forum_grade_item_update($forum, $grades);
1514     } else if ($userid and $nullifnone) {
1515         $grade = new stdClass();
1516         $grade->userid   = $userid;
1517         $grade->rawgrade = NULL;
1518         forum_grade_item_update($forum, $grade);
1520     } else {
1521         forum_grade_item_update($forum);
1522     }
1525 /**
1526  * Update all grades in gradebook.
1527  * @global object
1528  */
1529 function forum_upgrade_grades() {
1530     global $DB;
1532     $sql = "SELECT COUNT('x')
1533               FROM {forum} f, {course_modules} cm, {modules} m
1534              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1535     $count = $DB->count_records_sql($sql);
1537     $sql = "SELECT f.*, cm.idnumber AS cmidnumber, f.course AS courseid
1538               FROM {forum} f, {course_modules} cm, {modules} m
1539              WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id";
1540     $rs = $DB->get_recordset_sql($sql);
1541     if ($rs->valid()) {
1542         $pbar = new progress_bar('forumupgradegrades', 500, true);
1543         $i=0;
1544         foreach ($rs as $forum) {
1545             $i++;
1546             upgrade_set_timeout(60*5); // set up timeout, may also abort execution
1547             forum_update_grades($forum, 0, false);
1548             $pbar->update($i, $count, "Updating Forum grades ($i/$count).");
1549         }
1550     }
1551     $rs->close();
1554 /**
1555  * Create/update grade item for given forum
1556  *
1557  * @category grade
1558  * @uses GRADE_TYPE_NONE
1559  * @uses GRADE_TYPE_VALUE
1560  * @uses GRADE_TYPE_SCALE
1561  * @param stdClass $forum Forum object with extra cmidnumber
1562  * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
1563  * @return int 0 if ok
1564  */
1565 function forum_grade_item_update($forum, $grades=NULL) {
1566     global $CFG;
1567     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
1568         require_once($CFG->libdir.'/gradelib.php');
1569     }
1571     $params = array('itemname'=>$forum->name, 'idnumber'=>$forum->cmidnumber);
1573     if (!$forum->assessed or $forum->scale == 0) {
1574         $params['gradetype'] = GRADE_TYPE_NONE;
1576     } else if ($forum->scale > 0) {
1577         $params['gradetype'] = GRADE_TYPE_VALUE;
1578         $params['grademax']  = $forum->scale;
1579         $params['grademin']  = 0;
1581     } else if ($forum->scale < 0) {
1582         $params['gradetype'] = GRADE_TYPE_SCALE;
1583         $params['scaleid']   = -$forum->scale;
1584     }
1586     if ($grades  === 'reset') {
1587         $params['reset'] = true;
1588         $grades = NULL;
1589     }
1591     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $grades, $params);
1594 /**
1595  * Delete grade item for given forum
1596  *
1597  * @category grade
1598  * @param stdClass $forum Forum object
1599  * @return grade_item
1600  */
1601 function forum_grade_item_delete($forum) {
1602     global $CFG;
1603     require_once($CFG->libdir.'/gradelib.php');
1605     return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1));
1609 /**
1610  * This function returns if a scale is being used by one forum
1611  *
1612  * @global object
1613  * @param int $forumid
1614  * @param int $scaleid negative number
1615  * @return bool
1616  */
1617 function forum_scale_used ($forumid,$scaleid) {
1618     global $DB;
1619     $return = false;
1621     $rec = $DB->get_record("forum",array("id" => "$forumid","scale" => "-$scaleid"));
1623     if (!empty($rec) && !empty($scaleid)) {
1624         $return = true;
1625     }
1627     return $return;
1630 /**
1631  * Checks if scale is being used by any instance of forum
1632  *
1633  * This is used to find out if scale used anywhere
1634  *
1635  * @global object
1636  * @param $scaleid int
1637  * @return boolean True if the scale is used by any forum
1638  */
1639 function forum_scale_used_anywhere($scaleid) {
1640     global $DB;
1641     if ($scaleid and $DB->record_exists('forum', array('scale' => -$scaleid))) {
1642         return true;
1643     } else {
1644         return false;
1645     }
1648 // SQL FUNCTIONS ///////////////////////////////////////////////////////////
1650 /**
1651  * Gets a post with all info ready for forum_print_post
1652  * Most of these joins are just to get the forum id
1653  *
1654  * @global object
1655  * @global object
1656  * @param int $postid
1657  * @return mixed array of posts or false
1658  */
1659 function forum_get_post_full($postid) {
1660     global $CFG, $DB;
1662     return $DB->get_record_sql("SELECT p.*, d.forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1663                              FROM {forum_posts} p
1664                                   JOIN {forum_discussions} d ON p.discussion = d.id
1665                                   LEFT JOIN {user} u ON p.userid = u.id
1666                             WHERE p.id = ?", array($postid));
1669 /**
1670  * Gets posts with all info ready for forum_print_post
1671  * We pass forumid in because we always know it so no need to make a
1672  * complicated join to find it out.
1673  *
1674  * @global object
1675  * @global object
1676  * @return mixed array of posts or false
1677  */
1678 function forum_get_discussion_posts($discussion, $sort, $forumid) {
1679     global $CFG, $DB;
1681     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1682                               FROM {forum_posts} p
1683                          LEFT JOIN {user} u ON p.userid = u.id
1684                              WHERE p.discussion = ?
1685                                AND p.parent > 0 $sort", array($discussion));
1688 /**
1689  * Gets all posts in discussion including top parent.
1690  *
1691  * @global object
1692  * @global object
1693  * @global object
1694  * @param int $discussionid
1695  * @param string $sort
1696  * @param bool $tracking does user track the forum?
1697  * @return array of posts
1698  */
1699 function forum_get_all_discussion_posts($discussionid, $sort, $tracking=false) {
1700     global $CFG, $DB, $USER;
1702     $tr_sel  = "";
1703     $tr_join = "";
1704     $params = array();
1706     if ($tracking) {
1707         $now = time();
1708         $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
1709         $tr_sel  = ", fr.id AS postread";
1710         $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
1711         $params[] = $USER->id;
1712     }
1714     $params[] = $discussionid;
1715     if (!$posts = $DB->get_records_sql("SELECT p.*, u.firstname, u.lastname, u.email, u.picture, u.imagealt $tr_sel
1716                                      FROM {forum_posts} p
1717                                           LEFT JOIN {user} u ON p.userid = u.id
1718                                           $tr_join
1719                                     WHERE p.discussion = ?
1720                                  ORDER BY $sort", $params)) {
1721         return array();
1722     }
1724     foreach ($posts as $pid=>$p) {
1725         if ($tracking) {
1726             if (forum_tp_is_post_old($p)) {
1727                  $posts[$pid]->postread = true;
1728             }
1729         }
1730         if (!$p->parent) {
1731             continue;
1732         }
1733         if (!isset($posts[$p->parent])) {
1734             continue; // parent does not exist??
1735         }
1736         if (!isset($posts[$p->parent]->children)) {
1737             $posts[$p->parent]->children = array();
1738         }
1739         $posts[$p->parent]->children[$pid] =& $posts[$pid];
1740     }
1742     return $posts;
1745 /**
1746  * Gets posts with all info ready for forum_print_post
1747  * We pass forumid in because we always know it so no need to make a
1748  * complicated join to find it out.
1749  *
1750  * @global object
1751  * @global object
1752  * @param int $parent
1753  * @param int $forumid
1754  * @return array
1755  */
1756 function forum_get_child_posts($parent, $forumid) {
1757     global $CFG, $DB;
1759     return $DB->get_records_sql("SELECT p.*, $forumid AS forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
1760                               FROM {forum_posts} p
1761                          LEFT JOIN {user} u ON p.userid = u.id
1762                              WHERE p.parent = ?
1763                           ORDER BY p.created ASC", array($parent));
1766 /**
1767  * An array of forum objects that the user is allowed to read/search through.
1768  *
1769  * @global object
1770  * @global object
1771  * @global object
1772  * @param int $userid
1773  * @param int $courseid if 0, we look for forums throughout the whole site.
1774  * @return array of forum objects, or false if no matches
1775  *         Forum objects have the following attributes:
1776  *         id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
1777  *         viewhiddentimedposts
1778  */
1779 function forum_get_readable_forums($userid, $courseid=0) {
1781     global $CFG, $DB, $USER;
1782     require_once($CFG->dirroot.'/course/lib.php');
1784     if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
1785         print_error('notinstalled', 'forum');
1786     }
1788     if ($courseid) {
1789         $courses = $DB->get_records('course', array('id' => $courseid));
1790     } else {
1791         // If no course is specified, then the user can see SITE + his courses.
1792         $courses1 = $DB->get_records('course', array('id' => SITEID));
1793         $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
1794         $courses = array_merge($courses1, $courses2);
1795     }
1796     if (!$courses) {
1797         return array();
1798     }
1800     $readableforums = array();
1802     foreach ($courses as $course) {
1804         $modinfo = get_fast_modinfo($course);
1805         if (is_null($modinfo->groups)) {
1806             $modinfo->groups = groups_get_user_groups($course->id, $userid);
1807         }
1809         if (empty($modinfo->instances['forum'])) {
1810             // hmm, no forums?
1811             continue;
1812         }
1814         $courseforums = $DB->get_records('forum', array('course' => $course->id));
1816         foreach ($modinfo->instances['forum'] as $forumid => $cm) {
1817             if (!$cm->uservisible or !isset($courseforums[$forumid])) {
1818                 continue;
1819             }
1820             $context = get_context_instance(CONTEXT_MODULE, $cm->id);
1821             $forum = $courseforums[$forumid];
1822             $forum->context = $context;
1823             $forum->cm = $cm;
1825             if (!has_capability('mod/forum:viewdiscussion', $context)) {
1826                 continue;
1827             }
1829          /// group access
1830             if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
1831                 if (is_null($modinfo->groups)) {
1832                     $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
1833                 }
1834                 if (isset($modinfo->groups[$cm->groupingid])) {
1835                     $forum->onlygroups = $modinfo->groups[$cm->groupingid];
1836                     $forum->onlygroups[] = -1;
1837                 } else {
1838                     $forum->onlygroups = array(-1);
1839                 }
1840             }
1842         /// hidden timed discussions
1843             $forum->viewhiddentimedposts = true;
1844             if (!empty($CFG->forum_enabletimedposts)) {
1845                 if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1846                     $forum->viewhiddentimedposts = false;
1847                 }
1848             }
1850         /// qanda access
1851             if ($forum->type == 'qanda'
1852                     && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
1854                 // We need to check whether the user has posted in the qanda forum.
1855                 $forum->onlydiscussions = array();  // Holds discussion ids for the discussions
1856                                                     // the user is allowed to see in this forum.
1857                 if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
1858                     foreach ($discussionspostedin as $d) {
1859                         $forum->onlydiscussions[] = $d->id;
1860                     }
1861                 }
1862             }
1864             $readableforums[$forum->id] = $forum;
1865         }
1867         unset($modinfo);
1869     } // End foreach $courses
1871     return $readableforums;
1874 /**
1875  * Returns a list of posts found using an array of search terms.
1876  *
1877  * @global object
1878  * @global object
1879  * @global object
1880  * @param array $searchterms array of search terms, e.g. word +word -word
1881  * @param int $courseid if 0, we search through the whole site
1882  * @param int $limitfrom
1883  * @param int $limitnum
1884  * @param int &$totalcount
1885  * @param string $extrasql
1886  * @return array|bool Array of posts found or false
1887  */
1888 function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=50,
1889                             &$totalcount, $extrasql='') {
1890     global $CFG, $DB, $USER;
1891     require_once($CFG->libdir.'/searchlib.php');
1893     $forums = forum_get_readable_forums($USER->id, $courseid);
1895     if (count($forums) == 0) {
1896         $totalcount = 0;
1897         return false;
1898     }
1900     $now = round(time(), -2); // db friendly
1902     $fullaccess = array();
1903     $where = array();
1904     $params = array();
1906     foreach ($forums as $forumid => $forum) {
1907         $select = array();
1909         if (!$forum->viewhiddentimedposts) {
1910             $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
1911             $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
1912         }
1914         $cm = $forum->cm;
1915         $context = $forum->context;
1917         if ($forum->type == 'qanda'
1918             && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
1919             if (!empty($forum->onlydiscussions)) {
1920                 list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
1921                 $params = array_merge($params, $discussionid_params);
1922                 $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
1923             } else {
1924                 $select[] = "p.parent = 0";
1925             }
1926         }
1928         if (!empty($forum->onlygroups)) {
1929             list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
1930             $params = array_merge($params, $groupid_params);
1931             $select[] = "d.groupid $groupid_sql";
1932         }
1934         if ($select) {
1935             $selects = implode(" AND ", $select);
1936             $where[] = "(d.forum = :forum{$forumid} AND $selects)";
1937             $params['forum'.$forumid] = $forumid;
1938         } else {
1939             $fullaccess[] = $forumid;
1940         }
1941     }
1943     if ($fullaccess) {
1944         list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
1945         $params = array_merge($params, $fullid_params);
1946         $where[] = "(d.forum $fullid_sql)";
1947     }
1949     $selectdiscussion = "(".implode(" OR ", $where).")";
1951     $messagesearch = '';
1952     $searchstring = '';
1954     // Need to concat these back together for parser to work.
1955     foreach($searchterms as $searchterm){
1956         if ($searchstring != '') {
1957             $searchstring .= ' ';
1958         }
1959         $searchstring .= $searchterm;
1960     }
1962     // We need to allow quoted strings for the search. The quotes *should* be stripped
1963     // by the parser, but this should be examined carefully for security implications.
1964     $searchstring = str_replace("\\\"","\"",$searchstring);
1965     $parser = new search_parser();
1966     $lexer = new search_lexer($parser);
1968     if ($lexer->parse($searchstring)) {
1969         $parsearray = $parser->get_parsed_array();
1970     // Experimental feature under 1.8! MDL-8830
1971     // Use alternative text searches if defined
1972     // This feature only works under mysql until properly implemented for other DBs
1973     // Requires manual creation of text index for forum_posts before enabling it:
1974     // CREATE FULLTEXT INDEX foru_post_tix ON [prefix]forum_posts (subject, message)
1975     // Experimental feature under 1.8! MDL-8830
1976         if (!empty($CFG->forum_usetextsearches)) {
1977             list($messagesearch, $msparams) = search_generate_text_SQL($parsearray, 'p.message', 'p.subject',
1978                                                  'p.userid', 'u.id', 'u.firstname',
1979                                                  'u.lastname', 'p.modified', 'd.forum');
1980         } else {
1981             list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
1982                                                  'p.userid', 'u.id', 'u.firstname',
1983                                                  'u.lastname', 'p.modified', 'd.forum');
1984         }
1985         $params = array_merge($params, $msparams);
1986     }
1988     $fromsql = "{forum_posts} p,
1989                   {forum_discussions} d,
1990                   {user} u";
1992     $selectsql = " $messagesearch
1993                AND p.discussion = d.id
1994                AND p.userid = u.id
1995                AND $selectdiscussion
1996                    $extrasql";
1998     $countsql = "SELECT COUNT(*)
1999                    FROM $fromsql
2000                   WHERE $selectsql";
2002     $searchsql = "SELECT p.*,
2003                          d.forum,
2004                          u.firstname,
2005                          u.lastname,
2006                          u.email,
2007                          u.picture,
2008                          u.imagealt,
2009                          u.email
2010                     FROM $fromsql
2011                    WHERE $selectsql
2012                 ORDER BY p.modified DESC";
2014     $totalcount = $DB->count_records_sql($countsql, $params);
2016     return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
2019 /**
2020  * Returns a list of ratings for a particular post - sorted.
2021  *
2022  * TODO: Check if this function is actually used anywhere.
2023  * Up until the fix for MDL-27471 this function wasn't even returning.
2024  *
2025  * @param stdClass $context
2026  * @param int $postid
2027  * @param string $sort
2028  * @return array Array of ratings or false
2029  */
2030 function forum_get_ratings($context, $postid, $sort = "u.firstname ASC") {
2031     $options = new stdClass;
2032     $options->context = $context;
2033     $options->component = 'mod_forum';
2034     $options->ratingarea = 'post';
2035     $options->itemid = $postid;
2036     $options->sort = "ORDER BY $sort";
2038     $rm = new rating_manager();
2039     return $rm->get_all_ratings_for_item($options);
2042 /**
2043  * Returns a list of all new posts that have not been mailed yet
2044  *
2045  * @param int $starttime posts created after this time
2046  * @param int $endtime posts created before this
2047  * @param int $now used for timed discussions only
2048  * @return array
2049  */
2050 function forum_get_unmailed_posts($starttime, $endtime, $now=null) {
2051     global $CFG, $DB;
2053     $params = array($starttime, $endtime);
2054     if (!empty($CFG->forum_enabletimedposts)) {
2055         if (empty($now)) {
2056             $now = time();
2057         }
2058         $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2059         $params[] = $now;
2060         $params[] = $now;
2061     } else {
2062         $timedsql = "";
2063     }
2065     return $DB->get_records_sql("SELECT p.*, d.course, d.forum
2066                               FROM {forum_posts} p
2067                                    JOIN {forum_discussions} d ON d.id = p.discussion
2068                              WHERE p.mailed = 0
2069                                    AND p.created >= ?
2070                                    AND (p.created < ? OR p.mailnow = 1)
2071                                    $timedsql
2072                           ORDER BY p.modified ASC", $params);
2075 /**
2076  * Marks posts before a certain time as being mailed already
2077  *
2078  * @global object
2079  * @global object
2080  * @param int $endtime
2081  * @param int $now Defaults to time()
2082  * @return bool
2083  */
2084 function forum_mark_old_posts_as_mailed($endtime, $now=null) {
2085     global $CFG, $DB;
2086     if (empty($now)) {
2087         $now = time();
2088     }
2090     if (empty($CFG->forum_enabletimedposts)) {
2091         return $DB->execute("UPDATE {forum_posts}
2092                                SET mailed = '1'
2093                              WHERE (created < ? OR mailnow = 1)
2094                                    AND mailed = 0", array($endtime));
2096     } else {
2097         return $DB->execute("UPDATE {forum_posts}
2098                                SET mailed = '1'
2099                              WHERE discussion NOT IN (SELECT d.id
2100                                                         FROM {forum_discussions} d
2101                                                        WHERE d.timestart > ?)
2102                                    AND (created < ? OR mailnow = 1)
2103                                    AND mailed = 0", array($now, $endtime));
2104     }
2107 /**
2108  * Get all the posts for a user in a forum suitable for forum_print_post
2109  *
2110  * @global object
2111  * @global object
2112  * @uses CONTEXT_MODULE
2113  * @return array
2114  */
2115 function forum_get_user_posts($forumid, $userid) {
2116     global $CFG, $DB;
2118     $timedsql = "";
2119     $params = array($forumid, $userid);
2121     if (!empty($CFG->forum_enabletimedposts)) {
2122         $cm = get_coursemodule_from_instance('forum', $forumid);
2123         if (!has_capability('mod/forum:viewhiddentimedposts' , get_context_instance(CONTEXT_MODULE, $cm->id))) {
2124             $now = time();
2125             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2126             $params[] = $now;
2127             $params[] = $now;
2128         }
2129     }
2131     return $DB->get_records_sql("SELECT p.*, d.forum, u.firstname, u.lastname, u.email, u.picture, u.imagealt
2132                               FROM {forum} f
2133                                    JOIN {forum_discussions} d ON d.forum = f.id
2134                                    JOIN {forum_posts} p       ON p.discussion = d.id
2135                                    JOIN {user} u              ON u.id = p.userid
2136                              WHERE f.id = ?
2137                                    AND p.userid = ?
2138                                    $timedsql
2139                           ORDER BY p.modified ASC", $params);
2142 /**
2143  * Get all the discussions user participated in
2144  *
2145  * @global object
2146  * @global object
2147  * @uses CONTEXT_MODULE
2148  * @param int $forumid
2149  * @param int $userid
2150  * @return array Array or false
2151  */
2152 function forum_get_user_involved_discussions($forumid, $userid) {
2153     global $CFG, $DB;
2155     $timedsql = "";
2156     $params = array($forumid, $userid);
2157     if (!empty($CFG->forum_enabletimedposts)) {
2158         $cm = get_coursemodule_from_instance('forum', $forumid);
2159         if (!has_capability('mod/forum:viewhiddentimedposts' , get_context_instance(CONTEXT_MODULE, $cm->id))) {
2160             $now = time();
2161             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2162             $params[] = $now;
2163             $params[] = $now;
2164         }
2165     }
2167     return $DB->get_records_sql("SELECT DISTINCT d.*
2168                               FROM {forum} f
2169                                    JOIN {forum_discussions} d ON d.forum = f.id
2170                                    JOIN {forum_posts} p       ON p.discussion = d.id
2171                              WHERE f.id = ?
2172                                    AND p.userid = ?
2173                                    $timedsql", $params);
2176 /**
2177  * Get all the posts for a user in a forum suitable for forum_print_post
2178  *
2179  * @global object
2180  * @global object
2181  * @param int $forumid
2182  * @param int $userid
2183  * @return array of counts or false
2184  */
2185 function forum_count_user_posts($forumid, $userid) {
2186     global $CFG, $DB;
2188     $timedsql = "";
2189     $params = array($forumid, $userid);
2190     if (!empty($CFG->forum_enabletimedposts)) {
2191         $cm = get_coursemodule_from_instance('forum', $forumid);
2192         if (!has_capability('mod/forum:viewhiddentimedposts' , get_context_instance(CONTEXT_MODULE, $cm->id))) {
2193             $now = time();
2194             $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
2195             $params[] = $now;
2196             $params[] = $now;
2197         }
2198     }
2200     return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
2201                              FROM {forum} f
2202                                   JOIN {forum_discussions} d ON d.forum = f.id
2203                                   JOIN {forum_posts} p       ON p.discussion = d.id
2204                                   JOIN {user} u              ON u.id = p.userid
2205                             WHERE f.id = ?
2206                                   AND p.userid = ?
2207                                   $timedsql", $params);
2210 /**
2211  * Given a log entry, return the forum post details for it.
2212  *
2213  * @global object
2214  * @global object
2215  * @param object $log
2216  * @return array|null
2217  */
2218 function forum_get_post_from_log($log) {
2219     global $CFG, $DB;
2221     if ($log->action == "add post") {
2223         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
2224                                            u.firstname, u.lastname, u.email, u.picture
2225                                  FROM {forum_discussions} d,
2226                                       {forum_posts} p,
2227                                       {forum} f,
2228                                       {user} u
2229                                 WHERE p.id = ?
2230                                   AND d.id = p.discussion
2231                                   AND p.userid = u.id
2232                                   AND u.deleted <> '1'
2233                                   AND f.id = d.forum", array($log->info));
2236     } else if ($log->action == "add discussion") {
2238         return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
2239                                            u.firstname, u.lastname, u.email, u.picture
2240                                  FROM {forum_discussions} d,
2241                                       {forum_posts} p,
2242                                       {forum} f,
2243                                       {user} u
2244                                 WHERE d.id = ?
2245                                   AND d.firstpost = p.id
2246                                   AND p.userid = u.id
2247                                   AND u.deleted <> '1'
2248                                   AND f.id = d.forum", array($log->info));
2249     }
2250     return NULL;
2253 /**
2254  * Given a discussion id, return the first post from the discussion
2255  *
2256  * @global object
2257  * @global object
2258  * @param int $dicsussionid
2259  * @return array
2260  */
2261 function forum_get_firstpost_from_discussion($discussionid) {
2262     global $CFG, $DB;
2264     return $DB->get_record_sql("SELECT p.*
2265                              FROM {forum_discussions} d,
2266                                   {forum_posts} p
2267                             WHERE d.id = ?
2268                               AND d.firstpost = p.id ", array($discussionid));
2271 /**
2272  * Returns an array of counts of replies to each discussion
2273  *
2274  * @global object
2275  * @global object
2276  * @param int $forumid
2277  * @param string $forumsort
2278  * @param int $limit
2279  * @param int $page
2280  * @param int $perpage
2281  * @return array
2282  */
2283 function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $page=-1, $perpage=0) {
2284     global $CFG, $DB;
2286     if ($limit > 0) {
2287         $limitfrom = 0;
2288         $limitnum  = $limit;
2289     } else if ($page != -1) {
2290         $limitfrom = $page*$perpage;
2291         $limitnum  = $perpage;
2292     } else {
2293         $limitfrom = 0;
2294         $limitnum  = 0;
2295     }
2297     if ($forumsort == "") {
2298         $orderby = "";
2299         $groupby = "";
2301     } else {
2302         $orderby = "ORDER BY $forumsort";
2303         $groupby = ", ".strtolower($forumsort);
2304         $groupby = str_replace('desc', '', $groupby);
2305         $groupby = str_replace('asc', '', $groupby);
2306     }
2308     if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
2309         $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
2310                   FROM {forum_posts} p
2311                        JOIN {forum_discussions} d ON p.discussion = d.id
2312                  WHERE p.parent > 0 AND d.forum = ?
2313               GROUP BY p.discussion";
2314         return $DB->get_records_sql($sql, array($forumid));
2316     } else {
2317         $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
2318                   FROM {forum_posts} p
2319                        JOIN {forum_discussions} d ON p.discussion = d.id
2320                  WHERE d.forum = ?
2321               GROUP BY p.discussion $groupby
2322               $orderby";
2323         return $DB->get_records_sql("SELECT * FROM ($sql) sq", array($forumid), $limitfrom, $limitnum);
2324     }
2327 /**
2328  * @global object
2329  * @global object
2330  * @global object
2331  * @staticvar array $cache
2332  * @param object $forum
2333  * @param object $cm
2334  * @param object $course
2335  * @return mixed
2336  */
2337 function forum_count_discussions($forum, $cm, $course) {
2338     global $CFG, $DB, $USER;
2340     static $cache = array();
2342     $now = round(time(), -2); // db cache friendliness
2344     $params = array($course->id);
2346     if (!isset($cache[$course->id])) {
2347         if (!empty($CFG->forum_enabletimedposts)) {
2348             $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
2349             $params[] = $now;
2350             $params[] = $now;
2351         } else {
2352             $timedsql = "";
2353         }
2355         $sql = "SELECT f.id, COUNT(d.id) as dcount
2356                   FROM {forum} f
2357                        JOIN {forum_discussions} d ON d.forum = f.id
2358                  WHERE f.course = ?
2359                        $timedsql
2360               GROUP BY f.id";
2362         if ($counts = $DB->get_records_sql($sql, $params)) {
2363             foreach ($counts as $count) {
2364                 $counts[$count->id] = $count->dcount;
2365             }
2366             $cache[$course->id] = $counts;
2367         } else {
2368             $cache[$course->id] = array();
2369         }
2370     }
2372     if (empty($cache[$course->id][$forum->id])) {
2373         return 0;
2374     }
2376     $groupmode = groups_get_activity_groupmode($cm, $course);
2378     if ($groupmode != SEPARATEGROUPS) {
2379         return $cache[$course->id][$forum->id];
2380     }
2382     if (has_capability('moodle/site:accessallgroups', get_context_instance(CONTEXT_MODULE, $cm->id))) {
2383         return $cache[$course->id][$forum->id];
2384     }
2386     require_once($CFG->dirroot.'/course/lib.php');
2388     $modinfo = get_fast_modinfo($course);
2389     if (is_null($modinfo->groups)) {
2390         $modinfo->groups = groups_get_user_groups($course->id, $USER->id);
2391     }
2393     if (array_key_exists($cm->groupingid, $modinfo->groups)) {
2394         $mygroups = $modinfo->groups[$cm->groupingid];
2395     } else {
2396         $mygroups = false; // Will be set below
2397     }
2399     // add all groups posts
2400     if (empty($mygroups)) {
2401         $mygroups = array(-1=>-1);
2402     } else {
2403         $mygroups[-1] = -1;
2404     }
2406     list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
2407     $params[] = $forum->id;
2409     if (!empty($CFG->forum_enabletimedposts)) {
2410         $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
2411         $params[] = $now;
2412         $params[] = $now;
2413     } else {
2414         $timedsql = "";
2415     }
2417     $sql = "SELECT COUNT(d.id)
2418               FROM {forum_discussions} d
2419              WHERE d.groupid $mygroups_sql AND d.forum = ?
2420                    $timedsql";
2422     return $DB->get_field_sql($sql, $params);
2425 /**
2426  * How many posts by other users are unrated by a given user in the given discussion?
2427  *
2428  * TODO: Is this function still used anywhere?
2429  *
2430  * @param int $discussionid
2431  * @param int $userid
2432  * @return mixed
2433  */
2434 function forum_count_unrated_posts($discussionid, $userid) {
2435     global $CFG, $DB;
2437     $sql = "SELECT COUNT(*) as num
2438               FROM {forum_posts}
2439              WHERE parent > 0
2440                AND discussion = :discussionid
2441                AND userid <> :userid";
2442     $params = array('discussionid' => $discussionid, 'userid' => $userid);
2443     $posts = $DB->get_record_sql($sql, $params);
2444     if ($posts) {
2445         $sql = "SELECT count(*) as num
2446                   FROM {forum_posts} p,
2447                        {rating} r
2448                  WHERE p.discussion = :discussionid AND
2449                        p.id = r.itemid AND
2450                        r.userid = userid AND
2451                        r.component = 'mod_forum' AND
2452                        r.ratingarea = 'post'";
2453         $rated = $DB->get_record_sql($sql, $params);
2454         if ($rated) {
2455             if ($posts->num > $rated->num) {
2456                 return $posts->num - $rated->num;
2457             } else {
2458                 return 0;    // Just in case there was a counting error
2459             }
2460         } else {
2461             return $posts->num;
2462         }
2463     } else {
2464         return 0;
2465     }
2468 /**
2469  * Get all discussions in a forum
2470  *
2471  * @global object
2472  * @global object
2473  * @global object
2474  * @uses CONTEXT_MODULE
2475  * @uses VISIBLEGROUPS
2476  * @param object $cm
2477  * @param string $forumsort
2478  * @param bool $fullpost
2479  * @param int $unused
2480  * @param int $limit
2481  * @param bool $userlastmodified
2482  * @param int $page
2483  * @param int $perpage
2484  * @return array
2485  */
2486 function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=true, $unused=-1, $limit=-1, $userlastmodified=false, $page=-1, $perpage=0) {
2487     global $CFG, $DB, $USER;
2489     $timelimit = '';
2491     $now = round(time(), -2);
2492     $params = array($cm->instance);
2494     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2496     if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
2497         return array();
2498     }
2500     if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
2502         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2503             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2504             $params[] = $now;
2505             $params[] = $now;
2506             if (isloggedin()) {
2507                 $timelimit .= " OR d.userid = ?";
2508                 $params[] = $USER->id;
2509             }
2510             $timelimit .= ")";
2511         }
2512     }
2514     if ($limit > 0) {
2515         $limitfrom = 0;
2516         $limitnum  = $limit;
2517     } else if ($page != -1) {
2518         $limitfrom = $page*$perpage;
2519         $limitnum  = $perpage;
2520     } else {
2521         $limitfrom = 0;
2522         $limitnum  = 0;
2523     }
2525     $groupmode    = groups_get_activity_groupmode($cm);
2526     $currentgroup = groups_get_activity_group($cm);
2528     if ($groupmode) {
2529         if (empty($modcontext)) {
2530             $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2531         }
2533         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2534             if ($currentgroup) {
2535                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2536                 $params[] = $currentgroup;
2537             } else {
2538                 $groupselect = "";
2539             }
2541         } else {
2542             //seprate groups without access all
2543             if ($currentgroup) {
2544                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2545                 $params[] = $currentgroup;
2546             } else {
2547                 $groupselect = "AND d.groupid = -1";
2548             }
2549         }
2550     } else {
2551         $groupselect = "";
2552     }
2555     if (empty($forumsort)) {
2556         $forumsort = "d.timemodified DESC";
2557     }
2558     if (empty($fullpost)) {
2559         $postdata = "p.id,p.subject,p.modified,p.discussion,p.userid";
2560     } else {
2561         $postdata = "p.*";
2562     }
2564     if (empty($userlastmodified)) {  // We don't need to know this
2565         $umfields = "";
2566         $umtable  = "";
2567     } else {
2568         $umfields = ", um.firstname AS umfirstname, um.lastname AS umlastname";
2569         $umtable  = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
2570     }
2572     $sql = "SELECT $postdata, d.name, d.timemodified, d.usermodified, d.groupid, d.timestart, d.timeend,
2573                    u.firstname, u.lastname, u.email, u.picture, u.imagealt $umfields
2574               FROM {forum_discussions} d
2575                    JOIN {forum_posts} p ON p.discussion = d.id
2576                    JOIN {user} u ON p.userid = u.id
2577                    $umtable
2578              WHERE d.forum = ? AND p.parent = 0
2579                    $timelimit $groupselect
2580           ORDER BY $forumsort";
2581     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
2584 /**
2585  *
2586  * @global object
2587  * @global object
2588  * @global object
2589  * @uses CONTEXT_MODULE
2590  * @uses VISIBLEGROUPS
2591  * @param object $cm
2592  * @return array
2593  */
2594 function forum_get_discussions_unread($cm) {
2595     global $CFG, $DB, $USER;
2597     $now = round(time(), -2);
2598     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2600     $params = array();
2601     $groupmode    = groups_get_activity_groupmode($cm);
2602     $currentgroup = groups_get_activity_group($cm);
2604     if ($groupmode) {
2605         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2607         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2608             if ($currentgroup) {
2609                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2610                 $params['currentgroup'] = $currentgroup;
2611             } else {
2612                 $groupselect = "";
2613             }
2615         } else {
2616             //separate groups without access all
2617             if ($currentgroup) {
2618                 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
2619                 $params['currentgroup'] = $currentgroup;
2620             } else {
2621                 $groupselect = "AND d.groupid = -1";
2622             }
2623         }
2624     } else {
2625         $groupselect = "";
2626     }
2628     if (!empty($CFG->forum_enabletimedposts)) {
2629         $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
2630         $params['now1'] = $now;
2631         $params['now2'] = $now;
2632     } else {
2633         $timedsql = "";
2634     }
2636     $sql = "SELECT d.id, COUNT(p.id) AS unread
2637               FROM {forum_discussions} d
2638                    JOIN {forum_posts} p     ON p.discussion = d.id
2639                    LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
2640              WHERE d.forum = {$cm->instance}
2641                    AND p.modified >= :cutoffdate AND r.id is NULL
2642                    $groupselect
2643                    $timedsql
2644           GROUP BY d.id";
2645     $params['cutoffdate'] = $cutoffdate;
2647     if ($unreads = $DB->get_records_sql($sql, $params)) {
2648         foreach ($unreads as $unread) {
2649             $unreads[$unread->id] = $unread->unread;
2650         }
2651         return $unreads;
2652     } else {
2653         return array();
2654     }
2657 /**
2658  * @global object
2659  * @global object
2660  * @global object
2661  * @uses CONEXT_MODULE
2662  * @uses VISIBLEGROUPS
2663  * @param object $cm
2664  * @return array
2665  */
2666 function forum_get_discussions_count($cm) {
2667     global $CFG, $DB, $USER;
2669     $now = round(time(), -2);
2670     $params = array($cm->instance);
2671     $groupmode    = groups_get_activity_groupmode($cm);
2672     $currentgroup = groups_get_activity_group($cm);
2674     if ($groupmode) {
2675         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2677         if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2678             if ($currentgroup) {
2679                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2680                 $params[] = $currentgroup;
2681             } else {
2682                 $groupselect = "";
2683             }
2685         } else {
2686             //seprate groups without access all
2687             if ($currentgroup) {
2688                 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2689                 $params[] = $currentgroup;
2690             } else {
2691                 $groupselect = "AND d.groupid = -1";
2692             }
2693         }
2694     } else {
2695         $groupselect = "";
2696     }
2698     $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
2700     $timelimit = "";
2702     if (!empty($CFG->forum_enabletimedposts)) {
2704         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2706         if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2707             $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2708             $params[] = $now;
2709             $params[] = $now;
2710             if (isloggedin()) {
2711                 $timelimit .= " OR d.userid = ?";
2712                 $params[] = $USER->id;
2713             }
2714             $timelimit .= ")";
2715         }
2716     }
2718     $sql = "SELECT COUNT(d.id)
2719               FROM {forum_discussions} d
2720                    JOIN {forum_posts} p ON p.discussion = d.id
2721              WHERE d.forum = ? AND p.parent = 0
2722                    $groupselect $timelimit";
2724     return $DB->get_field_sql($sql, $params);
2728 /**
2729  * Get all discussions started by a particular user in a course (or group)
2730  * This function no longer used ...
2731  *
2732  * @todo Remove this function if no longer used
2733  * @global object
2734  * @global object
2735  * @param int $courseid
2736  * @param int $userid
2737  * @param int $groupid
2738  * @return array
2739  */
2740 function forum_get_user_discussions($courseid, $userid, $groupid=0) {
2741     global $CFG, $DB;
2742     $params = array($courseid, $userid);
2743     if ($groupid) {
2744         $groupselect = " AND d.groupid = ? ";
2745         $params[] = $groupid;
2746     } else  {
2747         $groupselect = "";
2748     }
2750     return $DB->get_records_sql("SELECT p.*, d.groupid, u.firstname, u.lastname, u.email, u.picture, u.imagealt,
2751                                    f.type as forumtype, f.name as forumname, f.id as forumid
2752                               FROM {forum_discussions} d,
2753                                    {forum_posts} p,
2754                                    {user} u,
2755                                    {forum} f
2756                              WHERE d.course = ?
2757                                AND p.discussion = d.id
2758                                AND p.parent = 0
2759                                AND p.userid = u.id
2760                                AND u.id = ?
2761                                AND d.forum = f.id $groupselect
2762                           ORDER BY p.created DESC", $params);
2765 /**
2766  * Get the list of potential subscribers to a forum.
2767  *
2768  * @param object $forumcontext the forum context.
2769  * @param integer $groupid the id of a group, or 0 for all groups.
2770  * @param string $fields the list of fields to return for each user. As for get_users_by_capability.
2771  * @param string $sort sort order. As for get_users_by_capability.
2772  * @return array list of users.
2773  */
2774 function forum_get_potential_subscribers($forumcontext, $groupid, $fields, $sort) {
2775     global $DB;
2777     // only active enrolled users or everybody on the frontpage
2778     list($esql, $params) = get_enrolled_sql($forumcontext, '', $groupid, true);
2780     $sql = "SELECT $fields
2781               FROM {user} u
2782               JOIN ($esql) je ON je.id = u.id";
2783     if ($sort) {
2784         $sql = "$sql ORDER BY $sort";
2785     } else {
2786         $sql = "$sql ORDER BY u.lastname ASC, u.firstname ASC";
2787     }
2789     return $DB->get_records_sql($sql, $params);
2792 /**
2793  * Returns list of user objects that are subscribed to this forum
2794  *
2795  * @global object
2796  * @global object
2797  * @param object $course the course
2798  * @param forum $forum the forum
2799  * @param integer $groupid group id, or 0 for all.
2800  * @param object $context the forum context, to save re-fetching it where possible.
2801  * @param string $fields requested user fields (with "u." table prefix)
2802  * @return array list of users.
2803  */
2804 function forum_subscribed_users($course, $forum, $groupid=0, $context = null, $fields = null) {
2805     global $CFG, $DB;
2807     if (empty($fields)) {
2808         $fields ="u.id,
2809                   u.username,
2810                   u.firstname,
2811                   u.lastname,
2812                   u.maildisplay,
2813                   u.mailformat,
2814                   u.maildigest,
2815                   u.imagealt,
2816                   u.email,
2817                   u.emailstop,
2818                   u.city,
2819                   u.country,
2820                   u.lastaccess,
2821                   u.lastlogin,
2822                   u.picture,
2823                   u.timezone,
2824                   u.theme,
2825                   u.lang,
2826                   u.trackforums,
2827                   u.mnethostid";
2828     }
2830     if (empty($context)) {
2831         $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id);
2832         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
2833     }
2835     if (forum_is_forcesubscribed($forum)) {
2836         $results = forum_get_potential_subscribers($context, $groupid, $fields, "u.email ASC");
2838     } else {
2839         // only active enrolled users or everybody on the frontpage
2840         list($esql, $params) = get_enrolled_sql($context, '', $groupid, true);
2841         $params['forumid'] = $forum->id;
2842         $results = $DB->get_records_sql("SELECT $fields
2843                                            FROM {user} u
2844                                            JOIN ($esql) je ON je.id = u.id
2845                                            JOIN {forum_subscriptions} s ON s.userid = u.id
2846                                           WHERE s.forum = :forumid
2847                                        ORDER BY u.email ASC", $params);
2848     }
2850     // Guest user should never be subscribed to a forum.
2851     unset($results[$CFG->siteguest]);
2853     return $results;
2858 // OTHER FUNCTIONS ///////////////////////////////////////////////////////////
2861 /**
2862  * @global object
2863  * @global object
2864  * @param int $courseid
2865  * @param string $type
2866  */
2867 function forum_get_course_forum($courseid, $type) {
2868 // How to set up special 1-per-course forums
2869     global $CFG, $DB, $OUTPUT;
2871     if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
2872         // There should always only be ONE, but with the right combination of
2873         // errors there might be more.  In this case, just return the oldest one (lowest ID).
2874         foreach ($forums as $forum) {
2875             return $forum;   // ie the first one
2876         }
2877     }
2879     // Doesn't exist, so create one now.
2880     $forum = new stdClass();
2881     $forum->course = $courseid;
2882     $forum->type = "$type";
2883     switch ($forum->type) {
2884         case "news":
2885             $forum->name  = get_string("namenews", "forum");
2886             $forum->intro = get_string("intronews", "forum");
2887             $forum->forcesubscribe = FORUM_FORCESUBSCRIBE;
2888             $forum->assessed = 0;
2889             if ($courseid == SITEID) {
2890                 $forum->name  = get_string("sitenews");
2891                 $forum->forcesubscribe = 0;
2892             }
2893             break;
2894         case "social":
2895             $forum->name  = get_string("namesocial", "forum");
2896             $forum->intro = get_string("introsocial", "forum");
2897             $forum->assessed = 0;
2898             $forum->forcesubscribe = 0;
2899             break;
2900         case "blog":
2901             $forum->name = get_string('blogforum', 'forum');
2902             $forum->intro = get_string('introblog', 'forum');
2903             $forum->assessed = 0;
2904             $forum->forcesubscribe = 0;
2905             break;
2906         default:
2907             echo $OUTPUT->notification("That forum type doesn't exist!");
2908             return false;
2909             break;
2910     }
2912     $forum->timemodified = time();
2913     $forum->id = $DB->insert_record("forum", $forum);
2915     if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
2916         echo $OUTPUT->notification("Could not find forum module!!");
2917         return false;
2918     }
2919     $mod = new stdClass();
2920     $mod->course = $courseid;
2921     $mod->module = $module->id;
2922     $mod->instance = $forum->id;
2923     $mod->section = 0;
2924     if (! $mod->coursemodule = add_course_module($mod) ) {   // assumes course/lib.php is loaded
2925         echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
2926         return false;
2927     }
2928     if (! $sectionid = add_mod_to_section($mod) ) {   // assumes course/lib.php is loaded
2929         echo $OUTPUT->notification("Could not add the new course module to that section");
2930         return false;
2931     }
2932     $DB->set_field("course_modules", "section", $sectionid, array("id" => $mod->coursemodule));
2934     include_once("$CFG->dirroot/course/lib.php");
2935     rebuild_course_cache($courseid);
2937     return $DB->get_record("forum", array("id" => "$forum->id"));
2941 /**
2942  * Given the data about a posting, builds up the HTML to display it and
2943  * returns the HTML in a string.  This is designed for sending via HTML email.
2944  *
2945  * @global object
2946  * @param object $course
2947  * @param object $cm
2948  * @param object $forum
2949  * @param object $discussion
2950  * @param object $post
2951  * @param object $userform
2952  * @param object $userto
2953  * @param bool $ownpost
2954  * @param bool $reply
2955  * @param bool $link
2956  * @param bool $rate
2957  * @param string $footer
2958  * @return string
2959  */
2960 function forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfrom, $userto,
2961                               $ownpost=false, $reply=false, $link=false, $rate=false, $footer="") {
2963     global $CFG, $OUTPUT;
2965     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
2967     if (!isset($userto->viewfullnames[$forum->id])) {
2968         $viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
2969     } else {
2970         $viewfullnames = $userto->viewfullnames[$forum->id];
2971     }
2973     // add absolute file links
2974     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
2976     // format the post body
2977     $options = new stdClass();
2978     $options->para = true;
2979     $formattedtext = format_text($post->message, $post->messageformat, $options, $course->id);
2981     $output = '<table border="0" cellpadding="3" cellspacing="0" class="forumpost">';
2983     $output .= '<tr class="header"><td width="35" valign="top" class="picture left">';
2984     $output .= $OUTPUT->user_picture($userfrom, array('courseid'=>$course->id));
2985     $output .= '</td>';
2987     if ($post->parent) {
2988         $output .= '<td class="topic">';
2989     } else {
2990         $output .= '<td class="topic starter">';
2991     }
2992     $output .= '<div class="subject">'.format_string($post->subject).'</div>';
2994     $fullname = fullname($userfrom, $viewfullnames);
2995     $by = new stdClass();
2996     $by->name = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$userfrom->id.'&amp;course='.$course->id.'">'.$fullname.'</a>';
2997     $by->date = userdate($post->modified, '', $userto->timezone);
2998     $output .= '<div class="author">'.get_string('bynameondate', 'forum', $by).'</div>';
3000     $output .= '</td></tr>';
3002     $output .= '<tr><td class="left side" valign="top">';
3004     if (isset($userfrom->groups)) {
3005         $groups = $userfrom->groups[$forum->id];
3006     } else {
3007         $groups = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
3008     }
3010     if ($groups) {
3011         $output .= print_group_picture($groups, $course->id, false, true, true);
3012     } else {
3013         $output .= '&nbsp;';
3014     }
3016     $output .= '</td><td class="content">';
3018     $attachments = forum_print_attachments($post, $cm, 'html');
3019     if ($attachments !== '') {
3020         $output .= '<div class="attachments">';
3021         $output .= $attachments;
3022         $output .= '</div>';
3023     }
3025     $output .= $formattedtext;
3027 // Commands
3028     $commands = array();
3030     if ($post->parent) {
3031         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.
3032                       $post->discussion.'&amp;parent='.$post->parent.'">'.get_string('parent', 'forum').'</a>';
3033     }
3035     if ($reply) {
3036         $commands[] = '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/post.php?reply='.$post->id.'">'.
3037                       get_string('reply', 'forum').'</a>';
3038     }
3040     $output .= '<div class="commands">';
3041     $output .= implode(' | ', $commands);
3042     $output .= '</div>';
3044 // Context link to post if required
3045     if ($link) {
3046         $output .= '<div class="link">';
3047         $output .= '<a target="_blank" href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id.'">'.
3048                      get_string('postincontext', 'forum').'</a>';
3049         $output .= '</div>';
3050     }
3052     if ($footer) {
3053         $output .= '<div class="footer">'.$footer.'</div>';
3054     }
3055     $output .= '</td></tr></table>'."\n\n";
3057     return $output;
3060 /**
3061  * Print a forum post
3062  *
3063  * @global object
3064  * @global object
3065  * @uses FORUM_MODE_THREADED
3066  * @uses PORTFOLIO_FORMAT_PLAINHTML
3067  * @uses PORTFOLIO_FORMAT_FILE
3068  * @uses PORTFOLIO_FORMAT_RICHHTML
3069  * @uses PORTFOLIO_ADD_TEXT_LINK
3070  * @uses CONTEXT_MODULE
3071  * @param object $post The post to print.
3072  * @param object $discussion
3073  * @param object $forum
3074  * @param object $cm
3075  * @param object $course
3076  * @param boolean $ownpost Whether this post belongs to the current user.
3077  * @param boolean $reply Whether to print a 'reply' link at the bottom of the message.
3078  * @param boolean $link Just print a shortened version of the post as a link to the full post.
3079  * @param string $footer Extra stuff to print after the message.
3080  * @param string $highlight Space-separated list of terms to highlight.
3081  * @param int $post_read true, false or -99. If we already know whether this user
3082  *          has read this post, pass that in, otherwise, pass in -99, and this
3083  *          function will work it out.
3084  * @param boolean $dummyifcantsee When forum_user_can_see_post says that
3085  *          the current user can't see this post, if this argument is true
3086  *          (the default) then print a dummy 'you can't see this post' post.
3087  *          If false, don't output anything at all.
3088  * @param bool|null $istracked
3089  * @return void
3090  */
3091 function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=false, $reply=false, $link=false,
3092                           $footer="", $highlight="", $postisread=null, $dummyifcantsee=true, $istracked=null, $return=false) {
3093     global $USER, $CFG, $OUTPUT;
3095     require_once($CFG->libdir . '/filelib.php');
3097     // String cache
3098     static $str;
3100     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
3102     $post->course = $course->id;
3103     $post->forum  = $forum->id;
3104     $post->message = file_rewrite_pluginfile_urls($post->message, 'pluginfile.php', $modcontext->id, 'mod_forum', 'post', $post->id);
3106     // caching
3107     if (!isset($cm->cache)) {
3108         $cm->cache = new stdClass;
3109     }
3111     if (!isset($cm->cache->caps)) {
3112         $cm->cache->caps = array();
3113         $cm->cache->caps['mod/forum:viewdiscussion']   = has_capability('mod/forum:viewdiscussion', $modcontext);
3114         $cm->cache->caps['moodle/site:viewfullnames']  = has_capability('moodle/site:viewfullnames', $modcontext);
3115         $cm->cache->caps['mod/forum:editanypost']      = has_capability('mod/forum:editanypost', $modcontext);
3116         $cm->cache->caps['mod/forum:splitdiscussions'] = has_capability('mod/forum:splitdiscussions', $modcontext);
3117         $cm->cache->caps['mod/forum:deleteownpost']    = has_capability('mod/forum:deleteownpost', $modcontext);
3118         $cm->cache->caps['mod/forum:deleteanypost']    = has_capability('mod/forum:deleteanypost', $modcontext);
3119         $cm->cache->caps['mod/forum:viewanyrating']    = has_capability('mod/forum:viewanyrating', $modcontext);
3120         $cm->cache->caps['mod/forum:exportpost']       = has_capability('mod/forum:exportpost', $modcontext);
3121         $cm->cache->caps['mod/forum:exportownpost']    = has_capability('mod/forum:exportownpost', $modcontext);
3122     }
3124     if (!isset($cm->uservisible)) {
3125         $cm->uservisible = coursemodule_visible_for_user($cm);
3126     }
3128     if ($istracked && is_null($postisread)) {
3129         $postisread = forum_tp_is_post_read($USER->id, $post);
3130     }
3132     if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
3133         $output = '';
3134         if (!$dummyifcantsee) {
3135             if ($return) {
3136                 return $output;
3137             }
3138             echo $output;
3139             return;
3140         }
3141         $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3142         $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'));
3143         $output .= html_writer::start_tag('div', array('class'=>'row header'));
3144         $output .= html_writer::tag('div', '', array('class'=>'left picture')); // Picture
3145         if ($post->parent) {
3146             $output .= html_writer::start_tag('div', array('class'=>'topic'));
3147         } else {
3148             $output .= html_writer::start_tag('div', array('class'=>'topic starter'));
3149         }
3150         $output .= html_writer::tag('div', get_string('forumsubjecthidden','forum'), array('class'=>'subject')); // Subject
3151         $output .= html_writer::tag('div', get_string('forumauthorhidden','forum'), array('class'=>'author')); // author
3152         $output .= html_writer::end_tag('div');
3153         $output .= html_writer::end_tag('div'); // row
3154         $output .= html_writer::start_tag('div', array('class'=>'row'));
3155         $output .= html_writer::tag('div', '&nbsp;', array('class'=>'left side')); // Groups
3156         $output .= html_writer::tag('div', get_string('forumbodyhidden','forum'), array('class'=>'content')); // Content
3157         $output .= html_writer::end_tag('div'); // row
3158         $output .= html_writer::end_tag('div'); // forumpost
3160         if ($return) {
3161             return $output;
3162         }
3163         echo $output;
3164         return;
3165     }
3167     if (empty($str)) {
3168         $str = new stdClass;
3169         $str->edit         = get_string('edit', 'forum');
3170         $str->delete       = get_string('delete', 'forum');
3171         $str->reply        = get_string('reply', 'forum');
3172         $str->parent       = get_string('parent', 'forum');
3173         $str->pruneheading = get_string('pruneheading', 'forum');
3174         $str->prune        = get_string('prune', 'forum');
3175         $str->displaymode     = get_user_preferences('forum_displaymode', $CFG->forum_displaymode);
3176         $str->markread     = get_string('markread', 'forum');
3177         $str->markunread   = get_string('markunread', 'forum');
3178     }
3180     $discussionlink = new moodle_url('/mod/forum/discuss.php', array('d'=>$post->discussion));
3182     // Build an object that represents the posting user
3183     $postuser = new stdClass;
3184     $postuser->id        = $post->userid;
3185     $postuser->firstname = $post->firstname;
3186     $postuser->lastname  = $post->lastname;
3187     $postuser->imagealt  = $post->imagealt;
3188     $postuser->picture   = $post->picture;
3189     $postuser->email     = $post->email;
3190     // Some handy things for later on
3191     $postuser->fullname    = fullname($postuser, $cm->cache->caps['moodle/site:viewfullnames']);
3192     $postuser->profilelink = new moodle_url('/user/view.php', array('id'=>$post->userid, 'course'=>$course->id));
3194     // Prepare the groups the posting user belongs to
3195     if (isset($cm->cache->usersgroups)) {
3196         $groups = array();
3197         if (isset($cm->cache->usersgroups[$post->userid])) {
3198             foreach ($cm->cache->usersgroups[$post->userid] as $gid) {
3199                 $groups[$gid] = $cm->cache->groups[$gid];
3200             }
3201         }
3202     } else {
3203         $groups = groups_get_all_groups($course->id, $post->userid, $cm->groupingid);
3204     }
3206     // Prepare the attachements for the post, files then images
3207     list($attachments, $attachedimages) = forum_print_attachments($post, $cm, 'separateimages');
3209     // Determine if we need to shorten this post
3210     $shortenpost = ($link && (strlen(strip_tags($post->message)) > $CFG->forum_longpost));
3213     // Prepare an array of commands
3214     $commands = array();
3216     // SPECIAL CASE: The front page can display a news item post to non-logged in users.
3217     // Don't display the mark read / unread controls in this case.
3218     if ($istracked && $CFG->forum_usermarksread && isloggedin()) {
3219         $url = new moodle_url($discussionlink, array('postid'=>$post->id, 'mark'=>'unread'));
3220         $text = $str->markunread;
3221         if (!$postisread) {
3222             $url->param('mark', 'read');
3223             $text = $str->markread;
3224         }
3225         if ($str->displaymode == FORUM_MODE_THREADED) {
3226             $url->param('parent', $post->parent);
3227         } else {
3228             $url->set_anchor('p'.$post->id);
3229         }
3230         $commands[] = array('url'=>$url, 'text'=>$text);
3231     }
3233     // Zoom in to the parent specifically
3234     if ($post->parent) {
3235         $url = new moodle_url($discussionlink);
3236         if ($str->displaymode == FORUM_MODE_THREADED) {
3237             $url->param('parent', $post->parent);
3238         } else {
3239             $url->set_anchor('p'.$post->parent);
3240         }
3241         $commands[] = array('url'=>$url, 'text'=>$str->parent);
3242     }
3244     // Hack for allow to edit news posts those are not displayed yet until they are displayed
3245     $age = time() - $post->created;
3246     if (!$post->parent && $forum->type == 'news' && $discussion->timestart > time()) {
3247         $age = 0;
3248     }
3249     if (($ownpost && $age < $CFG->maxeditingtime) || $cm->cache->caps['mod/forum:editanypost']) {
3250         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('edit'=>$post->id)), 'text'=>$str->edit);
3251     }
3253     if ($cm->cache->caps['mod/forum:splitdiscussions'] && $post->parent && $forum->type != 'single') {
3254         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('prune'=>$post->id)), 'text'=>$str->prune, 'title'=>$str->pruneheading);
3255     }
3257     if (($ownpost && $age < $CFG->maxeditingtime && $cm->cache->caps['mod/forum:deleteownpost']) || $cm->cache->caps['mod/forum:deleteanypost']) {
3258         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('delete'=>$post->id)), 'text'=>$str->delete);
3259     }
3261     if ($reply) {
3262         $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('reply'=>$post->id)), 'text'=>$str->reply);
3263     }
3265     if ($CFG->enableportfolios && ($cm->cache->caps['mod/forum:exportpost'] || ($ownpost && $cm->cache->caps['mod/forum:exportownpost']))) {
3266         $p = array('postid' => $post->id);
3267         require_once($CFG->libdir.'/portfoliolib.php');
3268         $button = new portfolio_add_button();
3269         $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id), '/mod/forum/locallib.php');
3270         if (empty($attachments)) {
3271             $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3272         } else {
3273             $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3274         }
3276         $porfoliohtml = $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3277         if (!empty($porfoliohtml)) {
3278             $commands[] = $porfoliohtml;
3279         }
3280     }
3281     // Finished building commands
3284     // Begin output
3286     $output  = '';
3288     if ($istracked) {
3289         if ($postisread) {
3290             $forumpostclass = ' read';
3291         } else {
3292             $forumpostclass = ' unread';
3293             $output .= html_writer::tag('a', '', array('name'=>'unread'));
3294         }
3295     } else {
3296         // ignore trackign status if not tracked or tracked param missing
3297         $forumpostclass = '';
3298     }
3300     $topicclass = '';
3301     if (empty($post->parent)) {
3302         $topicclass = ' firstpost starter';
3303     }
3305     $output .= html_writer::tag('a', '', array('id'=>'p'.$post->id));
3306     $output .= html_writer::start_tag('div', array('class'=>'forumpost clearfix'.$forumpostclass.$topicclass));
3307     $output .= html_writer::start_tag('div', array('class'=>'row header clearfix'));
3308     $output .= html_writer::start_tag('div', array('class'=>'left picture'));
3309     $output .= $OUTPUT->user_picture($postuser, array('courseid'=>$course->id));
3310     $output .= html_writer::end_tag('div');
3313     $output .= html_writer::start_tag('div', array('class'=>'topic'.$topicclass));
3315     $postsubject = $post->subject;
3316     if (empty($post->subjectnoformat)) {
3317         $postsubject = format_string($postsubject);
3318     }
3319     $output .= html_writer::tag('div', $postsubject, array('class'=>'subject'));
3321     $by = new stdClass();
3322     $by->name = html_writer::link($postuser->profilelink, $postuser->fullname);
3323     $by->date = userdate($post->modified);
3324     $output .= html_writer::tag('div', get_string('bynameondate', 'forum', $by), array('class'=>'author'));
3326     $output .= html_writer::end_tag('div'); //topic
3327     $output .= html_writer::end_tag('div'); //row
3329     $output .= html_writer::start_tag('div', array('class'=>'row maincontent clearfix'));
3330     $output .= html_writer::start_tag('div', array('class'=>'left'));
3332     $groupoutput = '';
3333     if ($groups) {
3334         $groupoutput = print_group_picture($groups, $course->id, false, true, true);
3335     }
3336     if (empty($groupoutput)) {
3337         $groupoutput = '&nbsp;';
3338     }
3339     $output .= html_writer::tag('div', $groupoutput, array('class'=>'grouppictures'));
3341     $output .= html_writer::end_tag('div'); //left side
3342     $output .= html_writer::start_tag('div', array('class'=>'no-overflow'));
3343     $output .= html_writer::start_tag('div', array('class'=>'content'));
3344     if (!empty($attachments)) {
3345         $output .= html_writer::tag('div', $attachments, array('class'=>'attachments'));
3346     }
3348     $options = new stdClass;
3349     $options->para    = false;
3350     $options->trusted = $post->messagetrust;
3351     $options->context = $modcontext;
3352     if ($shortenpost) {
3353         // Prepare shortened version
3354         $postclass    = 'shortenedpost';
3355         $postcontent  = format_text(forum_shorten_post($post->message), $post->messageformat, $options, $course->id);
3356         $postcontent .= html_writer::link($discussionlink, get_string('readtherest', 'forum'));
3357         $postcontent .= html_writer::tag('span', '('.get_string('numwords', 'moodle', count_words(strip_tags($post->message))).')...', array('class'=>'post-word-count'));
3358     } else {
3359         // Prepare whole post
3360         $postclass    = 'fullpost';
3361         $postcontent  = format_text($post->message, $post->messageformat, $options, $course->id);
3362         if (!empty($highlight)) {
3363             $postcontent = highlight($highlight, $postcontent);
3364         }
3365         $postcontent .= html_writer::tag('div', $attachedimages, array('class'=>'attachedimages'));
3366     }
3367     // Output the post content
3368     $output .= html_writer::tag('div', $postcontent, array('class'=>'posting '.$postclass));
3369     $output .= html_writer::end_tag('div'); // Content
3370     $output .= html_writer::end_tag('div'); // Content mask
3371     $output .= html_writer::end_tag('div'); // Row
3373     $output .= html_writer::start_tag('div', array('class'=>'row side'));
3374     $output .= html_writer::tag('div','&nbsp;', array('class'=>'left'));
3375     $output .= html_writer::start_tag('div', array('class'=>'options clearfix'));
3377     // Output ratings
3378     if (!empty($post->rating)) {
3379         $output .= html_writer::tag('div', $OUTPUT->render($post->rating), array('class'=>'forum-post-rating'));
3380     }
3382     // Output the commands
3383     $commandhtml = array();
3384     foreach ($commands as $command) {
3385         if (is_array($command)) {
3386             $commandhtml[] = html_writer::link($command['url'], $command['text']);
3387         } else {
3388             $commandhtml[] = $command;
3389         }
3390     }
3391     $output .= html_writer::tag('div', implode(' | ', $commandhtml), array('class'=>'commands'));
3393     // Output link to post if required
3394     if ($link) {
3395         if ($post->replies == 1) {
3396             $replystring = get_string('repliesone', 'forum', $post->replies);
3397         } else {
3398             $replystring = get_string('repliesmany', 'forum', $post->replies);
3399         }
3401         $output .= html_writer::start_tag('div', array('class'=>'link'));
3402         $output .= html_writer::link($discussionlink, get_string('discussthistopic', 'forum'));
3403         $output .= '&nbsp;('.$replystring.')';
3404         $output .= html_writer::end_tag('div'); // link
3405     }
3407     // Output footer if required
3408     if ($footer) {
3409         $output .= html_writer::tag('div', $footer, array('class'=>'footer'));
3410     }
3412     // Close remaining open divs
3413     $output .= html_writer::end_tag('div'); // content
3414     $output .= html_writer::end_tag('div'); // row
3415     $output .= html_writer::end_tag('div'); // forumpost
3417     // Mark the forum post as read if required
3418     if ($istracked && !$CFG->forum_usermarksread && !$postisread) {
3419         forum_tp_mark_post_read($USER->id, $post, $forum->id);
3420     }
3422     if ($return) {
3423         return $output;
3424     }
3425     echo $output;
3426     return;
3429 /**
3430  * Return rating related permissions
3431  *
3432  * @param string $options the context id
3433  * @return array an associative array of the user's rating permissions
3434  */
3435 function forum_rating_permissions($contextid, $component, $ratingarea) {
3436     $context = get_context_instance_by_id($contextid, MUST_EXIST);
3437     if ($component != 'mod_forum' || $ratingarea != 'post') {
3438         // We don't know about this component/ratingarea so just return null to get the
3439         // default restrictive permissions.
3440         return null;
3441     }
3442     return array(
3443         'view'    => has_capability('mod/forum:viewrating', $context),
3444         'viewany' => has_capability('mod/forum:viewanyrating', $context),
3445         'viewall' => has_capability('mod/forum:viewallratings', $context),
3446         'rate'    => has_capability('mod/forum:rate', $context)
3447     );
3450 /**
3451  * Validates a submitted rating
3452  * @param array $params submitted data
3453  *            context => object the context in which the rated items exists [required]
3454  *            component => The component for this module - should always be mod_forum [required]
3455  *            ratingarea => object the context in which the rated items exists [required]
3456  *            itemid => int the ID of the object being rated [required]
3457  *            scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
3458  *            rating => int the submitted rating [required]
3459  *            rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
3460  *            aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
3461  * @return boolean true if the rating is valid. Will throw rating_exception if not
3462  */
3463 function forum_rating_validate($params) {
3464     global $DB, $USER;
3466     // Check the component is mod_forum
3467     if ($params['component'] != 'mod_forum') {
3468         throw new rating_exception('invalidcomponent');
3469     }
3471     // Check the ratingarea is post (the only rating area in forum)
3472     if ($params['ratingarea'] != 'post') {
3473         throw new rating_exception('invalidratingarea');
3474     }
3476     // Check the rateduserid is not the current user .. you can't rate your own posts
3477     if ($params['rateduserid'] == $USER->id) {
3478         throw new rating_exception('nopermissiontorate');
3479     }
3481     // Fetch all the related records ... we need to do this anyway to call forum_user_can_see_post
3482     $post = $DB->get_record('forum_posts', array('id' => $params['itemid'], 'userid' => $params['rateduserid']), '*', MUST_EXIST);
3483     $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion), '*', MUST_EXIST);
3484     $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
3485     $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
3486     $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST);
3487     $context = get_context_instance(CONTEXT_MODULE, $cm->id);
3489     // Make sure the context provided is the context of the forum
3490     if ($context->id != $params['context']->id) {
3491         throw new rating_exception('invalidcontext');
3492     }
3494     if ($forum->scale != $params['scaleid']) {
3495         //the scale being submitted doesnt match the one in the database
3496         throw new rating_exception('invalidscaleid');
3497     }
3499     // check the item we're rating was created in the assessable time window
3500     if (!empty($forum->assesstimestart) && !empty($forum->assesstimefinish)) {
3501         if ($post->created < $forum->assesstimestart || $post->created > $forum->assesstimefinish) {
3502             throw new rating_exception('notavailable');
3503         }
3504     }
3506     //check that the submitted rating is valid for the scale
3508     // lower limit
3509     if ($params['rating'] < 0  && $params['rating'] != RATING_UNSET_RATING) {
3510         throw new rating_exception('invalidnum');
3511     }
3513     // upper limit
3514     if ($forum->scale < 0) {
3515         //its a custom scale
3516         $scalerecord = $DB->get_record('scale', array('id' => -$forum->scale));
3517         if ($scalerecord) {
3518             $scalearray = explode(',', $scalerecord->scale);
3519             if ($params['rating'] > count($scalearray)) {
3520                 throw new rating_exception('invalidnum');
3521             }
3522         } else {
3523             throw new rating_exception('invalidscaleid');
3524         }
3525     } else if ($params['rating'] > $forum->scale) {
3526         //if its numeric and submitted rating is above maximum
3527         throw new rating_exception('invalidnum');
3528     }
3530     // Make sure groups allow this user to see the item they're rating
3531     if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {   // Groups are being used
3532         if (!groups_group_exists($discussion->groupid)) { // Can't find group
3533             throw new rating_exception('cannotfindgroup');//something is wrong
3534         }
3536         if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) {
3537             // do not allow rating of posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS
3538             throw new rating_exception('notmemberofgroup');
3539         }
3540     }
3542     // perform some final capability checks
3543     if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
3544         throw new rating_exception('nopermissiontorate');
3545     }
3547     return true;
3551 /**
3552  * This function prints the overview of a discussion in the forum listing.
3553  * It needs some discussion information and some post information, these
3554  * happen to be combined for efficiency in the $post parameter by the function
3555  * that calls this one: forum_print_latest_discussions()
3556  *
3557  * @global object
3558  * @global object
3559  * @param object $post The post object (passed by reference for speed).
3560  * @param object $forum The forum object.
3561  * @param int $group Current group.
3562  * @param string $datestring Format to use for the dates.
3563  * @param boolean $cantrack Is tracking enabled for this forum.
3564  * @param boolean $forumtracked Is the user tracking this forum.
3565  * @param boolean $canviewparticipants True if user has the viewparticipants permission for this course
3566  */
3567 function forum_print_discussion_header(&$post, $forum, $group=-1, $datestring="",
3568                                         $cantrack=true, $forumtracked=true, $canviewparticipants=true, $modcontext=NULL) {
3570     global $USER, $CFG, $OUTPUT;
3572     static $rowcount;
3573     static $strmarkalldread;
3575     if (empty($modcontext)) {
3576         if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
3577             print_error('invalidcoursemodule');
3578         }
3579         $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
3580     }
3582     if (!isset($rowcount)) {
3583         $rowcount = 0;
3584         $strmarkalldread = get_string('markalldread', 'forum');
3585     } else {
3586         $rowcount = ($rowcount + 1) % 2;
3587     }
3589     $post->subject = format_string($post->subject,true);
3591     echo "\n\n";
3592     echo '<tr class="discussion r'.$rowcount.'">';
3594     // Topic
3595     echo '<td class="topic starter">';
3596     echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">'.$post->subject.'</a>';
3597     echo "</td>\n";
3599     // Picture
3600     $postuser = new stdClass();
3601     $postuser->id = $post->userid;
3602     $postuser->firstname = $post->firstname;
3603     $postuser->lastname = $post->lastname;
3604     $postuser->imagealt = $post->imagealt;
3605     $postuser->picture = $post->picture;
3606     $postuser->email = $post->email;
3608     echo '<td class="picture">';
3609     echo $OUTPUT->user_picture($postuser, array('courseid'=>$forum->course));
3610     echo "</td>\n";
3612     // User name
3613     $fullname = fullname($post, has_capability('moodle/site:viewfullnames', $modcontext));
3614     echo '<td class="author">';
3615     echo '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$post->userid.'&amp;course='.$forum->course.'">'.$fullname.'</a>';
3616     echo "</td>\n";
3618     // Group picture
3619     if ($group !== -1) {  // Groups are active - group is a group data object or NULL
3620         echo '<td class="picture group">';
3621         if (!empty($group->picture) and empty($group->hidepicture)) {
3622             print_group_picture($group, $forum->course, false, false, true);
3623         } else if (isset($group->id)) {
3624             if($canviewparticipants) {
3625                 echo '<a href="'.$CFG->wwwroot.'/user/index.php?id='.$forum->course.'&amp;group='.$group->id.'">'.$group->name.'</a>';
3626             } else {
3627                 echo $group->name;
3628             }
3629         }
3630         echo "</td>\n";
3631     }
3633     if (has_capability('mod/forum:viewdiscussion', $modcontext)) {   // Show the column with replies
3634         echo '<td class="replies">';
3635         echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">';
3636         echo $post->replies.'</a>';
3637         echo "</td>\n";
3639         if ($cantrack) {
3640             echo '<td class="replies">';
3641             if ($forumtracked) {
3642                 if ($post->unread > 0) {
3643                     echo '<span class="unread">';
3644                     echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#unread">';
3645                     echo $post->unread;
3646                     echo '</a>';
3647                     echo '<a title="'.$strmarkalldread.'" href="'.$CFG->wwwroot.'/mod/forum/markposts.php?f='.
3648                          $forum->id.'&amp;d='.$post->discussion.'&amp;mark=read&amp;returnpage=view.php">' .
3649                          '<img src="'.$OUTPUT->pix_url('t/clear') . '" class="iconsmall" alt="'.$strmarkalldread.'" /></a>';
3650                     echo '</span>';
3651                 } else {
3652                     echo '<span class="read">';
3653                     echo $post->unread;
3654                     echo '</span>';
3655                 }
3656             } else {
3657                 echo '<span class="read">';
3658                 echo '-';
3659                 echo '</span>';
3660             }
3661             echo "</td>\n";
3662         }
3663     }
3665     echo '<td class="lastpost">';
3666     $usedate = (empty($post->timemodified)) ? $post->modified : $post->timemodified;  // Just in case
3667     $parenturl = (empty($post->lastpostid)) ? '' : '&amp;parent='.$post->lastpostid;
3668     $usermodified = new stdClass();
3669     $usermodified->id        = $post->usermodified;
3670     $usermodified->firstname = $post->umfirstname;
3671     $usermodified->lastname  = $post->umlastname;
3672     echo '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$post->usermodified.'&amp;course='.$forum->course.'">'.
3673          fullname($usermodified).'</a><br />';
3674     echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.$parenturl.'">'.
3675           userdate($usedate, $datestring).'</a>';
3676     echo "</td>\n";
3678     echo "</tr>\n\n";
3683 /**
3684  * Given a post object that we already know has a long message
3685  * this function truncates the message nicely to the first
3686  * sane place between $CFG->forum_longpost and $CFG->forum_shortpost
3687  *
3688  * @global object
3689  * @param string $message
3690  * @return string
3691  */
3692 function forum_shorten_post($message) {
3694    global $CFG;
3696    $i = 0;
3697    $tag = false;
3698    $length = strlen($message);
3699    $count = 0;
3700    $stopzone = false;
3701    $truncate = 0;
3703    for ($i=0; $i<$length; $i++) {
3704        $char = $message[$i];
3706        switch ($char) {
3707            case "<":
3708                $tag = true;
3709                break;
3710            case ">":
3711                $tag = false;
3712                break;
3713            default:
3714                if (!$tag) {
3715                    if ($stopzone) {
3716                        if ($char == ".") {
3717                            $truncate = $i+1;
3718                            break 2;
3719                        }
3720                    }
3721                    $count++;
3722                }
3723                break;
3724        }
3725        if (!$stopzone) {
3726            if ($count > $CFG->forum_shortpost) {
3727                $stopzone = true;
3728            }
3729        }
3730    }
3732    if (!$truncate) {
3733        $truncate = $i;
3734    }
3736    return substr($message, 0, $truncate);
3739 /**
3740  * Print the drop down that allows the user to select how they want to have
3741  * the discussion displayed.
3742  *
3743  * @param int $id forum id if $forumtype is 'single',
3744  *              discussion id for any other forum type
3745  * @param mixed $mode forum layout mode
3746  * @param string $forumtype optional
3747  */
3748 function forum_print_mode_form($id, $mode, $forumtype='') {
3749     global $OUTPUT;
3750     if ($forumtype == 'single') {
3751         $select = new single_select(new moodle_url("/mod/forum/view.php", array('f'=>$id)), 'mode', forum_get_layout_modes(), $mode, null, "mode");
3752         $select->class = "forummode";
3753     } else {
3754         $select = new single_select(new moodle_url("/mod/forum/discuss.php", array('d'=>$id)), 'mode', forum_get_layout_modes(), $mode, null, "mode");
3755     }
3756     echo $OUTPUT->render($select);
3759 /**
3760  * @global object
3761  * @param object $course
3762  * @param string $search
3763  * @return string
3764  */
3765 function forum_search_form($course, $search='') {
3766     global $CFG, $OUTPUT;
3768     $output  = '<div class="forumsearch">';
3769     $output .= '<form action="'.$CFG->wwwroot.'/mod/forum/search.php" style="display:inline">';
3770     $output .= '<fieldset class="invisiblefieldset">';
3771     $output .= $OUTPUT->help_icon('search');
3772     $output .= '<label class="accesshide" for="search" >'.get_string('search', 'forum').'</label>';
3773     $output .= '<input id="search" name="search" type="text" size="18" value="'.s($search, true).'" alt="search" />';
3774     $output .= '<label class="accesshide" for="searchforums" >'.get_string('searchforums', 'forum').'</label>';
3775     $output .= '<input id="searchforums" value="'.get_string('searchforums', 'forum').'" type="submit" />';
3776     $output .= '<input name="id" type="hidden" value="'.$course->id.'" />';
3777     $output .= '</fieldset>';
3778     $output .= '</form>';
3779     $output .= '</div>';
3781     return $output;
3785 /**
3786  * @global object
3787  * @global object
3788  */
3789 function forum_set_return() {
3790     global $CFG, $SESSION;
3792     if (! isset($SESSION->fromdiscussion)) {
3793         if (!empty($_SERVER['HTTP_REFERER'])) {
3794             $referer = $_SERVER['HTTP_REFERER'];
3795         } else {
3796             $referer = "";
3797         }
3798         // If the referer is NOT a login screen then save it.
3799         if (! strncasecmp("$CFG->wwwroot/login", $referer, 300)) {
3800             $SESSION->fromdiscussion = $_SERVER["HTTP_REFERER"];
3801         }
3802     }
3806 /**
3807  * @global object
3808  * @param string $default
3809  * @return string
3810  */
3811 function forum_go_back_to($default) {
3812     global $SESSION;
3814     if (!empty($SESSION->fromdiscussion)) {
3815         $returnto = $SESSION->fromdiscussion;
3816         unset($SESSION->fromdiscussion);
3817         return $returnto;
3818     } else {
3819         return $default;
3820     }
3823 /**
3824  * Given a discussion object that is being moved to $forumto,
3825  * this function checks all posts in that discussion
3826  * for attachments, and if any are found, these are
3827  * moved to the new forum directory.
3828  *
3829  * @global object
3830  * @param object $discussion
3831  * @param int $forumfrom source forum id
3832  * @param int $forumto target forum id
3833  * @return bool success
3834  */
3835 function forum_move_attachments($discussion, $forumfrom, $forumto) {
3836     global $DB;
3838     $fs = get_file_storage();
3840     $newcm = get_coursemodule_from_instance('forum', $forumto);
3841     $oldcm = get_coursemodule_from_instance('forum', $forumfrom);
3843     $newcontext = get_context_instance(CONTEXT_MODULE, $newcm->id);
3844     $oldcontext = get_context_instance(CONTEXT_MODULE, $oldcm->id);
3846     // loop through all posts, better not use attachment flag ;-)
3847     if ($posts = $DB->get_records('forum_posts', array('discussion'=>$discussion->id), '', 'id, attachment')) {
3848         foreach ($posts as $post) {
3849             $fs->move_area_files_to_new_context($oldcontext->id,
3850                     $newcontext->id, 'mod_forum', 'post', $post->id);
3851             $attachmentsmoved = $fs->move_area_files_to_new_context($oldcontext->id,
3852                     $newcontext->id, 'mod_forum', 'attachment', $post->id);
3853             if ($attachmentsmoved > 0 && $post->attachment != '1') {
3854                 // Weird - let's fix it
3855                 $post->attachment = '1';
3856                 $DB->update_record('forum_posts', $post);
3857             } else if ($attachmentsmoved == 0 && $post->attachment != '') {
3858                 // Weird - let's fix it
3859                 $post->attachment = '';
3860                 $DB->update_record('forum_posts', $post);
3861             }
3862         }
3863     }
3865     return true;
3868 /**
3869  * Returns attachments as formated text/html optionally with separate images
3870  *
3871  * @global object
3872  * @global object
3873  * @global object
3874  * @param object $post
3875  * @param object $cm
3876  * @param string $type html/text/separateimages
3877  * @return mixed string or array of (html text withouth images and image HTML)
3878  */
3879 function forum_print_attachments($post, $cm, $type) {
3880     global $CFG, $DB, $USER, $OUTPUT;
3882     if (empty($post->attachment)) {
3883         return $type !== 'separateimages' ? '' : array('', '');
3884     }
3886     if (!in_array($type, array('separateimages', 'html', 'text'))) {
3887         return $type !== 'separateimages' ? '' : array('', '');
3888     }
3890     if (!$context = get_context_instance(CONTEXT_MODULE, $cm->id)) {
3891         return $type !== 'separateimages' ? '' : array('', '');
3892     }
3893     $strattachment = get_string('attachment', 'forum');
3895     $fs = get_file_storage();
3897     $imagereturn = '';
3898     $output = '';
3900     $canexport = !empty($CFG->enableportfolios) && (has_capability('mod/forum:exportpost', $context) || ($post->userid == $USER->id && has_capability('mod/forum:exportownpost', $context)));
3902     if ($canexport) {
3903         require_once($CFG->libdir.'/portfoliolib.php');
3904     }
3906     $files = $fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id, "timemodified", false);
3907     if ($files) {
3908         if ($canexport) {
3909             $button = new portfolio_add_button();
3910         }
3911         foreach ($files as $file) {
3912             $filename = $file->get_filename();
3913             $mimetype = $file->get_mimetype();
3914             $iconimage = '<img src="'.$OUTPUT->pix_url(file_mimetype_icon($mimetype)).'" class="icon" alt="'.$mimetype.'" />';
3915             $path = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.$context->id.'/mod_forum/attachment/'.$post->id.'/'.$filename);
3917             if ($type == 'html') {
3918                 $output .= "<a href=\"$path\">$iconimage</a> ";
3919                 $output .= "<a href=\"$path\">".s($filename)."</a>";
3920                 if ($canexport) {
3921                     $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), '/mod/forum/locallib.php');
3922                     $button->set_format_by_file($file);
3923                     $output .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
3924                 }
3925                 $output .= "<br />";
3927             } else if ($type == 'text') {
3928                 $output .= "$strattachment ".s($filename).":\n$path\n";
3930             } else { //'returnimages'
3931                 if (in_array($mimetype, array('image/gif', 'image/jpeg', 'image/png'))) {
3932                     // Image attachments don't get printed as links
3933                     $imagereturn .= "<br /><img src=\"$path\" alt=\"\" />";
3934                     if ($canexport) {
3935                         $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), '/mod/forum/locallib.php');
3936                         $button->set_format_by_file($file);
3937                         $imagereturn .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
3938                     }
3939                 } else {
3940                     $output .= "<a href=\"$path\">$iconimage</a> ";
3941                     $output .= format_text("<a href=\"$path\">".s($filename)."</a>", FORMAT_HTML, array('context'=>$context));
3942                     if ($canexport) {
3943                         $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), '/mod/forum/locallib.php');
3944                         $button->set_format_by_file($file);
3945                         $output .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
3946                     }
3947                     $output .= '<br />';
3948                 }
3949             }
3950         }
3951     }
3953     if ($type !== 'separateimages') {
3954         return $output;
3956     } else {
3957         return array($output, $imagereturn);
3958     }
3961 ////////////////////////////////////////////////////////////////////////////////
3962 // File API                                                                   //
3963 ////////////////////////////////////////////////////////////////////////////////
3965 /**
3966  * Lists all browsable file areas
3967  *
3968  * @package  mod_forum
3969  * @category files
3970  * @param stdClass $course course object
3971  * @param stdClass $cm course module object
3972  * @param stdClass $context context object
3973  * @return array
3974  */
3975 function forum_get_file_areas($course, $cm, $context) {
3976     return array(
3977         'attachment' => get_string('areaattachment', 'mod_forum'),
3978         'post' => get_string('areapost', 'mod_forum'),
3979     );
3982 /**
3983  * File browsing support for forum module.
3984  *
3985  * @package  mod_forum
3986  * @category files
3987  * @param stdClass $browser file browser object
3988  * @param stdClass $areas file areas
3989  * @param stdClass $course course object
3990  * @param stdClass $cm course module
3991  * @param stdClass $context context module
3992  * @param string $filearea file area
3993  * @param int $itemid item ID
3994  * @param string $filepath file path
3995  * @param string $filename file name
3996  * @return file_info instance or null if not found
3997  */
3998 function forum_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
3999     global $CFG, $DB;
4001     if ($context->contextlevel != CONTEXT_MODULE) {
4002         return null;
4003     }
4005     // filearea must contain a real area
4006     if (!isset($areas[$filearea])) {
4007         return null;
4008     }
4010     // this is enforced by {@link file_info_context_course} currently
4011     if (!has_capability('moodle/course:managefiles', $context)) {
4012         return null;
4013     }
4015     // Note that forum_user_can_see_post() additionally allows access for parent roles
4016     // and it explicitly checks qanda forum type, too. One day, when we stop requiring
4017     // course:managefiles, we will need to extend this.
4018     if (!has_capability('mod/forum:viewdiscussion', $context)) {
4019         return null;
4020     }
4022     if (is_null($itemid)) {
4023         require_once($CFG->dirroot.'/mod/forum/locallib.php');
4024         return new forum_file_info_container($browser, $course, $cm, $context, $areas, $filearea);
4025     }
4027     if (!$post = $DB->get_record('forum_posts', array('id' => $itemid))) {
4028         return null;
4029     }
4031     if (!$discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion))) {
4032         return null;
4033     }
4035     if (!$forum = $DB->get_record('forum', array('id' => $cm->instance))) {
4036         return null;
4037     }
4039     $fs = get_file_storage();
4040     $filepath = is_null($filepath) ? '/' : $filepath;
4041     $filename = is_null($filename) ? '.' : $filename;
4042     if (!($storedfile = $fs->get_file($context->id, 'mod_forum', $filearea, $itemid, $filepath, $filename))) {
4043         return null;
4044     }
4046     // Make sure groups allow this user to see this file
4047     if ($discussion->groupid > 0) {
4048         $groupmode = groups_get_activity_groupmode($cm, $course);
4049         if ($groupmode == SEPARATEGROUPS) {
4050             if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) {
4051                 return null;
4052             }
4053         }
4054     }
4056     // Make sure we're allowed to see it...
4057     if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
4058         return null;
4059     }
4061     $urlbase = $CFG->wwwroot.'/pluginfile.php';
4062     return new file_info_stored($browser, $context, $storedfile, $urlbase, $itemid, true, true, false, false);
4065 /**
4066  * Serves the forum attachments. Implements needed access control ;-)
4067  *
4068  * @package  mod_forum
4069  * @category files
4070  * @param stdClass $course course object
4071  * @param stdClass $cm course module object
4072  * @param stdClass $context context object
4073  * @param string $filearea file area
4074  * @param array $args extra arguments
4075  * @param bool $forcedownload whether or not force download
4076  * @param array $options additional options affecting the file serving
4077  * @return bool false if file not found, does not return if found - justsend the file
4078  */
4079 function forum_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
4080     global $CFG, $DB;
4082     if ($context->contextlevel != CONTEXT_MODULE) {
4083         return false;
4084     }
4086     require_course_login($course, true, $cm);
4088     $areas = forum_get_file_areas($course, $cm, $context);
4090     // filearea must contain a real area
4091     if (!isset($areas[$filearea])) {
4092         return false;
4093     }
4095     $postid = (int)array_shift($args);
4097     if (!$post = $DB->get_record('forum_posts', array('id'=>$postid))) {
4098         return false;
4099     }
4101     if (!$discussion = $DB->get_record('forum_discussions', array('id'=>$post->discussion))) {
4102         return false;
4103     }
4105     if (!$forum = $DB->get_record('forum', array('id'=>$cm->instance))) {
4106         return false;
4107     }
4109     $fs = get_file_storage();
4110     $relativepath = implode('/', $args);
4111     $fullpath = "/$context->id/mod_forum/$filearea/$postid/$relativepath";
4112     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
4113         return false;
4114     }
4116     // Make sure groups allow this user to see this file
4117     if ($discussion->groupid > 0) {
4118         $groupmode = groups_get_activity_groupmode($cm, $course);
4119         if ($groupmode == SEPARATEGROUPS) {
4120             if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) {
4121                 return false;
4122             }
4123         }
4124     }
4126     // Make sure we're allowed to see it...
4127     if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
4128         return false;
4129     }
4131     // finally send the file
4132     send_stored_file($file, 0, 0, true, $options); // download MUST be forced - security!
4135 /**
4136  * If successful, this function returns the name of the file
4137  *
4138  * @global object
4139  * @param object $post is a full post record, including course and forum
4140  * @param object $forum
4141  * @param object $cm
4142  * @param mixed