MDL-9469 mod_forum: add export node to settings menu
[moodle.git] / mod / forum / lib.php
CommitLineData
df1ba0f4 1<?php
df1ba0f4 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/>.
16
17/**
01030f1b
SH
18 * @package mod_forum
19 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
20 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
df1ba0f4 21 */
22
bc196cbe
PS
23defined('MOODLE_INTERNAL') || die();
24
df1ba0f4 25/** Include required files */
39de876c 26require_once(__DIR__ . '/deprecatedlib.php');
f1e0649c 27require_once($CFG->libdir.'/filelib.php');
7f6689e4 28
501cdbd8 29/// CONSTANTS ///////////////////////////////////////////////////////////
f93f848a 30
d3583b41 31define('FORUM_MODE_FLATOLDEST', 1);
32define('FORUM_MODE_FLATNEWEST', -1);
33define('FORUM_MODE_THREADED', 2);
34define('FORUM_MODE_NESTED', 3);
2e2e71a8 35
afef965e 36define('FORUM_CHOOSESUBSCRIBE', 0);
d3583b41 37define('FORUM_FORCESUBSCRIBE', 1);
38define('FORUM_INITIALSUBSCRIBE', 2);
098d27d4 39define('FORUM_DISALLOWSUBSCRIBE',3);
709f0ec8 40
bd8f5d45
EM
41/**
42 * FORUM_TRACKING_OFF - Tracking is not available for this forum.
43 */
eaf50aef 44define('FORUM_TRACKING_OFF', 0);
bd8f5d45
EM
45
46/**
47 * FORUM_TRACKING_OPTIONAL - Tracking is based on user preference.
48 */
eaf50aef 49define('FORUM_TRACKING_OPTIONAL', 1);
bd8f5d45
EM
50
51/**
52 * FORUM_TRACKING_FORCED - Tracking is on, regardless of user setting.
53 * Treated as FORUM_TRACKING_OPTIONAL if $CFG->forum_allowforcedreadtracking is off.
54 */
55define('FORUM_TRACKING_FORCED', 2);
56
8076010d 57define('FORUM_MAILED_PENDING', 0);
da484043
MO
58define('FORUM_MAILED_SUCCESS', 1);
59define('FORUM_MAILED_ERROR', 2);
60
103f34d8
PS
61if (!defined('FORUM_CRON_USER_CACHE')) {
62 /** Defines how many full user records are cached in forum cron. */
63 define('FORUM_CRON_USER_CACHE', 5000);
64}
65
4f3a2d21
JL
66/**
67 * FORUM_POSTS_ALL_USER_GROUPS - All the posts in groups where the user is enrolled.
68 */
69define('FORUM_POSTS_ALL_USER_GROUPS', -2);
70
87b007b4
CF
71define('FORUM_DISCUSSION_PINNED', 1);
72define('FORUM_DISCUSSION_UNPINNED', 0);
73
caadf009 74/// STANDARD FUNCTIONS ///////////////////////////////////////////////////////////
75
0a4ac01b 76/**
77 * Given an object containing all the necessary data,
7cac0c4b 78 * (defined by the form in mod_form.php) this function
0a4ac01b 79 * will create a new instance and return the id number
80 * of the new instance.
df1ba0f4 81 *
6b04fdc0
PS
82 * @param stdClass $forum add forum instance
83 * @param mod_forum_mod_form $mform
6b7de0bb 84 * @return int intance id
3a5e1d06 85 */
6b04fdc0 86function forum_add_instance($forum, $mform = null) {
c18269c7 87 global $CFG, $DB;
caadf009 88
bbbf1820
SR
89 require_once($CFG->dirroot.'/mod/forum/locallib.php');
90
caadf009 91 $forum->timemodified = time();
92
353228d8 93 if (empty($forum->assessed)) {
f2f56406 94 $forum->assessed = 0;
95 }
2b63df96 96
353228d8 97 if (empty($forum->ratingtime) or empty($forum->assessed)) {
98914efd 98 $forum->assesstimestart = 0;
99 $forum->assesstimefinish = 0;
100 }
caadf009 101
a8f3a651 102 $forum->id = $DB->insert_record('forum', $forum);
bf0f06b1 103 $modcontext = context_module::instance($forum->coursemodule);
cb9a975f 104
d3583b41 105 if ($forum->type == 'single') { // Create related discussion.
39790bd8 106 $discussion = new stdClass();
e2d7687f 107 $discussion->course = $forum->course;
108 $discussion->forum = $forum->id;
109 $discussion->name = $forum->name;
e2d7687f 110 $discussion->assessed = $forum->assessed;
6606c00f
MD
111 $discussion->message = $forum->intro;
112 $discussion->messageformat = $forum->introformat;
bf0f06b1 113 $discussion->messagetrust = trusttext_trusted(context_course::instance($forum->course));
e2d7687f 114 $discussion->mailnow = false;
115 $discussion->groupid = -1;
caadf009 116
dbf4d660 117 $message = '';
118
42776c94
PS
119 $discussion->id = forum_add_discussion($discussion, null, $message);
120
121 if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
a11aa38e 122 // Ugly hack - we need to copy the files somehow.
42776c94
PS
123 $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST);
124 $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
125
a11aa38e
PS
126 $options = array('subdirs'=>true); // Use the same options as intro field!
127 $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
42776c94 128 $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
caadf009 129 }
130 }
8f0cd6ef 131
bbbf1820 132 forum_update_calendar($forum, $forum->coursemodule);
8e6775d8
AN
133 forum_grade_item_update($forum);
134
b3bd7a66
MN
135 $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null;
136 \core_completion\api::update_completion_date_event($forum->coursemodule, 'forum', $forum->id, $completiontimeexpected);
137
8e6775d8
AN
138 return $forum->id;
139}
140
141/**
142 * Handle changes following the creation of a forum instance.
143 * This function is typically called by the course_module_created observer.
144 *
145 * @param object $context the forum context
146 * @param stdClass $forum The forum object
147 * @return void
148 */
149function forum_instance_created($context, $forum) {
3c268f25 150 if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) {
59075a43 151 $users = \mod_forum\subscriptions::get_potential_subscribers($context, 0, 'u.id, u.email');
709f0ec8 152 foreach ($users as $user) {
59075a43 153 \mod_forum\subscriptions::subscribe_user($user->id, $forum, $context);
709f0ec8 154 }
155 }
caadf009 156}
157
13bbe067 158/**
0a4ac01b 159 * Given an object containing all the necessary data,
7cac0c4b 160 * (defined by the form in mod_form.php) this function
0a4ac01b 161 * will update an existing instance with new data.
df1ba0f4 162 *
163 * @global object
90f4745c 164 * @param object $forum forum instance (with magic quotes)
6b7de0bb 165 * @return bool success
90f4745c 166 */
42776c94 167function forum_update_instance($forum, $mform) {
bbbf1820
SR
168 global $CFG, $DB, $OUTPUT, $USER;
169
170 require_once($CFG->dirroot.'/mod/forum/locallib.php');
c18269c7 171
caadf009 172 $forum->timemodified = time();
353228d8 173 $forum->id = $forum->instance;
caadf009 174
f0da6b85 175 if (empty($forum->assessed)) {
f2f56406 176 $forum->assessed = 0;
177 }
2b63df96 178
353228d8 179 if (empty($forum->ratingtime) or empty($forum->assessed)) {
98914efd 180 $forum->assesstimestart = 0;
181 $forum->assesstimefinish = 0;
182 }
183
c18269c7 184 $oldforum = $DB->get_record('forum', array('id'=>$forum->id));
13bbe067 185
186 // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum
187 // if scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
188 // 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
189 if (($oldforum->assessed<>$forum->assessed) or ($oldforum->scale<>$forum->scale)) {
190 forum_update_grades($forum); // recalculate grades for the forum
191 }
192
d3583b41 193 if ($forum->type == 'single') { // Update related discussion and post.
35b81f27
RT
194 $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC');
195 if (!empty($discussions)) {
196 if (count($discussions) > 1) {
197 echo $OUTPUT->notification(get_string('warnformorepost', 'forum'));
198 }
199 $discussion = array_pop($discussions);
200 } else {
201 // try to recover by creating initial discussion - MDL-16262
202 $discussion = new stdClass();
203 $discussion->course = $forum->course;
204 $discussion->forum = $forum->id;
205 $discussion->name = $forum->name;
206 $discussion->assessed = $forum->assessed;
207 $discussion->message = $forum->intro;
208 $discussion->messageformat = $forum->introformat;
209 $discussion->messagetrust = true;
210 $discussion->mailnow = false;
211 $discussion->groupid = -1;
212
213 $message = '';
214
215 forum_add_discussion($discussion, null, $message);
216
217 if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) {
218 print_error('cannotadd', 'forum');
caadf009 219 }
220 }
c18269c7 221 if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) {
12e57b92 222 print_error('cannotfindfirstpost', 'forum');
caadf009 223 }
224
6606c00f 225 $cm = get_coursemodule_from_instance('forum', $forum->id);
bf0f06b1 226 $modcontext = context_module::instance($cm->id, MUST_EXIST);
42776c94 227
a11aa38e 228 $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
6606c00f
MD
229 $post->subject = $forum->name;
230 $post->message = $forum->intro;
231 $post->messageformat = $forum->introformat;
232 $post->messagetrust = trusttext_trusted($modcontext);
233 $post->modified = $forum->timemodified;
a11aa38e
PS
234 $post->userid = $USER->id; // MDL-18599, so that current teacher can take ownership of activities.
235
236 if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
237 // Ugly hack - we need to copy the files somehow.
238 $options = array('subdirs'=>true); // Use the same options as intro field!
239 $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
240 }
caadf009 241
a8d6ef8c 242 $DB->update_record('forum_posts', $post);
caadf009 243 $discussion->name = $forum->name;
a8d6ef8c 244 $DB->update_record('forum_discussions', $discussion);
caadf009 245 }
246
a8d6ef8c 247 $DB->update_record('forum', $forum);
353228d8 248
4a913724
GPL
249 $modcontext = context_module::instance($forum->coursemodule);
250 if (($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) && ($oldforum->forcesubscribe <> $forum->forcesubscribe)) {
59075a43 251 $users = \mod_forum\subscriptions::get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
4a913724 252 foreach ($users as $user) {
59075a43 253 \mod_forum\subscriptions::subscribe_user($user->id, $forum, $modcontext);
4a913724
GPL
254 }
255 }
256
bbbf1820 257 forum_update_calendar($forum, $forum->coursemodule);
353228d8 258 forum_grade_item_update($forum);
259
b3bd7a66
MN
260 $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null;
261 \core_completion\api::update_completion_date_event($forum->coursemodule, 'forum', $forum->id, $completiontimeexpected);
262
353228d8 263 return true;
caadf009 264}
265
266
0a4ac01b 267/**
268 * Given an ID of an instance of this module,
269 * this function will permanently delete the instance
270 * and any data that depends on it.
df1ba0f4 271 *
272 * @global object
273 * @param int $id forum instance id
90f4745c 274 * @return bool success
0a4ac01b 275 */
caadf009 276function forum_delete_instance($id) {
c18269c7 277 global $DB;
caadf009 278
c18269c7 279 if (!$forum = $DB->get_record('forum', array('id'=>$id))) {
caadf009 280 return false;
281 }
455a8fed 282 if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
283 return false;
284 }
285 if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
286 return false;
287 }
288
bf0f06b1 289 $context = context_module::instance($cm->id);
caadf009 290
fa686bc4 291 // now get rid of all files
292 $fs = get_file_storage();
455a8fed 293 $fs->delete_area_files($context->id);
fa686bc4 294
caadf009 295 $result = true;
296
b3bd7a66
MN
297 \core_completion\api::update_completion_date_event($cm->id, 'forum', $forum->id, null);
298
361a41d3
AN
299 // Delete digest and subscription preferences.
300 $DB->delete_records('forum_digests', array('forum' => $forum->id));
301 $DB->delete_records('forum_subscriptions', array('forum'=>$forum->id));
302 $DB->delete_records('forum_discussion_subs', array('forum' => $forum->id));
303
c18269c7 304 if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) {
caadf009 305 foreach ($discussions as $discussion) {
455a8fed 306 if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) {
caadf009 307 $result = false;
308 }
309 }
310 }
311
f37da850 312 forum_tp_delete_read_records(-1, -1, -1, $forum->id);
313
40fe1538
DW
314 forum_grade_item_delete($forum);
315
316 // We must delete the module record after we delete the grade item.
415f8c3e 317 if (!$DB->delete_records('forum', array('id'=>$forum->id))) {
caadf009 318 $result = false;
319 }
320
321 return $result;
322}
323
324
4e781c7b 325/**
326 * Indicates API features that the forum supports.
327 *
df1ba0f4 328 * @uses FEATURE_GROUPS
329 * @uses FEATURE_GROUPINGS
df1ba0f4 330 * @uses FEATURE_MOD_INTRO
331 * @uses FEATURE_COMPLETION_TRACKS_VIEWS
332 * @uses FEATURE_COMPLETION_HAS_RULES
333 * @uses FEATURE_GRADE_HAS_GRADE
334 * @uses FEATURE_GRADE_OUTCOMES
4e781c7b 335 * @param string $feature
336 * @return mixed True if yes (some features may use other values)
337 */
338function forum_supports($feature) {
339 switch($feature) {
42f103be 340 case FEATURE_GROUPS: return true;
341 case FEATURE_GROUPINGS: return true;
dc5c2bd9 342 case FEATURE_MOD_INTRO: return true;
4e781c7b 343 case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
42f103be 344 case FEATURE_COMPLETION_HAS_RULES: return true;
345 case FEATURE_GRADE_HAS_GRADE: return true;
346 case FEATURE_GRADE_OUTCOMES: return true;
4bfdcfcf
EL
347 case FEATURE_RATE: return true;
348 case FEATURE_BACKUP_MOODLE2: return true;
3e4c2435 349 case FEATURE_SHOW_DESCRIPTION: return true;
50da4ddd 350 case FEATURE_PLAGIARISM: return true;
42f103be 351
49f6e5f4 352 default: return null;
4e781c7b 353 }
354}
355
356
357/**
358 * Obtains the automatic completion state for this forum based on any conditions
359 * in forum settings.
360 *
df1ba0f4 361 * @global object
362 * @global object
4e781c7b 363 * @param object $course Course
364 * @param object $cm Course-module
365 * @param int $userid User ID
366 * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
367 * @return bool True if completed, false if not. (If no conditions, then return
368 * value depends on comparison type)
369 */
370function forum_get_completion_state($course,$cm,$userid,$type) {
371 global $CFG,$DB;
372
373 // Get forum details
6093af9b 374 if (!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) {
4e781c7b 375 throw new Exception("Can't find forum {$cm->instance}");
376 }
377
378 $result=$type; // Default return value
379
380 $postcountparams=array('userid'=>$userid,'forumid'=>$forum->id);
381 $postcountsql="
5e7f2b0b 382SELECT
383 COUNT(1)
384FROM
385 {forum_posts} fp
49f6e5f4 386 INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
4e781c7b 387WHERE
388 fp.userid=:userid AND fd.forum=:forumid";
389
6093af9b 390 if ($forum->completiondiscussions) {
4e781c7b 391 $value = $forum->completiondiscussions <=
6093af9b 392 $DB->count_records('forum_discussions',array('forum'=>$forum->id,'userid'=>$userid));
393 if ($type == COMPLETION_AND) {
394 $result = $result && $value;
4e781c7b 395 } else {
6093af9b 396 $result = $result || $value;
4e781c7b 397 }
398 }
6093af9b 399 if ($forum->completionreplies) {
5e7f2b0b 400 $value = $forum->completionreplies <=
6093af9b 401 $DB->get_field_sql( $postcountsql.' AND fp.parent<>0',$postcountparams);
402 if ($type==COMPLETION_AND) {
403 $result = $result && $value;
4e781c7b 404 } else {
6093af9b 405 $result = $result || $value;
4e781c7b 406 }
407 }
6093af9b 408 if ($forum->completionposts) {
4e781c7b 409 $value = $forum->completionposts <= $DB->get_field_sql($postcountsql,$postcountparams);
6093af9b 410 if ($type == COMPLETION_AND) {
411 $result = $result && $value;
4e781c7b 412 } else {
6093af9b 413 $result = $result || $value;
4e781c7b 414 }
415 }
416
5e7f2b0b 417 return $result;
4e781c7b 418}
419
8260050d
AD
420/**
421 * Create a message-id string to use in the custom headers of forum notification emails
422 *
423 * message-id is used by email clients to identify emails and to nest conversations
424 *
425 * @param int $postid The ID of the forum post we are notifying the user about
426 * @param int $usertoid The ID of the user being notified
8260050d
AD
427 * @return string A unique message-id
428 */
54dceeed
BH
429function forum_get_email_message_id($postid, $usertoid) {
430 return generate_email_messageid(hash('sha256', $postid . 'to' . $usertoid));
1376b0dd 431}
4e781c7b 432
0a4ac01b 433/**
13bbe067 434 *
1670305d 435 * @param object $course
436 * @param object $user
437 * @param object $mod TODO this is not used in this function, refactor
438 * @param object $forum
439 * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
0a4ac01b 440 */
caadf009 441function forum_user_outline($course, $user, $mod, $forum) {
1a96363a
NC
442 global $CFG;
443 require_once("$CFG->libdir/gradelib.php");
444 $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
445 if (empty($grades->items[0]->grades)) {
446 $grade = false;
447 } else {
448 $grade = reset($grades->items[0]->grades);
449 }
450
451 $count = forum_count_user_posts($forum->id, $user->id);
452
453 if ($count && $count->postcount > 0) {
39790bd8 454 $result = new stdClass();
1a96363a
NC
455 $result->info = get_string("numposts", "forum", $count->postcount);
456 $result->time = $count->lastpost;
457 if ($grade) {
f5d1dbb3
MF
458 if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
459 $result->info .= ', ' . get_string('grade') . ': ' . $grade->str_long_grade;
460 } else {
461 $result->info = get_string('grade') . ': ' . get_string('hidden', 'grades');
462 }
90f4745c 463 }
1a96363a
NC
464 return $result;
465 } else if ($grade) {
39790bd8 466 $result = new stdClass();
f5d1dbb3
MF
467 if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
468 $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
469 } else {
470 $result->info = get_string('grade') . ': ' . get_string('hidden', 'grades');
471 }
4433f871
AD
472
473 //datesubmitted == time created. dategraded == time modified or time overridden
474 //if grade was last modified by the user themselves use date graded. Otherwise use date submitted
94a74f54 475 //TODO: move this copied & pasted code somewhere in the grades API. See MDL-26704
4433f871
AD
476 if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
477 $result->time = $grade->dategraded;
478 } else {
479 $result->time = $grade->datesubmitted;
480 }
481
1a96363a 482 return $result;
caadf009 483 }
484 return NULL;
485}
486
487
0a4ac01b 488/**
df1ba0f4 489 * @global object
490 * @global object
491 * @param object $coure
492 * @param object $user
493 * @param object $mod
494 * @param object $forum
0a4ac01b 495 */
caadf009 496function forum_user_complete($course, $user, $mod, $forum) {
1a96363a
NC
497 global $CFG,$USER, $OUTPUT;
498 require_once("$CFG->libdir/gradelib.php");
499
500 $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
501 if (!empty($grades->items[0]->grades)) {
502 $grade = reset($grades->items[0]->grades);
f5d1dbb3
MF
503 if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
504 echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
505 if ($grade->str_feedback) {
506 echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
507 }
508 } else {
509 echo $OUTPUT->container(get_string('grade') . ': ' . get_string('hidden', 'grades'));
1a96363a
NC
510 }
511 }
caadf009 512
1f48942e 513 if ($posts = forum_get_user_posts($forum->id, $user->id)) {
e3ff14ca 514
65bcf17b 515 if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
12e57b92 516 print_error('invalidcoursemodule');
65bcf17b 517 }
2e19ca18 518 $context = context_module::instance($cm->id);
90f4745c 519 $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
2e19ca18
RW
520 $posts = array_filter($posts, function($post) use ($discussions) {
521 return isset($discussions[$post->discussion]);
522 });
523 $entityfactory = mod_forum\local\container::get_entity_factory();
524 $rendererfactory = mod_forum\local\container::get_renderer_factory();
525 $postrenderer = $rendererfactory->get_posts_renderer();
526
527 echo $postrenderer->render(
528 $USER,
529 [$forum->id => $entityfactory->get_forum_from_stdclass($forum, $context, $cm, $course)],
530 array_map(function($discussion) use ($entityfactory) {
531 return $entityfactory->get_discussion_from_stdclass($discussion);
532 }, $discussions),
533 array_map(function($post) use ($entityfactory) {
534 return $entityfactory->get_post_from_stdclass($post);
535 }, $posts)
536 );
caadf009 537 } else {
41905731 538 echo "<p>".get_string("noposts", "forum")."</p>";
caadf009 539 }
caadf009 540}
541
8538c562 542/**
b18015c6 543 * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
8538c562 544 */
b18015c6
SA
545function forum_filter_user_groups_discussions() {
546 throw new coding_exception('forum_filter_user_groups_discussions() can not be used any more and is obsolete.');
8538c562
DM
547}
548
549/**
550 * Returns whether the discussion group is visible by the current user or not.
551 *
552 * @since Moodle 2.8, 2.7.1, 2.6.4
553 * @param cm_info $cm The discussion course module
554 * @param int $discussiongroupid The discussion groupid
555 * @return bool
556 */
557function forum_is_user_group_discussion(cm_info $cm, $discussiongroupid) {
558
559 if ($discussiongroupid == -1 || $cm->effectivegroupmode != SEPARATEGROUPS) {
560 return true;
561 }
562
563 if (isguestuser()) {
564 return false;
565 }
566
567 if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id)) ||
568 in_array($discussiongroupid, $cm->get_modinfo()->get_groups($cm->groupingid))) {
569 return true;
570 }
571
572 return false;
573}
5e7f2b0b 574
0a4ac01b 575/**
b18015c6 576 * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
0a4ac01b 577 */
b18015c6
SA
578function forum_print_overview() {
579 throw new coding_exception('forum_print_overview() can not be used any more and is obsolete.');
0d6b9d4f 580}
581
0a4ac01b 582/**
583 * Given a course and a date, prints a summary of all the new
584 * messages posted in the course since that date
df1ba0f4 585 *
586 * @global object
587 * @global object
588 * @global object
589 * @uses CONTEXT_MODULE
590 * @uses VISIBLEGROUPS
90f4745c 591 * @param object $course
592 * @param bool $viewfullnames capability
593 * @param int $timestart
594 * @return bool success
0a4ac01b 595 */
dd97c328 596function forum_print_recent_activity($course, $viewfullnames, $timestart) {
cb860491 597 global $CFG, $USER, $DB, $OUTPUT;
caadf009 598
dd97c328 599 // do not use log table if possible, it may be huge and is expensive to join with other tables
caadf009 600
dda60f88 601 $allnamefields = user_picture::fields('u', null, 'duserid');
4e445355 602 if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
dda60f88 603 d.timestart, d.timeend, $allnamefields
4e445355 604 FROM {forum_posts} p
605 JOIN {forum_discussions} d ON d.id = p.discussion
606 JOIN {forum} f ON f.id = d.forum
607 JOIN {user} u ON u.id = p.userid
3e95e09b 608 WHERE p.created > ? AND f.course = ? AND p.deleted <> 1
4e445355 609 ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
dd97c328 610 return false;
1b5910c4 611 }
612
f20edd52 613 $modinfo = get_fast_modinfo($course);
dcde9f02 614
dd97c328 615 $groupmodes = array();
616 $cms = array();
d05956ac 617
dd97c328 618 $strftimerecent = get_string('strftimerecent');
d05956ac 619
dd97c328 620 $printposts = array();
621 foreach ($posts as $post) {
622 if (!isset($modinfo->instances['forum'][$post->forum])) {
623 // not visible
624 continue;
625 }
626 $cm = $modinfo->instances['forum'][$post->forum];
627 if (!$cm->uservisible) {
628 continue;
629 }
bf0f06b1 630 $context = context_module::instance($cm->id);
6b7de0bb 631
632 if (!has_capability('mod/forum:viewdiscussion', $context)) {
633 continue;
634 }
583b57b4 635
dd97c328 636 if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
637 and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
6b7de0bb 638 if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
dd97c328 639 continue;
ac1d9a22 640 }
dd97c328 641 }
583b57b4 642
bc4c7337
AN
643 if (!forum_post_is_visible_privately($post, $cm)) {
644 continue;
645 }
646
8538c562
DM
647 // Check that the user can see the discussion.
648 if (forum_is_user_group_discussion($cm, $post->groupid)) {
649 $printposts[] = $post;
dd97c328 650 }
8f7dc7f1 651
dd97c328 652 }
653 unset($posts);
8f7dc7f1 654
dd97c328 655 if (!$printposts) {
656 return false;
657 }
658
cb860491 659 echo $OUTPUT->heading(get_string('newforumposts', 'forum').':', 3);
af0fc6a2 660 $list = html_writer::start_tag('ul', ['class' => 'unlist']);
dd97c328 661
662 foreach ($printposts as $post) {
663 $subjectclass = empty($post->parent) ? ' bold' : '';
af0fc6a2 664 $authorhidden = forum_is_author_hidden($post, (object) ['type' => $post->forumtype]);
dd97c328 665
af0fc6a2
JP
666 $list .= html_writer::start_tag('li');
667 $list .= html_writer::start_div('head');
1bf3a76a 668 $list .= html_writer::div(userdate_htmltime($post->modified, $strftimerecent), 'date');
af0fc6a2
JP
669 if (!$authorhidden) {
670 $list .= html_writer::div(fullname($post, $viewfullnames), 'name');
671 }
672 $list .= html_writer::end_div(); // Head.
673
674 $list .= html_writer::start_div('info' . $subjectclass);
675 $discussionurl = new moodle_url('/mod/forum/discuss.php', ['d' => $post->discussion]);
676 if (!empty($post->parent)) {
677 $discussionurl->param('parent', $post->parent);
678 $discussionurl->set_anchor('p'. $post->id);
caadf009 679 }
dd97c328 680 $post->subject = break_up_long_words(format_string($post->subject, true));
934ee47b 681 $list .= html_writer::link($discussionurl, $post->subject, ['rel' => 'bookmark']);
af0fc6a2
JP
682 $list .= html_writer::end_div(); // Info.
683 $list .= html_writer::end_tag('li');
caadf009 684 }
dd97c328 685
af0fc6a2
JP
686 $list .= html_writer::end_tag('ul');
687 echo $list;
dd97c328 688
689 return true;
caadf009 690}
691
353228d8 692/**
693 * Return grade for given user or all users.
694 *
df1ba0f4 695 * @global object
696 * @global object
697 * @param object $forum
353228d8 698 * @param int $userid optional user id, 0 means all users
699 * @return array array of grades, false if none
700 */
2b04c41c 701function forum_get_user_grades($forum, $userid = 0) {
63e87951 702 global $CFG;
df997f84 703
63e87951 704 require_once($CFG->dirroot.'/rating/lib.php');
df997f84 705
2b04c41c
SH
706 $ratingoptions = new stdClass;
707 $ratingoptions->component = 'mod_forum';
708 $ratingoptions->ratingarea = 'post';
353228d8 709
63e87951
AD
710 //need these to work backwards to get a context id. Is there a better way to get contextid from a module instance?
711 $ratingoptions->modulename = 'forum';
712 $ratingoptions->moduleid = $forum->id;
63e87951
AD
713 $ratingoptions->userid = $userid;
714 $ratingoptions->aggregationmethod = $forum->assessed;
715 $ratingoptions->scaleid = $forum->scale;
716 $ratingoptions->itemtable = 'forum_posts';
717 $ratingoptions->itemtableusercolumn = 'userid';
353228d8 718
2b04c41c 719 $rm = new rating_manager();
63e87951 720 return $rm->get_user_grades($ratingoptions);
353228d8 721}
caadf009 722
0a4ac01b 723/**
775f811a 724 * Update activity grades
353228d8 725 *
a153c9f2 726 * @category grade
775f811a 727 * @param object $forum
728 * @param int $userid specific user only, 0 means all
6b7de0bb 729 * @param boolean $nullifnone return null if grade does not exist
90f4745c 730 * @return void
0a4ac01b 731 */
775f811a 732function forum_update_grades($forum, $userid=0, $nullifnone=true) {
4e445355 733 global $CFG, $DB;
775f811a 734 require_once($CFG->libdir.'/gradelib.php');
caadf009 735
775f811a 736 if (!$forum->assessed) {
737 forum_grade_item_update($forum);
02ebf404 738
775f811a 739 } else if ($grades = forum_get_user_grades($forum, $userid)) {
740 forum_grade_item_update($forum, $grades);
eafb9d9e 741
775f811a 742 } else if ($userid and $nullifnone) {
39790bd8 743 $grade = new stdClass();
775f811a 744 $grade->userid = $userid;
745 $grade->rawgrade = NULL;
746 forum_grade_item_update($forum, $grade);
02ebf404 747
353228d8 748 } else {
775f811a 749 forum_grade_item_update($forum);
750 }
751}
752
353228d8 753/**
612607bd 754 * Create/update grade item for given forum
353228d8 755 *
a153c9f2 756 * @category grade
df1ba0f4 757 * @uses GRADE_TYPE_NONE
758 * @uses GRADE_TYPE_VALUE
759 * @uses GRADE_TYPE_SCALE
a153c9f2
AD
760 * @param stdClass $forum Forum object with extra cmidnumber
761 * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
612607bd 762 * @return int 0 if ok
353228d8 763 */
0b5a80a1 764function forum_grade_item_update($forum, $grades=NULL) {
612607bd 765 global $CFG;
766 if (!function_exists('grade_update')) { //workaround for buggy PHP versions
767 require_once($CFG->libdir.'/gradelib.php');
353228d8 768 }
769
612607bd 770 $params = array('itemname'=>$forum->name, 'idnumber'=>$forum->cmidnumber);
353228d8 771
5980d52f 772 if (!$forum->assessed or $forum->scale == 0) {
612607bd 773 $params['gradetype'] = GRADE_TYPE_NONE;
353228d8 774
775 } else if ($forum->scale > 0) {
776 $params['gradetype'] = GRADE_TYPE_VALUE;
777 $params['grademax'] = $forum->scale;
778 $params['grademin'] = 0;
779
780 } else if ($forum->scale < 0) {
781 $params['gradetype'] = GRADE_TYPE_SCALE;
782 $params['scaleid'] = -$forum->scale;
783 }
784
0b5a80a1 785 if ($grades === 'reset') {
786 $params['reset'] = true;
787 $grades = NULL;
788 }
789
790 return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $grades, $params);
353228d8 791}
792
793/**
794 * Delete grade item for given forum
795 *
a153c9f2
AD
796 * @category grade
797 * @param stdClass $forum Forum object
798 * @return grade_item
353228d8 799 */
800function forum_grade_item_delete($forum) {
612607bd 801 global $CFG;
802 require_once($CFG->libdir.'/gradelib.php');
803
b67ec72f 804 return grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, NULL, array('deleted'=>1));
caadf009 805}
806
85c9ebb9 807/**
808 * Checks if scale is being used by any instance of forum
809 *
810 * This is used to find out if scale used anywhere
df1ba0f4 811 *
812 * @global object
85c9ebb9 813 * @param $scaleid int
814 * @return boolean True if the scale is used by any forum
815 */
816function forum_scale_used_anywhere($scaleid) {
4e445355 817 global $DB;
818 if ($scaleid and $DB->record_exists('forum', array('scale' => -$scaleid))) {
85c9ebb9 819 return true;
820 } else {
821 return false;
822 }
823}
824
0a4ac01b 825// SQL FUNCTIONS ///////////////////////////////////////////////////////////
9fa49e22 826
0a4ac01b 827/**
828 * Gets a post with all info ready for forum_print_post
829 * Most of these joins are just to get the forum id
df1ba0f4 830 *
831 * @global object
832 * @global object
90f4745c 833 * @param int $postid
834 * @return mixed array of posts or false
0a4ac01b 835 */
1f48942e 836function forum_get_post_full($postid) {
4e445355 837 global $CFG, $DB;
1f48942e 838
a327f25e
AG
839 $allnames = get_all_user_name_fields(true, 'u');
840 return $DB->get_record_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
4e445355 841 FROM {forum_posts} p
842 JOIN {forum_discussions} d ON p.discussion = d.id
843 LEFT JOIN {user} u ON p.userid = u.id
844 WHERE p.id = ?", array($postid));
1f48942e 845}
846
65bcf17b 847/**
848 * Gets all posts in discussion including top parent.
df1ba0f4 849 *
bc4c7337
AN
850 * @param int $discussionid The Discussion to fetch.
851 * @param string $sort The sorting to apply.
852 * @param bool $tracking Whether the user tracks this forum.
853 * @return array The posts in the discussion.
65bcf17b 854 */
bc4c7337 855function forum_get_all_discussion_posts($discussionid, $sort, $tracking = false) {
4e445355 856 global $CFG, $DB, $USER;
65bcf17b 857
3c2bf848 858 $tr_sel = "";
859 $tr_join = "";
4e445355 860 $params = array();
3c2bf848 861
90f4745c 862 if ($tracking) {
90f4745c 863 $tr_sel = ", fr.id AS postread";
4e445355 864 $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
865 $params[] = $USER->id;
90f4745c 866 }
867
a327f25e 868 $allnames = get_all_user_name_fields(true, 'u');
4e445355 869 $params[] = $discussionid;
a327f25e 870 if (!$posts = $DB->get_records_sql("SELECT p.*, $allnames, u.email, u.picture, u.imagealt $tr_sel
4e445355 871 FROM {forum_posts} p
872 LEFT JOIN {user} u ON p.userid = u.id
90f4745c 873 $tr_join
4e445355 874 WHERE p.discussion = ?
875 ORDER BY $sort", $params)) {
65bcf17b 876 return array();
877 }
878
879 foreach ($posts as $pid=>$p) {
90f4745c 880 if ($tracking) {
881 if (forum_tp_is_post_old($p)) {
6b7de0bb 882 $posts[$pid]->postread = true;
90f4745c 883 }
884 }
65bcf17b 885 if (!$p->parent) {
886 continue;
887 }
888 if (!isset($posts[$p->parent])) {
889 continue; // parent does not exist??
890 }
891 if (!isset($posts[$p->parent]->children)) {
892 $posts[$p->parent]->children = array();
893 }
894 $posts[$p->parent]->children[$pid] =& $posts[$pid];
895 }
896
bfa7bece
AN
897 // Start with the last child of the first post.
898 $post = &$posts[reset($posts)->id];
899
900 $lastpost = false;
901 while (!$lastpost) {
902 if (!isset($post->children)) {
903 $post->lastpost = true;
904 $lastpost = true;
905 } else {
906 // Go to the last child of this post.
907 $post = &$posts[end($post->children)->id];
908 }
909 }
910
65bcf17b 911 return $posts;
912}
913
42fb3c85 914/**
915 * An array of forum objects that the user is allowed to read/search through.
df1ba0f4 916 *
917 * @global object
918 * @global object
919 * @global object
920 * @param int $userid
921 * @param int $courseid if 0, we look for forums throughout the whole site.
42fb3c85 922 * @return array of forum objects, or false if no matches
923 * Forum objects have the following attributes:
924 * id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
925 * viewhiddentimedposts
926 */
927function forum_get_readable_forums($userid, $courseid=0) {
2b63df96 928
4e445355 929 global $CFG, $DB, $USER;
6b7de0bb 930 require_once($CFG->dirroot.'/course/lib.php');
2b63df96 931
4e445355 932 if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
12e57b92 933 print_error('notinstalled', 'forum');
42fb3c85 934 }
2b63df96 935
42fb3c85 936 if ($courseid) {
4e445355 937 $courses = $DB->get_records('course', array('id' => $courseid));
42fb3c85 938 } else {
65bcf17b 939 // If no course is specified, then the user can see SITE + his courses.
4e445355 940 $courses1 = $DB->get_records('course', array('id' => SITEID));
b1d5d015 941 $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
6155150c 942 $courses = array_merge($courses1, $courses2);
42fb3c85 943 }
944 if (!$courses) {
6b7de0bb 945 return array();
42fb3c85 946 }
947
948 $readableforums = array();
2b63df96 949
6527b5c2 950 foreach ($courses as $course) {
951
f20edd52 952 $modinfo = get_fast_modinfo($course);
2b63df96 953
6b7de0bb 954 if (empty($modinfo->instances['forum'])) {
955 // hmm, no forums?
956 continue;
957 }
2b63df96 958
4e445355 959 $courseforums = $DB->get_records('forum', array('course' => $course->id));
2b63df96 960
6b7de0bb 961 foreach ($modinfo->instances['forum'] as $forumid => $cm) {
962 if (!$cm->uservisible or !isset($courseforums[$forumid])) {
963 continue;
964 }
bf0f06b1 965 $context = context_module::instance($cm->id);
6b7de0bb 966 $forum = $courseforums[$forumid];
2e945910
AB
967 $forum->context = $context;
968 $forum->cm = $cm;
d50704bf 969
6b7de0bb 970 if (!has_capability('mod/forum:viewdiscussion', $context)) {
971 continue;
972 }
6527b5c2 973
6b7de0bb 974 /// group access
975 if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
2c1bbbc5
MG
976
977 $forum->onlygroups = $modinfo->get_groups($cm->groupingid);
978 $forum->onlygroups[] = -1;
6b7de0bb 979 }
2b63df96 980
6b7de0bb 981 /// hidden timed discussions
982 $forum->viewhiddentimedposts = true;
983 if (!empty($CFG->forum_enabletimedposts)) {
984 if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
985 $forum->viewhiddentimedposts = false;
986 }
987 }
d50704bf 988
6b7de0bb 989 /// qanda access
990 if ($forum->type == 'qanda'
991 && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
2b63df96 992
6b7de0bb 993 // We need to check whether the user has posted in the qanda forum.
994 $forum->onlydiscussions = array(); // Holds discussion ids for the discussions
995 // the user is allowed to see in this forum.
996 if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
997 foreach ($discussionspostedin as $d) {
998 $forum->onlydiscussions[] = $d->id;
d50704bf 999 }
42fb3c85 1000 }
1001 }
6b7de0bb 1002
1003 $readableforums[$forum->id] = $forum;
42fb3c85 1004 }
6b7de0bb 1005
1006 unset($modinfo);
1007
42fb3c85 1008 } // End foreach $courses
2b63df96 1009
42fb3c85 1010 return $readableforums;
1011}
1012
bbbf2d40 1013/**
1014 * Returns a list of posts found using an array of search terms.
df1ba0f4 1015 *
1016 * @global object
1017 * @global object
1018 * @global object
1019 * @param array $searchterms array of search terms, e.g. word +word -word
1020 * @param int $courseid if 0, we search through the whole site
1021 * @param int $limitfrom
1022 * @param int $limitnum
1023 * @param int &$totalcount
1024 * @param string $extrasql
1025 * @return array|bool Array of posts found or false
42fb3c85 1026 */
2b63df96 1027function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=50,
42fb3c85 1028 &$totalcount, $extrasql='') {
4e445355 1029 global $CFG, $DB, $USER;
42fb3c85 1030 require_once($CFG->libdir.'/searchlib.php');
1031
1032 $forums = forum_get_readable_forums($USER->id, $courseid);
2b63df96 1033
67875aa1 1034 if (count($forums) == 0) {
6b7de0bb 1035 $totalcount = 0;
67875aa1 1036 return false;
1037 }
42fb3c85 1038
098f4337 1039 $now = floor(time() / 60) * 60; // DB Cache Friendly.
6b7de0bb 1040
1041 $fullaccess = array();
1042 $where = array();
4e445355 1043 $params = array();
6b7de0bb 1044
1045 foreach ($forums as $forumid => $forum) {
1046 $select = array();
1047
1048 if (!$forum->viewhiddentimedposts) {
b1d5d015
PS
1049 $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
1050 $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
42fb3c85 1051 }
6b7de0bb 1052
2e945910
AB
1053 $cm = $forum->cm;
1054 $context = $forum->context;
ad9c22aa 1055
1056 if ($forum->type == 'qanda'
1057 && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
6b7de0bb 1058 if (!empty($forum->onlydiscussions)) {
cf717dc2 1059 list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
4e445355 1060 $params = array_merge($params, $discussionid_params);
1061 $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
d50704bf 1062 } else {
6b7de0bb 1063 $select[] = "p.parent = 0";
d50704bf 1064 }
1065 }
6b7de0bb 1066
1067 if (!empty($forum->onlygroups)) {
cf717dc2 1068 list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
4e445355 1069 $params = array_merge($params, $groupid_params);
1070 $select[] = "d.groupid $groupid_sql";
42fb3c85 1071 }
6b7de0bb 1072
1073 if ($select) {
1074 $selects = implode(" AND ", $select);
b1d5d015
PS
1075 $where[] = "(d.forum = :forum{$forumid} AND $selects)";
1076 $params['forum'.$forumid] = $forumid;
6b7de0bb 1077 } else {
1078 $fullaccess[] = $forumid;
1079 }
1080 }
1081
1082 if ($fullaccess) {
cf717dc2 1083 list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
4e445355 1084 $params = array_merge($params, $fullid_params);
1085 $where[] = "(d.forum $fullid_sql)";
42fb3c85 1086 }
42fb3c85 1087
dd223096
P
1088 $favjoin = "";
1089 if (in_array('starredonly:on', $searchterms)) {
1090 $usercontext = context_user::instance($USER->id);
1091 $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
1092 list($favjoin, $favparams) = $ufservice->get_join_sql_by_type('mod_forum', 'discussions',
1093 "favourited", "d.id");
1094
1095 $searchterms = array_values(array_diff($searchterms, array('starredonly:on')));
1096 $params = array_merge($params, $favparams);
1097 $extrasql .= " AND favourited.itemid IS NOT NULL AND favourited.itemid != 0";
1098 }
1099
6b7de0bb 1100 $selectdiscussion = "(".implode(" OR ", $where).")";
42fb3c85 1101
42fb3c85 1102 $messagesearch = '';
1103 $searchstring = '';
2b63df96 1104
42fb3c85 1105 // Need to concat these back together for parser to work.
1106 foreach($searchterms as $searchterm){
1107 if ($searchstring != '') {
1108 $searchstring .= ' ';
1109 }
1110 $searchstring .= $searchterm;
1111 }
1112
1113 // We need to allow quoted strings for the search. The quotes *should* be stripped
1114 // by the parser, but this should be examined carefully for security implications.
1115 $searchstring = str_replace("\\\"","\"",$searchstring);
1116 $parser = new search_parser();
1117 $lexer = new search_lexer($parser);
1118
1119 if ($lexer->parse($searchstring)) {
1120 $parsearray = $parser->get_parsed_array();
d2ba493c
AH
1121
1122 $tagjoins = '';
1123 $tagfields = [];
e9c46768 1124 $tagfieldcount = 0;
dd223096
P
1125 if ($parsearray) {
1126 foreach ($parsearray as $token) {
1127 if ($token->getType() == TOKEN_TAGS) {
1128 for ($i = 0; $i <= substr_count($token->getValue(), ','); $i++) {
1129 // Queries can only have a limited number of joins so set a limit sensible users won't exceed.
1130 if ($tagfieldcount > 10) {
1131 continue;
1132 }
1133 $tagjoins .= " LEFT JOIN {tag_instance} ti_$tagfieldcount
e9c46768
AH
1134 ON p.id = ti_$tagfieldcount.itemid
1135 AND ti_$tagfieldcount.component = 'mod_forum'
1136 AND ti_$tagfieldcount.itemtype = 'forum_posts'";
dd223096
P
1137 $tagjoins .= " LEFT JOIN {tag} t_$tagfieldcount ON t_$tagfieldcount.id = ti_$tagfieldcount.tagid";
1138 $tagfields[] = "t_$tagfieldcount.rawname";
1139 $tagfieldcount++;
1140 }
d2ba493c
AH
1141 }
1142 }
dd223096
P
1143 list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
1144 'p.userid', 'u.id', 'u.firstname',
1145 'u.lastname', 'p.modified', 'd.forum',
1146 $tagfields);
1147
1148 $params = ($msparams ? array_merge($params, $msparams) : $params);
d2ba493c 1149 }
42fb3c85 1150 }
1151
d2ba493c
AH
1152 $fromsql = "{forum_posts} p
1153 INNER JOIN {forum_discussions} d ON d.id = p.discussion
dd223096 1154 INNER JOIN {user} u ON u.id = p.userid $tagjoins $favjoin";
42fb3c85 1155
dd223096
P
1156 $selectsql = ($messagesearch ? $messagesearch . " AND " : "").
1157 " p.discussion = d.id
42fb3c85 1158 AND p.userid = u.id
1159 AND $selectdiscussion
1160 $extrasql";
1161
1162 $countsql = "SELECT COUNT(*)
1163 FROM $fromsql
1164 WHERE $selectsql";
1165
a327f25e 1166 $allnames = get_all_user_name_fields(true, 'u');
7f094149 1167 $searchsql = "SELECT p.*,
42fb3c85 1168 d.forum,
a327f25e 1169 $allnames,
42fb3c85 1170 u.email,
8ba59d07 1171 u.picture,
ce7382c9 1172 u.imagealt
42fb3c85 1173 FROM $fromsql
1174 WHERE $selectsql
1175 ORDER BY p.modified DESC";
1176
4e445355 1177 $totalcount = $DB->count_records_sql($countsql, $params);
d50704bf 1178
4e445355 1179 return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
42fb3c85 1180}
1181
0a4ac01b 1182/**
1183 * Get all the posts for a user in a forum suitable for forum_print_post
df1ba0f4 1184 *
1185 * @global object
1186 * @global object
1187 * @uses CONTEXT_MODULE
1188 * @return array
0a4ac01b 1189 */
1f48942e 1190function forum_get_user_posts($forumid, $userid) {
4e445355 1191 global $CFG, $DB;
1f48942e 1192
90f4745c 1193 $timedsql = "";
4e445355 1194 $params = array($forumid, $userid);
1195
90f4745c 1196 if (!empty($CFG->forum_enabletimedposts)) {
1197 $cm = get_coursemodule_from_instance('forum', $forumid);
bf0f06b1 1198 if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
90f4745c 1199 $now = time();
4e445355 1200 $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
1201 $params[] = $now;
1202 $params[] = $now;
6b7de0bb 1203 }
90f4745c 1204 }
1205
a327f25e
AG
1206 $allnames = get_all_user_name_fields(true, 'u');
1207 return $DB->get_records_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
4e445355 1208 FROM {forum} f
1209 JOIN {forum_discussions} d ON d.forum = f.id
1210 JOIN {forum_posts} p ON p.discussion = d.id
1211 JOIN {user} u ON u.id = p.userid
1212 WHERE f.id = ?
1213 AND p.userid = ?
90f4745c 1214 $timedsql
4e445355 1215 ORDER BY p.modified ASC", $params);
1f48942e 1216}
1217
90f4745c 1218/**
1219 * Get all the discussions user participated in
df1ba0f4 1220 *
1221 * @global object
1222 * @global object
1223 * @uses CONTEXT_MODULE
90f4745c 1224 * @param int $forumid
1225 * @param int $userid
df1ba0f4 1226 * @return array Array or false
90f4745c 1227 */
1228function forum_get_user_involved_discussions($forumid, $userid) {
4e445355 1229 global $CFG, $DB;
90f4745c 1230
1231 $timedsql = "";
4e445355 1232 $params = array($forumid, $userid);
90f4745c 1233 if (!empty($CFG->forum_enabletimedposts)) {
1234 $cm = get_coursemodule_from_instance('forum', $forumid);
bf0f06b1 1235 if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
90f4745c 1236 $now = time();
4e445355 1237 $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
1238 $params[] = $now;
1239 $params[] = $now;
6b7de0bb 1240 }
90f4745c 1241 }
1242
4e445355 1243 return $DB->get_records_sql("SELECT DISTINCT d.*
1244 FROM {forum} f
1245 JOIN {forum_discussions} d ON d.forum = f.id
1246 JOIN {forum_posts} p ON p.discussion = d.id
1247 WHERE f.id = ?
1248 AND p.userid = ?
1249 $timedsql", $params);
90f4745c 1250}
1251
1252/**
1253 * Get all the posts for a user in a forum suitable for forum_print_post
df1ba0f4 1254 *
1255 * @global object
1256 * @global object
90f4745c 1257 * @param int $forumid
1258 * @param int $userid
1259 * @return array of counts or false
1260 */
1261function forum_count_user_posts($forumid, $userid) {
4e445355 1262 global $CFG, $DB;
90f4745c 1263
1264 $timedsql = "";
4e445355 1265 $params = array($forumid, $userid);
90f4745c 1266 if (!empty($CFG->forum_enabletimedposts)) {
1267 $cm = get_coursemodule_from_instance('forum', $forumid);
bf0f06b1 1268 if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
90f4745c 1269 $now = time();
4e445355 1270 $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
1271 $params[] = $now;
1272 $params[] = $now;
6b7de0bb 1273 }
90f4745c 1274 }
1275
4e445355 1276 return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
1277 FROM {forum} f
1278 JOIN {forum_discussions} d ON d.forum = f.id
1279 JOIN {forum_posts} p ON p.discussion = d.id
1280 JOIN {user} u ON u.id = p.userid
1281 WHERE f.id = ?
1282 AND p.userid = ?
1283 $timedsql", $params);
90f4745c 1284}
1285
0a4ac01b 1286/**
1287 * Given a log entry, return the forum post details for it.
df1ba0f4 1288 *
1289 * @global object
1290 * @global object
1291 * @param object $log
1292 * @return array|null
0a4ac01b 1293 */
1f48942e 1294function forum_get_post_from_log($log) {
4e445355 1295 global $CFG, $DB;
1f48942e 1296
a327f25e 1297 $allnames = get_all_user_name_fields(true, 'u');
1f48942e 1298 if ($log->action == "add post") {
1299
a327f25e 1300 return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
4e445355 1301 FROM {forum_discussions} d,
1302 {forum_posts} p,
1303 {forum} f,
1304 {user} u
1305 WHERE p.id = ?
65b0e537 1306 AND d.id = p.discussion
1307 AND p.userid = u.id
8f7dc7f1 1308 AND u.deleted <> '1'
4e445355 1309 AND f.id = d.forum", array($log->info));
1f48942e 1310
1311
1312 } else if ($log->action == "add discussion") {
1313
a327f25e 1314 return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
4e445355 1315 FROM {forum_discussions} d,
1316 {forum_posts} p,
1317 {forum} f,
1318 {user} u
1319 WHERE d.id = ?
65b0e537 1320 AND d.firstpost = p.id
1321 AND p.userid = u.id
8f7dc7f1 1322 AND u.deleted <> '1'
4e445355 1323 AND f.id = d.forum", array($log->info));
1f48942e 1324 }
1325 return NULL;
1326}
1327
0a4ac01b 1328/**
1329 * Given a discussion id, return the first post from the discussion
df1ba0f4 1330 *
1331 * @global object
1332 * @global object
1333 * @param int $dicsussionid
1334 * @return array
0a4ac01b 1335 */
d05956ac 1336function forum_get_firstpost_from_discussion($discussionid) {
4e445355 1337 global $CFG, $DB;
d05956ac 1338
4e445355 1339 return $DB->get_record_sql("SELECT p.*
1340 FROM {forum_discussions} d,
1341 {forum_posts} p
1342 WHERE d.id = ?
1343 AND d.firstpost = p.id ", array($discussionid));
d05956ac 1344}
1345
0a4ac01b 1346/**
90f4745c 1347 * Returns an array of counts of replies to each discussion
df1ba0f4 1348 *
bc4c7337
AN
1349 * @param int $forumid
1350 * @param string $forumsort
1351 * @param int $limit
1352 * @param int $page
1353 * @param int $perpage
1354 * @param boolean $canseeprivatereplies Whether the current user can see private replies.
1355 * @return array
0a4ac01b 1356 */
bc4c7337
AN
1357function forum_count_discussion_replies($forumid, $forumsort = "", $limit = -1, $page = -1, $perpage = 0,
1358 $canseeprivatereplies = false) {
1359 global $CFG, $DB, $USER;
1f48942e 1360
90f4745c 1361 if ($limit > 0) {
1362 $limitfrom = 0;
1363 $limitnum = $limit;
1364 } else if ($page != -1) {
1365 $limitfrom = $page*$perpage;
1366 $limitnum = $perpage;
1367 } else {
1368 $limitfrom = 0;
1369 $limitnum = 0;
1370 }
1371
1372 if ($forumsort == "") {
1373 $orderby = "";
1374 $groupby = "";
1375
1376 } else {
1377 $orderby = "ORDER BY $forumsort";
1378 $groupby = ", ".strtolower($forumsort);
1379 $groupby = str_replace('desc', '', $groupby);
1380 $groupby = str_replace('asc', '', $groupby);
1381 }
1382
bc4c7337
AN
1383 $params = ['forumid' => $forumid];
1384
1385 if (!$canseeprivatereplies) {
1386 $privatewhere = ' AND (p.privatereplyto = :currentuser1 OR p.userid = :currentuser2 OR p.privatereplyto = 0)';
1387 $params['currentuser1'] = $USER->id;
1388 $params['currentuser2'] = $USER->id;
1389 } else {
1390 $privatewhere = '';
1391 }
1392
bfeb10b7 1393 if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
1394 $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
4e445355 1395 FROM {forum_posts} p
1396 JOIN {forum_discussions} d ON p.discussion = d.id
bc4c7337
AN
1397 WHERE p.parent > 0 AND d.forum = :forumid
1398 $privatewhere
bfeb10b7 1399 GROUP BY p.discussion";
bc4c7337 1400 return $DB->get_records_sql($sql, $params);
bfeb10b7 1401
90f4745c 1402 } else {
bfeb10b7 1403 $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
4e445355 1404 FROM {forum_posts} p
1405 JOIN {forum_discussions} d ON p.discussion = d.id
bc4c7337
AN
1406 WHERE d.forum = :forumid
1407 $privatewhere
38ec3fb0 1408 GROUP BY p.discussion $groupby $orderby";
bc4c7337 1409 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
90f4745c 1410 }
1411}
1412
df1ba0f4 1413/**
1414 * @global object
1415 * @global object
1416 * @global object
1417 * @staticvar array $cache
1418 * @param object $forum
1419 * @param object $cm
1420 * @param object $course
1421 * @return mixed
1422 */
90f4745c 1423function forum_count_discussions($forum, $cm, $course) {
4e445355 1424 global $CFG, $DB, $USER;
90f4745c 1425
1426 static $cache = array();
1427
098f4337 1428 $now = floor(time() / 60) * 60; // DB Cache Friendly.
90f4745c 1429
4e445355 1430 $params = array($course->id);
1431
90f4745c 1432 if (!isset($cache[$course->id])) {
1433 if (!empty($CFG->forum_enabletimedposts)) {
4e445355 1434 $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
1435 $params[] = $now;
1436 $params[] = $now;
90f4745c 1437 } else {
1438 $timedsql = "";
1439 }
1440
1441 $sql = "SELECT f.id, COUNT(d.id) as dcount
4e445355 1442 FROM {forum} f
1443 JOIN {forum_discussions} d ON d.forum = f.id
1444 WHERE f.course = ?
90f4745c 1445 $timedsql
1446 GROUP BY f.id";
a48e8c4b 1447
4e445355 1448 if ($counts = $DB->get_records_sql($sql, $params)) {
90f4745c 1449 foreach ($counts as $count) {
1450 $counts[$count->id] = $count->dcount;
1451 }
1452 $cache[$course->id] = $counts;
1453 } else {
1454 $cache[$course->id] = array();
1455 }
a48e8c4b 1456 }
90f4745c 1457
1458 if (empty($cache[$course->id][$forum->id])) {
1459 return 0;
a48e8c4b 1460 }
90f4745c 1461
1462 $groupmode = groups_get_activity_groupmode($cm, $course);
1463
1464 if ($groupmode != SEPARATEGROUPS) {
1465 return $cache[$course->id][$forum->id];
1f48942e 1466 }
90f4745c 1467
bf0f06b1 1468 if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) {
90f4745c 1469 return $cache[$course->id][$forum->id];
1470 }
1471
1472 require_once($CFG->dirroot.'/course/lib.php');
1473
f20edd52 1474 $modinfo = get_fast_modinfo($course);
90f4745c 1475
2c1bbbc5 1476 $mygroups = $modinfo->get_groups($cm->groupingid);
90f4745c 1477
1478 // add all groups posts
2c1bbbc5 1479 $mygroups[-1] = -1;
4e445355 1480
1481 list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
1482 $params[] = $forum->id;
90f4745c 1483
1484 if (!empty($CFG->forum_enabletimedposts)) {
1485 $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
4e445355 1486 $params[] = $now;
1487 $params[] = $now;
90f4745c 1488 } else {
1489 $timedsql = "";
1490 }
1491
1492 $sql = "SELECT COUNT(d.id)
4e445355 1493 FROM {forum_discussions} d
342650a7 1494 WHERE d.groupid $mygroups_sql AND d.forum = ?
90f4745c 1495 $timedsql";
1496
4e445355 1497 return $DB->get_field_sql($sql, $params);
1f48942e 1498}
1499
0a4ac01b 1500/**
1501 * Get all discussions in a forum
df1ba0f4 1502 *
1503 * @global object
1504 * @global object
1505 * @global object
1506 * @uses CONTEXT_MODULE
1507 * @uses VISIBLEGROUPS
1508 * @param object $cm
1509 * @param string $forumsort
1510 * @param bool $fullpost
1511 * @param int $unused
1512 * @param int $limit
1513 * @param bool $userlastmodified
1514 * @param int $page
1515 * @param int $perpage
4f3a2d21
JL
1516 * @param int $groupid if groups enabled, get discussions for this group overriding the current group.
1517 * Use FORUM_POSTS_ALL_USER_GROUPS for all the user groups
65b2669d 1518 * @param int $updatedsince retrieve only discussions updated since the given time
df1ba0f4 1519 * @return array
0a4ac01b 1520 */
1e366657 1521function forum_get_discussions($cm, $forumsort="", $fullpost=true, $unused=-1, $limit=-1,
02a73d76 1522 $userlastmodified=false, $page=-1, $perpage=0, $groupid = -1,
65b2669d 1523 $updatedsince = 0) {
4e445355 1524 global $CFG, $DB, $USER;
0fcac008 1525
3d284127 1526 $timelimit = '';
1527
098f4337 1528 $now = floor(time() / 60) * 60;
4e445355 1529 $params = array($cm->instance);
90f4745c 1530
bf0f06b1 1531 $modcontext = context_module::instance($cm->id);
2b63df96 1532
4436a63b 1533 if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
1534 return array();
1535 }
1536
1537 if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
2b63df96 1538
0468976c 1539 if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
4e445355 1540 $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
1541 $params[] = $now;
1542 $params[] = $now;
90f4745c 1543 if (isloggedin()) {
4e445355 1544 $timelimit .= " OR d.userid = ?";
1545 $params[] = $USER->id;
3d284127 1546 }
90f4745c 1547 $timelimit .= ")";
fbc21e82 1548 }
fbc21e82 1549 }
1f48942e 1550
90f4745c 1551 if ($limit > 0) {
1552 $limitfrom = 0;
1553 $limitnum = $limit;
1554 } else if ($page != -1) {
1555 $limitfrom = $page*$perpage;
1556 $limitnum = $perpage;
1557 } else {
1558 $limitfrom = 0;
1559 $limitnum = 0;
90ec387a 1560 }
8f0cd6ef 1561
90f4745c 1562 $groupmode = groups_get_activity_groupmode($cm);
353228d8 1563
fffa8b35 1564 if ($groupmode) {
4f3a2d21 1565
fffa8b35 1566 if (empty($modcontext)) {
bf0f06b1 1567 $modcontext = context_module::instance($cm->id);
fffa8b35 1568 }
1569
4f3a2d21
JL
1570 // Special case, we received a groupid to override currentgroup.
1571 if ($groupid > 0) {
1572 $course = get_course($cm->course);
1573 if (!groups_group_visible($groupid, $course, $cm)) {
1574 // User doesn't belong to this group, return nothing.
1575 return array();
1576 }
1577 $currentgroup = $groupid;
1578 } else if ($groupid === -1) {
1579 $currentgroup = groups_get_activity_group($cm);
1580 } else {
1581 // Get discussions for all groups current user can see.
1582 $currentgroup = null;
1583 }
1584
fffa8b35 1585 if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
1586 if ($currentgroup) {
4e445355 1587 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
1588 $params[] = $currentgroup;
fffa8b35 1589 } else {
1590 $groupselect = "";
1591 }
1592
1593 } else {
4f3a2d21
JL
1594 // Separate groups.
1595
1596 // Get discussions for all groups current user can see.
1597 if ($currentgroup === null) {
1598 $mygroups = array_keys(groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.id'));
1599 if (empty($mygroups)) {
1600 $groupselect = "AND d.groupid = -1";
1601 } else {
1602 list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($mygroups);
1603 $groupselect = "AND (d.groupid = -1 OR d.groupid $insqlgroups)";
1604 $params = array_merge($params, $inparamsgroups);
1605 }
1606 } else if ($currentgroup) {
4e445355 1607 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
1608 $params[] = $currentgroup;
fffa8b35 1609 } else {
1610 $groupselect = "AND d.groupid = -1";
1611 }
1612 }
353228d8 1613 } else {
02509fe6 1614 $groupselect = "";
1615 }
29507631 1616 if (empty($forumsort)) {
1e366657 1617 $forumsort = forum_get_default_sort_order();
29507631 1618 }
2ab968e9 1619 if (empty($fullpost)) {
94df729b 1620 $postdata = "p.id, p.subject, p.modified, p.discussion, p.userid, p.created";
2ab968e9 1621 } else {
1622 $postdata = "p.*";
1623 }
9197e147 1624
13597d01 1625 if (empty($userlastmodified)) { // We don't need to know this
fffa8b35 1626 $umfields = "";
1627 $umtable = "";
13597d01 1628 } else {
d85bedf7
JL
1629 $umfields = ', ' . get_all_user_name_fields(true, 'um', null, 'um') . ', um.email AS umemail, um.picture AS umpicture,
1630 um.imagealt AS umimagealt';
4e445355 1631 $umtable = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
90f4745c 1632 }
1633
65b2669d
JL
1634 $updatedsincesql = '';
1635 if (!empty($updatedsince)) {
1636 $updatedsincesql = 'AND d.timemodified > ?';
1637 $params[] = $updatedsince;
1638 }
2893812e 1639
8666dfec 1640 $discussionfields = "d.id as discussionid, d.course, d.forum, d.name, d.firstpost, d.groupid, d.assessed," .
bdb4a87d 1641 " d.timemodified, d.usermodified, d.timestart, d.timeend, d.pinned, d.timelocked";
65b2669d 1642
a327f25e 1643 $allnames = get_all_user_name_fields(true, 'u');
2f6f3397 1644 $sql = "SELECT $postdata, $discussionfields,
424920f8 1645 $allnames, u.email, u.picture, u.imagealt $umfields
4e445355 1646 FROM {forum_discussions} d
1647 JOIN {forum_posts} p ON p.discussion = d.id
1648 JOIN {user} u ON p.userid = u.id
90f4745c 1649 $umtable
4e445355 1650 WHERE d.forum = ? AND p.parent = 0
65b2669d 1651 $timelimit $groupselect $updatedsincesql
5f219cf1 1652 ORDER BY $forumsort, d.id DESC";
65b2669d 1653
4e445355 1654 return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
90f4745c 1655}
1656
8d3a5ba1
FM
1657/**
1658 * Gets the neighbours (previous and next) of a discussion.
1659 *
5f219cf1
BK
1660 * The calculation is based on the timemodified when time modified or time created is identical
1661 * It will revert to using the ID to sort consistently. This is better tha skipping a discussion.
8d3a5ba1 1662 *
9e381efd
DM
1663 * For blog-style forums, the calculation is based on the original creation time of the
1664 * blog post.
1665 *
8d3a5ba1
FM
1666 * Please note that this does not check whether or not the discussion passed is accessible
1667 * by the user, it simply uses it as a reference to find the neighbours. On the other hand,
1668 * the returned neighbours are checked and are accessible to the current user.
1669 *
1670 * @param object $cm The CM record.
1671 * @param object $discussion The discussion record.
9e381efd 1672 * @param object $forum The forum instance record.
8d3a5ba1
FM
1673 * @return array That always contains the keys 'prev' and 'next'. When there is a result
1674 * they contain the record with minimal information such as 'id' and 'name'.
1675 * When the neighbour is not found the value is false.
1676 */
9e381efd 1677function forum_get_discussion_neighbours($cm, $discussion, $forum) {
8d3a5ba1
FM
1678 global $CFG, $DB, $USER;
1679
9e381efd 1680 if ($cm->instance != $discussion->forum or $discussion->forum != $forum->id or $forum->id != $cm->instance) {
8d3a5ba1
FM
1681 throw new coding_exception('Discussion is not part of the same forum.');
1682 }
1683
1684 $neighbours = array('prev' => false, 'next' => false);
098f4337 1685 $now = floor(time() / 60) * 60;
8d3a5ba1
FM
1686 $params = array();
1687
1688 $modcontext = context_module::instance($cm->id);
1689 $groupmode = groups_get_activity_groupmode($cm);
1690 $currentgroup = groups_get_activity_group($cm);
1691
1692 // Users must fulfill timed posts.
1693 $timelimit = '';
1694 if (!empty($CFG->forum_enabletimedposts)) {
1695 if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
1696 $timelimit = ' AND ((d.timestart <= :tltimestart AND (d.timeend = 0 OR d.timeend > :tltimeend))';
1697 $params['tltimestart'] = $now;
1698 $params['tltimeend'] = $now;
1699 if (isloggedin()) {
1700 $timelimit .= ' OR d.userid = :tluserid';
1701 $params['tluserid'] = $USER->id;
1702 }
1703 $timelimit .= ')';
1704 }
1705 }
1706
1707 // Limiting to posts accessible according to groups.
1708 $groupselect = '';
1709 if ($groupmode) {
1710 if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $modcontext)) {
1711 if ($currentgroup) {
1712 $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
1713 $params['groupid'] = $currentgroup;
1714 }
1715 } else {
1716 if ($currentgroup) {
1717 $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
1718 $params['groupid'] = $currentgroup;
1719 } else {
1720 $groupselect = 'AND d.groupid = -1';
1721 }
1722 }
1723 }
1724
5f219cf1
BK
1725 $params['forumid'] = $cm->instance;
1726 $params['discid1'] = $discussion->id;
1727 $params['discid2'] = $discussion->id;
1728 $params['discid3'] = $discussion->id;
1729 $params['discid4'] = $discussion->id;
1730 $params['disctimecompare1'] = $discussion->timemodified;
1731 $params['disctimecompare2'] = $discussion->timemodified;
1732 $params['pinnedstate1'] = (int) $discussion->pinned;
1733 $params['pinnedstate2'] = (int) $discussion->pinned;
1734 $params['pinnedstate3'] = (int) $discussion->pinned;
1735 $params['pinnedstate4'] = (int) $discussion->pinned;
9e381efd 1736
5f219cf1
BK
1737 $sql = "SELECT d.id, d.name, d.timemodified, d.groupid, d.timestart, d.timeend
1738 FROM {forum_discussions} d
1739 JOIN {forum_posts} p ON d.firstpost = p.id
1740 WHERE d.forum = :forumid
1741 AND d.id <> :discid1
1742 $timelimit
1743 $groupselect";
1744 $comparefield = "d.timemodified";
1745 $comparevalue = ":disctimecompare1";
1746 $comparevalue2 = ":disctimecompare2";
1747 if (!empty($CFG->forum_enabletimedposts)) {
1748 // Here we need to take into account the release time (timestart)
1749 // if one is set, of the neighbouring posts and compare it to the
1750 // timestart or timemodified of *this* post depending on if the
1751 // release date of this post is in the future or not.
1752 // This stops discussions that appear later because of the
1753 // timestart value from being buried under discussions that were
1754 // made afterwards.
1755 $comparefield = "CASE WHEN d.timemodified < d.timestart
1756 THEN d.timestart ELSE d.timemodified END";
1757 if ($discussion->timemodified < $discussion->timestart) {
1758 // Normally we would just use the timemodified for sorting
1759 // discussion posts. However, when timed discussions are enabled,
1760 // then posts need to be sorted base on the later of timemodified
1761 // or the release date of the post (timestart).
1762 $params['disctimecompare1'] = $discussion->timestart;
1763 $params['disctimecompare2'] = $discussion->timestart;
1764 }
1765 }
1766 $orderbydesc = forum_get_default_sort_order(true, $comparefield, 'd', false);
1767 $orderbyasc = forum_get_default_sort_order(false, $comparefield, 'd', false);
9e381efd 1768
5f219cf1
BK
1769 if ($forum->type === 'blog') {
1770 $subselect = "SELECT pp.created
1771 FROM {forum_discussions} dd
1772 JOIN {forum_posts} pp ON dd.firstpost = pp.id ";
8d3a5ba1 1773
5f219cf1
BK
1774 $subselectwhere1 = " WHERE dd.id = :discid3";
1775 $subselectwhere2 = " WHERE dd.id = :discid4";
8d3a5ba1 1776
5f219cf1 1777 $comparefield = "p.created";
9e381efd 1778
5f219cf1
BK
1779 $sub1 = $subselect.$subselectwhere1;
1780 $comparevalue = "($sub1)";
9e381efd 1781
5f219cf1
BK
1782 $sub2 = $subselect.$subselectwhere2;
1783 $comparevalue2 = "($sub2)";
8d3a5ba1 1784
5f219cf1
BK
1785 $orderbydesc = "d.pinned, p.created DESC";
1786 $orderbyasc = "d.pinned, p.created ASC";
1787 }
1e366657 1788
5f219cf1
BK
1789 $prevsql = $sql . " AND ( (($comparefield < $comparevalue) AND :pinnedstate1 = d.pinned)
1790 OR ($comparefield = $comparevalue2 AND (d.pinned = 0 OR d.pinned = :pinnedstate4) AND d.id < :discid2)
1791 OR (d.pinned = 0 AND d.pinned <> :pinnedstate2))
1792 ORDER BY CASE WHEN d.pinned = :pinnedstate3 THEN 1 ELSE 0 END DESC, $orderbydesc, d.id DESC";
9e381efd 1793
5f219cf1
BK
1794 $nextsql = $sql . " AND ( (($comparefield > $comparevalue) AND :pinnedstate1 = d.pinned)
1795 OR ($comparefield = $comparevalue2 AND (d.pinned = 1 OR d.pinned = :pinnedstate4) AND d.id > :discid2)
1796 OR (d.pinned = 1 AND d.pinned <> :pinnedstate2))
1797 ORDER BY CASE WHEN d.pinned = :pinnedstate3 THEN 1 ELSE 0 END DESC, $orderbyasc, d.id ASC";
8d3a5ba1 1798
5f219cf1
BK
1799 $neighbours['prev'] = $DB->get_record_sql($prevsql, $params, IGNORE_MULTIPLE);
1800 $neighbours['next'] = $DB->get_record_sql($nextsql, $params, IGNORE_MULTIPLE);
8d3a5ba1
FM
1801 return $neighbours;
1802}
1803
1e366657
AO
1804/**
1805 * Get the sql to use in the ORDER BY clause for forum discussions.
1806 *
1807 * This has the ordering take timed discussion windows into account.
1808 *
1809 * @param bool $desc True for DESC, False for ASC.
1810 * @param string $compare The field in the SQL to compare to normally sort by.
1811 * @param string $prefix The prefix being used for the discussion table.
5f219cf1 1812 * @param bool $pinned sort pinned posts to the top
1e366657
AO
1813 * @return string
1814 */
5f219cf1 1815function forum_get_default_sort_order($desc = true, $compare = 'd.timemodified', $prefix = 'd', $pinned = true) {
1e366657
AO
1816 global $CFG;
1817
1818 if (!empty($prefix)) {
1819 $prefix .= '.';
1820 }
1821
1822 $dir = $desc ? 'DESC' : 'ASC';
1823
5f219cf1
BK
1824 if ($pinned == true) {
1825 $pinned = "{$prefix}pinned DESC,";
1826 } else {
1827 $pinned = '';
1828 }
1829
1e366657
AO
1830 $sort = "{$prefix}timemodified";
1831 if (!empty($CFG->forum_enabletimedposts)) {
1832 $sort = "CASE WHEN {$compare} < {$prefix}timestart
1833 THEN {$prefix}timestart
1834 ELSE {$compare}
1835 END";
1836 }
5f219cf1 1837 return "$pinned $sort $dir";
1e366657
AO
1838}
1839
df1ba0f4 1840/**
1841 *
1842 * @global object
1843 * @global object
1844 * @global object
1845 * @uses CONTEXT_MODULE
1846 * @uses VISIBLEGROUPS
1847 * @param object $cm
1848 * @return array
1849 */
bfeb10b7 1850function forum_get_discussions_unread($cm) {
4e445355 1851 global $CFG, $DB, $USER;
90f4745c 1852
098f4337 1853 $now = floor(time() / 60) * 60;
ee151230 1854 $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
35716b86 1855
ee151230 1856 $params = array();
90f4745c 1857 $groupmode = groups_get_activity_groupmode($cm);
1858 $currentgroup = groups_get_activity_group($cm);
1859
1860 if ($groupmode) {
bf0f06b1 1861 $modcontext = context_module::instance($cm->id);
90f4745c 1862
1863 if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
1864 if ($currentgroup) {
ee151230
AD
1865 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
1866 $params['currentgroup'] = $currentgroup;
90f4745c 1867 } else {
1868 $groupselect = "";
1869 }
1870
1871 } else {
ee151230 1872 //separate groups without access all
90f4745c 1873 if ($currentgroup) {
ee151230
AD
1874 $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
1875 $params['currentgroup'] = $currentgroup;
90f4745c 1876 } else {
1877 $groupselect = "AND d.groupid = -1";
1878 }
1879 }
1880 } else {
1881 $groupselect = "";
13597d01 1882 }
1883
90f4745c 1884 if (!empty($CFG->forum_enabletimedposts)) {
ee151230
AD
1885 $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
1886 $params['now1'] = $now;
1887 $params['now2'] = $now;
90f4745c 1888 } else {
1889 $timedsql = "";
1890 }
1891
1892 $sql = "SELECT d.id, COUNT(p.id) AS unread
4e445355 1893 FROM {forum_discussions} d
1894 JOIN {forum_posts} p ON p.discussion = d.id
1895 LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
90f4745c 1896 WHERE d.forum = {$cm->instance}
ee151230 1897 AND p.modified >= :cutoffdate AND r.id is NULL
90f4745c 1898 $groupselect
4e445355 1899 $timedsql
bfeb10b7 1900 GROUP BY d.id";
ee151230
AD
1901 $params['cutoffdate'] = $cutoffdate;
1902
4e445355 1903 if ($unreads = $DB->get_records_sql($sql, $params)) {
90f4745c 1904 foreach ($unreads as $unread) {
1905 $unreads[$unread->id] = $unread->unread;
1906 }
1907 return $unreads;
1908 } else {
1909 return array();
1910 }
1f48942e 1911}
1912
df1ba0f4 1913/**
1914 * @global object
1915 * @global object
1916 * @global object
1917 * @uses CONEXT_MODULE
1918 * @uses VISIBLEGROUPS
1919 * @param object $cm
1920 * @return array
1921 */
90f4745c 1922function forum_get_discussions_count($cm) {
4e445355 1923 global $CFG, $DB, $USER;
90f4745c 1924
098f4337 1925 $now = floor(time() / 60) * 60;
4e445355 1926 $params = array($cm->instance);
90f4745c 1927 $groupmode = groups_get_activity_groupmode($cm);
1928 $currentgroup = groups_get_activity_group($cm);
1929
1930 if ($groupmode) {
bf0f06b1 1931 $modcontext = context_module::instance($cm->id);
90f4745c 1932
1933 if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
1934 if ($currentgroup) {
4e445355 1935 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
1936 $params[] = $currentgroup;
90f4745c 1937 } else {
1938 $groupselect = "";
1939 }
1940
1941 } else {
1942 //seprate groups without access all
1943 if ($currentgroup) {
4e445355 1944 $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
1945 $params[] = $currentgroup;
90f4745c 1946 } else {
1947 $groupselect = "AND d.groupid = -1";
1948 }
1949 }
1950 } else {
1951 $groupselect = "";
1952 }
1953
90f4745c 1954 $timelimit = "";
1955
1956 if (!empty($CFG->forum_enabletimedposts)) {
1957
bf0f06b1 1958 $modcontext = context_module::instance($cm->id);
90f4745c 1959
1960 if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
4e445355 1961 $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
1962 $params[] = $now;
1963 $params[] = $now;
90f4745c 1964 if (isloggedin()) {
4e445355 1965 $timelimit .= " OR d.userid = ?";
1966 $params[] = $USER->id;
90f4745c 1967 }
1968 $timelimit .= ")";
1969 }
1970 }
1971
1972 $sql = "SELECT COUNT(d.id)
4e445355 1973 FROM {forum_discussions} d
1974 JOIN {forum_posts} p ON p.discussion = d.id
1975 WHERE d.forum = ? AND p.parent = 0
1976 $groupselect $timelimit";
90f4745c 1977
4e445355 1978 return $DB->get_field_sql($sql, $params);
90f4745c 1979}
1f48942e 1980
1981
0a4ac01b 1982// OTHER FUNCTIONS ///////////////////////////////////////////////////////////
f93f848a 1983
1984
df1ba0f4 1985/**
1986 * @global object
1987 * @global object
1988 * @param int $courseid
1989 * @param string $type
1990 */
11b0c469 1991function forum_get_course_forum($courseid, $type) {
1992// How to set up special 1-per-course forums
c112bc60 1993 global $CFG, $DB, $OUTPUT, $USER;
a6fcdf98 1994
4e445355 1995 if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
65b0e537 1996 // There should always only be ONE, but with the right combination of
29cbd93a 1997 // errors there might be more. In this case, just return the oldest one (lowest ID).
1998 foreach ($forums as $forum) {
1999 return $forum; // ie the first one
11b0c469 2000 }
8daaf761 2001 }
e6874d9f 2002
8daaf761 2003 // Doesn't exist, so create one now.
b85b25eb 2004 $forum = new stdClass();
8daaf761 2005 $forum->course = $courseid;
2006 $forum->type = "$type";
c112bc60
ARN
2007 if (!empty($USER->htmleditor)) {
2008 $forum->introformat = $USER->htmleditor;
2009 }
8daaf761 2010 switch ($forum->type) {
2011 case "news":
294ce987 2012 $forum->name = get_string("namenews", "forum");
2013 $forum->intro = get_string("intronews", "forum");
490476a1 2014 $forum->introformat = FORMAT_HTML;
906fef94 2015 $forum->forcesubscribe = FORUM_FORCESUBSCRIBE;
8daaf761 2016 $forum->assessed = 0;
709f0ec8 2017 if ($courseid == SITEID) {
2018 $forum->name = get_string("sitenews");
2019 $forum->forcesubscribe = 0;
8f0cd6ef 2020 }
8daaf761 2021 break;
2022 case "social":
294ce987 2023 $forum->name = get_string("namesocial", "forum");
2024 $forum->intro = get_string("introsocial", "forum");
490476a1 2025 $forum->introformat = FORMAT_HTML;
8daaf761 2026 $forum->assessed = 0;
2027 $forum->forcesubscribe = 0;
2028 break;
1c7b8b93
NC
2029 case "blog":
2030 $forum->name = get_string('blogforum', 'forum');
2031 $forum->intro = get_string('introblog', 'forum');
490476a1 2032 $forum->introformat = FORMAT_HTML;
1c7b8b93
NC
2033 $forum->assessed = 0;
2034 $forum->forcesubscribe = 0;
2035 break;
8daaf761 2036 default:
9146b979 2037 echo $OUTPUT->notification("That forum type doesn't exist!");
8daaf761 2038 return false;
2039 break;
2040 }
2041
2042 $forum->timemodified = time();
4e445355 2043 $forum->id = $DB->insert_record("forum", $forum);
8daaf761 2044
4e445355 2045 if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
9146b979 2046 echo $OUTPUT->notification("Could not find forum module!!");
e1b5643f 2047 return false;
82aa0e8d 2048 }
39790bd8 2049 $mod = new stdClass();
e1b5643f 2050 $mod->course = $courseid;
2051 $mod->module = $module->id;
2052 $mod->instance = $forum->id;
2053 $mod->section = 0;
722e6ba9
MG
2054 include_once("$CFG->dirroot/course/lib.php");
2055 if (! $mod->coursemodule = add_course_module($mod) ) {
11cd754e 2056 echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
e1b5643f 2057 return false;
2058 }
722e6ba9 2059 $sectionid = course_add_cm_to_section($courseid, $mod->coursemodule, 0);
4e445355 2060 return $DB->get_record("forum", array("id" => "$forum->id"));
82aa0e8d 2061}
2062
d251b259
AD
2063/**
2064 * Return rating related permissions
2b04c41c 2065 *
d251b259
AD
2066 * @param string $options the context id
2067 * @return array an associative array of the user's rating permissions
2068 */
2b04c41c 2069function forum_rating_permissions($contextid, $component, $ratingarea) {
d197ea43 2070 $context = context::instance_by_id($contextid, MUST_EXIST);
2b04c41c
SH
2071 if ($component != 'mod_forum' || $ratingarea != 'post') {
2072 // We don't know about this component/ratingarea so just return null to get the
2073 // default restrictive permissions.
d251b259 2074 return null;
d251b259 2075 }
2b04c41c
SH
2076 return array(
2077 'view' => has_capability('mod/forum:viewrating', $context),
2078 'viewany' => has_capability('mod/forum:viewanyrating', $context),
2079 'viewall' => has_capability('mod/forum:viewallratings', $context),
2080 'rate' => has_capability('mod/forum:rate', $context)
2081 );
d251b259
AD
2082}
2083
aeafd436 2084/**
2c2ff8d5
AD
2085 * Validates a submitted rating
2086 * @param array $params submitted data
2087 * context => object the context in which the rated items exists [required]
2b04c41c
SH
2088 * component => The component for this module - should always be mod_forum [required]
2089 * ratingarea => object the context in which the rated items exists [required]
db4754e9 2090 *
2c2ff8d5
AD
2091 * itemid => int the ID of the object being rated [required]
2092 * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
2093 * rating => int the submitted rating [required]
2094 * rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
2095 * aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
778361c3 2096 * @return boolean true if the rating is valid. Will throw rating_exception if not
aeafd436 2097 */
778361c3 2098function forum_rating_validate($params) {
2c2ff8d5
AD
2099 global $DB, $USER;
2100
2b04c41c
SH
2101 // Check the component is mod_forum
2102 if ($params['component'] != 'mod_forum') {
2103 throw new rating_exception('invalidcomponent');
2c2ff8d5
AD
2104 }
2105
2b04c41c
SH
2106 // Check the ratingarea is post (the only rating area in forum)
2107 if ($params['ratingarea'] != 'post') {
2108 throw new rating_exception('invalidratingarea');
2c2ff8d5
AD
2109 }
2110
2b04c41c
SH
2111 // Check the rateduserid is not the current user .. you can't rate your own posts
2112 if ($params['rateduserid'] == $USER->id) {
778361c3
AD
2113 throw new rating_exception('nopermissiontorate');
2114 }
2115
2b04c41c
SH
2116 // Fetch all the related records ... we need to do this anyway to call forum_user_can_see_post
2117 $post = $DB->get_record('forum_posts', array('id' => $params['itemid'], 'userid' => $params['rateduserid']), '*', MUST_EXIST);
2118 $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion), '*', MUST_EXIST);
2119 $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
74df2951 2120 $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
2b04c41c 2121 $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST);
bf0f06b1 2122 $context = context_module::instance($cm->id);
2b04c41c
SH
2123
2124 // Make sure the context provided is the context of the forum
2125 if ($context->id != $params['context']->id) {
2126 throw new rating_exception('invalidcontext');
2127 }
f2e72593 2128
2b04c41c
SH
2129 if ($forum->scale != $params['scaleid']) {
2130 //the scale being submitted doesnt match the one in the database
2131 throw new rating_exception('invalidscaleid');
2c2ff8d5
AD
2132 }
2133
2b04c41c
SH
2134 // check the item we're rating was created in the assessable time window
2135 if (!empty($forum->assesstimestart) && !empty($forum->assesstimefinish)) {
2136 if ($post->created < $forum->assesstimestart || $post->created > $forum->assesstimefinish) {
2137 throw new rating_exception('notavailable');
2138 }
2139 }
f2e72593 2140
6ac149dc 2141 //check that the submitted rating is valid for the scale
5693d56c
EL
2142
2143 // lower limit
2144 if ($params['rating'] < 0 && $params['rating'] != RATING_UNSET_RATING) {
6ac149dc 2145 throw new rating_exception('invalidnum');
5693d56c
EL
2146 }
2147
2148 // upper limit
c4cee99c 2149 if ($forum->scale < 0) {
6ac149dc 2150 //its a custom scale
2b04c41c 2151 $scalerecord = $DB->get_record('scale', array('id' => -$forum->scale));
6ac149dc
AD
2152 if ($scalerecord) {
2153 $scalearray = explode(',', $scalerecord->scale);
2154 if ($params['rating'] > count($scalearray)) {
2155 throw new rating_exception('invalidnum');
2156 }
2157 } else {
2158 throw new rating_exception('invalidscaleid');
2159 }
2b04c41c 2160 } else if ($params['rating'] > $forum->scale) {
6ac149dc
AD
2161 //if its numeric and submitted rating is above maximum
2162 throw new rating_exception('invalidnum');
2163 }
2164
2c2ff8d5 2165 // Make sure groups allow this user to see the item they're rating
2b04c41c
SH
2166 if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) { // Groups are being used
2167 if (!groups_group_exists($discussion->groupid)) { // Can't find group
778361c3 2168 throw new rating_exception('cannotfindgroup');//something is wrong
2c2ff8d5
AD
2169 }
2170
2b04c41c 2171 if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) {
2c2ff8d5 2172 // do not allow rating of posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS
778361c3 2173 throw new rating_exception('notmemberofgroup');
2c2ff8d5
AD
2174 }
2175 }
2176
2b04c41c
SH
2177 // perform some final capability checks
2178 if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
778361c3 2179 throw new rating_exception('nopermissiontorate');
2c2ff8d5
AD
2180 }
2181
2182 return true;
aeafd436
AD
2183}
2184
731c2712
AA
2185/**
2186 * Can the current user see ratings for a given itemid?
2187 *
2188 * @param array $params submitted data
2189 * contextid => int contextid [required]
2190 * component => The component for this module - should always be mod_forum [required]
2191 * ratingarea => object the context in which the rated items exists [required]
2192 * itemid => int the ID of the object being rated [required]
2193 * scaleid => int scale id [optional]
2194 * @return bool
2195 * @throws coding_exception
2196 * @throws rating_exception
2197 */
2198function mod_forum_rating_can_see_item_ratings($params) {
2199 global $DB, $USER;
2200
2201 // Check the component is mod_forum.
2202 if (!isset($params['component']) || $params['component'] != 'mod_forum') {
2203 throw new rating_exception('invalidcomponent');
2204 }
2205
2206 // Check the ratingarea is post (the only rating area in forum).
2207 if (!isset($params['ratingarea']) || $params['ratingarea'] != 'post') {
2208 throw new rating_exception('invalidratingarea');
2209 }
2210
2211 if (!isset($params['itemid'])) {
2212 throw new rating_exception('invaliditemid');
2213 }
2214
2215 $post = $DB->get_record('forum_posts', array('id' => $params['itemid']), '*', MUST_EXIST);
2216 $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion), '*', MUST_EXIST);
2217 $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
2218 $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
2219 $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST);
2220
2221 // Perform some final capability checks.
2222 if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
2223 return false;
2224 }
3e95e09b 2225
731c2712
AA
2226 return true;
2227}
42fb3c85 2228
eaf50aef 2229/**
0a4ac01b 2230 * This function prints the overview of a discussion in the forum listing.
2231 * It needs some discussion information and some post information, these
2232 * happen to be combined for efficiency in the $post parameter by the function
2233 * that calls this one: forum_print_latest_discussions()
2234 *
df1ba0f4 2235 * @global object
2236 * @global object
0a4ac01b 2237 * @param object $post The post object (passed by reference for speed).
2238 * @param object $forum The forum object.
2239 * @param int $group Current group.
2240 * @param string $datestring Format to use for the dates.
2241 * @param boolean $cantrack Is tracking enabled for this forum.
2242 * @param boolean $forumtracked Is the user tracking this forum.
2243 * @param boolean $canviewparticipants True if user has the viewparticipants permission for this course
05f1455b 2244 * @param boolean $canviewhiddentimedposts True if user has the viewhiddentimedposts permission for this forum
0a4ac01b 2245 */
05f1455b
AO
2246function forum_print_discussion_header(&$post, $forum, $group = -1, $datestring = "",
2247 $cantrack = true, $forumtracked = true, $canviewparticipants = true, $modcontext = null,
2248 $canviewhiddentimedposts = false) {
43921b8a 2249
05f1455b 2250 global $COURSE, $USER, $CFG, $OUTPUT, $PAGE;
3335f6fb 2251
f51e8d7e 2252 static $rowcount;
5733262d 2253 static $strmarkalldread;
f51e8d7e 2254
2c894bb9 2255 if (empty($modcontext)) {
2256 if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
12e57b92 2257 print_error('invalidcoursemodule');
2c894bb9 2258 }
bf0f06b1 2259 $modcontext = context_module::instance($cm->id);
951e1073 2260 }
951e1073 2261
f51e8d7e 2262 if (!isset($rowcount)) {
2263 $rowcount = 0;
5733262d 2264 $strmarkalldread = get_string('markalldread', 'forum');
f51e8d7e 2265 } else {
2266 $rowcount = ($rowcount + 1) % 2;
2267 }
2268
17dc3f3c 2269 $post->subject = format_string($post->subject,true);
436a7cae 2270
26cee437 2271 $canviewfullnames = has_capability('moodle/site:viewfullnames', $modcontext);
05f1455b
AO
2272 $timeddiscussion = !empty($CFG->forum_enabletimedposts) && ($post->timestart || $post->timeend);
2273 $timedoutsidewindow = '';
2274 if ($timeddiscussion && ($post->timestart > time() || ($post->timeend != 0 && $post->timeend < time()))) {
2275 $timedoutsidewindow = ' dimmed_text';
2276 }
2277
f51e8d7e 2278 echo "\n\n";
05f1455b 2279 echo '<tr class="discussion r'.$rowcount.$timedoutsidewindow.'">';
3335f6fb 2280
87b007b4
CF
2281 $topicclass = 'topic starter';
2282 if (FORUM_DISCUSSION_PINNED == $post->pinned) {
2283 $topicclass .= ' pinned';
2284 }
2285 echo '<td class="'.$topicclass.'">';
5f219cf1
BK
2286 if (FORUM_DISCUSSION_PINNED == $post->pinned) {
2287 echo $OUTPUT->pix_icon('i/pinned', get_string('discussionpinned', 'forum'), 'mod_forum');
2288 }
05f1455b
AO
2289 $canalwaysseetimedpost = $USER->id == $post->userid || $canviewhiddentimedposts;
2290 if ($timeddiscussion && $canalwaysseetimedpost) {
2291 echo $PAGE->get_renderer('mod_forum')->timed_discussion_tooltip($post, empty($timedoutsidewindow));
2292 }
2293
f51e8d7e 2294 echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">'.$post->subject.'</a>';
29507631 2295 echo "</td>\n";
2296
2297 // Picture
39790bd8 2298 $postuser = new stdClass();
e351bed5 2299 $postuserfields = explode(',', user_picture::fields());
5b1944bb 2300 $postuser = username_load_fields_from_object($postuser, $post, null, $postuserfields);
e351bed5 2301 $postuser->id = $post->userid;
0433d36d 2302 echo '<td class="author">';
b1a74e40 2303 echo '<div class="media">';
5db1ce53 2304 echo '<span class="float-left">';
812dbaf7 2305 echo $OUTPUT->user_picture($postuser, array('courseid'=>$forum->course));
0433d36d 2306 echo '</span>';
29507631 2307 // User name
b1a74e40 2308 echo '<div class="media-body">';
26cee437 2309 $fullname = fullname($postuser, $canviewfullnames);
f51e8d7e 2310 echo '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$post->userid.'&amp;course='.$forum->course.'">'.$fullname.'</a>';
b1a74e40
BB
2311 echo '</div>';
2312 echo '</div>';
29507631 2313 echo "</td>\n";
2314
3a68fbbb 2315 // Group picture
425b4f1a 2316 if ($group !== -1) { // Groups are active - group is a group data object or NULL
f51e8d7e 2317 echo '<td class="picture group">';
c2b552fe 2318 if (!empty($group->picture) and empty($group->hidepicture)) {
800fa051
DM
2319 if ($canviewparticipants && $COURSE->groupmode) {
2320 $picturelink = true;
2321 } else {
2322 $picturelink = false;
2323 }
2324 print_group_picture($group, $forum->course, false, false, $picturelink);
3a68fbbb 2325 } else if (isset($group->id)) {
c980dd12 2326 if ($canviewparticipants && $COURSE->groupmode) {
18d6ef01 2327 echo '<a href="'.$CFG->wwwroot.'/user/index.php?id='.$forum->course.'&amp;group='.$group->id.'">'.$group->name.'</a>';
2328 } else {
2329 echo $group->name;
2330 }
3a68fbbb 2331 }
2332 echo "</td>\n";
2333 }
2334
f0da6b85 2335 if (has_capability('mod/forum:viewdiscussion', $modcontext)) { // Show the column with replies
f51e8d7e 2336 echo '<td class="replies">';
2337 echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'">';
2338 echo $post->replies.'</a>';
a796d0b8 2339 echo "</td>\n";
e3ff14ca 2340
eaf50aef 2341 if ($cantrack) {
f51e8d7e 2342 echo '<td class="replies">';
eaf50aef 2343 if ($forumtracked) {
2344 if ($post->unread > 0) {
2345 echo '<span class="unread">';
2346 echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#unread">';
2347 echo $post->unread;
2348 echo '</a>';
c39748f4 2349 echo '<a title="'.$strmarkalldread.'" href="'.$CFG->wwwroot.'/mod/forum/markposts.php?f='.
2e19ca18
RW
2350 $forum->id.'&amp;d='.$post->discussion.'&amp;mark=read&amp;return=/mod/forum/view.php&amp;sesskey=' .
2351 sesskey() . '">' . $OUTPUT->pix_icon('t/markasread', $strmarkalldread) . '</a>';
eaf50aef 2352 echo '</span>';
2353 } else {
2354 echo '<span class="read">';
eaf50aef 2355 echo $post->unread;
eaf50aef 2356 echo '</span>';
2357 }
f51e8d7e 2358 } else {
343af274 2359 echo '<span class="read">';
eaf50aef 2360 echo '-';
343af274 2361 echo '</span>';
3a68fbbb 2362 }
f37da850 2363 echo "</td>\n";
2364 }
a796d0b8 2365 }
3335f6fb 2366
f51e8d7e 2367 echo '<td class="lastpost">';
014fe0ad 2368 $usedate = (empty($post->timemodified)) ? $post->created : $post->timemodified;
8f2e8060 2369 $parenturl = '';
39790bd8 2370 $usermodified = new stdClass();
e351bed5
AG
2371 $usermodified->id = $post->usermodified;
2372 $usermodified = username_load_fields_from_object($usermodified, $post, 'um');
8f2e8060 2373
cbebd40d
DM
2374 // In QA forums we check that the user can view participants.
2375 if ($forum->type !== 'qanda' || $canviewparticipants) {
b53b69c1 2376 echo '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$post->usermodified.'&amp;course='.$forum->course.'">'.
26cee437 2377 fullname($usermodified, $canviewfullnames).'</a><br />';
8f2e8060 2378 $parenturl = (empty($post->lastpostid)) ? '' : '&amp;parent='.$post->lastpostid;
b53b69c1 2379 }
8f2e8060 2380
ac00b904 2381 echo '<a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.$parenturl.'">'.
1bf3a76a 2382 userdate_htmltime($usedate, $datestring).'</a>';
29507631 2383 echo "</td>\n";
2384
ddb4a31d
AN
2385 // is_guest should be used here as this also checks whether the user is a guest in the current course.
2386 // Guests and visitors cannot subscribe - only enrolled users.
2387 if ((!is_guest($modcontext, $USER) && isloggedin()) && has_capability('mod/forum:viewdiscussion', $modcontext)) {
ebfb73db
AN
2388 // Discussion subscription.
2389 if (\mod_forum\subscriptions::is_subscribable($forum)) {
2390 echo '<td class="discussionsubscription">';
2391 echo forum_get_discussion_subscription_icon($forum, $post->discussion);
2392 echo '</td>';
2393 }
2394 }
2395
f51e8d7e 2396 echo "</tr>\n\n";
3335f6fb 2397
3335f6fb 2398}
2399
ebfb73db
AN
2400/**
2401 * Return the markup for the discussion subscription toggling icon.
2402 *
2403 * @param stdClass $forum The forum object.
2404 * @param int $discussionid The discussion to create an icon for.
2405 * @return string The generated markup.
2406 */
517e1782 2407function forum_get_discussion_subscription_icon($forum, $discussionid, $returnurl = null, $includetext = false) {
ebfb73db
AN
2408 global $USER, $OUTPUT, $PAGE;
2409
2410 if ($returnurl === null && $PAGE->url) {
2411 $returnurl = $PAGE->url->out();
2412 }
2413
2414 $o = '';
2415 $subscriptionstatus = \mod_forum\subscriptions::is_subscribed($USER->id, $forum, $discussionid);
2416 $subscriptionlink = new moodle_url('/mod/forum/subscribe.php', array(
2417 'sesskey' => sesskey(),
2418 'id' => $forum->id,
2419 'd' => $discussionid,
2420 'returnurl' => $returnurl,
2421 ));
a35ce611
AN
2422
2423 if ($includetext) {
2424 $o .= $subscriptionstatus ? get_string('subscribed', 'mod_forum') : get_string('notsubscribed', 'mod_forum');
2425 }
2426
ebfb73db 2427 if ($subscriptionstatus) {
a35ce611
AN
2428 $output = $OUTPUT->pix_icon('t/subscribed', get_string('clicktounsubscribe', 'forum'), 'mod_forum');
2429 if ($includetext) {
2430 $output .= get_string('subscribed', 'mod_forum');
2431 }
2432
2433 return html_writer::link($subscriptionlink, $output, array(
ebfb73db
AN
2434 'title' => get_string('clicktounsubscribe', 'forum'),
2435 'class' => 'discussiontoggle iconsmall',
2436 'data-forumid' => $forum->id,
2437 'data-discussionid' => $discussionid,
79b4fb12 2438 'data-includetext' => $includetext,
a35ce611
AN
2439 ));
2440
ebfb73db 2441 } else {
a35ce611
AN
2442 $output = $OUTPUT->pix_icon('t/unsubscribed', get_string('clicktosubscribe', 'forum'), 'mod_forum');
2443 if ($includetext) {
2444 $output .= get_string('notsubscribed', 'mod_forum');
2445 }
2446
2447 return html_writer::link($subscriptionlink, $output, array(
ebfb73db
AN
2448 'title' => get_string('clicktosubscribe', 'forum'),
2449 'class' => 'discussiontoggle iconsmall',
2450 'data-forumid' => $forum->id,
2451 'data-discussionid' => $discussionid,
79b4fb12 2452 'data-includetext' => $includetext,
a35ce611 2453 ));
ebfb73db 2454 }
ebfb73db
AN
2455}
2456
548453ec
AN
2457/**
2458 * Return a pair of spans containing classes to allow the subscribe and
2459 * unsubscribe icons to be pre-loaded by a browser.
2460 *
2461 * @return string The generated markup
2462 */
2463function forum_get_discussion_subscription_icon_preloaders() {
2464 $o = '';
2465 $o .= html_writer::span('&nbsp;', 'preload-subscribe');
2466 $o .= html_writer::span('&nbsp;', 'preload-unsubscribe');
2467 return $o;
2468}
2469
83da3d28 2470/**
2471 * Print the drop down that allows the user to select how they want to have
2472 * the discussion displayed.
df1ba0f4 2473 *
2474 * @param int $id forum id if $forumtype is 'single',
83da3d28 2475 * discussion id for any other forum type
df1ba0f4 2476 * @param mixed $mode forum layout mode
2477 * @param string $forumtype optional
83da3d28 2478 */
2479function forum_print_mode_form($id, $mode, $forumtype='') {
9fc666e5 2480 global $OUTPUT;
83da3d28 2481 if ($forumtype == 'single') {
8295918f 2482 $select = new single_select(new moodle_url("/mod/forum/view.php", array('f'=>$id)), 'mode', forum_get_layout_modes(), $mode, null, "mode");
d1881bec 2483 $select->set_label(get_string('displaymode', 'forum'), array('class' => 'accesshide'));
f8dab966 2484 $select->class = "forummode";
8295918f
MD
2485 } else {
2486 $select = new single_select(new moodle_url("/mod/forum/discuss.php", array('d'=>$id)), 'mode', forum_get_layout_modes(), $mode, null, "mode");
d1881bec 2487 $select->set_label(get_string('displaymode', 'forum'), array('class' => 'accesshide'));
83da3d28 2488 }
f8dab966 2489 echo $OUTPUT->render($select);
501cdbd8 2490}
2491
0a4ac01b 2492/**
df1ba0f4 2493 * @global object
2494 * @param object $course
2495 * @param string $search
2496 * @return string
0a4ac01b 2497 */
6f1cc8d6 2498function forum_search_form($course, $search='') {
66bb9ead
FM
2499 global $CFG, $PAGE;
2500 $forumsearch = new \mod_forum\output\quick_search_form($course->id, $search);
2501 $output = $PAGE->get_renderer('mod_forum');
2502 return $output->render($forumsearch);
501cdbd8 2503}
2504
2505
0a4ac01b 2506/**
df1ba0f4 2507 * @global object
2508 * @global object
0a4ac01b 2509 */
11b0c469 2510function forum_set_return() {
607809b3 2511 global $CFG, $SESSION;
501cdbd8 2512
28e1e8b9 2513 if (! isset($SESSION->fromdiscussion)) {
dcee0b94 2514 $referer = get_local_referer(false);
28e1e8b9 2515 // If the referer is NOT a login screen then save it.
48d38fad 2516 if (! strncasecmp("$CFG->wwwroot/login", $referer, 300)) {
b2687a05 2517 $SESSION->fromdiscussion = $referer;
28e1e8b9 2518 }
501cdbd8 2519 }
2520}
2521
2522
0a4ac01b 2523/**
df1ba0f4 2524 * @global object
3d8d8a8c 2525 * @param string|\moodle_url $default
df1ba0f4 2526 * @return string
0a4ac01b 2527 */
11b0c469 2528function forum_go_back_to($default) {
501cdbd8 2529 global $SESSION;
2530
9c9f7d77 2531 if (!empty($SESSION->fromdiscussion)) {
501cdbd8 2532 $returnto = $SESSION->fromdiscussion;
2533 unset($SESSION->fromdiscussion);
2534 return $returnto;
2535 } else {
2536 return $default;
2537 }
2538}
2539
0a4ac01b 2540/**
0faf6791 2541 * Given a discussion object that is being moved to $forumto,
2542 * this function checks all posts in that discussion
2543 * for attachments, and if any are found, these are
2544 * moved to the new forum directory.
2545 *
df1ba0f4 2546 * @global object
2547 * @param object $discussion
0faf6791 2548 * @param int $forumfrom source forum id
2549 * @param int $forumto target forum id
2550 * @return bool success
0a4ac01b 2551 */
0faf6791 2552function forum_move_attachments($discussion, $forumfrom, $forumto) {
2553 global $DB;
65bcf17b 2554
0faf6791 2555 $fs = get_file_storage();
7f6689e4 2556
0faf6791 2557 $newcm = get_coursemodule_from_instance('forum', $forumto);
2558 $oldcm = get_coursemodule_from_instance('forum', $forumfrom);
7f6689e4 2559
bf0f06b1
AA
2560 $newcontext = context_module::instance($newcm->id);
2561 $oldcontext = context_module::instance($oldcm->id);
7f6689e4 2562
0faf6791 2563 // loop through all posts, better not use attachment flag ;-)
1f0e7702 2564 if ($posts = $DB->get_records('forum_posts', array('discussion'=>$discussion->id), '', 'id, attachment')) {
0faf6791 2565 foreach ($posts as $post) {
d2af1014
TH
2566 $fs->move_area_files_to_new_context($oldcontext->id,
2567 $newcontext->id, 'mod_forum', 'post', $post->id);
2568 $attachmentsmoved = $fs->move_area_files_to_new_context($oldcontext->id,
2569 $newcontext->id, 'mod_forum', 'attachment', $post->id);
2570 if ($attachmentsmoved > 0 && $post->attachment != '1') {
2571 // Weird - let's fix it
2572 $post->attachment = '1';
2573 $DB->update_record('forum_posts', $post);
2574 } else if ($attachmentsmoved == 0 && $post->attachment != '') {
2575 // Weird - let's fix it
2576 $post->attachment = '';
2577 $DB->update_record('forum_posts', $post);
7f6689e4 2578 }
2579 }
7f6689e4 2580 }
0faf6791 2581
2582 return true;
7f6689e4 2583}
2584
0a4ac01b 2585/**
0faf6791 2586 * Returns attachments as formated text/html optionally with separate images
df1ba0f4 2587 *
2588 * @global object
2589 * @global object
2590 * @global object
0faf6791 2591 * @param object $post
2592 * @param object $cm
df1ba0f4 2593 * @param string $type html/text/separateimages
0faf6791 2594 * @return mixed string or array of (html text withouth images and image HTML)
0a4ac01b 2595 */
0faf6791 2596function forum_print_attachments($post, $cm, $type) {
d436d197 2597 global $CFG, $DB, $USER, $OUTPUT;
cc2b7ea5 2598
0faf6791 2599 if (empty($post->attachment)) {
2600 return $type !== 'separateimages' ? '' : array('', '');
cc2b7ea5 2601 }
cc2b7ea5 2602
0faf6791 2603 if (!in_array($type, array('separateimages', 'html', 'text'))) {
2604 return $type !== 'separateimages' ? '' : array('', '');
2605 }
7f6689e4 2606
bf0f06b1 2607 if (!$context = context_module::instance($cm->id)) {
0faf6791 2608 return $type !== 'separateimages' ? '' : array('', '');
2609 }
2610 $strattachment = get_string('attachment', 'forum');
7f6689e4 2611
0faf6791 2612 $fs = get_file_storage();
7f6689e4 2613
0faf6791 2614 $imagereturn = '';
2615 $output = '';
72d497d4 2616
b8d3c3f7 2617 $canexport = !empty($CFG->enableportfolios) && (has_capability('mod/forum:exportpost', $context) || ($post->userid == $USER->id && has_capability('mod/forum:exportownpost', $context)));
7f6689e4 2618
b8d3c3f7 2619 if ($canexport) {
6708a1f5 2620 require_once($CFG->libdir.'/portfoliolib.php');
b8d3c3f7
SH
2621 }
2622
cc8bd704
AN
2623 // We retrieve all files according to the time that they were created. In the case that several files were uploaded
2624 // at the sametime (e.g. in the case of drag/drop upload) we revert to using the filename.
2625 $files = $fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id, "filename", false);
b8d3c3f7
SH
2626 if ($files) {
2627 if ($canexport) {
6708a1f5 2628 $button = new portfolio_add_button();
b8d3c3f7
SH
2629 }
2630 foreach ($files as $file) {
2631 $filename = $file->get_filename();
2632 $mimetype = $file->get_mimetype();
559276b1 2633 $iconimage = $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file), 'moodle', array('class' => 'icon'));
b8d3c3f7
SH
2634 $path = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.$context->id.'/mod_forum/attachment/'.$post->id.'/'.$filename);
2635
2636 if ($type == 'html') {
2637 $output .= "<a href=\"$path\">$iconimage</a> ";
2638 $output .= "<a href=\"$path\">".s($filename)."</a>";
2639 if ($canexport) {
37743241 2640 $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum');
b8d3c3f7
SH
2641 $button->set_format_by_file($file);
2642 $output .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
2643 }
2644 $output .= "<br />";
6708a1f5 2645
b8d3c3f7
SH
2646 } else if ($type == 'text') {
2647 $output .= "$strattachment ".s($filename).":\n$path\n";
2648
2649 } else { //'returnimages'
2650 if (in_array($mimetype, array('image/gif', 'image/jpeg', 'image/png'))) {
2651 // Image attachments don't get printed as links
2652 $imagereturn .= "<br /><img src=\"$path\" alt=\"\" />";
2653 if ($canexport) {
37743241 2654 $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum');
b8d3c3f7
SH
2655 $button->set_format_by_file($file);
2656 $imagereturn .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
2657 }
2658 } else {
0faf6791 2659 $output .= "<a href=\"$path\">$iconimage</a> ";
b8d3c3f7 2660 $output .= format_text("<a href=\"$path\">".s($filename)."</a>", FORMAT_HTML, array('context'=>$context));
f98b13a6 2661 if ($canexport) {
37743241 2662 $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum');
887160c7 2663 $button->set_format_by_file($file);
0d06b6fd 2664 $output .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
10ae55f9 2665 }
b8d3c3f7 2666 $output .= '<br />';
7f6689e4 2667 }
2668 }
103e7cba
KG
2669
2670 if (!empty($CFG->enableplagiarism)) {
2671 require_once($CFG->libdir.'/plagiarismlib.php');
2672 $output .= plagiarism_get_links(array('userid' => $post->userid,
2673 'file' => $file,
2674 'cmid' => $cm->id,
334bee5f
CC
2675 'course' => $cm->course,
2676 'forum' => $cm->instance));
103e7cba
KG
2677 $output .= '<br />';
2678 }
7f6689e4 2679 }
2680 }
72d497d4 2681
0faf6791 2682 if ($type !== 'separateimages') {
7f6689e4 2683 return $output;
0faf6791 2684
2685 } else {
2686 return array($output, $imagereturn);
2687 }
2688}
2689
9a1c0751
DM
2690////////////////////////////////////////////////////////////////////////////////
2691// File API //
2692////////////////////////////////////////////////////////////////////////////////
2693
d6f7cbfd 2694/**
2695 * Lists all browsable file areas
df1ba0f4 2696 *
d2b7803e
DC
2697 * @package mod_forum
2698 * @category files
2699 * @param stdClass $course course object
2700 * @param stdClass $cm course module object
2701 * @param stdClass $context context object
df1ba0f4 2702 * @return array
d6f7cbfd 2703 */
2704function forum_get_file_areas($course, $cm, $context) {
9a1c0751
DM
2705 return array(
2706 'attachment' => get_string('areaattachment', 'mod_forum'),
2707 'post' => get_string('areapost', 'mod_forum'),
2708 );
d6f7cbfd 2709}
2710
beb18068
MG
2711/**
2712 * File browsing support for forum module.
2713 *
d2b7803e
DC
2714 * @package mod_forum
2715 * @category files
2716 * @param stdClass $browser file browser object
2717 * @param stdClass $areas file areas
2718 * @param stdClass $course course object
2719 * @param stdClass $cm course module
2720 * @param stdClass $context context module
2721 * @param string $filearea file area
2722 * @param int $itemid item ID
2723 * @param string $filepath file path
2724 * @param string $filename file name
2725 * @return file_info instance or null if not found
beb18068
MG
2726 */
2727function forum_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
35ca63c1 2728 global $CFG, $DB, $USER;
beb18068
MG
2729
2730 if ($context->contextlevel != CONTEXT_MODULE) {
2731 return null;
2732 }
2733
9a1c0751
DM
2734 // filearea must contain a real area
2735 if (!isset($areas[$filearea])) {
2736 return null;
2737 }
2738
9a1c0751
DM
2739 // Note that forum_user_can_see_post() additionally allows access for parent roles
2740 // and it explicitly checks qanda forum type, too. One day, when we stop requiring
2741 // course:managefiles, we will need to extend this.
2742 if (!has_capability('mod/forum:viewdiscussion', $context)) {
beb18068
MG
2743 return null;
2744 }
2745
9a1c0751
DM
2746 if (is_null($itemid)) {
2747 require_once($CFG->dirroot.'/mod/forum/locallib.php');
2748 return new forum_file_info_container($browser, $course, $cm, $context, $areas, $filearea);
2749 }
2750
406f8dd8
MG
2751 static $cached = array();
2752 // $cached will store last retrieved post, discussion and forum. To make sure that the cache
2753 // is cleared between unit tests we check if this is the same session
2754 if (!isset($cached['sesskey']) || $cached['sesskey'] != sesskey()) {
2755 $cached = array('sesskey' => sesskey());
2756 }
2757
2758 if (isset($cached['post']) && $cached['post']->id == $itemid) {
2759 $post = $cached['post'];
2760 } else if ($post = $DB->get_record('forum_posts', array('id' => $itemid))) {
2761 $cached['post'] = $post;
2762 } else {
beb18068
MG
2763 return null;
2764 }
2765
406f8dd8
MG
2766 if (isset($cached['discussion']) && $cached['discussion']->id == $post->discussion) {
2767 $discussion = $cached['discussion'];
2768 } else if ($discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion))) {
2769 $cached['discussion'] = $discussion;
2770 } else {
beb18068
MG
2771 return null;
2772 }
2773
406f8dd8
MG
2774 if (isset($cached['forum']) && $cached['forum']->id == $cm->instance) {
2775 $forum = $cached['forum'];
2776 } else if ($forum = $DB->get_record('forum', array('id' => $cm->instance))) {
2777 $cached['forum'] = $forum;
2778 } else {
beb18068
MG
2779 return null;
2780 }
2781
2782 $fs = get_file_storage();
2783 $filepath = is_null($filepath) ? '/' : $filepath;
2784 $filename = is_null($filename) ? '.' : $filename;
2785 if (!($storedfile = $fs->get_file($context->id, 'mod_forum', $filearea, $itemid, $filepath, $filename))) {
2786 return null;
2787 }
2788
35ca63c1
AG
2789 // Checks to see if the user can manage files or is the owner.
2790 // TODO MDL-33805 - Do not use userid here and move the capability check above.
2791 if (!has_capability('moodle/course:managefiles', $context) && $storedfile->get_userid() != $USER->id) {
2792 return null;
2793 }
beb18068 2794 // Make sure groups allow this user to see this file
406f8dd8 2795 if ($discussion->groupid > 0 && !has_capability('moodle/site:accessallgroups', $context)) {
b75ec104 2796 $groupmode = groups_get_activity_groupmode($cm, $course);
406f8dd8
MG
2797 if ($groupmode == SEPARATEGROUPS && !groups_is_member($discussion->groupid)) {
2798 return null;
beb18068
MG
2799 }
2800 }
2801
2802 // Make sure we're allowed to see it...
2803 if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
2804 return null;
2805 }
2806
2807 $urlbase = $CFG->wwwroot.'/pluginfile.php';
9a1c0751 2808 return new file_info_stored($browser, $context, $storedfile, $urlbase, $itemid, true, true, false, false);
beb18068
MG
2809}
2810
0faf6791 2811/**
2812 * Serves the forum attachments. Implements needed access control ;-)
df1ba0f4 2813 *
d2b7803e
DC
2814 * @package mod_forum
2815 * @category files
2816 * @param stdClass $course course object
2817 * @param stdClass $cm course module object
2818 * @param stdClass $context context object
2819 * @param string $filearea file area
2820 * @param array $args extra arguments
2821 * @param bool $forcedownload whether or not force download
261cbbac 2822 * @param array $options additional options affecting the file serving
86900a93 2823 * @return bool false if file not found, does not return if found - justsend the file
0faf6791 2824 */
261cbbac 2825function forum_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
0faf6791 2826 global $CFG, $DB;
2827
64f93798 2828 if ($context->contextlevel != CONTEXT_MODULE) {
7e55d513 2829 return false;
2830 }
2831
64f93798
PS
2832 require_course_login($course, true, $cm);
2833
9a1c0751
DM
2834 $areas = forum_get_file_areas($course, $cm, $context);
2835
2836 // filearea must contain a real area
2837 if (!isset($areas[$filearea])) {
0faf6791 2838 return false;
2839 }
2840
2841 $postid = (int)array_shift($args);
2842
2843 if (!$post = $DB->get_record('forum_posts', array('id'=>$postid))) {
2844 return false;
7f6689e4 2845 }
72d497d4 2846
0faf6791 2847 if (!$discussion = $DB->get_record('forum_discussions', array('id'=>$post->discussion))) {
2848 return false;
2849 }
2850
64f93798 2851 if (!$forum = $DB->get_record('forum', array('id'=>$cm->instance))) {
0faf6791 2852 return false;
2853 }
2854
2855 $fs = get_file_storage();
64f93798
PS
2856 $relativepath = implode('/', $args);
2857 $fullpath = "/$context->id/mod_forum/$filearea/$postid/$relativepath";
0faf6791 2858 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
2859 return false;
2860 }
2861
2862 // Make sure groups allow this user to see this file
b75ec104
DM
2863 if ($discussion->groupid > 0) {
2864 $groupmode = groups_get_activity_groupmode($cm, $course);
2865 if ($groupmode == SEPARATEGROUPS) {
2866 if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) {
2867 return false;
2868 }
0faf6791 2869 }
2870 }
2871
2872 // Make sure we're allowed to see it...
1d797cac 2873 if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
0faf6791 2874 return false;
2875 }
2876
0faf6791 2877 // finally send the file
261cbbac 2878 send_stored_file($file, 0, 0, true, $options); // download MUST be forced - security!
7f6689e4 2879}
0faf6791 2880
db290a6e 2881/**
e3ff14ca 2882 * If successful, this function returns the name of the file
df1ba0f4 2883 *
2884 * @global object
2885 * @param object $post is a full post record, including course and forum
2886 * @param object $forum
2887 * @param object $cm
2888 * @param mixed $mform
a9a0cb69 2889 * @param string $unused
df1ba0f4 2890 * @return bool
e3ff14ca 2891 */
a9a0cb69 2892function forum_add_attachment($post, $forum, $cm, $mform=null, $unused=null) {
12fab708 2893 global $DB;
7f6689e4 2894
dbf4d660 2895 if (empty($mform)) {
2896 return false;
2897 }
7f6689e4 2898
6606c00f
MD
2899 if (empty($post->attachments)) {
2900 return true; // Nothing to do
2901 }
2902
bf0f06b1 2903 $context = context_module::instance($cm->id);
0faf6791 2904
edc0c493 2905 $info = file_get_draft_area_info($post->attachments);
12fab708 2906 $present = ($info['filecount']>0) ? '1' : '';
61a339e5
MG
2907 file_save_draft_area_files($post->attachments, $context->id, 'mod_forum', 'attachment', $post->id,
2908 mod_forum_post_form::attachment_options($forum));
0faf6791 2909
12fab708 2910 $DB->set_field('forum_posts', 'attachment', $present, array('id'=>$post->id));
274e2947 2911
12fab708 2912 return true;
927ce887 2913}
2914
2915/**
2916 * Add a new post in an existing discussion.
df1ba0f4 2917 *
2e48384d
AN
2918 * @param stdClass $post The post data
2919 * @param mixed $mform The submitted form
2920 * @param string $unused
df1ba0f4 2921 * @return int
0a4ac01b 2922 */
91223df6 2923function forum_add_new_post($post, $mform, $unused = null) {
2e48384d 2924 global $USER, $DB;
e3ff14ca 2925
4e445355 2926 $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion));
2927 $forum = $DB->get_record('forum', array('id' => $discussion->forum));
0faf6791 2928 $cm = get_coursemodule_from_instance('forum', $forum->id);
bf0f06b1 2929 $context = context_module::instance($cm->id);
bc4c7337
AN
2930 $privatereplyto = 0;
2931
2932 // Check whether private replies should be enabled for this post.
2933 if ($post->parent) {
2934 $parent = $DB->get_record('forum_posts', array('id' => $post->parent));
2935
2936 if (!empty($parent->privatereplyto)) {
2937 throw new \coding_exception('It should not be possible to reply to a private reply');
2938 }
2939
2940 if (!empty($post->isprivatereply) && forum_user_can_reply_privately($context, $parent)) {
2941 $privatereplyto = $parent->userid;
2942 }
2943 }
b9973acc 2944
2945 $post->created = $post->modified = time();
8076010d 2946 $post->mailed = FORUM_MAILED_PENDING;
b9973acc 2947 $post->userid = $USER->id;
bc4c7337 2948 $post->privatereplyto = $privatereplyto;
7f6689e4 2949 $post->attachment = "";
cd5be9a5
MG
2950 if (!isset($post->totalscore)) {
2951 $post->totalscore = 0;
2952 }
2953 if (!isset($post->mailnow)) {
2954 $post->mailnow = 0;
2955 }
7f6689e4 2956
a8f3a651 2957 $post->id = $DB->insert_record("forum_posts", $post);
61a339e5 2958 $post->message = file_save_draft_area_files($post->itemid, $context->id, 'mod_forum', 'post', $post->id,
5e95223e 2959 mod_forum_post_form::editor_options($context, null), $post->message);
e58357bf 2960 $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
91223df6 2961 forum_add_attachment($post, $forum, $cm, $mform);
29507631 2962
2963 // Update discussion modified date
4e445355 2964 $DB->set_field("forum_discussions", "timemodified", $post->modified, array("id" => $post->discussion));
2965 $DB->set_field("forum_discussions", "usermodified", $post->userid, array("id" => $post->discussion));
65b0e537 2966
90f4745c 2967 if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
6c635746 2968 forum_tp_mark_post_read($post->userid, $post);
f37da850 2969 }
2970
34c8585d
MG
2971 if (isset($post->tags)) {
2972 core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, $post->tags);
2973 }
2974
103e7cba
KG
2975 // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
2976 forum_trigger_content_uploaded_event($post, $cm, 'forum_add_new_post');
2977
7f6689e4 2978 return $post->id;
501cdbd8 2979}
2980
0a4ac01b 2981/**
2e48384d 2982 * Update a post.
df1ba0f4 2983 *
6528ec35 2984 * @param stdClass $newpost The post to update
2e48384d
AN
2985 * @param mixed $mform The submitted form
2986 * @param string $unused
2987 * @return bool
0a4ac01b 2988 */
6528ec35
AN
2989function forum_update_post($newpost, $mform, $unused = null) {
2990 global $DB, $USER;
a56f0d60 2991
6528ec35 2992 $post = $DB->get_record('forum_posts', array('id' => $newpost->id));
0faf6791 2993 $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion));
2994 $forum = $DB->get_record('forum', array('id' => $discussion->forum));
2995 $cm = get_coursemodule_from_instance('forum', $forum->id);
bf0f06b1 2996 $context = context_module::instance($cm->id);
90f4745c 2997
6528ec35
AN
2998 // Allowed modifiable fields.
2999 $modifiablefields = [
3000 'subject',
3001 'message',
3002 'messageformat',
3003 'messagetrust',
3004 'timestart',
3005 'timeend',
3006 'pinned',
1af801be 3007 'attachments',
6528ec35
AN
3008 ];
3009 foreach ($modifiablefields as $field) {
3010 if (isset($newpost->{$field})) {
3011 $post->{$field} = $newpost->{$field};
3012 }
3013 }
ffe11640 3014 $post->modified = time();
0936de64 3015
dd97c328 3016 if (!$post->parent) { // Post is a discussion starter - update discussion title and times too
0faf6791 3017 $discussion->name = $post->subject;
3018 $discussion->timestart = $post->timestart;
3019 $discussion->timeend = $post->timeend;
5f219cf1
BK
3020
3021 if (isset($post->pinned)) {
3022 $discussion->pinned = $post->pinned;
3023 }
dd97c328 3024 }
6528ec35 3025 $post->message = file_save_draft_area_files($newpost->itemid, $context->id, 'mod_forum', 'post', $post->id,
5e95223e 3026 mod_forum_post_form::editor_options($context, $post->id), $post->message);
6528ec35 3027 $DB->update_record('forum_posts', $post);
014fe0ad 3028 // Note: Discussion modified time/user are intentionally not updated, to enable them to track the latest new post.
9d749339 3029 $DB->update_record('forum_discussions', $discussion);
29507631 3030
2e48384d 3031 forum_add_attachment($post, $forum, $cm, $mform);
29507631 3032
34c8585d
MG
3033 if (isset($newpost->tags)) {
3034 core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, $newpost->tags);
3035 }
3036
90f4745c 3037 if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
6c635746 3038 forum_tp_mark_post_read($USER->id, $post);
f37da850 3039 }
3040
103e7cba
KG
3041 // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
3042 forum_trigger_content_uploaded_event($post, $cm, 'forum_update_post');
3043
0faf6791 3044 return true;
501cdbd8 3045}
3046
0a4ac01b 3047/**
3048 * Given an object containing all the necessary data,
3049 * create a new discussion and return the id
df1ba0f4 3050 *
df1ba0f4 3051 * @param object $post
3052 * @param mixed $mform
a9a0cb69 3053 * @param string $unused
1c7b8b93 3054 * @param int $userid
df1ba0f4 3055 * @return object
0a4ac01b 3056 */
a9a0cb69 3057function forum_add_discussion($discussion, $mform=null, $unused=null, $userid=null) {
c18269c7 3058 global $USER, $CFG, $DB;
501cdbd8 3059
5f219cf1 3060 $timenow = isset($discussion->timenow) ? $discussion->timenow : time();
501cdbd8 3061
1c7b8b93
NC
3062 if (is_null($userid)) {
3063 $userid = $USER->id;
3064 }
3065
65b0e537 3066 // The first post is stored as a real post, and linked
501cdbd8 3067 // to from the discuss entry.
3068
c18269c7 3069 $forum = $DB->get_record('forum', array('id'=>$discussion->forum));
0faf6791 3070 $cm = get_coursemodule_from_instance('forum', $forum->id);
b9973acc 3071
39790bd8 3072 $post = new stdClass();
e2d7687f 3073 $post->discussion = 0;
3074 $post->parent = 0;
bc4c7337 3075 $post->privatereplyto = 0;
1c7b8b93 3076 $post->userid = $userid;
e2d7687f 3077 $post->created = $timenow;
3078 $post->modified = $timenow;
8076010d 3079 $post->mailed = FORUM_MAILED_PENDING;
e2d7687f 3080 $post->subject = $discussion->name;
6606c00f 3081 $post->message = $discussion->message;
e2d7687f 3082 $post->messageformat = $discussion->messageformat;
3083 $post->messagetrust = $discussion->messagetrust;
6606c00f 3084 $post->attachments = isset($discussion->attachments) ? $discussion->attachments : null;
e2d7687f 3085 $post->forum = $forum->id; // speedup
3086 $post->course = $forum->course; // speedup
3087 $post->mailnow = $discussion->mailnow;
501cdbd8 3088
a8f3a651 3089 $post->id = $DB->insert_record("forum_posts", $post);
501cdbd8 3090
6606c00f
MD
3091 // TODO: Fix the calling code so that there always is a $cm when this function is called
3092 if (!empty($cm->id) && !empty($discussion->itemid)) { // In "single simple discussions" this may not exist yet
bf0f06b1 3093 $context = context_module::instance($cm->id);
61a339e5 3094 $text = file_save_draft_area_files($discussion->itemid, $context->id, 'mod_forum', 'post', $post->id,
5e95223e 3095 mod_forum_post_form::editor_options($context, null), $post->message);
6606c00f
MD
3096 $DB->set_field('forum_posts', 'message', $text, array('id'=>$post->id));
3097 }
8d96a7b4 3098
0faf6791 3099 // Now do the main entry for the discussion, linking to this first post
04eba58f 3100
caadf009 3101 $discussion->firstpost = $post->id;
3102 $discussion->timemodified = $timenow;
016cd6af 3103 $discussion->usermodified = $post->userid;
1c7b8b93 3104 $discussion->userid = $userid;
cd5be9a5 3105 $discussion->assessed = 0;
04eba58f 3106
a8f3a651 3107 $post->discussion = $DB->insert_record("forum_discussions", $discussion);
04eba58f 3108
caadf009 3109 // Finally, set the pointer on the post.
a8f3a651 3110 $DB->set_field("forum_posts", "discussion", $post->discussion, array("id"=>$post->id));
04eba58f 3111
274e2947 3112 if (!empty($cm->id)) {
a9a0cb69 3113 forum_add_attachment($post, $forum, $cm, $mform, $unused);
274e2947 3114 }
0faf6791 3115
34c8585d
MG
3116 if (isset($discussion->tags)) {
3117 core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, context_module::instance($cm->id), $discussion->tags);
3118 }
3119
90f4745c 3120 if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
6c635746 3121 forum_tp_mark_post_read($post->userid, $post);
f37da850 3122 }
3123
103e7cba 3124 // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
b53f4d55
DM
3125 if (!empty($cm->id)) {
3126 forum_trigger_content_uploaded_event($post, $cm, 'forum_add_discussion');
3127 }
103e7cba 3128
4e00a4d9 3129 return $post->discussion;
caadf009 3130}
04eba58f 3131
04eba58f 3132
0a4ac01b 3133/**
4e781c7b 3134 * Deletes a discussion and handles all associated cleanup.
df1ba0f4 3135 *
3136 * @global object
4e781c7b 3137 * @param object $discussion Discussion to delete
3138 * @param bool $fulldelete True when deleting entire forum
455a8fed 3139 * @param object $course Course
3140 * @param object $cm Course-module
3141 * @param object $forum Forum
df1ba0f4 3142 * @return bool
0a4ac01b 3143 */
455a8fed 3144function forum_delete_discussion($discussion, $fulldelete, $course, $cm, $forum) {
516c5eca
PS