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