Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * @package   mod_forum
19
 * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
20
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
21
 */
22
 
23
use mod_forum\local\entities\forum as forum_entity;
24
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
/** Include required files */
28
require_once(__DIR__ . '/deprecatedlib.php');
29
require_once($CFG->libdir.'/filelib.php');
30
 
31
/// CONSTANTS ///////////////////////////////////////////////////////////
32
 
33
define('FORUM_MODE_FLATOLDEST', 1);
34
define('FORUM_MODE_FLATNEWEST', -1);
35
define('FORUM_MODE_THREADED', 2);
36
define('FORUM_MODE_NESTED', 3);
37
define('FORUM_MODE_NESTED_V2', 4);
38
 
39
define('FORUM_CHOOSESUBSCRIBE', 0);
40
define('FORUM_FORCESUBSCRIBE', 1);
41
define('FORUM_INITIALSUBSCRIBE', 2);
42
define('FORUM_DISALLOWSUBSCRIBE',3);
43
 
44
/**
45
 * FORUM_TRACKING_OFF - Tracking is not available for this forum.
46
 */
47
define('FORUM_TRACKING_OFF', 0);
48
 
49
/**
50
 * FORUM_TRACKING_OPTIONAL - Tracking is based on user preference.
51
 */
52
define('FORUM_TRACKING_OPTIONAL', 1);
53
 
54
/**
55
 * FORUM_TRACKING_FORCED - Tracking is on, regardless of user setting.
56
 * Treated as FORUM_TRACKING_OPTIONAL if $CFG->forum_allowforcedreadtracking is off.
57
 */
58
define('FORUM_TRACKING_FORCED', 2);
59
 
60
define('FORUM_MAILED_PENDING', 0);
61
define('FORUM_MAILED_SUCCESS', 1);
62
define('FORUM_MAILED_ERROR', 2);
63
 
64
if (!defined('FORUM_CRON_USER_CACHE')) {
65
    /** Defines how many full user records are cached in forum cron. */
66
    define('FORUM_CRON_USER_CACHE', 5000);
67
}
68
 
69
/**
70
 * FORUM_POSTS_ALL_USER_GROUPS - All the posts in groups where the user is enrolled.
71
 */
72
define('FORUM_POSTS_ALL_USER_GROUPS', -2);
73
 
74
define('FORUM_DISCUSSION_PINNED', 1);
75
define('FORUM_DISCUSSION_UNPINNED', 0);
76
 
77
/// STANDARD FUNCTIONS ///////////////////////////////////////////////////////////
78
 
79
/**
80
 * Given an object containing all the necessary data,
81
 * (defined by the form in mod_form.php) this function
82
 * will create a new instance and return the id number
83
 * of the new instance.
84
 *
85
 * @param stdClass $forum add forum instance
86
 * @param mod_forum_mod_form $mform
87
 * @return int intance id
88
 */
89
function forum_add_instance($forum, $mform = null) {
90
    global $CFG, $DB;
91
 
92
    require_once($CFG->dirroot.'/mod/forum/locallib.php');
93
 
94
    $forum->timemodified = time();
95
 
96
    if (empty($forum->assessed)) {
97
        $forum->assessed = 0;
98
    }
99
 
100
    if (empty($forum->ratingtime) or empty($forum->assessed)) {
101
        $forum->assesstimestart  = 0;
102
        $forum->assesstimefinish = 0;
103
    }
104
 
105
    $forum->id = $DB->insert_record('forum', $forum);
106
    $modcontext = context_module::instance($forum->coursemodule);
107
 
108
    if ($forum->type == 'single') {  // Create related discussion.
109
        $discussion = new stdClass();
110
        $discussion->course        = $forum->course;
111
        $discussion->forum         = $forum->id;
112
        $discussion->name          = $forum->name;
113
        $discussion->assessed      = $forum->assessed;
114
        $discussion->message       = $forum->intro;
115
        $discussion->messageformat = $forum->introformat;
116
        $discussion->messagetrust  = trusttext_trusted(context_course::instance($forum->course));
117
        $discussion->mailnow       = false;
118
        $discussion->groupid       = -1;
119
 
120
        $message = '';
121
 
122
        $discussion->id = forum_add_discussion($discussion, null, $message);
123
 
124
        if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
125
            // Ugly hack - we need to copy the files somehow.
126
            $discussion = $DB->get_record('forum_discussions', array('id'=>$discussion->id), '*', MUST_EXIST);
127
            $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
128
 
129
            $options = array('subdirs'=>true); // Use the same options as intro field!
130
            $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
131
            $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
132
        }
133
    }
134
 
135
    forum_update_calendar($forum, $forum->coursemodule);
136
    forum_grade_item_update($forum);
137
 
138
    $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null;
139
    \core_completion\api::update_completion_date_event($forum->coursemodule, 'forum', $forum->id, $completiontimeexpected);
140
 
141
    return $forum->id;
142
}
143
 
144
/**
145
 * Handle changes following the creation of a forum instance.
146
 * This function is typically called by the course_module_created observer.
147
 *
148
 * @param object $context the forum context
149
 * @param stdClass $forum The forum object
150
 * @return void
151
 */
152
function forum_instance_created($context, $forum) {
153
    if ($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) {
154
        $users = \mod_forum\subscriptions::get_potential_subscribers($context, 0, 'u.id, u.email');
155
        foreach ($users as $user) {
156
            \mod_forum\subscriptions::subscribe_user($user->id, $forum, $context);
157
        }
158
    }
159
}
160
 
161
/**
162
 * Given an object containing all the necessary data,
163
 * (defined by the form in mod_form.php) this function
164
 * will update an existing instance with new data.
165
 *
166
 * @global object
167
 * @param object $forum forum instance (with magic quotes)
168
 * @return bool success
169
 */
170
function forum_update_instance($forum, $mform) {
171
    global $CFG, $DB, $OUTPUT, $USER;
172
 
173
    require_once($CFG->dirroot.'/mod/forum/locallib.php');
174
 
175
    $forum->timemodified = time();
176
    $forum->id           = $forum->instance;
177
 
178
    if (empty($forum->assessed)) {
179
        $forum->assessed = 0;
180
    }
181
 
182
    if (empty($forum->ratingtime) or empty($forum->assessed)) {
183
        $forum->assesstimestart  = 0;
184
        $forum->assesstimefinish = 0;
185
    }
186
 
187
    $oldforum = $DB->get_record('forum', array('id'=>$forum->id));
188
 
189
    // MDL-3942 - if the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire forum
190
    // if  scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
191
    // 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
192
    $updategrades = false;
193
 
194
    if ($oldforum->assessed <> $forum->assessed) {
195
        // Whether this forum is rated.
196
        $updategrades = true;
197
    }
198
 
199
    if ($oldforum->scale <> $forum->scale) {
200
        // The scale currently in use.
201
        $updategrades = true;
202
    }
203
 
204
    if (empty($oldforum->grade_forum) || $oldforum->grade_forum <> $forum->grade_forum) {
205
        // The whole forum grading.
206
        $updategrades = true;
207
    }
208
 
209
    if ($updategrades) {
210
        forum_update_grades($forum); // Recalculate grades for the forum.
211
    }
212
 
213
    if ($forum->type == 'single') {  // Update related discussion and post.
214
        $discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id), 'timemodified ASC');
215
        if (!empty($discussions)) {
216
            if (count($discussions) > 1) {
217
                echo $OUTPUT->notification(get_string('warnformorepost', 'forum'));
218
            }
219
            $discussion = array_pop($discussions);
220
        } else {
221
            // try to recover by creating initial discussion - MDL-16262
222
            $discussion = new stdClass();
223
            $discussion->course          = $forum->course;
224
            $discussion->forum           = $forum->id;
225
            $discussion->name            = $forum->name;
226
            $discussion->assessed        = $forum->assessed;
227
            $discussion->message         = $forum->intro;
228
            $discussion->messageformat   = $forum->introformat;
229
            $discussion->messagetrust    = true;
230
            $discussion->mailnow         = false;
231
            $discussion->groupid         = -1;
232
 
233
            $message = '';
234
 
235
            forum_add_discussion($discussion, null, $message);
236
 
237
            if (! $discussion = $DB->get_record('forum_discussions', array('forum'=>$forum->id))) {
238
                throw new \moodle_exception('cannotadd', 'forum');
239
            }
240
        }
241
        if (! $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost))) {
242
            throw new \moodle_exception('cannotfindfirstpost', 'forum');
243
        }
244
 
245
        $cm         = get_coursemodule_from_instance('forum', $forum->id);
246
        $modcontext = context_module::instance($cm->id, MUST_EXIST);
247
 
248
        $post = $DB->get_record('forum_posts', array('id'=>$discussion->firstpost), '*', MUST_EXIST);
249
        $post->subject       = $forum->name;
250
        $post->message       = $forum->intro;
251
        $post->messageformat = $forum->introformat;
252
        $post->messagetrust  = trusttext_trusted($modcontext);
253
        $post->modified      = $forum->timemodified;
254
        $post->userid        = $USER->id;    // MDL-18599, so that current teacher can take ownership of activities.
255
 
256
        if ($mform and $draftid = file_get_submitted_draft_itemid('introeditor')) {
257
            // Ugly hack - we need to copy the files somehow.
258
            $options = array('subdirs'=>true); // Use the same options as intro field!
259
            $post->message = file_save_draft_area_files($draftid, $modcontext->id, 'mod_forum', 'post', $post->id, $options, $post->message);
260
        }
261
 
262
        \mod_forum\local\entities\post::add_message_counts($post);
263
        $DB->update_record('forum_posts', $post);
264
        $discussion->name = $forum->name;
265
        $DB->update_record('forum_discussions', $discussion);
266
    }
267
 
268
    $DB->update_record('forum', $forum);
269
 
270
    $modcontext = context_module::instance($forum->coursemodule);
271
    if (($forum->forcesubscribe == FORUM_INITIALSUBSCRIBE) && ($oldforum->forcesubscribe <> $forum->forcesubscribe)) {
272
        $users = \mod_forum\subscriptions::get_potential_subscribers($modcontext, 0, 'u.id, u.email', '');
273
        foreach ($users as $user) {
274
            \mod_forum\subscriptions::subscribe_user($user->id, $forum, $modcontext);
275
        }
276
    }
277
 
278
    forum_update_calendar($forum, $forum->coursemodule);
279
    forum_grade_item_update($forum);
280
 
281
    $completiontimeexpected = !empty($forum->completionexpected) ? $forum->completionexpected : null;
282
    \core_completion\api::update_completion_date_event($forum->coursemodule, 'forum', $forum->id, $completiontimeexpected);
283
 
284
    return true;
285
}
286
 
287
 
288
/**
289
 * Given an ID of an instance of this module,
290
 * this function will permanently delete the instance
291
 * and any data that depends on it.
292
 *
293
 * @global object
294
 * @param int $id forum instance id
295
 * @return bool success
296
 */
297
function forum_delete_instance($id) {
298
    global $DB;
299
 
300
    if (!$forum = $DB->get_record('forum', array('id'=>$id))) {
301
        return false;
302
    }
303
    if (!$cm = get_coursemodule_from_instance('forum', $forum->id)) {
304
        return false;
305
    }
306
    if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
307
        return false;
308
    }
309
 
310
    $context = context_module::instance($cm->id);
311
 
312
    // now get rid of all files
313
    $fs = get_file_storage();
314
    $fs->delete_area_files($context->id);
315
 
316
    $result = true;
317
 
318
    \core_completion\api::update_completion_date_event($cm->id, 'forum', $forum->id, null);
319
 
320
    // Delete digest and subscription preferences.
321
    $DB->delete_records('forum_digests', array('forum' => $forum->id));
322
    $DB->delete_records('forum_subscriptions', array('forum'=>$forum->id));
323
    $DB->delete_records('forum_discussion_subs', array('forum' => $forum->id));
324
 
325
    if ($discussions = $DB->get_records('forum_discussions', array('forum'=>$forum->id))) {
326
        foreach ($discussions as $discussion) {
327
            if (!forum_delete_discussion($discussion, true, $course, $cm, $forum)) {
328
                $result = false;
329
            }
330
        }
331
    }
332
 
333
    forum_tp_delete_read_records(-1, -1, -1, $forum->id);
334
 
335
    forum_grade_item_delete($forum);
336
 
337
    // We must delete the module record after we delete the grade item.
338
    if (!$DB->delete_records('forum', array('id'=>$forum->id))) {
339
        $result = false;
340
    }
341
 
342
    return $result;
343
}
344
 
345
 
346
/**
347
 * Indicates API features that the forum supports.
348
 *
349
 * @uses FEATURE_GROUPS
350
 * @uses FEATURE_GROUPINGS
351
 * @uses FEATURE_MOD_INTRO
352
 * @uses FEATURE_COMPLETION_TRACKS_VIEWS
353
 * @uses FEATURE_COMPLETION_HAS_RULES
354
 * @uses FEATURE_GRADE_HAS_GRADE
355
 * @uses FEATURE_GRADE_OUTCOMES
356
 * @param string $feature
357
 * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose.
358
 */
359
function forum_supports($feature) {
360
    switch($feature) {
361
        case FEATURE_GROUPS:                  return true;
362
        case FEATURE_GROUPINGS:               return true;
363
        case FEATURE_MOD_INTRO:               return true;
364
        case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
365
        case FEATURE_COMPLETION_HAS_RULES:    return true;
366
        case FEATURE_GRADE_HAS_GRADE:         return true;
367
        case FEATURE_GRADE_OUTCOMES:          return true;
368
        case FEATURE_RATE:                    return true;
369
        case FEATURE_BACKUP_MOODLE2:          return true;
370
        case FEATURE_SHOW_DESCRIPTION:        return true;
371
        case FEATURE_PLAGIARISM:              return true;
372
        case FEATURE_ADVANCED_GRADING:        return true;
373
        case FEATURE_MOD_PURPOSE:             return MOD_PURPOSE_COLLABORATION;
374
 
375
        default: return null;
376
    }
377
}
378
 
379
/**
380
 * Create a message-id string to use in the custom headers of forum notification emails
381
 *
382
 * message-id is used by email clients to identify emails and to nest conversations
383
 *
384
 * @param int $postid The ID of the forum post we are notifying the user about
385
 * @param int $usertoid The ID of the user being notified
386
 * @return string A unique message-id
387
 */
388
function forum_get_email_message_id($postid, $usertoid) {
389
    return generate_email_messageid(hash('sha256', $postid . 'to' . $usertoid));
390
}
391
 
392
/**
393
 *
394
 * @param object $course
395
 * @param object $user
396
 * @param object $mod TODO this is not used in this function, refactor
397
 * @param object $forum
398
 * @return object A standard object with 2 variables: info (number of posts for this user) and time (last modified)
399
 */
400
function forum_user_outline($course, $user, $mod, $forum) {
401
    global $CFG;
402
    require_once("$CFG->libdir/gradelib.php");
403
 
404
    $gradeinfo = '';
405
    $gradetime = 0;
406
 
407
    $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
408
    if (!empty($grades->items[0]->grades)) {
409
        // Item 0 is the rating.
410
        $grade = reset($grades->items[0]->grades);
411
        $gradetime = max($gradetime, grade_get_date_for_user_grade($grade, $user));
412
        if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
413
            $gradeinfo .= get_string('gradeforrating', 'forum', $grade) .  html_writer::empty_tag('br');
414
        } else {
415
            $gradeinfo .= get_string('gradeforratinghidden', 'forum') . html_writer::empty_tag('br');
416
        }
417
    }
418
 
419
    // Item 1 is the whole-forum grade.
420
    if (!empty($grades->items[1]->grades)) {
421
        $grade = reset($grades->items[1]->grades);
422
        $gradetime = max($gradetime, grade_get_date_for_user_grade($grade, $user));
423
        if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
424
            $gradeinfo .= get_string('gradeforwholeforum', 'forum', $grade) .  html_writer::empty_tag('br');
425
        } else {
426
            $gradeinfo .= get_string('gradeforwholeforumhidden', 'forum') . html_writer::empty_tag('br');
427
        }
428
    }
429
 
430
    $count = forum_count_user_posts($forum->id, $user->id);
431
    if ($count && $count->postcount > 0) {
432
        $info = get_string("numposts", "forum", $count->postcount);
433
        $time = $count->lastpost;
434
 
435
        if ($gradeinfo) {
436
            $info .= ', ' . $gradeinfo;
437
            $time = max($time, $gradetime);
438
        }
439
 
440
        return (object) [
441
            'info' => $info,
442
            'time' => $time,
443
        ];
444
    } else if ($gradeinfo) {
445
        return (object) [
446
            'info' => $gradeinfo,
447
            'time' => $gradetime,
448
        ];
449
    }
450
 
451
    return null;
452
}
453
 
454
 
455
/**
456
 * @global object
457
 * @global object
458
 * @param object $coure
459
 * @param object $user
460
 * @param object $mod
461
 * @param object $forum
462
 */
463
function forum_user_complete($course, $user, $mod, $forum) {
464
    global $CFG, $USER;
465
    require_once("$CFG->libdir/gradelib.php");
466
 
467
    $getgradeinfo = function($grades, string $type) use ($course): string {
468
        global $OUTPUT;
469
 
470
        if (empty($grades)) {
471
            return '';
472
        }
473
 
474
        $result = '';
475
        $grade = reset($grades);
476
        if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
477
            $result .= $OUTPUT->container(get_string("gradefor{$type}", "forum", $grade));
478
            if ($grade->str_feedback) {
479
                $result .= $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
480
            }
481
        } else {
482
            $result .= $OUTPUT->container(get_string("gradefor{$type}hidden", "forum"));
483
        }
484
 
485
        return $result;
486
    };
487
 
488
    $grades = grade_get_grades($course->id, 'mod', 'forum', $forum->id, $user->id);
489
 
490
    // Item 0 is the rating.
491
    if (!empty($grades->items[0]->grades)) {
492
        echo $getgradeinfo($grades->items[0]->grades, 'rating');
493
    }
494
 
495
    // Item 1 is the whole-forum grade.
496
    if (!empty($grades->items[1]->grades)) {
497
        echo $getgradeinfo($grades->items[1]->grades, 'wholeforum');
498
    }
499
 
500
    if ($posts = forum_get_user_posts($forum->id, $user->id)) {
501
        if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) {
502
            throw new \moodle_exception('invalidcoursemodule');
503
        }
504
        $context = context_module::instance($cm->id);
505
        $discussions = forum_get_user_involved_discussions($forum->id, $user->id);
506
        $posts = array_filter($posts, function($post) use ($discussions) {
507
            return isset($discussions[$post->discussion]);
508
        });
509
        $entityfactory = mod_forum\local\container::get_entity_factory();
510
        $rendererfactory = mod_forum\local\container::get_renderer_factory();
511
        $postrenderer = $rendererfactory->get_posts_renderer();
512
 
513
        echo $postrenderer->render(
514
            $USER,
515
            [$forum->id => $entityfactory->get_forum_from_stdclass($forum, $context, $cm, $course)],
516
            array_map(function($discussion) use ($entityfactory) {
517
                return $entityfactory->get_discussion_from_stdclass($discussion);
518
            }, $discussions),
519
            array_map(function($post) use ($entityfactory) {
520
                return $entityfactory->get_post_from_stdclass($post);
521
            }, $posts)
522
        );
523
    } else {
524
        echo "<p>".get_string("noposts", "forum")."</p>";
525
    }
526
}
527
 
528
/**
529
 * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
530
 */
531
function forum_filter_user_groups_discussions() {
532
    throw new coding_exception('forum_filter_user_groups_discussions() can not be used any more and is obsolete.');
533
}
534
 
535
/**
536
 * Returns whether the discussion group is visible by the current user or not.
537
 *
538
 * @since Moodle 2.8, 2.7.1, 2.6.4
539
 * @param cm_info $cm The discussion course module
540
 * @param int $discussiongroupid The discussion groupid
541
 * @return bool
542
 */
543
function forum_is_user_group_discussion(cm_info $cm, $discussiongroupid) {
544
 
545
    if ($discussiongroupid == -1 || $cm->effectivegroupmode != SEPARATEGROUPS) {
546
        return true;
547
    }
548
 
549
    if (isguestuser()) {
550
        return false;
551
    }
552
 
553
    if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id)) ||
554
            in_array($discussiongroupid, $cm->get_modinfo()->get_groups($cm->groupingid))) {
555
        return true;
556
    }
557
 
558
    return false;
559
}
560
 
561
/**
562
 * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
563
 */
564
function forum_print_overview() {
565
    throw new coding_exception('forum_print_overview() can not be used any more and is obsolete.');
566
}
567
 
568
/**
569
 * Given a course and a date, prints a summary of all the new
570
 * messages posted in the course since that date
571
 *
572
 * @global object
573
 * @global object
574
 * @global object
575
 * @uses CONTEXT_MODULE
576
 * @uses VISIBLEGROUPS
577
 * @param object $course
578
 * @param bool $viewfullnames capability
579
 * @param int $timestart
580
 * @return bool success
581
 */
582
function forum_print_recent_activity($course, $viewfullnames, $timestart) {
583
    global $USER, $DB, $OUTPUT;
584
 
585
    // do not use log table if possible, it may be huge and is expensive to join with other tables
586
 
587
    $userfieldsapi = \core_user\fields::for_userpic();
588
    $allnamefields = $userfieldsapi->get_sql('u', false, '', 'duserid', false)->selects;
589
    if (!$posts = $DB->get_records_sql("SELECT p.*,
590
                                              f.course, f.type AS forumtype, f.name AS forumname, f.intro, f.introformat, f.duedate,
591
                                              f.cutoffdate, f.assessed AS forumassessed, f.assesstimestart, f.assesstimefinish,
592
                                              f.scale, f.grade_forum, f.maxbytes, f.maxattachments, f.forcesubscribe,
593
                                              f.trackingtype, f.rsstype, f.rssarticles, f.timemodified, f.warnafter, f.blockafter,
594
                                              f.blockperiod, f.completiondiscussions, f.completionreplies, f.completionposts,
595
                                              f.displaywordcount, f.lockdiscussionafter, f.grade_forum_notify,
596
                                              d.name AS discussionname, d.firstpost, d.userid AS discussionstarter,
597
                                              d.assessed AS discussionassessed, d.timemodified, d.usermodified, d.forum, d.groupid,
598
                                              d.timestart, d.timeend, d.pinned, d.timelocked,
599
                                              $allnamefields
600
                                         FROM {forum_posts} p
601
                                              JOIN {forum_discussions} d ON d.id = p.discussion
602
                                              JOIN {forum} f             ON f.id = d.forum
603
                                              JOIN {user} u              ON u.id = p.userid
604
                                        WHERE p.created > ? AND f.course = ? AND p.deleted <> 1
605
                                     ORDER BY p.id ASC", array($timestart, $course->id))) { // order by initial posting date
606
         return false;
607
    }
608
 
609
    $modinfo = get_fast_modinfo($course);
610
 
611
    $strftimerecent = get_string('strftimerecent');
612
 
613
    $managerfactory = mod_forum\local\container::get_manager_factory();
614
    $entityfactory = mod_forum\local\container::get_entity_factory();
615
 
616
    $discussions = [];
617
    $capmanagers = [];
618
    $printposts = [];
619
    foreach ($posts as $post) {
620
        if (!isset($modinfo->instances['forum'][$post->forum])) {
621
            // not visible
622
            continue;
623
        }
624
        $cm = $modinfo->instances['forum'][$post->forum];
625
        if (!$cm->uservisible) {
626
            continue;
627
        }
628
 
629
        // Get the discussion. Cache if not yet available.
630
        if (!isset($discussions[$post->discussion])) {
631
            // Build the discussion record object from the post data.
632
            $discussionrecord = (object)[
633
                'id' => $post->discussion,
634
                'course' => $post->course,
635
                'forum' => $post->forum,
636
                'name' => $post->discussionname,
637
                'firstpost' => $post->firstpost,
638
                'userid' => $post->discussionstarter,
639
                'groupid' => $post->groupid,
640
                'assessed' => $post->discussionassessed,
641
                'timemodified' => $post->timemodified,
642
                'usermodified' => $post->usermodified,
643
                'timestart' => $post->timestart,
644
                'timeend' => $post->timeend,
645
                'pinned' => $post->pinned,
646
                'timelocked' => $post->timelocked
647
            ];
648
            // Build the discussion entity from the factory and cache it.
649
            $discussions[$post->discussion] = $entityfactory->get_discussion_from_stdclass($discussionrecord);
650
        }
651
        $discussionentity = $discussions[$post->discussion];
652
 
653
        // Get the capability manager. Cache if not yet available.
654
        if (!isset($capmanagers[$post->forum])) {
655
            $context = context_module::instance($cm->id);
656
            $coursemodule = $cm->get_course_module_record();
657
            // Build the forum record object from the post data.
658
            $forumrecord = (object)[
659
                'id' => $post->forum,
660
                'course' => $post->course,
661
                'type' => $post->forumtype,
662
                'name' => $post->forumname,
663
                'intro' => $post->intro,
664
                'introformat' => $post->introformat,
665
                'duedate' => $post->duedate,
666
                'cutoffdate' => $post->cutoffdate,
667
                'assessed' => $post->forumassessed,
668
                'assesstimestart' => $post->assesstimestart,
669
                'assesstimefinish' => $post->assesstimefinish,
670
                'scale' => $post->scale,
671
                'grade_forum' => $post->grade_forum,
672
                'maxbytes' => $post->maxbytes,
673
                'maxattachments' => $post->maxattachments,
674
                'forcesubscribe' => $post->forcesubscribe,
675
                'trackingtype' => $post->trackingtype,
676
                'rsstype' => $post->rsstype,
677
                'rssarticles' => $post->rssarticles,
678
                'timemodified' => $post->timemodified,
679
                'warnafter' => $post->warnafter,
680
                'blockafter' => $post->blockafter,
681
                'blockperiod' => $post->blockperiod,
682
                'completiondiscussions' => $post->completiondiscussions,
683
                'completionreplies' => $post->completionreplies,
684
                'completionposts' => $post->completionposts,
685
                'displaywordcount' => $post->displaywordcount,
686
                'lockdiscussionafter' => $post->lockdiscussionafter,
687
                'grade_forum_notify' => $post->grade_forum_notify
688
            ];
689
            // Build the forum entity from the factory.
690
            $forumentity = $entityfactory->get_forum_from_stdclass($forumrecord, $context, $coursemodule, $course);
691
            // Get the capability manager of this forum and cache it.
692
            $capmanagers[$post->forum] = $managerfactory->get_capability_manager($forumentity);
693
        }
694
        $capabilitymanager = $capmanagers[$post->forum];
695
 
696
        // Get the post entity.
697
        $postentity = $entityfactory->get_post_from_stdclass($post);
698
 
699
        // Check if the user can view the post.
700
        if ($capabilitymanager->can_view_post($USER, $discussionentity, $postentity)) {
701
            $printposts[] = $post;
702
        }
703
    }
704
    unset($posts);
705
 
706
    if (!$printposts) {
707
        return false;
708
    }
709
 
710
    echo $OUTPUT->heading(get_string('newforumposts', 'forum') . ':', 6);
711
    $list = html_writer::start_tag('ul', ['class' => 'unlist']);
712
 
713
    foreach ($printposts as $post) {
714
        $subjectclass = empty($post->parent) ? ' bold' : '';
715
        $authorhidden = forum_is_author_hidden($post, (object) ['type' => $post->forumtype]);
716
 
717
        $list .= html_writer::start_tag('li');
718
        $list .= html_writer::start_div('head');
719
        $list .= html_writer::div(userdate_htmltime($post->modified, $strftimerecent), 'date');
720
        if (!$authorhidden) {
721
            $list .= html_writer::div(fullname($post, $viewfullnames), 'name');
722
        }
723
        $list .= html_writer::end_div(); // Head.
724
 
725
        $list .= html_writer::start_div('info' . $subjectclass);
726
        $discussionurl = new moodle_url('/mod/forum/discuss.php', ['d' => $post->discussion]);
727
        if (!empty($post->parent)) {
728
            $discussionurl->param('parent', $post->parent);
729
            $discussionurl->set_anchor('p'. $post->id);
730
        }
731
        $post->subject = break_up_long_words(format_string($post->subject, true));
732
        $list .= html_writer::link($discussionurl, $post->subject, ['rel' => 'bookmark']);
733
        $list .= html_writer::end_div(); // Info.
734
        $list .= html_writer::end_tag('li');
735
    }
736
 
737
    $list .= html_writer::end_tag('ul');
738
    echo $list;
739
 
740
    return true;
741
}
742
 
743
/**
744
 * Update activity grades.
745
 *
746
 * @param object $forum
747
 * @param int $userid specific user only, 0 means all
748
 */
749
function forum_update_grades($forum, $userid = 0): void {
750
    global $CFG, $DB;
751
    require_once($CFG->libdir.'/gradelib.php');
752
 
753
    $ratings = null;
754
    if ($forum->assessed) {
755
        require_once($CFG->dirroot.'/rating/lib.php');
756
 
757
        $cm = get_coursemodule_from_instance('forum', $forum->id);
758
 
759
        $rm = new rating_manager();
760
        $ratings = $rm->get_user_grades((object) [
761
            'component' => 'mod_forum',
762
            'ratingarea' => 'post',
763
            'contextid' => \context_module::instance($cm->id)->id,
764
 
765
            'modulename' => 'forum',
766
            'moduleid  ' => $forum->id,
767
            'userid' => $userid,
768
            'aggregationmethod' => $forum->assessed,
769
            'scaleid' => $forum->scale,
770
            'itemtable' => 'forum_posts',
771
            'itemtableusercolumn' => 'userid',
772
        ]);
773
    }
774
 
775
    $forumgrades = null;
776
    if ($forum->grade_forum) {
777
        $sql = <<<EOF
778
SELECT
779
    g.userid,
780
 
781
    g.grade as rawgrade,
782
    g.timemodified as dategraded
783
  FROM {forum} f
784
  JOIN {forum_grades} g ON g.forum = f.id
785
 WHERE f.id = :forumid
786
EOF;
787
 
788
        $params = [
789
            'forumid' => $forum->id,
790
        ];
791
 
792
        if ($userid) {
793
            $sql .= " AND g.userid = :userid";
794
            $params['userid'] = $userid;
795
        }
796
 
797
        $forumgrades = [];
798
        if ($grades = $DB->get_recordset_sql($sql, $params)) {
799
            foreach ($grades as $userid => $grade) {
800
                if ($grade->rawgrade != -1) {
801
                    $forumgrades[$userid] = $grade;
802
                }
803
            }
804
            $grades->close();
805
        }
806
    }
807
 
808
    forum_grade_item_update($forum, $ratings, $forumgrades);
809
}
810
 
811
/**
812
 * Create/update grade items for given forum.
813
 *
814
 * @param stdClass $forum Forum object with extra cmidnumber
815
 * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook
816
 */
817
function forum_grade_item_update($forum, $ratings = null, $forumgrades = null): void {
818
    global $CFG;
819
    require_once("{$CFG->libdir}/gradelib.php");
820
 
821
    // Update the rating.
822
    $item = [
823
        'itemname' => get_string('gradeitemnameforrating', 'forum', $forum),
824
        'idnumber' => $forum->cmidnumber,
825
    ];
826
 
827
    if (!$forum->assessed || $forum->scale == 0) {
828
        $item['gradetype'] = GRADE_TYPE_NONE;
829
    } else if ($forum->scale > 0) {
830
        $item['gradetype'] = GRADE_TYPE_VALUE;
831
        $item['grademax']  = $forum->scale;
832
        $item['grademin']  = 0;
833
    } else if ($forum->scale < 0) {
834
        $item['gradetype'] = GRADE_TYPE_SCALE;
835
        $item['scaleid']   = -$forum->scale;
836
    }
837
 
838
    if ($ratings === 'reset') {
839
        $item['reset'] = true;
840
        $ratings = null;
841
    }
842
    // Itemnumber 0 is the rating.
843
    grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, $ratings, $item);
844
 
845
    // Whole forum grade.
846
    $item = [
847
        'itemname' => get_string('gradeitemnameforwholeforum', 'forum', $forum),
848
        // Note: We do not need to store the idnumber here.
849
    ];
850
 
851
    if (!$forum->grade_forum) {
852
        $item['gradetype'] = GRADE_TYPE_NONE;
853
    } else if ($forum->grade_forum > 0) {
854
        $item['gradetype'] = GRADE_TYPE_VALUE;
855
        $item['grademax'] = $forum->grade_forum;
856
        $item['grademin'] = 0;
857
    } else if ($forum->grade_forum < 0) {
858
        $item['gradetype'] = GRADE_TYPE_SCALE;
859
        $item['scaleid'] = $forum->grade_forum * -1;
860
    }
861
 
862
    if ($forumgrades === 'reset') {
863
        $item['reset'] = true;
864
        $forumgrades = null;
865
    }
866
    // Itemnumber 1 is the whole forum grade.
867
    grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 1, $forumgrades, $item);
868
}
869
 
870
/**
871
 * Delete grade item for given forum.
872
 *
873
 * @param stdClass $forum Forum object
874
 */
875
function forum_grade_item_delete($forum) {
876
    global $CFG;
877
    require_once($CFG->libdir.'/gradelib.php');
878
 
879
    grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 0, null, ['deleted' => 1]);
880
    grade_update('mod/forum', $forum->course, 'mod', 'forum', $forum->id, 1, null, ['deleted' => 1]);
881
}
882
 
883
/**
884
 * Checks if scale is being used by any instance of forum.
885
 *
886
 * This is used to find out if scale used anywhere.
887
 *
888
 * @param $scaleid int
889
 * @return boolean True if the scale is used by any forum
890
 */
891
function forum_scale_used_anywhere(int $scaleid): bool {
892
    global $DB;
893
 
894
    if (empty($scaleid)) {
895
        return false;
896
    }
897
 
898
    return $DB->record_exists_select('forum', "scale = ? and assessed > 0", [$scaleid * -1]);
899
}
900
 
901
// SQL FUNCTIONS ///////////////////////////////////////////////////////////
902
 
903
/**
904
 * Gets a post with all info ready for forum_print_post
905
 * Most of these joins are just to get the forum id
906
 *
907
 * @global object
908
 * @global object
909
 * @param int $postid
910
 * @return mixed array of posts or false
911
 */
912
function forum_get_post_full($postid) {
913
    global $CFG, $DB;
914
 
915
    $userfieldsapi = \core_user\fields::for_name();
916
    $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
917
    return $DB->get_record_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
918
                             FROM {forum_posts} p
919
                                  JOIN {forum_discussions} d ON p.discussion = d.id
920
                                  LEFT JOIN {user} u ON p.userid = u.id
921
                            WHERE p.id = ?", array($postid));
922
}
923
 
924
/**
925
 * Gets all posts in discussion including top parent.
926
 *
927
 * @param   int     $discussionid   The Discussion to fetch.
928
 * @param   string  $sort           The sorting to apply.
929
 * @param   bool    $tracking       Whether the user tracks this forum.
930
 * @return  array                   The posts in the discussion.
931
 */
932
function forum_get_all_discussion_posts($discussionid, $sort, $tracking = false) {
933
    global $CFG, $DB, $USER;
934
 
935
    $tr_sel  = "";
936
    $tr_join = "";
937
    $params = array();
938
 
939
    if ($tracking) {
940
        $tr_sel  = ", fr.id AS postread";
941
        $tr_join = "LEFT JOIN {forum_read} fr ON (fr.postid = p.id AND fr.userid = ?)";
942
        $params[] = $USER->id;
943
    }
944
 
945
    $userfieldsapi = \core_user\fields::for_name();
946
    $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
947
    $params[] = $discussionid;
948
    if (!$posts = $DB->get_records_sql("SELECT p.*, $allnames, u.email, u.picture, u.imagealt $tr_sel
949
                                     FROM {forum_posts} p
950
                                          LEFT JOIN {user} u ON p.userid = u.id
951
                                          $tr_join
952
                                    WHERE p.discussion = ?
953
                                 ORDER BY $sort", $params)) {
954
        return array();
955
    }
956
 
957
    foreach ($posts as $pid=>$p) {
958
        if ($tracking) {
959
            if (forum_tp_is_post_old($p)) {
960
                 $posts[$pid]->postread = true;
961
            }
962
        }
963
        if (!$p->parent) {
964
            continue;
965
        }
966
        if (!isset($posts[$p->parent])) {
967
            continue; // parent does not exist??
968
        }
969
        if (!isset($posts[$p->parent]->children)) {
970
            $posts[$p->parent]->children = array();
971
        }
972
        $posts[$p->parent]->children[$pid] =& $posts[$pid];
973
    }
974
 
975
    // Start with the last child of the first post.
976
    $post = &$posts[reset($posts)->id];
977
 
978
    $lastpost = false;
979
    while (!$lastpost) {
980
        if (!isset($post->children)) {
981
            $post->lastpost = true;
982
            $lastpost = true;
983
        } else {
984
             // Go to the last child of this post.
985
            $post = &$posts[end($post->children)->id];
986
        }
987
    }
988
 
989
    return $posts;
990
}
991
 
992
/**
993
 * An array of forum objects that the user is allowed to read/search through.
994
 *
995
 * @global object
996
 * @global object
997
 * @global object
998
 * @param int $userid
999
 * @param int $courseid if 0, we look for forums throughout the whole site.
1000
 * @return array of forum objects, or false if no matches
1001
 *         Forum objects have the following attributes:
1002
 *         id, type, course, cmid, cmvisible, cmgroupmode, accessallgroups,
1003
 *         viewhiddentimedposts
1004
 */
1005
function forum_get_readable_forums($userid, $courseid=0) {
1006
 
1007
    global $CFG, $DB, $USER;
1008
    require_once($CFG->dirroot.'/course/lib.php');
1009
 
1010
    if (!$forummod = $DB->get_record('modules', array('name' => 'forum'))) {
1011
        throw new \moodle_exception('notinstalled', 'forum');
1012
    }
1013
 
1014
    if ($courseid) {
1015
        $courses = $DB->get_records('course', array('id' => $courseid));
1016
    } else {
1017
        // If no course is specified, then the user can see SITE + his courses.
1018
        $courses1 = $DB->get_records('course', array('id' => SITEID));
1019
        $courses2 = enrol_get_users_courses($userid, true, array('modinfo'));
1020
        $courses = array_merge($courses1, $courses2);
1021
    }
1022
    if (!$courses) {
1023
        return array();
1024
    }
1025
 
1026
    $readableforums = array();
1027
 
1028
    foreach ($courses as $course) {
1029
 
1030
        $modinfo = get_fast_modinfo($course);
1031
 
1032
        if (empty($modinfo->instances['forum'])) {
1033
            // hmm, no forums?
1034
            continue;
1035
        }
1036
 
1037
        $courseforums = $DB->get_records('forum', array('course' => $course->id));
1038
 
1039
        foreach ($modinfo->instances['forum'] as $forumid => $cm) {
1040
            if (!$cm->uservisible or !isset($courseforums[$forumid])) {
1041
                continue;
1042
            }
1043
            $context = context_module::instance($cm->id);
1044
            $forum = $courseforums[$forumid];
1045
            $forum->context = $context;
1046
            $forum->cm = $cm;
1047
 
1048
            if (!has_capability('mod/forum:viewdiscussion', $context)) {
1049
                continue;
1050
            }
1051
 
1052
         /// group access
1053
            if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) {
1054
 
1055
                $forum->onlygroups = $modinfo->get_groups($cm->groupingid);
1056
                $forum->onlygroups[] = -1;
1057
            }
1058
 
1059
        /// hidden timed discussions
1060
            $forum->viewhiddentimedposts = true;
1061
            if (!empty($CFG->forum_enabletimedposts)) {
1062
                if (!has_capability('mod/forum:viewhiddentimedposts', $context)) {
1063
                    $forum->viewhiddentimedposts = false;
1064
                }
1065
            }
1066
 
1067
        /// qanda access
1068
            if ($forum->type == 'qanda'
1069
                    && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
1070
 
1071
                // We need to check whether the user has posted in the qanda forum.
1072
                $forum->onlydiscussions = array();  // Holds discussion ids for the discussions
1073
                                                    // the user is allowed to see in this forum.
1074
                if ($discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $USER->id)) {
1075
                    foreach ($discussionspostedin as $d) {
1076
                        $forum->onlydiscussions[] = $d->id;
1077
                    }
1078
                }
1079
            }
1080
 
1081
            $readableforums[$forum->id] = $forum;
1082
        }
1083
 
1084
        unset($modinfo);
1085
 
1086
    } // End foreach $courses
1087
 
1088
    return $readableforums;
1089
}
1090
 
1091
/**
1092
 * Returns a list of posts found using an array of search terms.
1093
 *
1094
 * @global object
1095
 * @global object
1096
 * @global object
1097
 * @param array $searchterms array of search terms, e.g. word +word -word
1098
 * @param int $courseid if 0, we search through the whole site
1099
 * @param int $limitfrom
1100
 * @param int $limitnum
1101
 * @param int &$totalcount
1102
 * @param string $extrasql
1103
 * @return array|bool Array of posts found or false
1104
 */
1105
function forum_search_posts($searchterms, $courseid, $limitfrom, $limitnum,
1106
                            &$totalcount, $extrasql='') {
1107
    global $CFG, $DB, $USER;
1108
    require_once($CFG->libdir.'/searchlib.php');
1109
 
1110
    $forums = forum_get_readable_forums($USER->id, $courseid);
1111
 
1112
    if (count($forums) == 0) {
1113
        $totalcount = 0;
1114
        return false;
1115
    }
1116
 
1117
    $now = floor(time() / 60) * 60; // DB Cache Friendly.
1118
 
1119
    $fullaccess = array();
1120
    $where = array();
1121
    $params = array();
1122
 
1123
    foreach ($forums as $forumid => $forum) {
1124
        $select = array();
1125
 
1126
        if (!$forum->viewhiddentimedposts) {
1127
            $select[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
1128
            $params = array_merge($params, array('userid'.$forumid=>$USER->id, 'timestart'.$forumid=>$now, 'timeend'.$forumid=>$now));
1129
        }
1130
 
1131
        $cm = $forum->cm;
1132
        $context = $forum->context;
1133
 
1134
        if ($forum->type == 'qanda'
1135
            && !has_capability('mod/forum:viewqandawithoutposting', $context)) {
1136
            if (!empty($forum->onlydiscussions)) {
1137
                list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forum->onlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
1138
                $params = array_merge($params, $discussionid_params);
1139
                $select[] = "(d.id $discussionid_sql OR p.parent = 0)";
1140
            } else {
1141
                $select[] = "p.parent = 0";
1142
            }
1143
        }
1144
 
1145
        if (!empty($forum->onlygroups)) {
1146
            list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($forum->onlygroups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
1147
            $params = array_merge($params, $groupid_params);
1148
            $select[] = "d.groupid $groupid_sql";
1149
        }
1150
 
1151
        if ($select) {
1152
            $selects = implode(" AND ", $select);
1153
            $where[] = "(d.forum = :forum{$forumid} AND $selects)";
1154
            $params['forum'.$forumid] = $forumid;
1155
        } else {
1156
            $fullaccess[] = $forumid;
1157
        }
1158
    }
1159
 
1160
    if ($fullaccess) {
1161
        list($fullid_sql, $fullid_params) = $DB->get_in_or_equal($fullaccess, SQL_PARAMS_NAMED, 'fula');
1162
        $params = array_merge($params, $fullid_params);
1163
        $where[] = "(d.forum $fullid_sql)";
1164
    }
1165
 
1166
    $favjoin = "";
1167
    if (in_array('starredonly:on', $searchterms)) {
1168
        $usercontext = context_user::instance($USER->id);
1169
        $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
1170
        list($favjoin, $favparams) = $ufservice->get_join_sql_by_type('mod_forum', 'discussions',
1171
            "favourited", "d.id");
1172
 
1173
        $searchterms = array_values(array_diff($searchterms, array('starredonly:on')));
1174
        $params = array_merge($params, $favparams);
1175
        $extrasql .= " AND favourited.itemid IS NOT NULL AND favourited.itemid != 0";
1176
    }
1177
 
1178
    $selectdiscussion = "(".implode(" OR ", $where).")";
1179
 
1180
    $messagesearch = '';
1181
    $searchstring = '';
1182
 
1183
    // Need to concat these back together for parser to work.
1184
    foreach($searchterms as $searchterm){
1185
        if ($searchstring != '') {
1186
            $searchstring .= ' ';
1187
        }
1188
        $searchstring .= $searchterm;
1189
    }
1190
 
1191
    // We need to allow quoted strings for the search. The quotes *should* be stripped
1192
    // by the parser, but this should be examined carefully for security implications.
1193
    $searchstring = str_replace("\\\"","\"",$searchstring);
1194
    $parser = new search_parser();
1195
    $lexer = new search_lexer($parser);
1196
 
1197
    if ($lexer->parse($searchstring)) {
1198
        $parsearray = $parser->get_parsed_array();
1199
 
1200
        $tagjoins = '';
1201
        $tagfields = [];
1202
        $tagfieldcount = 0;
1203
        if ($parsearray) {
1204
            foreach ($parsearray as $token) {
1205
                if ($token->getType() == TOKEN_TAGS) {
1206
                    for ($i = 0; $i <= substr_count($token->getValue(), ','); $i++) {
1207
                        // Queries can only have a limited number of joins so set a limit sensible users won't exceed.
1208
                        if ($tagfieldcount > 10) {
1209
                            continue;
1210
                        }
1211
                        $tagjoins .= " LEFT JOIN {tag_instance} ti_$tagfieldcount
1212
                                        ON p.id = ti_$tagfieldcount.itemid
1213
                                            AND ti_$tagfieldcount.component = 'mod_forum'
1214
                                            AND ti_$tagfieldcount.itemtype = 'forum_posts'";
1215
                        $tagjoins .= " LEFT JOIN {tag} t_$tagfieldcount ON t_$tagfieldcount.id = ti_$tagfieldcount.tagid";
1216
                        $tagfields[] = "t_$tagfieldcount.rawname";
1217
                        $tagfieldcount++;
1218
                    }
1219
                }
1220
            }
1221
            list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
1222
                'p.userid', 'u.id', 'u.firstname',
1223
                'u.lastname', 'p.modified', 'd.forum',
1224
                $tagfields);
1225
 
1226
            $params = ($msparams ? array_merge($params, $msparams) : $params);
1227
        }
1228
    }
1229
 
1230
    $fromsql = "{forum_posts} p
1231
                  INNER JOIN {forum_discussions} d ON d.id = p.discussion
1232
                  INNER JOIN {user} u ON u.id = p.userid $tagjoins $favjoin";
1233
 
1234
    $selectsql = ($messagesearch ? $messagesearch . " AND " : "").
1235
                " p.discussion = d.id
1236
               AND p.userid = u.id
1237
               AND $selectdiscussion
1238
                   $extrasql";
1239
 
1240
    $countsql = "SELECT COUNT(*)
1241
                   FROM $fromsql
1242
                  WHERE $selectsql";
1243
 
1244
    $userfieldsapi = \core_user\fields::for_name();
1245
    $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
1246
    $searchsql = "SELECT p.*,
1247
                         d.forum,
1248
                         $allnames,
1249
                         u.email,
1250
                         u.picture,
1251
                         u.imagealt
1252
                    FROM $fromsql
1253
                   WHERE $selectsql
1254
                ORDER BY p.modified DESC";
1255
 
1256
    $totalcount = $DB->count_records_sql($countsql, $params);
1257
 
1258
    return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
1259
}
1260
 
1261
/**
1262
 * Get all the posts for a user in a forum suitable for forum_print_post
1263
 *
1264
 * @global object
1265
 * @global object
1266
 * @uses CONTEXT_MODULE
1267
 * @return array
1268
 */
1269
function forum_get_user_posts($forumid, $userid) {
1270
    global $CFG, $DB;
1271
 
1272
    $timedsql = "";
1273
    $params = array($forumid, $userid);
1274
 
1275
    if (!empty($CFG->forum_enabletimedposts)) {
1276
        $cm = get_coursemodule_from_instance('forum', $forumid);
1277
        if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
1278
            $now = time();
1279
            $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
1280
            $params[] = $now;
1281
            $params[] = $now;
1282
        }
1283
    }
1284
 
1285
    $userfieldsapi = \core_user\fields::for_name();
1286
    $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
1287
    return $DB->get_records_sql("SELECT p.*, d.forum, $allnames, u.email, u.picture, u.imagealt
1288
                              FROM {forum} f
1289
                                   JOIN {forum_discussions} d ON d.forum = f.id
1290
                                   JOIN {forum_posts} p       ON p.discussion = d.id
1291
                                   JOIN {user} u              ON u.id = p.userid
1292
                             WHERE f.id = ?
1293
                                   AND p.userid = ?
1294
                                   $timedsql
1295
                          ORDER BY p.modified ASC", $params);
1296
}
1297
 
1298
/**
1299
 * Get all the discussions user participated in
1300
 *
1301
 * @global object
1302
 * @global object
1303
 * @uses CONTEXT_MODULE
1304
 * @param int $forumid
1305
 * @param int $userid
1306
 * @return array Array or false
1307
 */
1308
function forum_get_user_involved_discussions($forumid, $userid) {
1309
    global $CFG, $DB;
1310
 
1311
    $timedsql = "";
1312
    $params = array($forumid, $userid);
1313
    if (!empty($CFG->forum_enabletimedposts)) {
1314
        $cm = get_coursemodule_from_instance('forum', $forumid);
1315
        if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
1316
            $now = time();
1317
            $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
1318
            $params[] = $now;
1319
            $params[] = $now;
1320
        }
1321
    }
1322
 
1323
    return $DB->get_records_sql("SELECT DISTINCT d.*
1324
                              FROM {forum} f
1325
                                   JOIN {forum_discussions} d ON d.forum = f.id
1326
                                   JOIN {forum_posts} p       ON p.discussion = d.id
1327
                             WHERE f.id = ?
1328
                                   AND p.userid = ?
1329
                                   $timedsql", $params);
1330
}
1331
 
1332
/**
1333
 * Get all the posts for a user in a forum suitable for forum_print_post
1334
 *
1335
 * @global object
1336
 * @global object
1337
 * @param int $forumid
1338
 * @param int $userid
1339
 * @return stdClass|false collection of counts or false
1340
 */
1341
function forum_count_user_posts($forumid, $userid) {
1342
    global $CFG, $DB;
1343
 
1344
    $timedsql = "";
1345
    $params = array($forumid, $userid);
1346
    if (!empty($CFG->forum_enabletimedposts)) {
1347
        $cm = get_coursemodule_from_instance('forum', $forumid);
1348
        if (!has_capability('mod/forum:viewhiddentimedposts' , context_module::instance($cm->id))) {
1349
            $now = time();
1350
            $timedsql = "AND (d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?))";
1351
            $params[] = $now;
1352
            $params[] = $now;
1353
        }
1354
    }
1355
 
1356
    return $DB->get_record_sql("SELECT COUNT(p.id) AS postcount, MAX(p.modified) AS lastpost
1357
                             FROM {forum} f
1358
                                  JOIN {forum_discussions} d ON d.forum = f.id
1359
                                  JOIN {forum_posts} p       ON p.discussion = d.id
1360
                                  JOIN {user} u              ON u.id = p.userid
1361
                            WHERE f.id = ?
1362
                                  AND p.userid = ?
1363
                                  $timedsql", $params);
1364
}
1365
 
1366
/**
1367
 * Given a log entry, return the forum post details for it.
1368
 *
1369
 * @global object
1370
 * @global object
1371
 * @param object $log
1372
 * @return array|null
1373
 */
1374
function forum_get_post_from_log($log) {
1375
    global $CFG, $DB;
1376
 
1377
    $userfieldsapi = \core_user\fields::for_name();
1378
    $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
1379
    if ($log->action == "add post") {
1380
 
1381
        return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
1382
                                 FROM {forum_discussions} d,
1383
                                      {forum_posts} p,
1384
                                      {forum} f,
1385
                                      {user} u
1386
                                WHERE p.id = ?
1387
                                  AND d.id = p.discussion
1388
                                  AND p.userid = u.id
1389
                                  AND u.deleted <> '1'
1390
                                  AND f.id = d.forum", array($log->info));
1391
 
1392
 
1393
    } else if ($log->action == "add discussion") {
1394
 
1395
        return $DB->get_record_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid, $allnames, u.email, u.picture
1396
                                 FROM {forum_discussions} d,
1397
                                      {forum_posts} p,
1398
                                      {forum} f,
1399
                                      {user} u
1400
                                WHERE d.id = ?
1401
                                  AND d.firstpost = p.id
1402
                                  AND p.userid = u.id
1403
                                  AND u.deleted <> '1'
1404
                                  AND f.id = d.forum", array($log->info));
1405
    }
1406
    return NULL;
1407
}
1408
 
1409
/**
1410
 * Given a discussion id, return the first post from the discussion
1411
 *
1412
 * @global object
1413
 * @global object
1414
 * @param int $dicsussionid
1415
 * @return array
1416
 */
1417
function forum_get_firstpost_from_discussion($discussionid) {
1418
    global $CFG, $DB;
1419
 
1420
    return $DB->get_record_sql("SELECT p.*
1421
                             FROM {forum_discussions} d,
1422
                                  {forum_posts} p
1423
                            WHERE d.id = ?
1424
                              AND d.firstpost = p.id ", array($discussionid));
1425
}
1426
 
1427
/**
1428
 * Returns an array of counts of replies to each discussion
1429
 *
1430
 * @param   int     $forumid
1431
 * @param   string  $forumsort
1432
 * @param   int     $limit
1433
 * @param   int     $page
1434
 * @param   int     $perpage
1435
 * @param   boolean $canseeprivatereplies   Whether the current user can see private replies.
1436
 * @return  array
1437
 */
1438
function forum_count_discussion_replies($forumid, $forumsort = "", $limit = -1, $page = -1, $perpage = 0,
1439
                                        $canseeprivatereplies = false) {
1440
    global $CFG, $DB, $USER;
1441
 
1442
    if ($limit > 0) {
1443
        $limitfrom = 0;
1444
        $limitnum  = $limit;
1445
    } else if ($page != -1) {
1446
        $limitfrom = $page*$perpage;
1447
        $limitnum  = $perpage;
1448
    } else {
1449
        $limitfrom = 0;
1450
        $limitnum  = 0;
1451
    }
1452
 
1453
    if ($forumsort == "") {
1454
        $orderby = "";
1455
        $groupby = "";
1456
 
1457
    } else {
1458
        $orderby = "ORDER BY $forumsort";
1459
        $groupby = ", ".strtolower($forumsort);
1460
        $groupby = str_replace('desc', '', $groupby);
1461
        $groupby = str_replace('asc', '', $groupby);
1462
    }
1463
 
1464
    $params = ['forumid' => $forumid];
1465
 
1466
    if (!$canseeprivatereplies) {
1467
        $privatewhere = ' AND (p.privatereplyto = :currentuser1 OR p.userid = :currentuser2 OR p.privatereplyto = 0)';
1468
        $params['currentuser1'] = $USER->id;
1469
        $params['currentuser2'] = $USER->id;
1470
    } else {
1471
        $privatewhere = '';
1472
    }
1473
 
1474
    if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
1475
        $sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
1476
                  FROM {forum_posts} p
1477
                       JOIN {forum_discussions} d ON p.discussion = d.id
1478
                 WHERE p.parent > 0 AND d.forum = :forumid
1479
                       $privatewhere
1480
              GROUP BY p.discussion";
1481
        return $DB->get_records_sql($sql, $params);
1482
 
1483
    } else {
1484
        $sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
1485
                  FROM {forum_posts} p
1486
                       JOIN {forum_discussions} d ON p.discussion = d.id
1487
                 WHERE d.forum = :forumid
1488
                       $privatewhere
1489
              GROUP BY p.discussion $groupby $orderby";
1490
        return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1491
    }
1492
}
1493
 
1494
/**
1495
 * @global object
1496
 * @global object
1497
 * @global object
1498
 * @param object $forum
1499
 * @param object $cm
1500
 * @param object $course
1501
 * @return mixed
1502
 */
1503
function forum_count_discussions($forum, $cm, $course) {
1504
    global $CFG, $DB, $USER;
1505
 
1506
    $cache = cache::make('mod_forum', 'forum_count_discussions');
1507
    $cachedcounts = $cache->get($course->id);
1508
    if ($cachedcounts === false) {
1509
        $cachedcounts = [];
1510
    }
1511
 
1512
    $now = floor(time() / 60) * 60; // DB Cache Friendly.
1513
 
1514
    $params = array($course->id);
1515
 
1516
    if (!isset($cachedcounts[$forum->id])) {
1517
        // Initialize the cachedcounts for this forum id to 0 by default. After the
1518
        // database query, if there are discussions then it should update the count.
1519
        $cachedcounts[$forum->id] = 0;
1520
 
1521
        if (!empty($CFG->forum_enabletimedposts)) {
1522
            $timedsql = "AND d.timestart < ? AND (d.timeend = 0 OR d.timeend > ?)";
1523
            $params[] = $now;
1524
            $params[] = $now;
1525
        } else {
1526
            $timedsql = "";
1527
        }
1528
 
1529
        $sql = "SELECT f.id, COUNT(d.id) as dcount
1530
                  FROM {forum} f
1531
                       JOIN {forum_discussions} d ON d.forum = f.id
1532
                 WHERE f.course = ?
1533
                       $timedsql
1534
              GROUP BY f.id";
1535
 
1536
        if ($counts = $DB->get_records_sql($sql, $params)) {
1537
            foreach ($counts as $count) {
1538
                $cachedcounts[$count->id] = $count->dcount;
1539
            }
1540
 
1541
            $cache->set($course->id, $cachedcounts);
1542
        } else {
1543
            $cache->set($course->id, $cachedcounts);
1544
            return $cachedcounts[$forum->id];
1545
        }
1546
    }
1547
 
1548
    $groupmode = groups_get_activity_groupmode($cm, $course);
1549
 
1550
    if ($groupmode != SEPARATEGROUPS) {
1551
        return $cachedcounts[$forum->id];
1552
    }
1553
 
1554
    if (has_capability('moodle/site:accessallgroups', context_module::instance($cm->id))) {
1555
        return $cachedcounts[$forum->id];
1556
    }
1557
 
1558
    require_once($CFG->dirroot.'/course/lib.php');
1559
 
1560
    $modinfo = get_fast_modinfo($course);
1561
 
1562
    $mygroups = $modinfo->get_groups($cm->groupingid);
1563
 
1564
    // add all groups posts
1565
    $mygroups[-1] = -1;
1566
 
1567
    list($mygroups_sql, $params) = $DB->get_in_or_equal($mygroups);
1568
    $params[] = $forum->id;
1569
 
1570
    if (!empty($CFG->forum_enabletimedposts)) {
1571
        $timedsql = "AND d.timestart < $now AND (d.timeend = 0 OR d.timeend > $now)";
1572
        $params[] = $now;
1573
        $params[] = $now;
1574
    } else {
1575
        $timedsql = "";
1576
    }
1577
 
1578
    $sql = "SELECT COUNT(d.id)
1579
              FROM {forum_discussions} d
1580
             WHERE d.groupid $mygroups_sql AND d.forum = ?
1581
                   $timedsql";
1582
 
1583
    return $DB->get_field_sql($sql, $params);
1584
}
1585
 
1586
/**
1587
 * Get all discussions in a forum
1588
 *
1589
 * @global object
1590
 * @global object
1591
 * @global object
1592
 * @uses CONTEXT_MODULE
1593
 * @uses VISIBLEGROUPS
1594
 * @param object $cm
1595
 * @param string $forumsort
1596
 * @param bool $fullpost
1597
 * @param int $unused
1598
 * @param int $limit
1599
 * @param bool $userlastmodified
1600
 * @param int $page
1601
 * @param int $perpage
1602
 * @param int $groupid if groups enabled, get discussions for this group overriding the current group.
1603
 *                     Use FORUM_POSTS_ALL_USER_GROUPS for all the user groups
1604
 * @param int $updatedsince retrieve only discussions updated since the given time
1605
 * @return array
1606
 */
1607
function forum_get_discussions($cm, $forumsort="", $fullpost=true, $unused=-1, $limit=-1,
1608
                                $userlastmodified=false, $page=-1, $perpage=0, $groupid = -1,
1609
                                $updatedsince = 0) {
1610
    global $CFG, $DB, $USER;
1611
 
1612
    $timelimit = '';
1613
 
1614
    $now = floor(time() / 60) * 60;
1615
    $params = array($cm->instance);
1616
 
1617
    $modcontext = context_module::instance($cm->id);
1618
 
1619
    if (!has_capability('mod/forum:viewdiscussion', $modcontext)) { /// User must have perms to view discussions
1620
        return array();
1621
    }
1622
 
1623
    if (!empty($CFG->forum_enabletimedposts)) { /// Users must fulfill timed posts
1624
 
1625
        if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
1626
            $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
1627
            $params[] = $now;
1628
            $params[] = $now;
1629
            if (isloggedin()) {
1630
                $timelimit .= " OR d.userid = ?";
1631
                $params[] = $USER->id;
1632
            }
1633
            $timelimit .= ")";
1634
        }
1635
    }
1636
 
1637
    if ($limit > 0) {
1638
        $limitfrom = 0;
1639
        $limitnum  = $limit;
1640
    } else if ($page != -1) {
1641
        $limitfrom = $page*$perpage;
1642
        $limitnum  = $perpage;
1643
    } else {
1644
        $limitfrom = 0;
1645
        $limitnum  = 0;
1646
    }
1647
 
1648
    $groupmode    = groups_get_activity_groupmode($cm);
1649
 
1650
    if ($groupmode) {
1651
 
1652
        if (empty($modcontext)) {
1653
            $modcontext = context_module::instance($cm->id);
1654
        }
1655
 
1656
        // Special case, we received a groupid to override currentgroup.
1657
        if ($groupid > 0) {
1658
            $course = get_course($cm->course);
1659
            if (!groups_group_visible($groupid, $course, $cm)) {
1660
                // User doesn't belong to this group, return nothing.
1661
                return array();
1662
            }
1663
            $currentgroup = $groupid;
1664
        } else if ($groupid === -1) {
1665
            $currentgroup = groups_get_activity_group($cm);
1666
        } else {
1667
            // Get discussions for all groups current user can see.
1668
            $currentgroup = null;
1669
        }
1670
 
1671
        if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
1672
            if ($currentgroup) {
1673
                $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
1674
                $params[] = $currentgroup;
1675
            } else {
1676
                $groupselect = "";
1677
            }
1678
 
1679
        } else {
1680
            // Separate groups.
1681
 
1682
            // Get discussions for all groups current user can see.
1683
            if ($currentgroup === null) {
1684
                $mygroups = array_keys(groups_get_all_groups($cm->course, $USER->id, $cm->groupingid, 'g.id'));
1685
                if (empty($mygroups)) {
1686
                     $groupselect = "AND d.groupid = -1";
1687
                } else {
1688
                    list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($mygroups);
1689
                    $groupselect = "AND (d.groupid = -1 OR d.groupid $insqlgroups)";
1690
                    $params = array_merge($params, $inparamsgroups);
1691
                }
1692
            } else if ($currentgroup) {
1693
                $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
1694
                $params[] = $currentgroup;
1695
            } else {
1696
                $groupselect = "AND d.groupid = -1";
1697
            }
1698
        }
1699
    } else {
1700
        $groupselect = "";
1701
    }
1702
    if (empty($forumsort)) {
1703
        $forumsort = forum_get_default_sort_order();
1704
    }
1705
    if (empty($fullpost)) {
1706
        $postdata = "p.id, p.subject, p.modified, p.discussion, p.userid, p.created";
1707
    } else {
1708
        $postdata = "p.*";
1709
    }
1710
 
1711
    $userfieldsapi = \core_user\fields::for_name();
1712
 
1713
    if (empty($userlastmodified)) {  // We don't need to know this
1714
        $umfields = "";
1715
        $umtable  = "";
1716
    } else {
1717
        $umfields = $userfieldsapi->get_sql('um', false, 'um')->selects . ', um.email AS umemail, um.picture AS umpicture,
1718
                        um.imagealt AS umimagealt';
1719
        $umtable  = " LEFT JOIN {user} um ON (d.usermodified = um.id)";
1720
    }
1721
 
1722
    $updatedsincesql = '';
1723
    if (!empty($updatedsince)) {
1724
        $updatedsincesql = 'AND d.timemodified > ?';
1725
        $params[] = $updatedsince;
1726
    }
1727
 
1728
    $discussionfields = "d.id as discussionid, d.course, d.forum, d.name, d.firstpost, d.groupid, d.assessed," .
1729
    " d.timemodified, d.usermodified, d.timestart, d.timeend, d.pinned, d.timelocked";
1730
 
1731
    $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
1732
    $sql = "SELECT $postdata, $discussionfields,
1733
                   $allnames, u.email, u.picture, u.imagealt, u.deleted AS userdeleted $umfields
1734
              FROM {forum_discussions} d
1735
                   JOIN {forum_posts} p ON p.discussion = d.id
1736
                   JOIN {user} u ON p.userid = u.id
1737
                   $umtable
1738
             WHERE d.forum = ? AND p.parent = 0
1739
                   $timelimit $groupselect $updatedsincesql
1740
          ORDER BY $forumsort, d.id DESC";
1741
 
1742
    return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
1743
}
1744
 
1745
/**
1746
 * Gets the neighbours (previous and next) of a discussion.
1747
 *
1748
 * The calculation is based on the timemodified when time modified or time created is identical
1749
 * It will revert to using the ID to sort consistently. This is better tha skipping a discussion.
1750
 *
1751
 * For blog-style forums, the calculation is based on the original creation time of the
1752
 * blog post.
1753
 *
1754
 * Please note that this does not check whether or not the discussion passed is accessible
1755
 * by the user, it simply uses it as a reference to find the neighbours. On the other hand,
1756
 * the returned neighbours are checked and are accessible to the current user.
1757
 *
1758
 * @param object $cm The CM record.
1759
 * @param object $discussion The discussion record.
1760
 * @param object $forum The forum instance record.
1761
 * @return array That always contains the keys 'prev' and 'next'. When there is a result
1762
 *               they contain the record with minimal information such as 'id' and 'name'.
1763
 *               When the neighbour is not found the value is false.
1764
 */
1765
function forum_get_discussion_neighbours($cm, $discussion, $forum) {
1766
    global $CFG, $DB, $USER;
1767
 
1768
    if ($cm->instance != $discussion->forum or $discussion->forum != $forum->id or $forum->id != $cm->instance) {
1769
        throw new coding_exception('Discussion is not part of the same forum.');
1770
    }
1771
 
1772
    $neighbours = array('prev' => false, 'next' => false);
1773
    $now = floor(time() / 60) * 60;
1774
    $params = array();
1775
 
1776
    $modcontext = context_module::instance($cm->id);
1777
    $groupmode    = groups_get_activity_groupmode($cm);
1778
    $currentgroup = groups_get_activity_group($cm);
1779
 
1780
    // Users must fulfill timed posts.
1781
    $timelimit = '';
1782
    if (!empty($CFG->forum_enabletimedposts)) {
1783
        if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
1784
            $timelimit = ' AND ((d.timestart <= :tltimestart AND (d.timeend = 0 OR d.timeend > :tltimeend))';
1785
            $params['tltimestart'] = $now;
1786
            $params['tltimeend'] = $now;
1787
            if (isloggedin()) {
1788
                $timelimit .= ' OR d.userid = :tluserid';
1789
                $params['tluserid'] = $USER->id;
1790
            }
1791
            $timelimit .= ')';
1792
        }
1793
    }
1794
 
1795
    // Limiting to posts accessible according to groups.
1796
    $groupselect = '';
1797
    if ($groupmode) {
1798
        if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $modcontext)) {
1799
            if ($currentgroup) {
1800
                $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
1801
                $params['groupid'] = $currentgroup;
1802
            }
1803
        } else {
1804
            if ($currentgroup) {
1805
                $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
1806
                $params['groupid'] = $currentgroup;
1807
            } else {
1808
                $groupselect = 'AND d.groupid = -1';
1809
            }
1810
        }
1811
    }
1812
 
1813
    $params['forumid'] = $cm->instance;
1814
    $params['discid1'] = $discussion->id;
1815
    $params['discid2'] = $discussion->id;
1816
    $params['discid3'] = $discussion->id;
1817
    $params['discid4'] = $discussion->id;
1818
    $params['disctimecompare1'] = $discussion->timemodified;
1819
    $params['disctimecompare2'] = $discussion->timemodified;
1820
    $params['pinnedstate1'] = (int) $discussion->pinned;
1821
    $params['pinnedstate2'] = (int) $discussion->pinned;
1822
    $params['pinnedstate3'] = (int) $discussion->pinned;
1823
    $params['pinnedstate4'] = (int) $discussion->pinned;
1824
 
1825
    $sql = "SELECT d.id, d.name, d.timemodified, d.groupid, d.timestart, d.timeend
1826
              FROM {forum_discussions} d
1827
              JOIN {forum_posts} p ON d.firstpost = p.id
1828
             WHERE d.forum = :forumid
1829
               AND d.id <> :discid1
1830
                   $timelimit
1831
                   $groupselect";
1832
    $comparefield = "d.timemodified";
1833
    $comparevalue = ":disctimecompare1";
1834
    $comparevalue2  = ":disctimecompare2";
1835
    if (!empty($CFG->forum_enabletimedposts)) {
1836
        // Here we need to take into account the release time (timestart)
1837
        // if one is set, of the neighbouring posts and compare it to the
1838
        // timestart or timemodified of *this* post depending on if the
1839
        // release date of this post is in the future or not.
1840
        // This stops discussions that appear later because of the
1841
        // timestart value from being buried under discussions that were
1842
        // made afterwards.
1843
        $comparefield = "CASE WHEN d.timemodified < d.timestart
1844
                                THEN d.timestart ELSE d.timemodified END";
1845
        if ($discussion->timemodified < $discussion->timestart) {
1846
            // Normally we would just use the timemodified for sorting
1847
            // discussion posts. However, when timed discussions are enabled,
1848
            // then posts need to be sorted base on the later of timemodified
1849
            // or the release date of the post (timestart).
1850
            $params['disctimecompare1'] = $discussion->timestart;
1851
            $params['disctimecompare2'] = $discussion->timestart;
1852
        }
1853
    }
1854
    $orderbydesc = forum_get_default_sort_order(true, $comparefield, 'd', false);
1855
    $orderbyasc = forum_get_default_sort_order(false, $comparefield, 'd', false);
1856
 
1857
    if ($forum->type === 'blog') {
1858
         $subselect = "SELECT pp.created
1859
                   FROM {forum_discussions} dd
1860
                   JOIN {forum_posts} pp ON dd.firstpost = pp.id ";
1861
 
1862
         $subselectwhere1 = " WHERE dd.id = :discid3";
1863
         $subselectwhere2 = " WHERE dd.id = :discid4";
1864
 
1865
         $comparefield = "p.created";
1866
 
1867
         $sub1 = $subselect.$subselectwhere1;
1868
         $comparevalue = "($sub1)";
1869
 
1870
         $sub2 = $subselect.$subselectwhere2;
1871
         $comparevalue2 = "($sub2)";
1872
 
1873
         $orderbydesc = "d.pinned, p.created DESC";
1874
         $orderbyasc = "d.pinned, p.created ASC";
1875
    }
1876
 
1877
    $prevsql = $sql . " AND ( (($comparefield < $comparevalue) AND :pinnedstate1 = d.pinned)
1878
                         OR ($comparefield = $comparevalue2 AND (d.pinned = 0 OR d.pinned = :pinnedstate4) AND d.id < :discid2)
1879
                         OR (d.pinned = 0 AND d.pinned <> :pinnedstate2))
1880
                   ORDER BY CASE WHEN d.pinned = :pinnedstate3 THEN 1 ELSE 0 END DESC, $orderbydesc, d.id DESC";
1881
 
1882
    $nextsql = $sql . " AND ( (($comparefield > $comparevalue) AND :pinnedstate1 = d.pinned)
1883
                         OR ($comparefield = $comparevalue2 AND (d.pinned = 1 OR d.pinned = :pinnedstate4) AND d.id > :discid2)
1884
                         OR (d.pinned = 1 AND d.pinned <> :pinnedstate2))
1885
                   ORDER BY CASE WHEN d.pinned = :pinnedstate3 THEN 1 ELSE 0 END DESC, $orderbyasc, d.id ASC";
1886
 
1887
    $neighbours['prev'] = $DB->get_record_sql($prevsql, $params, IGNORE_MULTIPLE);
1888
    $neighbours['next'] = $DB->get_record_sql($nextsql, $params, IGNORE_MULTIPLE);
1889
    return $neighbours;
1890
}
1891
 
1892
/**
1893
 * Get the sql to use in the ORDER BY clause for forum discussions.
1894
 *
1895
 * This has the ordering take timed discussion windows into account.
1896
 *
1897
 * @param bool $desc True for DESC, False for ASC.
1898
 * @param string $compare The field in the SQL to compare to normally sort by.
1899
 * @param string $prefix The prefix being used for the discussion table.
1900
 * @param bool $pinned sort pinned posts to the top
1901
 * @return string
1902
 */
1903
function forum_get_default_sort_order($desc = true, $compare = 'd.timemodified', $prefix = 'd', $pinned = true) {
1904
    global $CFG;
1905
 
1906
    if (!empty($prefix)) {
1907
        $prefix .= '.';
1908
    }
1909
 
1910
    $dir = $desc ? 'DESC' : 'ASC';
1911
 
1912
    if ($pinned == true) {
1913
        $pinned = "{$prefix}pinned DESC,";
1914
    } else {
1915
        $pinned = '';
1916
    }
1917
 
1918
    $sort = "{$prefix}timemodified";
1919
    if (!empty($CFG->forum_enabletimedposts)) {
1920
        $sort = "CASE WHEN {$compare} < {$prefix}timestart
1921
                 THEN {$prefix}timestart
1922
                 ELSE {$compare}
1923
                 END";
1924
    }
1925
    return "$pinned $sort $dir";
1926
}
1927
 
1928
/**
1929
 *
1930
 * @global object
1931
 * @global object
1932
 * @global object
1933
 * @uses CONTEXT_MODULE
1934
 * @uses VISIBLEGROUPS
1935
 * @param object $cm
1936
 * @return array
1937
 */
1938
function forum_get_discussions_unread($cm) {
1939
    global $CFG, $DB, $USER;
1940
 
1941
    $now = floor(time() / 60) * 60;
1942
    $cutoffdate = $now - ($CFG->forum_oldpostdays*24*60*60);
1943
 
1944
    $params = array();
1945
    $groupmode    = groups_get_activity_groupmode($cm);
1946
    $currentgroup = groups_get_activity_group($cm);
1947
 
1948
    if ($groupmode) {
1949
        $modcontext = context_module::instance($cm->id);
1950
 
1951
        if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
1952
            if ($currentgroup) {
1953
                $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
1954
                $params['currentgroup'] = $currentgroup;
1955
            } else {
1956
                $groupselect = "";
1957
            }
1958
 
1959
        } else {
1960
            //separate groups without access all
1961
            if ($currentgroup) {
1962
                $groupselect = "AND (d.groupid = :currentgroup OR d.groupid = -1)";
1963
                $params['currentgroup'] = $currentgroup;
1964
            } else {
1965
                $groupselect = "AND d.groupid = -1";
1966
            }
1967
        }
1968
    } else {
1969
        $groupselect = "";
1970
    }
1971
 
1972
    if (!empty($CFG->forum_enabletimedposts)) {
1973
        $timedsql = "AND d.timestart < :now1 AND (d.timeend = 0 OR d.timeend > :now2)";
1974
        $params['now1'] = $now;
1975
        $params['now2'] = $now;
1976
    } else {
1977
        $timedsql = "";
1978
    }
1979
 
1980
    $sql = "SELECT d.id, COUNT(p.id) AS unread
1981
              FROM {forum_discussions} d
1982
                   JOIN {forum_posts} p     ON p.discussion = d.id
1983
                   LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = $USER->id)
1984
             WHERE d.forum = {$cm->instance}
1985
                   AND p.modified >= :cutoffdate AND r.id is NULL
1986
                   $groupselect
1987
                   $timedsql
1988
          GROUP BY d.id";
1989
    $params['cutoffdate'] = $cutoffdate;
1990
 
1991
    if ($unreads = $DB->get_records_sql($sql, $params)) {
1992
        foreach ($unreads as $unread) {
1993
            $unreads[$unread->id] = $unread->unread;
1994
        }
1995
        return $unreads;
1996
    } else {
1997
        return array();
1998
    }
1999
}
2000
 
2001
/**
2002
 * @global object
2003
 * @global object
2004
 * @global object
2005
 * @uses CONEXT_MODULE
2006
 * @uses VISIBLEGROUPS
2007
 * @param object $cm
2008
 * @return array
2009
 */
2010
function forum_get_discussions_count($cm) {
2011
    global $CFG, $DB, $USER;
2012
 
2013
    $now = floor(time() / 60) * 60;
2014
    $params = array($cm->instance);
2015
    $groupmode    = groups_get_activity_groupmode($cm);
2016
    $currentgroup = groups_get_activity_group($cm);
2017
 
2018
    if ($groupmode) {
2019
        $modcontext = context_module::instance($cm->id);
2020
 
2021
        if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $modcontext)) {
2022
            if ($currentgroup) {
2023
                $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2024
                $params[] = $currentgroup;
2025
            } else {
2026
                $groupselect = "";
2027
            }
2028
 
2029
        } else {
2030
            //seprate groups without access all
2031
            if ($currentgroup) {
2032
                $groupselect = "AND (d.groupid = ? OR d.groupid = -1)";
2033
                $params[] = $currentgroup;
2034
            } else {
2035
                $groupselect = "AND d.groupid = -1";
2036
            }
2037
        }
2038
    } else {
2039
        $groupselect = "";
2040
    }
2041
 
2042
    $timelimit = "";
2043
 
2044
    if (!empty($CFG->forum_enabletimedposts)) {
2045
 
2046
        $modcontext = context_module::instance($cm->id);
2047
 
2048
        if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
2049
            $timelimit = " AND ((d.timestart <= ? AND (d.timeend = 0 OR d.timeend > ?))";
2050
            $params[] = $now;
2051
            $params[] = $now;
2052
            if (isloggedin()) {
2053
                $timelimit .= " OR d.userid = ?";
2054
                $params[] = $USER->id;
2055
            }
2056
            $timelimit .= ")";
2057
        }
2058
    }
2059
 
2060
    $sql = "SELECT COUNT(d.id)
2061
              FROM {forum_discussions} d
2062
                   JOIN {forum_posts} p ON p.discussion = d.id
2063
             WHERE d.forum = ? AND p.parent = 0
2064
                   $groupselect $timelimit";
2065
 
2066
    return $DB->get_field_sql($sql, $params);
2067
}
2068
 
2069
 
2070
// OTHER FUNCTIONS ///////////////////////////////////////////////////////////
2071
 
2072
 
2073
/**
2074
 * @global object
2075
 * @global object
2076
 * @param int $courseid
2077
 * @param string $type
2078
 */
2079
function forum_get_course_forum($courseid, $type) {
2080
// How to set up special 1-per-course forums
2081
    global $CFG, $DB, $OUTPUT, $USER;
2082
 
2083
    if ($forums = $DB->get_records_select("forum", "course = ? AND type = ?", array($courseid, $type), "id ASC")) {
2084
        // There should always only be ONE, but with the right combination of
2085
        // errors there might be more.  In this case, just return the oldest one (lowest ID).
2086
        foreach ($forums as $forum) {
2087
            return $forum;   // ie the first one
2088
        }
2089
    }
2090
 
2091
    // Doesn't exist, so create one now.
2092
    $forum = new stdClass();
2093
    $forum->course = $courseid;
2094
    $forum->type = "$type";
2095
    if (!empty($USER->htmleditor)) {
2096
        $forum->introformat = $USER->htmleditor;
2097
    }
2098
    switch ($forum->type) {
2099
        case "news":
2100
            $forum->name  = get_string("namenews", "forum");
2101
            $forum->intro = get_string("intronews", "forum");
2102
            $forum->introformat = FORMAT_HTML;
2103
            $forum->forcesubscribe = $CFG->forum_announcementsubscription;
2104
            $forum->maxattachments = $CFG->forum_announcementmaxattachments;
2105
            $forum->assessed = 0;
2106
            if ($courseid == SITEID) {
2107
                $forum->name  = get_string("sitenews");
2108
                $forum->forcesubscribe = 0;
2109
            }
2110
            break;
2111
        case "social":
2112
            $forum->name  = get_string("namesocial", "forum");
2113
            $forum->intro = get_string("introsocial", "forum");
2114
            $forum->introformat = FORMAT_HTML;
2115
            $forum->assessed = 0;
2116
            $forum->forcesubscribe = 0;
2117
            break;
2118
        case "blog":
2119
            $forum->name = get_string('blogforum', 'forum');
2120
            $forum->intro = get_string('introblog', 'forum');
2121
            $forum->introformat = FORMAT_HTML;
2122
            $forum->assessed = 0;
2123
            $forum->forcesubscribe = 0;
2124
            break;
2125
        default:
2126
            echo $OUTPUT->notification("That forum type doesn't exist!");
2127
            return false;
2128
            break;
2129
    }
2130
 
2131
    $forum->timemodified = time();
2132
    $forum->id = $DB->insert_record("forum", $forum);
2133
 
2134
    if (! $module = $DB->get_record("modules", array("name" => "forum"))) {
2135
        echo $OUTPUT->notification("Could not find forum module!!");
2136
        return false;
2137
    }
2138
    $mod = new stdClass();
2139
    $mod->course = $courseid;
2140
    $mod->module = $module->id;
2141
    $mod->instance = $forum->id;
2142
    $mod->section = 0;
2143
    include_once("$CFG->dirroot/course/lib.php");
2144
    if (! $mod->coursemodule = add_course_module($mod) ) {
2145
        echo $OUTPUT->notification("Could not add a new course module to the course '" . $courseid . "'");
2146
        return false;
2147
    }
2148
    $sectionid = course_add_cm_to_section($courseid, $mod->coursemodule, 0);
2149
    return $DB->get_record("forum", array("id" => "$forum->id"));
2150
}
2151
 
2152
/**
2153
 * Return rating related permissions
2154
 *
2155
 * @param string $options the context id
2156
 * @return array an associative array of the user's rating permissions
2157
 */
2158
function forum_rating_permissions($contextid, $component, $ratingarea) {
2159
    $context = context::instance_by_id($contextid, MUST_EXIST);
2160
    if ($component != 'mod_forum' || $ratingarea != 'post') {
2161
        // We don't know about this component/ratingarea so just return null to get the
2162
        // default restrictive permissions.
2163
        return null;
2164
    }
2165
    return array(
2166
        'view'    => has_capability('mod/forum:viewrating', $context),
2167
        'viewany' => has_capability('mod/forum:viewanyrating', $context),
2168
        'viewall' => has_capability('mod/forum:viewallratings', $context),
2169
        'rate'    => has_capability('mod/forum:rate', $context)
2170
    );
2171
}
2172
 
2173
/**
2174
 * Validates a submitted rating
2175
 * @param array $params submitted data
2176
 *            context => object the context in which the rated items exists [required]
2177
 *            component => The component for this module - should always be mod_forum [required]
2178
 *            ratingarea => object the context in which the rated items exists [required]
2179
 *
2180
 *            itemid => int the ID of the object being rated [required]
2181
 *            scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
2182
 *            rating => int the submitted rating [required]
2183
 *            rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
2184
 *            aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
2185
 * @return boolean true if the rating is valid. Will throw rating_exception if not
2186
 */
2187
function forum_rating_validate($params) {
2188
    global $DB, $USER;
2189
 
2190
    // Check the component is mod_forum
2191
    if ($params['component'] != 'mod_forum') {
2192
        throw new rating_exception('invalidcomponent');
2193
    }
2194
 
2195
    // Check the ratingarea is post (the only rating area in forum)
2196
    if ($params['ratingarea'] != 'post') {
2197
        throw new rating_exception('invalidratingarea');
2198
    }
2199
 
2200
    // Check the rateduserid is not the current user .. you can't rate your own posts
2201
    if ($params['rateduserid'] == $USER->id) {
2202
        throw new rating_exception('nopermissiontorate');
2203
    }
2204
 
2205
    // Fetch all the related records ... we need to do this anyway to call forum_user_can_see_post
2206
    $post = $DB->get_record('forum_posts', array('id' => $params['itemid'], 'userid' => $params['rateduserid']), '*', MUST_EXIST);
2207
    $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion), '*', MUST_EXIST);
2208
    $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
2209
    $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
2210
    $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST);
2211
    $context = context_module::instance($cm->id);
2212
 
2213
    // Make sure the context provided is the context of the forum
2214
    if ($context->id != $params['context']->id) {
2215
        throw new rating_exception('invalidcontext');
2216
    }
2217
 
2218
    if ($forum->scale != $params['scaleid']) {
2219
        //the scale being submitted doesnt match the one in the database
2220
        throw new rating_exception('invalidscaleid');
2221
    }
2222
 
2223
    // check the item we're rating was created in the assessable time window
2224
    if (!empty($forum->assesstimestart) && !empty($forum->assesstimefinish)) {
2225
        if ($post->created < $forum->assesstimestart || $post->created > $forum->assesstimefinish) {
2226
            throw new rating_exception('notavailable');
2227
        }
2228
    }
2229
 
2230
    //check that the submitted rating is valid for the scale
2231
 
2232
    // lower limit
2233
    if ($params['rating'] < 0  && $params['rating'] != RATING_UNSET_RATING) {
2234
        throw new rating_exception('invalidnum');
2235
    }
2236
 
2237
    // upper limit
2238
    if ($forum->scale < 0) {
2239
        //its a custom scale
2240
        $scalerecord = $DB->get_record('scale', array('id' => -$forum->scale));
2241
        if ($scalerecord) {
2242
            $scalearray = explode(',', $scalerecord->scale);
2243
            if ($params['rating'] > count($scalearray)) {
2244
                throw new rating_exception('invalidnum');
2245
            }
2246
        } else {
2247
            throw new rating_exception('invalidscaleid');
2248
        }
2249
    } else if ($params['rating'] > $forum->scale) {
2250
        //if its numeric and submitted rating is above maximum
2251
        throw new rating_exception('invalidnum');
2252
    }
2253
 
2254
    // Make sure groups allow this user to see the item they're rating
2255
    if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {   // Groups are being used
2256
        if (!groups_group_exists($discussion->groupid)) { // Can't find group
2257
            throw new rating_exception('cannotfindgroup');//something is wrong
2258
        }
2259
 
2260
        if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) {
2261
            // do not allow rating of posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS
2262
            throw new rating_exception('notmemberofgroup');
2263
        }
2264
    }
2265
 
2266
    // perform some final capability checks
2267
    if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
2268
        throw new rating_exception('nopermissiontorate');
2269
    }
2270
 
2271
    return true;
2272
}
2273
 
2274
/**
2275
 * Can the current user see ratings for a given itemid?
2276
 *
2277
 * @param array $params submitted data
2278
 *            contextid => int contextid [required]
2279
 *            component => The component for this module - should always be mod_forum [required]
2280
 *            ratingarea => object the context in which the rated items exists [required]
2281
 *            itemid => int the ID of the object being rated [required]
2282
 *            scaleid => int scale id [optional]
2283
 * @return bool
2284
 * @throws coding_exception
2285
 * @throws rating_exception
2286
 */
2287
function mod_forum_rating_can_see_item_ratings($params) {
2288
    global $DB, $USER;
2289
 
2290
    // Check the component is mod_forum.
2291
    if (!isset($params['component']) || $params['component'] != 'mod_forum') {
2292
        throw new rating_exception('invalidcomponent');
2293
    }
2294
 
2295
    // Check the ratingarea is post (the only rating area in forum).
2296
    if (!isset($params['ratingarea']) || $params['ratingarea'] != 'post') {
2297
        throw new rating_exception('invalidratingarea');
2298
    }
2299
 
2300
    if (!isset($params['itemid'])) {
2301
        throw new rating_exception('invaliditemid');
2302
    }
2303
 
2304
    $post = $DB->get_record('forum_posts', array('id' => $params['itemid']), '*', MUST_EXIST);
2305
    $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion), '*', MUST_EXIST);
2306
    $forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
2307
    $course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
2308
    $cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST);
2309
 
2310
    // Perform some final capability checks.
2311
    if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
2312
        return false;
2313
    }
2314
 
2315
    return true;
2316
}
2317
 
2318
/**
2319
 * Return the markup for the discussion subscription toggling icon.
2320
 *
2321
 * @param stdClass $forum The forum object.
2322
 * @param int $discussionid The discussion to create an icon for.
2323
 * @return string The generated markup.
2324
 */
2325
function forum_get_discussion_subscription_icon($forum, $discussionid, $returnurl = null, $includetext = false) {
2326
    global $USER, $OUTPUT, $PAGE;
2327
 
2328
    if ($returnurl === null && $PAGE->url) {
2329
        $returnurl = $PAGE->url->out();
2330
    }
2331
 
2332
    $o = '';
2333
    $subscriptionstatus = \mod_forum\subscriptions::is_subscribed($USER->id, $forum, $discussionid);
2334
    $subscriptionlink = new moodle_url('/mod/forum/subscribe.php', array(
2335
        'sesskey' => sesskey(),
2336
        'id' => $forum->id,
2337
        'd' => $discussionid,
2338
        'returnurl' => $returnurl,
2339
    ));
2340
 
2341
    if ($includetext) {
2342
        $o .= $subscriptionstatus ? get_string('subscribed', 'mod_forum') : get_string('notsubscribed', 'mod_forum');
2343
    }
2344
 
2345
    if ($subscriptionstatus) {
2346
        $output = $OUTPUT->pix_icon('t/subscribed', get_string('clicktounsubscribe', 'forum'), 'mod_forum');
2347
        if ($includetext) {
2348
            $output .= get_string('subscribed', 'mod_forum');
2349
        }
2350
 
2351
        return html_writer::link($subscriptionlink, $output, array(
2352
                'title' => get_string('clicktounsubscribe', 'forum'),
2353
                'class' => 'discussiontoggle btn btn-link',
2354
                'data-forumid' => $forum->id,
2355
                'data-discussionid' => $discussionid,
2356
                'data-includetext' => $includetext,
2357
            ));
2358
 
2359
    } else {
2360
        $output = $OUTPUT->pix_icon('t/unsubscribed', get_string('clicktosubscribe', 'forum'), 'mod_forum');
2361
        if ($includetext) {
2362
            $output .= get_string('notsubscribed', 'mod_forum');
2363
        }
2364
 
2365
        return html_writer::link($subscriptionlink, $output, array(
2366
                'title' => get_string('clicktosubscribe', 'forum'),
2367
                'class' => 'discussiontoggle btn btn-link',
2368
                'data-forumid' => $forum->id,
2369
                'data-discussionid' => $discussionid,
2370
                'data-includetext' => $includetext,
2371
            ));
2372
    }
2373
}
2374
 
2375
/**
2376
 * Return a pair of spans containing classes to allow the subscribe and
2377
 * unsubscribe icons to be pre-loaded by a browser.
2378
 *
2379
 * @return string The generated markup
2380
 */
2381
function forum_get_discussion_subscription_icon_preloaders() {
2382
    $o = '';
2383
    $o .= html_writer::span('&nbsp;', 'preload-subscribe');
2384
    $o .= html_writer::span('&nbsp;', 'preload-unsubscribe');
2385
    return $o;
2386
}
2387
 
2388
/**
2389
 * Print the drop down that allows the user to select how they want to have
2390
 * the discussion displayed.
2391
 *
2392
 * @param int $id forum id if $forumtype is 'single',
2393
 *              discussion id for any other forum type
2394
 * @param mixed $mode forum layout mode
2395
 * @param string $forumtype optional
2396
 */
2397
function forum_print_mode_form($id, $mode, $forumtype='') {
2398
    global $OUTPUT;
2399
    $useexperimentalui = get_user_preferences('forum_useexperimentalui', false);
2400
    if ($forumtype == 'single') {
2401
        $select = new single_select(
2402
            new moodle_url("/mod/forum/view.php",
2403
            array('f' => $id)),
2404
            'mode',
2405
            forum_get_layout_modes($useexperimentalui),
2406
            $mode,
2407
            null,
2408
            "mode"
2409
        );
2410
        $select->set_label(get_string('displaymode', 'forum'), array('class' => 'accesshide'));
2411
        $select->class = "forummode";
2412
    } else {
2413
        $select = new single_select(
2414
            new moodle_url("/mod/forum/discuss.php",
2415
            array('d' => $id)),
2416
            'mode',
2417
            forum_get_layout_modes($useexperimentalui),
2418
            $mode,
2419
            null,
2420
            "mode"
2421
        );
2422
        $select->set_label(get_string('displaymode', 'forum'), array('class' => 'accesshide'));
2423
    }
2424
    echo $OUTPUT->render($select);
2425
}
2426
 
2427
/**
2428
 * @global object
2429
 * @param object $course
2430
 * @param string $search
2431
 * @return string
2432
 */
2433
function forum_search_form($course, $search='') {
2434
    global $CFG, $PAGE;
2435
    $forumsearch = new \mod_forum\output\quick_search_form($course->id, $search);
2436
    $output = $PAGE->get_renderer('mod_forum');
2437
    return $output->render($forumsearch);
2438
}
2439
 
2440
/**
2441
 * Retrieve HTML for the page action
2442
 *
2443
 * @param forum_entity|null $forum The forum entity.
2444
 * @param mixed $groupid false if groups not used, int if groups used, 0 means all groups
2445
 * @param stdClass $course The course object.
2446
 * @param string $search The search string.
2447
 * @return string rendered HTML string.
2448
 */
2449
function forum_activity_actionbar(?forum_entity $forum, $groupid, stdClass $course, string $search=''): string {
2450
    global $PAGE;
2451
 
2452
    $actionbar = new mod_forum\output\forum_actionbar($forum, $course, $groupid, $search);
2453
    $output = $PAGE->get_renderer('mod_forum');
2454
    return $output->render($actionbar);
2455
}
2456
 
2457
/**
2458
 * @global object
2459
 * @global object
2460
 */
2461
function forum_set_return() {
2462
    global $CFG, $SESSION;
2463
 
2464
    if (! isset($SESSION->fromdiscussion)) {
2465
        $referer = get_local_referer(false);
2466
        // If the referer is NOT a login screen then save it.
2467
        if (! strncasecmp("$CFG->wwwroot/login", $referer, 300)) {
2468
            $SESSION->fromdiscussion = $referer;
2469
        }
2470
    }
2471
}
2472
 
2473
 
2474
/**
2475
 * @global object
2476
 * @param string|\moodle_url $default
2477
 * @return string
2478
 */
2479
function forum_go_back_to($default) {
2480
    global $SESSION;
2481
 
2482
    if (!empty($SESSION->fromdiscussion)) {
2483
        $returnto = $SESSION->fromdiscussion;
2484
        unset($SESSION->fromdiscussion);
2485
        return $returnto;
2486
    } else {
2487
        return $default;
2488
    }
2489
}
2490
 
2491
/**
2492
 * Given a discussion object that is being moved to $forumto,
2493
 * this function checks all posts in that discussion
2494
 * for attachments, and if any are found, these are
2495
 * moved to the new forum directory.
2496
 *
2497
 * @global object
2498
 * @param object $discussion
2499
 * @param int $forumfrom source forum id
2500
 * @param int $forumto target forum id
2501
 * @return bool success
2502
 */
2503
function forum_move_attachments($discussion, $forumfrom, $forumto) {
2504
    global $DB;
2505
 
2506
    $fs = get_file_storage();
2507
 
2508
    $newcm = get_coursemodule_from_instance('forum', $forumto);
2509
    $oldcm = get_coursemodule_from_instance('forum', $forumfrom);
2510
 
2511
    $newcontext = context_module::instance($newcm->id);
2512
    $oldcontext = context_module::instance($oldcm->id);
2513
 
2514
    // loop through all posts, better not use attachment flag ;-)
2515
    if ($posts = $DB->get_records('forum_posts', array('discussion'=>$discussion->id), '', 'id, attachment')) {
2516
        foreach ($posts as $post) {
2517
            $fs->move_area_files_to_new_context($oldcontext->id,
2518
                    $newcontext->id, 'mod_forum', 'post', $post->id);
2519
            $attachmentsmoved = $fs->move_area_files_to_new_context($oldcontext->id,
2520
                    $newcontext->id, 'mod_forum', 'attachment', $post->id);
2521
            if ($attachmentsmoved > 0 && $post->attachment != '1') {
2522
                // Weird - let's fix it
2523
                $post->attachment = '1';
2524
                $DB->update_record('forum_posts', $post);
2525
            } else if ($attachmentsmoved == 0 && $post->attachment != '') {
2526
                // Weird - let's fix it
2527
                $post->attachment = '';
2528
                $DB->update_record('forum_posts', $post);
2529
            }
2530
        }
2531
    }
2532
 
2533
    return true;
2534
}
2535
 
2536
/**
2537
 * Returns attachments as formated text/html optionally with separate images
2538
 *
2539
 * @global object
2540
 * @global object
2541
 * @global object
2542
 * @param object $post
2543
 * @param object $cm
2544
 * @param string $type html/text/separateimages
2545
 * @return mixed string or array of (html text withouth images and image HTML)
2546
 */
2547
function forum_print_attachments($post, $cm, $type) {
2548
    global $CFG, $DB, $USER, $OUTPUT;
2549
 
2550
    if (empty($post->attachment)) {
2551
        return $type !== 'separateimages' ? '' : array('', '');
2552
    }
2553
 
2554
    if (!in_array($type, array('separateimages', 'html', 'text'))) {
2555
        return $type !== 'separateimages' ? '' : array('', '');
2556
    }
2557
 
2558
    if (!$context = context_module::instance($cm->id)) {
2559
        return $type !== 'separateimages' ? '' : array('', '');
2560
    }
2561
    $strattachment = get_string('attachment', 'forum');
2562
 
2563
    $fs = get_file_storage();
2564
 
2565
    $imagereturn = '';
2566
    $output = '';
2567
 
2568
    $canexport = !empty($CFG->enableportfolios) && (has_capability('mod/forum:exportpost', $context) || ($post->userid == $USER->id && has_capability('mod/forum:exportownpost', $context)));
2569
 
2570
    if ($canexport) {
2571
        require_once($CFG->libdir.'/portfoliolib.php');
2572
    }
2573
 
2574
    // We retrieve all files according to the time that they were created.  In the case that several files were uploaded
2575
    // at the sametime (e.g. in the case of drag/drop upload) we revert to using the filename.
2576
    $files = $fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id, "filename", false);
2577
    if ($files) {
2578
        if ($canexport) {
2579
            $button = new portfolio_add_button();
2580
        }
2581
        foreach ($files as $file) {
2582
            $filename = $file->get_filename();
2583
            $mimetype = $file->get_mimetype();
2584
            $iconimage = $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file), 'moodle', array('class' => 'icon'));
2585
            $path = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.$context->id.'/mod_forum/attachment/'.$post->id.'/'.$filename);
2586
 
2587
            if ($type == 'html') {
2588
                $output .= "<a href=\"$path\">$iconimage</a> ";
2589
                $output .= "<a href=\"$path\">".s($filename)."</a>";
2590
                if ($canexport) {
2591
                    $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum');
2592
                    $button->set_format_by_file($file);
2593
                    $output .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
2594
                }
2595
                $output .= "<br />";
2596
 
2597
            } else if ($type == 'text') {
2598
                $output .= "$strattachment ".s($filename).":\n$path\n";
2599
 
2600
            } else { //'returnimages'
2601
                if (in_array($mimetype, array('image/gif', 'image/jpeg', 'image/png'))) {
2602
                    // Image attachments don't get printed as links
2603
                    $imagereturn .= "<br /><img src=\"$path\" alt=\"\" />";
2604
                    if ($canexport) {
2605
                        $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum');
2606
                        $button->set_format_by_file($file);
2607
                        $imagereturn .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
2608
                    }
2609
                } else {
2610
                    $output .= "<a href=\"$path\">$iconimage</a> ";
2611
                    $output .= format_text("<a href=\"$path\">".s($filename)."</a>", FORMAT_HTML, array('context'=>$context));
2612
                    if ($canexport) {
2613
                        $button->set_callback_options('forum_portfolio_caller', array('postid' => $post->id, 'attachment' => $file->get_id()), 'mod_forum');
2614
                        $button->set_format_by_file($file);
2615
                        $output .= $button->to_html(PORTFOLIO_ADD_ICON_LINK);
2616
                    }
2617
                    $output .= '<br />';
2618
                }
2619
            }
2620
 
2621
            if (!empty($CFG->enableplagiarism)) {
2622
                require_once($CFG->libdir.'/plagiarismlib.php');
2623
                $output .= plagiarism_get_links(array('userid' => $post->userid,
2624
                    'file' => $file,
2625
                    'cmid' => $cm->id,
2626
                    'course' => $cm->course,
2627
                    'forum' => $cm->instance));
2628
                $output .= '<br />';
2629
            }
2630
        }
2631
    }
2632
 
2633
    if ($type !== 'separateimages') {
2634
        return $output;
2635
 
2636
    } else {
2637
        return array($output, $imagereturn);
2638
    }
2639
}
2640
 
2641
////////////////////////////////////////////////////////////////////////////////
2642
// File API                                                                   //
2643
////////////////////////////////////////////////////////////////////////////////
2644
 
2645
/**
2646
 * Lists all browsable file areas
2647
 *
2648
 * @package  mod_forum
2649
 * @category files
2650
 * @param stdClass $course course object
2651
 * @param stdClass $cm course module object
2652
 * @param stdClass $context context object
2653
 * @return array
2654
 */
2655
function forum_get_file_areas($course, $cm, $context) {
2656
    return array(
2657
        'attachment' => get_string('areaattachment', 'mod_forum'),
2658
        'post' => get_string('areapost', 'mod_forum'),
2659
    );
2660
}
2661
 
2662
/**
2663
 * File browsing support for forum module.
2664
 *
2665
 * @package  mod_forum
2666
 * @category files
2667
 * @param stdClass $browser file browser object
2668
 * @param stdClass $areas file areas
2669
 * @param stdClass $course course object
2670
 * @param stdClass $cm course module
2671
 * @param stdClass $context context module
2672
 * @param string $filearea file area
2673
 * @param int $itemid item ID
2674
 * @param string $filepath file path
2675
 * @param string $filename file name
2676
 * @return file_info instance or null if not found
2677
 */
2678
function forum_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
2679
    global $CFG, $DB, $USER;
2680
 
2681
    if ($context->contextlevel != CONTEXT_MODULE) {
2682
        return null;
2683
    }
2684
 
2685
    // filearea must contain a real area
2686
    if (!isset($areas[$filearea])) {
2687
        return null;
2688
    }
2689
 
2690
    // Note that forum_user_can_see_post() additionally allows access for parent roles
2691
    // and it explicitly checks qanda forum type, too. One day, when we stop requiring
2692
    // course:managefiles, we will need to extend this.
2693
    if (!has_capability('mod/forum:viewdiscussion', $context)) {
2694
        return null;
2695
    }
2696
 
2697
    if (is_null($itemid)) {
2698
        require_once($CFG->dirroot.'/mod/forum/locallib.php');
2699
        return new forum_file_info_container($browser, $course, $cm, $context, $areas, $filearea);
2700
    }
2701
 
2702
    static $cached = array();
2703
    // $cached will store last retrieved post, discussion and forum. To make sure that the cache
2704
    // is cleared between unit tests we check if this is the same session
2705
    if (!isset($cached['sesskey']) || $cached['sesskey'] != sesskey()) {
2706
        $cached = array('sesskey' => sesskey());
2707
    }
2708
 
2709
    if (isset($cached['post']) && $cached['post']->id == $itemid) {
2710
        $post = $cached['post'];
2711
    } else if ($post = $DB->get_record('forum_posts', array('id' => $itemid))) {
2712
        $cached['post'] = $post;
2713
    } else {
2714
        return null;
2715
    }
2716
 
2717
    if (isset($cached['discussion']) && $cached['discussion']->id == $post->discussion) {
2718
        $discussion = $cached['discussion'];
2719
    } else if ($discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion))) {
2720
        $cached['discussion'] = $discussion;
2721
    } else {
2722
        return null;
2723
    }
2724
 
2725
    if (isset($cached['forum']) && $cached['forum']->id == $cm->instance) {
2726
        $forum = $cached['forum'];
2727
    } else if ($forum = $DB->get_record('forum', array('id' => $cm->instance))) {
2728
        $cached['forum'] = $forum;
2729
    } else {
2730
        return null;
2731
    }
2732
 
2733
    $fs = get_file_storage();
2734
    $filepath = is_null($filepath) ? '/' : $filepath;
2735
    $filename = is_null($filename) ? '.' : $filename;
2736
    if (!($storedfile = $fs->get_file($context->id, 'mod_forum', $filearea, $itemid, $filepath, $filename))) {
2737
        return null;
2738
    }
2739
 
2740
    // Checks to see if the user can manage files or is the owner.
2741
    // TODO MDL-33805 - Do not use userid here and move the capability check above.
2742
    if (!has_capability('moodle/course:managefiles', $context) && $storedfile->get_userid() != $USER->id) {
2743
        return null;
2744
    }
2745
    // Make sure groups allow this user to see this file
2746
    if ($discussion->groupid > 0 && !has_capability('moodle/site:accessallgroups', $context)) {
2747
        $groupmode = groups_get_activity_groupmode($cm, $course);
2748
        if ($groupmode == SEPARATEGROUPS && !groups_is_member($discussion->groupid)) {
2749
            return null;
2750
        }
2751
    }
2752
 
2753
    // Make sure we're allowed to see it...
2754
    if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
2755
        return null;
2756
    }
2757
 
2758
    $urlbase = $CFG->wwwroot.'/pluginfile.php';
2759
    return new file_info_stored($browser, $context, $storedfile, $urlbase, $itemid, true, true, false, false);
2760
}
2761
 
2762
/**
2763
 * Serves the forum attachments. Implements needed access control ;-)
2764
 *
2765
 * @package  mod_forum
2766
 * @category files
2767
 * @param stdClass $course course object
2768
 * @param stdClass $cm course module object
2769
 * @param stdClass $context context object
2770
 * @param string $filearea file area
2771
 * @param array $args extra arguments
2772
 * @param bool $forcedownload whether or not force download
2773
 * @param array $options additional options affecting the file serving
2774
 * @return bool false if file not found, does not return if found - justsend the file
2775
 */
2776
function forum_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
2777
    global $CFG, $DB;
2778
 
2779
    if ($context->contextlevel != CONTEXT_MODULE) {
2780
        return false;
2781
    }
2782
 
2783
    require_course_login($course, true, $cm);
2784
 
2785
    $areas = forum_get_file_areas($course, $cm, $context);
2786
 
2787
    // filearea must contain a real area
2788
    if (!isset($areas[$filearea])) {
2789
        return false;
2790
    }
2791
 
2792
    $postid = (int)array_shift($args);
2793
 
2794
    if (!$post = $DB->get_record('forum_posts', array('id'=>$postid))) {
2795
        return false;
2796
    }
2797
 
2798
    if (!$discussion = $DB->get_record('forum_discussions', array('id'=>$post->discussion))) {
2799
        return false;
2800
    }
2801
 
2802
    if (!$forum = $DB->get_record('forum', array('id'=>$cm->instance))) {
2803
        return false;
2804
    }
2805
 
2806
    $fs = get_file_storage();
2807
    $relativepath = implode('/', $args);
2808
    $fullpath = "/$context->id/mod_forum/$filearea/$postid/$relativepath";
2809
    if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
2810
        return false;
2811
    }
2812
 
2813
    // Make sure groups allow this user to see this file
2814
    if ($discussion->groupid > 0) {
2815
        $groupmode = groups_get_activity_groupmode($cm, $course);
2816
        if ($groupmode == SEPARATEGROUPS) {
2817
            if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $context)) {
2818
                return false;
2819
            }
2820
        }
2821
    }
2822
 
2823
    // Make sure we're allowed to see it...
2824
    if (!forum_user_can_see_post($forum, $discussion, $post, NULL, $cm)) {
2825
        return false;
2826
    }
2827
 
2828
    // finally send the file
2829
    send_stored_file($file, 0, 0, true, $options); // download MUST be forced - security!
2830
}
2831
 
2832
/**
2833
 * If successful, this function returns the name of the file
2834
 *
2835
 * @global object
2836
 * @param object $post is a full post record, including course and forum
2837
 * @param object $forum
2838
 * @param object $cm
2839
 * @param mixed $mform
2840
 * @param string $unused
2841
 * @return bool
2842
 */
2843
function forum_add_attachment($post, $forum, $cm, $mform=null, $unused=null) {
2844
    global $DB;
2845
 
2846
    if (empty($mform)) {
2847
        return false;
2848
    }
2849
 
2850
    if (empty($post->attachments)) {
2851
        return true;   // Nothing to do
2852
    }
2853
 
2854
    $context = context_module::instance($cm->id);
2855
 
2856
    $info = file_get_draft_area_info($post->attachments);
2857
    $present = ($info['filecount']>0) ? '1' : '';
2858
    file_save_draft_area_files($post->attachments, $context->id, 'mod_forum', 'attachment', $post->id,
2859
            mod_forum_post_form::attachment_options($forum));
2860
 
2861
    $DB->set_field('forum_posts', 'attachment', $present, array('id'=>$post->id));
2862
 
2863
    return true;
2864
}
2865
 
2866
/**
2867
 * Add a new post in an existing discussion.
2868
 *
2869
 * @param   stdClass    $post       The post data
2870
 * @param   mixed       $mform      The submitted form
2871
 * @param   string      $unused
2872
 * @return int
2873
 */
2874
function forum_add_new_post($post, $mform, $unused = null) {
2875
    global $USER, $DB;
2876
 
2877
    $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion));
2878
    $forum      = $DB->get_record('forum', array('id' => $discussion->forum));
2879
    $cm         = get_coursemodule_from_instance('forum', $forum->id);
2880
    $context    = context_module::instance($cm->id);
2881
    $privatereplyto = 0;
2882
 
2883
    // Check whether private replies should be enabled for this post.
2884
    if ($post->parent) {
2885
        $parent = $DB->get_record('forum_posts', array('id' => $post->parent));
2886
 
2887
        if (!empty($parent->privatereplyto)) {
2888
            throw new \coding_exception('It should not be possible to reply to a private reply');
2889
        }
2890
 
2891
        if (!empty($post->isprivatereply) && forum_user_can_reply_privately($context, $parent)) {
2892
            $privatereplyto = $parent->userid;
2893
        }
2894
    }
2895
 
2896
    $post->created    = $post->modified = time();
2897
    $post->mailed     = FORUM_MAILED_PENDING;
2898
    $post->userid     = $USER->id;
2899
    $post->privatereplyto = $privatereplyto;
2900
    $post->attachment = "";
2901
    if (!isset($post->totalscore)) {
2902
        $post->totalscore = 0;
2903
    }
2904
    if (!isset($post->mailnow)) {
2905
        $post->mailnow    = 0;
2906
    }
2907
 
2908
    \mod_forum\local\entities\post::add_message_counts($post);
2909
    $post->id = $DB->insert_record("forum_posts", $post);
2910
    $post->message = file_save_draft_area_files($post->itemid, $context->id, 'mod_forum', 'post', $post->id,
2911
            mod_forum_post_form::editor_options($context, null), $post->message);
2912
    $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
2913
    forum_add_attachment($post, $forum, $cm, $mform);
2914
 
2915
    // Update discussion modified date
2916
    $DB->set_field("forum_discussions", "timemodified", $post->modified, array("id" => $post->discussion));
2917
    $DB->set_field("forum_discussions", "usermodified", $post->userid, array("id" => $post->discussion));
2918
 
2919
    if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
2920
        forum_tp_mark_post_read($post->userid, $post);
2921
    }
2922
 
2923
    if (isset($post->tags)) {
2924
        core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, $post->tags);
2925
    }
2926
 
2927
    // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
2928
    forum_trigger_content_uploaded_event($post, $cm, 'forum_add_new_post');
2929
 
2930
    return $post->id;
2931
}
2932
 
2933
/**
2934
 * Trigger post updated event.
2935
 *
2936
 * @param object $post forum post object
2937
 * @param object $discussion discussion object
2938
 * @param object $context forum context object
2939
 * @param object $forum forum object
2940
 * @since Moodle 3.8
2941
 * @return void
2942
 */
2943
function forum_trigger_post_updated_event($post, $discussion, $context, $forum) {
2944
    global $USER;
2945
 
2946
    $params = array(
2947
        'context' => $context,
2948
        'objectid' => $post->id,
2949
        'other' => array(
2950
            'discussionid' => $discussion->id,
2951
            'forumid' => $forum->id,
2952
            'forumtype' => $forum->type,
2953
        )
2954
    );
2955
 
2956
    if ($USER->id !== $post->userid) {
2957
        $params['relateduserid'] = $post->userid;
2958
    }
2959
 
2960
    $event = \mod_forum\event\post_updated::create($params);
2961
    $event->add_record_snapshot('forum_discussions', $discussion);
2962
    $event->trigger();
2963
}
2964
 
2965
/**
2966
 * Update a post.
2967
 *
2968
 * @param   stdClass    $newpost    The post to update
2969
 * @param   mixed       $mform      The submitted form
2970
 * @param   string      $unused
2971
 * @return  bool
2972
 */
2973
function forum_update_post($newpost, $mform, $unused = null) {
2974
    global $DB, $USER;
2975
 
2976
    $post       = $DB->get_record('forum_posts', array('id' => $newpost->id));
2977
    $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion));
2978
    $forum      = $DB->get_record('forum', array('id' => $discussion->forum));
2979
    $cm         = get_coursemodule_from_instance('forum', $forum->id);
2980
    $context    = context_module::instance($cm->id);
2981
 
2982
    // Allowed modifiable fields.
2983
    $modifiablefields = [
2984
        'subject',
2985
        'message',
2986
        'messageformat',
2987
        'messagetrust',
2988
        'timestart',
2989
        'timeend',
2990
        'pinned',
2991
        'attachments',
2992
    ];
2993
    foreach ($modifiablefields as $field) {
2994
        if (isset($newpost->{$field})) {
2995
            $post->{$field} = $newpost->{$field};
2996
        }
2997
    }
2998
    $post->modified = time();
2999
 
3000
    if (!$post->parent) {   // Post is a discussion starter - update discussion title and times too
3001
        $discussion->name      = $post->subject;
3002
        $discussion->timestart = $post->timestart;
3003
        $discussion->timeend   = $post->timeend;
3004
 
3005
        if (isset($post->pinned)) {
3006
            $discussion->pinned = $post->pinned;
3007
        }
3008
    }
3009
    $post->message = file_save_draft_area_files($newpost->itemid, $context->id, 'mod_forum', 'post', $post->id,
3010
            mod_forum_post_form::editor_options($context, $post->id), $post->message);
3011
    \mod_forum\local\entities\post::add_message_counts($post);
3012
    $DB->update_record('forum_posts', $post);
3013
    // Note: Discussion modified time/user are intentionally not updated, to enable them to track the latest new post.
3014
    $DB->update_record('forum_discussions', $discussion);
3015
 
3016
    forum_add_attachment($post, $forum, $cm, $mform);
3017
 
3018
    if ($forum->type == 'single' && $post->parent == '0') {
3019
        // Updating first post of single discussion type -> updating forum intro.
3020
        $forum->intro = $post->message;
3021
        $forum->timemodified = time();
3022
        $DB->update_record("forum", $forum);
3023
    }
3024
 
3025
    if (isset($newpost->tags)) {
3026
        core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, $context, $newpost->tags);
3027
    }
3028
 
3029
    if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
3030
        forum_tp_mark_post_read($USER->id, $post);
3031
    }
3032
 
3033
    // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
3034
    forum_trigger_content_uploaded_event($post, $cm, 'forum_update_post');
3035
 
3036
    return true;
3037
}
3038
 
3039
/**
3040
 * Given an object containing all the necessary data,
3041
 * create a new discussion and return the id
3042
 *
3043
 * @param object $post
3044
 * @param mixed $mform
3045
 * @param string $unused
3046
 * @param int $userid
3047
 * @return object
3048
 */
3049
function forum_add_discussion($discussion, $mform=null, $unused=null, $userid=null) {
3050
    global $USER, $CFG, $DB;
3051
 
3052
    $timenow = isset($discussion->timenow) ? $discussion->timenow : time();
3053
 
3054
    if (is_null($userid)) {
3055
        $userid = $USER->id;
3056
    }
3057
 
3058
    // The first post is stored as a real post, and linked
3059
    // to from the discuss entry.
3060
 
3061
    $forum = $DB->get_record('forum', array('id'=>$discussion->forum));
3062
    $cm    = get_coursemodule_from_instance('forum', $forum->id);
3063
 
3064
    $post = new stdClass();
3065
    $post->discussion    = 0;
3066
    $post->parent        = 0;
3067
    $post->privatereplyto = 0;
3068
    $post->userid        = $userid;
3069
    $post->created       = $timenow;
3070
    $post->modified      = $timenow;
3071
    $post->mailed        = FORUM_MAILED_PENDING;
3072
    $post->subject       = $discussion->name;
3073
    $post->message       = $discussion->message;
3074
    $post->messageformat = $discussion->messageformat;
3075
    $post->messagetrust  = $discussion->messagetrust;
3076
    $post->attachments   = isset($discussion->attachments) ? $discussion->attachments : null;
3077
    $post->forum         = $forum->id;     // speedup
3078
    $post->course        = $forum->course; // speedup
3079
    $post->mailnow       = $discussion->mailnow;
3080
 
3081
    \mod_forum\local\entities\post::add_message_counts($post);
3082
    $post->id = $DB->insert_record("forum_posts", $post);
3083
 
3084
    // TODO: Fix the calling code so that there always is a $cm when this function is called
3085
    if (!empty($cm->id) && !empty($discussion->itemid)) {   // In "single simple discussions" this may not exist yet
3086
        $context = context_module::instance($cm->id);
3087
        $text = file_save_draft_area_files($discussion->itemid, $context->id, 'mod_forum', 'post', $post->id,
3088
                mod_forum_post_form::editor_options($context, null), $post->message);
3089
        $DB->set_field('forum_posts', 'message', $text, array('id'=>$post->id));
3090
    }
3091
 
3092
    // Now do the main entry for the discussion, linking to this first post
3093
 
3094
    $discussion->firstpost    = $post->id;
3095
    $discussion->timemodified = $timenow;
3096
    $discussion->usermodified = $post->userid;
3097
    $discussion->userid       = $userid;
3098
    $discussion->assessed     = 0;
3099
 
3100
    $post->discussion = $DB->insert_record("forum_discussions", $discussion);
3101
 
3102
    // Finally, set the pointer on the post.
3103
    $DB->set_field("forum_posts", "discussion", $post->discussion, array("id"=>$post->id));
3104
 
3105
    if (!empty($cm->id)) {
3106
        forum_add_attachment($post, $forum, $cm, $mform, $unused);
3107
    }
3108
 
3109
    if (isset($discussion->tags)) {
3110
        $tags = is_array($discussion->tags) ? $discussion->tags : explode(',', $discussion->tags);
3111
 
3112
        core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id,
3113
            context_module::instance($cm->id), $tags);
3114
    }
3115
 
3116
    if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
3117
        forum_tp_mark_post_read($post->userid, $post);
3118
    }
3119
 
3120
    // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
3121
    if (!empty($cm->id)) {
3122
        forum_trigger_content_uploaded_event($post, $cm, 'forum_add_discussion');
3123
    }
3124
 
3125
    // Clear the discussion count cache just in case it's in the same request.
3126
    \cache_helper::purge_by_event('changesinforumdiscussions');
3127
 
3128
    return $post->discussion;
3129
}
3130
 
3131
 
3132
/**
3133
 * Deletes a discussion and handles all associated cleanup.
3134
 *
3135
 * @global object
3136
 * @param object $discussion Discussion to delete
3137
 * @param bool $fulldelete True when deleting entire forum
3138
 * @param object $course Course
3139
 * @param object $cm Course-module
3140
 * @param object $forum Forum
3141
 * @return bool
3142
 */
3143
function forum_delete_discussion($discussion, $fulldelete, $course, $cm, $forum) {
3144
    global $DB, $CFG;
3145
    require_once($CFG->libdir.'/completionlib.php');
3146
 
3147
    $result = true;
3148
 
3149
    if ($posts = $DB->get_records("forum_posts", array("discussion" => $discussion->id))) {
3150
        foreach ($posts as $post) {
3151
            $post->course = $discussion->course;
3152
            $post->forum  = $discussion->forum;
3153
            if (!forum_delete_post($post, 'ignore', $course, $cm, $forum, $fulldelete)) {
3154
                $result = false;
3155
            }
3156
        }
3157
    }
3158
 
3159
    forum_tp_delete_read_records(-1, -1, $discussion->id);
3160
 
3161
    // Discussion subscriptions must be removed before discussions because of key constraints.
3162
    $DB->delete_records('forum_discussion_subs', array('discussion' => $discussion->id));
3163
    if (!$DB->delete_records("forum_discussions", array("id" => $discussion->id))) {
3164
        $result = false;
3165
    }
3166
 
3167
    // Update completion state if we are tracking completion based on number of posts
3168
    // But don't bother when deleting whole thing
3169
    if (!$fulldelete) {
3170
        $completion = new completion_info($course);
3171
        if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC &&
3172
           ($forum->completiondiscussions || $forum->completionreplies || $forum->completionposts)) {
3173
            $completion->update_state($cm, COMPLETION_INCOMPLETE, $discussion->userid);
3174
        }
3175
    }
3176
 
3177
    $params = array(
3178
        'objectid' => $discussion->id,
3179
        'context' => context_module::instance($cm->id),
3180
        'other' => array(
3181
            'forumid' => $forum->id,
3182
        )
3183
    );
3184
    $event = \mod_forum\event\discussion_deleted::create($params);
3185
    $event->add_record_snapshot('forum_discussions', $discussion);
3186
    $event->trigger();
3187
 
3188
    // Clear the discussion count cache just in case it's in the same request.
3189
    \cache_helper::purge_by_event('changesinforumdiscussions');
3190
 
3191
    return $result;
3192
}
3193
 
3194
 
3195
/**
3196
 * Deletes a single forum post.
3197
 *
3198
 * @global object
3199
 * @param object $post Forum post object
3200
 * @param mixed $children Whether to delete children. If false, returns false
3201
 *   if there are any children (without deleting the post). If true,
3202
 *   recursively deletes all children. If set to special value 'ignore', deletes
3203
 *   post regardless of children (this is for use only when deleting all posts
3204
 *   in a disussion).
3205
 * @param object $course Course
3206
 * @param object $cm Course-module
3207
 * @param object $forum Forum
3208
 * @param bool $skipcompletion True to skip updating completion state if it
3209
 *   would otherwise be updated, i.e. when deleting entire forum anyway.
3210
 * @return bool
3211
 */
3212
function forum_delete_post($post, $children, $course, $cm, $forum, $skipcompletion=false) {
3213
    global $DB, $CFG, $USER;
3214
    require_once($CFG->libdir.'/completionlib.php');
3215
 
3216
    $context = context_module::instance($cm->id);
3217
 
3218
    if ($children !== 'ignore' && ($childposts = $DB->get_records('forum_posts', array('parent'=>$post->id)))) {
3219
       if ($children) {
3220
           foreach ($childposts as $childpost) {
3221
               forum_delete_post($childpost, true, $course, $cm, $forum, $skipcompletion);
3222
           }
3223
       } else {
3224
           return false;
3225
       }
3226
    }
3227
 
3228
    // Delete ratings.
3229
    require_once($CFG->dirroot.'/rating/lib.php');
3230
    $delopt = new stdClass;
3231
    $delopt->contextid = $context->id;
3232
    $delopt->component = 'mod_forum';
3233
    $delopt->ratingarea = 'post';
3234
    $delopt->itemid = $post->id;
3235
    $rm = new rating_manager();
3236
    $rm->delete_ratings($delopt);
3237
 
3238
    // Delete attachments.
3239
    $fs = get_file_storage();
3240
    $fs->delete_area_files($context->id, 'mod_forum', 'attachment', $post->id);
3241
    $fs->delete_area_files($context->id, 'mod_forum', 'post', $post->id);
3242
 
3243
    // Delete cached RSS feeds.
3244
    if (!empty($CFG->enablerssfeeds)) {
3245
        require_once($CFG->dirroot.'/mod/forum/rsslib.php');
3246
        forum_rss_delete_file($forum);
3247
    }
3248
 
3249
    if ($DB->delete_records("forum_posts", array("id" => $post->id))) {
3250
 
3251
        forum_tp_delete_read_records(-1, $post->id);
3252
 
3253
    // Just in case we are deleting the last post
3254
        forum_discussion_update_last_post($post->discussion);
3255
 
3256
        // Update completion state if we are tracking completion based on number of posts
3257
        // But don't bother when deleting whole thing
3258
 
3259
        if (!$skipcompletion) {
3260
            $completion = new completion_info($course);
3261
            if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC &&
3262
               ($forum->completiondiscussions || $forum->completionreplies || $forum->completionposts)) {
3263
                $completion->update_state($cm, COMPLETION_INCOMPLETE, $post->userid);
3264
            }
3265
        }
3266
 
3267
        $params = array(
3268
            'context' => $context,
3269
            'objectid' => $post->id,
3270
            'other' => array(
3271
                'discussionid' => $post->discussion,
3272
                'forumid' => $forum->id,
3273
                'forumtype' => $forum->type,
3274
            )
3275
        );
3276
        $post->deleted = 1;
3277
        if ($post->userid !== $USER->id) {
3278
            $params['relateduserid'] = $post->userid;
3279
        }
3280
        $event = \mod_forum\event\post_deleted::create($params);
3281
        $event->add_record_snapshot('forum_posts', $post);
3282
        $event->trigger();
3283
 
3284
        return true;
3285
    }
3286
    return false;
3287
}
3288
 
3289
/**
3290
 * Sends post content to plagiarism plugin
3291
 * @param object $post Forum post object
3292
 * @param object $cm Course-module
3293
 * @param string $name
3294
 * @return bool
3295
*/
3296
function forum_trigger_content_uploaded_event($post, $cm, $name) {
3297
    $context = context_module::instance($cm->id);
3298
    $fs = get_file_storage();
3299
    $files = $fs->get_area_files($context->id, 'mod_forum', 'attachment', $post->id, "timemodified", false);
3300
    $params = array(
3301
        'context' => $context,
3302
        'objectid' => $post->id,
3303
        'other' => array(
3304
            'content' => $post->message,
3305
            'pathnamehashes' => array_keys($files),
3306
            'discussionid' => $post->discussion,
3307
            'triggeredfrom' => $name,
3308
        )
3309
    );
3310
    $event = \mod_forum\event\assessable_uploaded::create($params);
3311
    $event->trigger();
3312
    return true;
3313
}
3314
 
3315
/**
3316
 * Given a new post, subscribes or unsubscribes as appropriate.
3317
 * Returns some text which describes what happened.
3318
 *
3319
 * @param object $fromform The submitted form
3320
 * @param stdClass $forum The forum record
3321
 * @param stdClass $discussion The forum discussion record
3322
 * @return string
3323
 */
3324
function forum_post_subscription($fromform, $forum, $discussion) {
3325
    global $USER;
3326
 
3327
    if (\mod_forum\subscriptions::is_forcesubscribed($forum)) {
3328
        return "";
3329
    } else if (\mod_forum\subscriptions::subscription_disabled($forum)) {
3330
        $subscribed = \mod_forum\subscriptions::is_subscribed($USER->id, $forum);
3331
        if ($subscribed && !has_capability('moodle/course:manageactivities', context_course::instance($forum->course), $USER->id)) {
3332
            // This user should not be subscribed to the forum.
3333
            \mod_forum\subscriptions::unsubscribe_user($USER->id, $forum);
3334
        }
3335
        return "";
3336
    }
3337
 
3338
    $info = new stdClass();
3339
    $info->name  = fullname($USER);
3340
    $info->discussion = format_string($discussion->name);
3341
    $info->forum = format_string($forum->name);
3342
 
3343
    if (isset($fromform->discussionsubscribe) && $fromform->discussionsubscribe) {
3344
        if ($result = \mod_forum\subscriptions::subscribe_user_to_discussion($USER->id, $discussion)) {
3345
            return html_writer::tag('p', get_string('discussionnowsubscribed', 'forum', $info));
3346
        }
3347
    } else {
3348
        if ($result = \mod_forum\subscriptions::unsubscribe_user_from_discussion($USER->id, $discussion)) {
3349
            return html_writer::tag('p', get_string('discussionnownotsubscribed', 'forum', $info));
3350
        }
3351
    }
3352
 
3353
    return '';
3354
}
3355
 
3356
/**
3357
 * Generate and return the subscribe or unsubscribe link for a forum.
3358
 *
3359
 * @param object $forum the forum. Fields used are $forum->id and $forum->forcesubscribe.
3360
 * @param object $context the context object for this forum.
3361
 * @param array $messages text used for the link in its various states
3362
 *      (subscribed, unsubscribed, forcesubscribed or cantsubscribe).
3363
 *      Any strings not passed in are taken from the $defaultmessages array
3364
 *      at the top of the function.
3365
 * @param bool $cantaccessagroup
3366
 * @param bool $unused1
3367
 * @param bool $backtoindex
3368
 * @param array $unused2
3369
 * @return string
3370
 */
3371
function forum_get_subscribe_link($forum, $context, $messages = array(), $cantaccessagroup = false, $unused1 = true,
3372
    $backtoindex = false, $unused2 = null) {
3373
    global $CFG, $USER, $PAGE, $OUTPUT;
3374
    $defaultmessages = array(
3375
        'subscribed' => get_string('unsubscribe', 'forum'),
3376
        'unsubscribed' => get_string('subscribe', 'forum'),
3377
        'cantaccessgroup' => get_string('no'),
3378
        'forcesubscribed' => get_string('everyoneissubscribed', 'forum'),
3379
        'cantsubscribe' => get_string('disallowsubscribe','forum')
3380
    );
3381
    $messages = $messages + $defaultmessages;
3382
 
3383
    if (\mod_forum\subscriptions::is_forcesubscribed($forum)) {
3384
        return $messages['forcesubscribed'];
3385
    } else if (\mod_forum\subscriptions::subscription_disabled($forum) &&
3386
            !has_capability('mod/forum:managesubscriptions', $context)) {
3387
        return $messages['cantsubscribe'];
3388
    } else if ($cantaccessagroup) {
3389
        return $messages['cantaccessgroup'];
3390
    } else {
3391
        if (!is_enrolled($context, $USER, '', true)) {
3392
            return '';
3393
        }
3394
 
3395
        $subscribed = \mod_forum\subscriptions::is_subscribed($USER->id, $forum);
3396
        if ($subscribed) {
3397
            $linktext = $messages['subscribed'];
3398
            $linktitle = get_string('subscribestop', 'forum');
3399
        } else {
3400
            $linktext = $messages['unsubscribed'];
3401
            $linktitle = get_string('subscribestart', 'forum');
3402
        }
3403
 
3404
        $options = array();
3405
        if ($backtoindex) {
3406
            $backtoindexlink = '&amp;backtoindex=1';
3407
            $options['backtoindex'] = 1;
3408
        } else {
3409
            $backtoindexlink = '';
3410
        }
3411
 
3412
        $options['id'] = $forum->id;
3413
        $options['sesskey'] = sesskey();
3414
        $url = new moodle_url('/mod/forum/subscribe.php', $options);
3415
        return $OUTPUT->single_button($url, $linktext, 'get', array('title' => $linktitle));
3416
    }
3417
}
3418
 
3419
/**
3420
 * Returns true if user created new discussion already.
3421
 *
3422
 * @param int $forumid  The forum to check for postings
3423
 * @param int $userid   The user to check for postings
3424
 * @param int $groupid  The group to restrict the check to
3425
 * @return bool
3426
 */
3427
function forum_user_has_posted_discussion($forumid, $userid, $groupid = null) {
3428
    global $CFG, $DB;
3429
 
3430
    $sql = "SELECT 'x'
3431
              FROM {forum_discussions} d, {forum_posts} p
3432
             WHERE d.forum = ? AND p.discussion = d.id AND p.parent = 0 AND p.userid = ?";
3433
 
3434
    $params = [$forumid, $userid];
3435
 
3436
    if ($groupid) {
3437
        $sql .= " AND d.groupid = ?";
3438
        $params[] = $groupid;
3439
    }
3440
 
3441
    return $DB->record_exists_sql($sql, $params);
3442
}
3443
 
3444
/**
3445
 * @global object
3446
 * @global object
3447
 * @param int $forumid
3448
 * @param int $userid
3449
 * @return array
3450
 */
3451
function forum_discussions_user_has_posted_in($forumid, $userid) {
3452
    global $CFG, $DB;
3453
 
3454
    $haspostedsql = "SELECT d.id AS id,
3455
                            d.*
3456
                       FROM {forum_posts} p,
3457
                            {forum_discussions} d
3458
                      WHERE p.discussion = d.id
3459
                        AND d.forum = ?
3460
                        AND p.userid = ?";
3461
 
3462
    return $DB->get_records_sql($haspostedsql, array($forumid, $userid));
3463
}
3464
 
3465
/**
3466
 * @global object
3467
 * @global object
3468
 * @param int $forumid
3469
 * @param int $did
3470
 * @param int $userid
3471
 * @return bool
3472
 */
3473
function forum_user_has_posted($forumid, $did, $userid) {
3474
    global $DB;
3475
 
3476
    if (empty($did)) {
3477
        // posted in any forum discussion?
3478
        $sql = "SELECT 'x'
3479
                  FROM {forum_posts} p
3480
                  JOIN {forum_discussions} d ON d.id = p.discussion
3481
                 WHERE p.userid = :userid AND d.forum = :forumid";
3482
        return $DB->record_exists_sql($sql, array('forumid'=>$forumid,'userid'=>$userid));
3483
    } else {
3484
        return $DB->record_exists('forum_posts', array('discussion'=>$did,'userid'=>$userid));
3485
    }
3486
}
3487
 
3488
/**
3489
 * Returns true if user posted with mailnow in given discussion
3490
 * @param int $did Discussion id
3491
 * @param int $userid User id
3492
 * @return bool
3493
 */
3494
function forum_get_user_posted_mailnow(int $did, int $userid): bool {
3495
    global $DB;
3496
 
3497
    $postmailnow = $DB->get_field('forum_posts', 'MAX(mailnow)', ['userid' => $userid, 'discussion' => $did]);
3498
    return !empty($postmailnow);
3499
}
3500
 
3501
/**
3502
 * Returns creation time of the first user's post in given discussion
3503
 * @global object $DB
3504
 * @param int $did Discussion id
3505
 * @param int $userid User id
3506
 * @return int|bool post creation time stamp or return false
3507
 */
3508
function forum_get_user_posted_time($did, $userid) {
3509
    global $DB;
3510
 
3511
    $posttime = $DB->get_field('forum_posts', 'MIN(created)', array('userid'=>$userid, 'discussion'=>$did));
3512
    if (empty($posttime)) {
3513
        return false;
3514
    }
3515
    return $posttime;
3516
}
3517
 
3518
/**
3519
 * @global object
3520
 * @param object $forum
3521
 * @param object $currentgroup
3522
 * @param int $unused
3523
 * @param object $cm
3524
 * @param object $context
3525
 * @return bool
3526
 */
3527
function forum_user_can_post_discussion($forum, $currentgroup=null, $unused=-1, $cm=NULL, $context=NULL) {
3528
// $forum is an object
3529
    global $USER;
3530
 
3531
    // shortcut - guest and not-logged-in users can not post
3532
    if (isguestuser() or !isloggedin()) {
3533
        return false;
3534
    }
3535
 
3536
    if (!$cm) {
3537
        debugging('missing cm', DEBUG_DEVELOPER);
3538
        if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
3539
            throw new \moodle_exception('invalidcoursemodule');
3540
        }
3541
    }
3542
 
3543
    if (!$context) {
3544
        $context = context_module::instance($cm->id);
3545
    }
3546
 
3547
    if (forum_is_cutoff_date_reached($forum)) {
3548
        if (!has_capability('mod/forum:canoverridecutoff', $context)) {
3549
            return false;
3550
        }
3551
    }
3552
 
3553
    if ($currentgroup === null) {
3554
        $currentgroup = groups_get_activity_group($cm);
3555
    }
3556
 
3557
    $groupmode = groups_get_activity_groupmode($cm);
3558
 
3559
    if ($forum->type == 'news') {
3560
        $capname = 'mod/forum:addnews';
3561
    } else if ($forum->type == 'qanda') {
3562
        $capname = 'mod/forum:addquestion';
3563
    } else {
3564
        $capname = 'mod/forum:startdiscussion';
3565
    }
3566
 
3567
    if (!has_capability($capname, $context)) {
3568
        return false;
3569
    }
3570
 
3571
    if ($forum->type == 'single') {
3572
        return false;
3573
    }
3574
 
3575
    if ($forum->type == 'eachuser') {
3576
        if (forum_user_has_posted_discussion($forum->id, $USER->id, $currentgroup)) {
3577
            return false;
3578
        }
3579
    }
3580
 
3581
    if (!$groupmode or has_capability('moodle/site:accessallgroups', $context)) {
3582
        return true;
3583
    }
3584
 
3585
    if ($currentgroup) {
3586
        return groups_is_member($currentgroup);
3587
    } else {
3588
        // no group membership and no accessallgroups means no new discussions
3589
        // reverted to 1.7 behaviour in 1.9+,  buggy in 1.8.0-1.9.0
3590
        return false;
3591
    }
3592
}
3593
 
3594
/**
3595
 * This function checks whether the user can reply to posts in a forum
3596
 * discussion. Use forum_user_can_post_discussion() to check whether the user
3597
 * can start discussions.
3598
 *
3599
 * @global object
3600
 * @global object
3601
 * @uses DEBUG_DEVELOPER
3602
 * @uses CONTEXT_MODULE
3603
 * @uses VISIBLEGROUPS
3604
 * @param object $forum forum object
3605
 * @param object $discussion
3606
 * @param object $user
3607
 * @param object $cm
3608
 * @param object $course
3609
 * @param object $context
3610
 * @return bool
3611
 */
3612
function forum_user_can_post($forum, $discussion, $user=NULL, $cm=NULL, $course=NULL, $context=NULL) {
3613
    global $USER, $DB;
3614
    if (empty($user)) {
3615
        $user = $USER;
3616
    }
3617
 
3618
    // shortcut - guest and not-logged-in users can not post
3619
    if (isguestuser($user) or empty($user->id)) {
3620
        return false;
3621
    }
3622
 
3623
    if (!isset($discussion->groupid)) {
3624
        debugging('incorrect discussion parameter', DEBUG_DEVELOPER);
3625
        return false;
3626
    }
3627
 
3628
    if (!$cm) {
3629
        debugging('missing cm', DEBUG_DEVELOPER);
3630
        if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
3631
            throw new \moodle_exception('invalidcoursemodule');
3632
        }
3633
    }
3634
 
3635
    if (!$course) {
3636
        debugging('missing course', DEBUG_DEVELOPER);
3637
        if (!$course = $DB->get_record('course', array('id' => $forum->course))) {
3638
            throw new \moodle_exception('invalidcourseid');
3639
        }
3640
    }
3641
 
3642
    if (!$context) {
3643
        $context = context_module::instance($cm->id);
3644
    }
3645
 
3646
    if (forum_is_cutoff_date_reached($forum)) {
3647
        if (!has_capability('mod/forum:canoverridecutoff', $context)) {
3648
            return false;
3649
        }
3650
    }
3651
 
3652
    // Check whether the discussion is locked.
3653
    if (forum_discussion_is_locked($forum, $discussion)) {
3654
        if (!has_capability('mod/forum:canoverridediscussionlock', $context)) {
3655
            return false;
3656
        }
3657
    }
3658
 
3659
    // normal users with temporary guest access can not post, suspended users can not post either
3660
    if (!is_viewing($context, $user->id) and !is_enrolled($context, $user->id, '', true)) {
3661
        return false;
3662
    }
3663
 
3664
    if ($forum->type == 'news') {
3665
        $capname = 'mod/forum:replynews';
3666
    } else {
3667
        $capname = 'mod/forum:replypost';
3668
    }
3669
 
3670
    if (!has_capability($capname, $context, $user->id)) {
3671
        return false;
3672
    }
3673
 
3674
    if (!$groupmode = groups_get_activity_groupmode($cm, $course)) {
3675
        return true;
3676
    }
3677
 
3678
    if (has_capability('moodle/site:accessallgroups', $context)) {
3679
        return true;
3680
    }
3681
 
3682
    if ($groupmode == VISIBLEGROUPS) {
3683
        if ($discussion->groupid == -1) {
3684
            // allow students to reply to all participants discussions - this was not possible in Moodle <1.8
3685
            return true;
3686
        }
3687
        return groups_is_member($discussion->groupid);
3688
 
3689
    } else {
3690
        //separate groups
3691
        if ($discussion->groupid == -1) {
3692
            return false;
3693
        }
3694
        return groups_is_member($discussion->groupid);
3695
    }
3696
}
3697
 
3698
/**
3699
* Check to ensure a user can view a timed discussion.
3700
*
3701
* @param object $discussion
3702
* @param object $user
3703
* @param object $context
3704
* @return boolean returns true if they can view post, false otherwise
3705
*/
3706
function forum_user_can_see_timed_discussion($discussion, $user, $context) {
3707
    global $CFG;
3708
 
3709
    // Check that the user can view a discussion that is normally hidden due to access times.
3710
    if (!empty($CFG->forum_enabletimedposts)) {
3711
        $time = time();
3712
        if (($discussion->timestart != 0 && $discussion->timestart > $time)
3713
            || ($discussion->timeend != 0 && $discussion->timeend < $time)) {
3714
            if (!has_capability('mod/forum:viewhiddentimedposts', $context, $user->id)) {
3715
                return false;
3716
            }
3717
        }
3718
    }
3719
 
3720
    return true;
3721
}
3722
 
3723
/**
3724
* Check to ensure a user can view a group discussion.
3725
*
3726
* @param object $discussion
3727
* @param object $cm
3728
* @param object $context
3729
* @return boolean returns true if they can view post, false otherwise
3730
*/
3731
function forum_user_can_see_group_discussion($discussion, $cm, $context) {
3732
 
3733
    // If it's a grouped discussion, make sure the user is a member.
3734
    if ($discussion->groupid > 0) {
3735
        $groupmode = groups_get_activity_groupmode($cm);
3736
        if ($groupmode == SEPARATEGROUPS) {
3737
            return groups_is_member($discussion->groupid) || has_capability('moodle/site:accessallgroups', $context);
3738
        }
3739
    }
3740
 
3741
    return true;
3742
}
3743
 
3744
/**
3745
 * @global object
3746
 * @global object
3747
 * @uses DEBUG_DEVELOPER
3748
 * @param object $forum
3749
 * @param object $discussion
3750
 * @param object $context
3751
 * @param object $user
3752
 * @return bool
3753
 */
3754
function forum_user_can_see_discussion($forum, $discussion, $context, $user=NULL) {
3755
    global $USER, $DB;
3756
 
3757
    if (empty($user) || empty($user->id)) {
3758
        $user = $USER;
3759
    }
3760
 
3761
    // retrieve objects (yuk)
3762
    if (is_numeric($forum)) {
3763
        debugging('missing full forum', DEBUG_DEVELOPER);
3764
        if (!$forum = $DB->get_record('forum',array('id'=>$forum))) {
3765
            return false;
3766
        }
3767
    }
3768
    if (is_numeric($discussion)) {
3769
        debugging('missing full discussion', DEBUG_DEVELOPER);
3770
        if (!$discussion = $DB->get_record('forum_discussions',array('id'=>$discussion))) {
3771
            return false;
3772
        }
3773
    }
3774
    if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
3775
        throw new \moodle_exception('invalidcoursemodule');
3776
    }
3777
 
3778
    if (!has_capability('mod/forum:viewdiscussion', $context)) {
3779
        return false;
3780
    }
3781
 
3782
    if (!forum_user_can_see_timed_discussion($discussion, $user, $context)) {
3783
        return false;
3784
    }
3785
 
3786
    if (!forum_user_can_see_group_discussion($discussion, $cm, $context)) {
3787
        return false;
3788
    }
3789
 
3790
    return true;
3791
}
3792
 
3793
/**
3794
 * Check whether a user can see the specified post.
3795
 *
3796
 * @param   \stdClass $forum The forum to chcek
3797
 * @param   \stdClass $discussion The discussion the post is in
3798
 * @param   \stdClass $post The post in question
3799
 * @param   \stdClass $user The user to test - if not specified, the current user is checked.
3800
 * @param   \stdClass $cm The Course Module that the forum is in (required).
3801
 * @param   bool      $checkdeleted Whether to check the deleted flag on the post.
3802
 * @return  bool
3803
 */
3804
function forum_user_can_see_post($forum, $discussion, $post, $user = null, $cm = null, $checkdeleted = true) {
3805
    global $CFG, $USER, $DB;
3806
 
3807
    // retrieve objects (yuk)
3808
    if (is_numeric($forum)) {
3809
        debugging('missing full forum', DEBUG_DEVELOPER);
3810
        if (!$forum = $DB->get_record('forum',array('id'=>$forum))) {
3811
            return false;
3812
        }
3813
    }
3814
 
3815
    if (is_numeric($discussion)) {
3816
        debugging('missing full discussion', DEBUG_DEVELOPER);
3817
        if (!$discussion = $DB->get_record('forum_discussions',array('id'=>$discussion))) {
3818
            return false;
3819
        }
3820
    }
3821
    if (is_numeric($post)) {
3822
        debugging('missing full post', DEBUG_DEVELOPER);
3823
        if (!$post = $DB->get_record('forum_posts',array('id'=>$post))) {
3824
            return false;
3825
        }
3826
    }
3827
 
3828
    if (!isset($post->id) && isset($post->parent)) {
3829
        $post->id = $post->parent;
3830
    }
3831
 
3832
    if ($checkdeleted && !empty($post->deleted)) {
3833
        return false;
3834
    }
3835
 
3836
    if (!$cm) {
3837
        debugging('missing cm', DEBUG_DEVELOPER);
3838
        if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
3839
            throw new \moodle_exception('invalidcoursemodule');
3840
        }
3841
    }
3842
 
3843
    // Context used throughout function.
3844
    $modcontext = context_module::instance($cm->id);
3845
 
3846
    if (empty($user) || empty($user->id)) {
3847
        $user = $USER;
3848
    }
3849
 
3850
    $canviewdiscussion = (isset($cm->cache) && !empty($cm->cache->caps['mod/forum:viewdiscussion']))
3851
        || has_capability('mod/forum:viewdiscussion', $modcontext, $user->id);
3852
    if (!$canviewdiscussion && !has_all_capabilities(array('moodle/user:viewdetails', 'moodle/user:readuserposts'), context_user::instance($post->userid))) {
3853
        return false;
3854
    }
3855
 
3856
    if (!forum_post_is_visible_privately($post, $cm)) {
3857
        return false;
3858
    }
3859
 
3860
    if (isset($cm->uservisible)) {
3861
        if (!$cm->uservisible) {
3862
            return false;
3863
        }
3864
    } else {
3865
        if (!\core_availability\info_module::is_user_visible($cm, $user->id, false)) {
3866
            return false;
3867
        }
3868
    }
3869
 
3870
    if (!forum_user_can_see_timed_discussion($discussion, $user, $modcontext)) {
3871
        return false;
3872
    }
3873
 
3874
    if (!forum_user_can_see_group_discussion($discussion, $cm, $modcontext)) {
3875
        return false;
3876
    }
3877
 
3878
    if ($forum->type == 'qanda') {
3879
        if (has_capability('mod/forum:viewqandawithoutposting', $modcontext, $user->id) || $post->userid == $user->id
3880
                || (isset($discussion->firstpost) && $discussion->firstpost == $post->id)) {
3881
            return true;
3882
        }
3883
        $firstpost = forum_get_firstpost_from_discussion($discussion->id);
3884
        if ($firstpost->userid == $user->id) {
3885
            return true;
3886
        }
3887
        $userpostmailnow = forum_get_user_posted_mailnow($discussion->id, $user->id);
3888
        if ($userpostmailnow) {
3889
            return true;
3890
        }
3891
        $userfirstpost = forum_get_user_posted_time($discussion->id, $user->id);
3892
        return (($userfirstpost !== false && (time() - $userfirstpost >= $CFG->maxeditingtime)));
3893
    }
3894
    return true;
3895
}
3896
 
3897
/**
3898
 * Returns all forum posts since a given time in specified forum.
3899
 *
3900
 * @todo Document this functions args
3901
 * @global object
3902
 * @global object
3903
 * @global object
3904
 * @global object
3905
 */
3906
function forum_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid=0, $groupid=0)  {
3907
    global $CFG, $COURSE, $USER, $DB;
3908
 
3909
    if ($COURSE->id == $courseid) {
3910
        $course = $COURSE;
3911
    } else {
3912
        $course = $DB->get_record('course', array('id' => $courseid));
3913
    }
3914
 
3915
    $modinfo = get_fast_modinfo($course);
3916
 
3917
    $cm = $modinfo->cms[$cmid];
3918
    $params = array($timestart, $cm->instance);
3919
 
3920
    if ($userid) {
3921
        $userselect = "AND u.id = ?";
3922
        $params[] = $userid;
3923
    } else {
3924
        $userselect = "";
3925
    }
3926
 
3927
    if ($groupid) {
3928
        $groupselect = "AND d.groupid = ?";
3929
        $params[] = $groupid;
3930
    } else {
3931
        $groupselect = "";
3932
    }
3933
 
3934
    $userfieldsapi = \core_user\fields::for_name();
3935
    $allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
3936
    if (!$posts = $DB->get_records_sql("SELECT p.*, f.type AS forumtype, d.forum, d.groupid,
3937
                                              d.timestart, d.timeend, d.userid AS duserid,
3938
                                              $allnames, u.email, u.picture, u.imagealt, u.email
3939
                                         FROM {forum_posts} p
3940
                                              JOIN {forum_discussions} d ON d.id = p.discussion
3941
                                              JOIN {forum} f             ON f.id = d.forum
3942
                                              JOIN {user} u              ON u.id = p.userid
3943
                                        WHERE p.created > ? AND f.id = ?
3944
                                              $userselect $groupselect
3945
                                     ORDER BY p.id ASC", $params)) { // order by initial posting date
3946
         return;
3947
    }
3948
 
3949
    $groupmode       = groups_get_activity_groupmode($cm, $course);
3950
    $cm_context      = context_module::instance($cm->id);
3951
    $viewhiddentimed = has_capability('mod/forum:viewhiddentimedposts', $cm_context);
3952
    $accessallgroups = has_capability('moodle/site:accessallgroups', $cm_context);
3953
 
3954
    $printposts = array();
3955
    foreach ($posts as $post) {
3956
 
3957
        if (!empty($CFG->forum_enabletimedposts) and $USER->id != $post->duserid
3958
          and (($post->timestart > 0 and $post->timestart > time()) or ($post->timeend > 0 and $post->timeend < time()))) {
3959
            if (!$viewhiddentimed) {
3960
                continue;
3961
            }
3962
        }
3963
 
3964
        if ($groupmode) {
3965
            if ($post->groupid == -1 or $groupmode == VISIBLEGROUPS or $accessallgroups) {
3966
                // oki (Open discussions have groupid -1)
3967
            } else {
3968
                // separate mode
3969
                if (isguestuser()) {
3970
                    // shortcut
3971
                    continue;
3972
                }
3973
 
3974
                if (!in_array($post->groupid, $modinfo->get_groups($cm->groupingid))) {
3975
                    continue;
3976
                }
3977
            }
3978
        }
3979
 
3980
        $printposts[] = $post;
3981
    }
3982
 
3983
    if (!$printposts) {
3984
        return;
3985
    }
3986
 
3987
    $aname = format_string($cm->name,true);
3988
 
3989
    foreach ($printposts as $post) {
3990
        $tmpactivity = new stdClass();
3991
 
3992
        $tmpactivity->type         = 'forum';
3993
        $tmpactivity->cmid         = $cm->id;
3994
        $tmpactivity->name         = $aname;
3995
        $tmpactivity->sectionnum   = $cm->sectionnum;
3996
        $tmpactivity->timestamp    = $post->modified;
3997
 
3998
        $tmpactivity->content = new stdClass();
3999
        $tmpactivity->content->id         = $post->id;
4000
        $tmpactivity->content->discussion = $post->discussion;
4001
        $tmpactivity->content->subject    = format_string($post->subject);
4002
        $tmpactivity->content->parent     = $post->parent;
4003
        $tmpactivity->content->forumtype  = $post->forumtype;
4004
 
4005
        $tmpactivity->user = new stdClass();
4006
        $additionalfields = array('id' => 'userid', 'picture', 'imagealt', 'email');
4007
        $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
4008
        $tmpactivity->user = username_load_fields_from_object($tmpactivity->user, $post, null, $additionalfields);
4009
        $tmpactivity->user->id = $post->userid;
4010
 
4011
        $activities[$index++] = $tmpactivity;
4012
    }
4013
 
4014
    return;
4015
}
4016
 
4017
/**
4018
 * Outputs the forum post indicated by $activity.
4019
 *
4020
 * @param object $activity      the activity object the forum resides in
4021
 * @param int    $courseid      the id of the course the forum resides in
4022
 * @param bool   $detail        not used, but required for compatibilty with other modules
4023
 * @param int    $modnames      not used, but required for compatibilty with other modules
4024
 * @param bool   $viewfullnames not used, but required for compatibilty with other modules
4025
 */
4026
function forum_print_recent_mod_activity($activity, $courseid, $detail, $modnames, $viewfullnames) {
4027
    global $OUTPUT;
4028
 
4029
    $content = $activity->content;
4030
    if ($content->parent) {
4031
        $class = 'reply';
4032
    } else {
4033
        $class = 'discussion';
4034
    }
4035
 
4036
    $tableoptions = [
4037
        'border' => '0',
4038
        'cellpadding' => '3',
4039
        'cellspacing' => '0',
4040
        'class' => 'forum-recent'
4041
    ];
4042
    $output = html_writer::start_tag('table', $tableoptions);
4043
    $output .= html_writer::start_tag('tr');
4044
 
4045
    $post = (object) ['parent' => $content->parent];
4046
    $forum = (object) ['type' => $content->forumtype];
4047
    $authorhidden = forum_is_author_hidden($post, $forum);
4048
 
4049
    // Show user picture if author should not be hidden.
4050
    if (!$authorhidden) {
4051
        $pictureoptions = [
4052
            'courseid' => $courseid,
4053
            'link' => $authorhidden,
4054
            'alttext' => $authorhidden,
4055
        ];
4056
        $picture = $OUTPUT->user_picture($activity->user, $pictureoptions);
4057
        $output .= html_writer::tag('td', $picture, ['class' => 'userpicture', 'valign' => 'top']);
4058
    }
4059
 
4060
    // Discussion title and author.
4061
    $output .= html_writer::start_tag('td', ['class' => $class]);
4062
    if ($content->parent) {
4063
        $class = 'title';
4064
    } else {
4065
        // Bold the title of new discussions so they stand out.
4066
        $class = 'title bold';
4067
    }
4068
 
4069
    $output .= html_writer::start_div($class);
4070
    if ($detail) {
4071
        $aname = s($activity->name);
4072
        $output .= $OUTPUT->image_icon('monologo', $aname, $activity->type);
4073
    }
4074
    $discussionurl = new moodle_url('/mod/forum/discuss.php', ['d' => $content->discussion]);
4075
    $discussionurl->set_anchor('p' . $activity->content->id);
4076
    $output .= html_writer::link($discussionurl, $content->subject);
4077
    $output .= html_writer::end_div();
4078
 
4079
    $timestamp = userdate_htmltime($activity->timestamp);
4080
    if ($authorhidden) {
4081
        $authornamedate = $timestamp;
4082
    } else {
4083
        $fullname = fullname($activity->user, $viewfullnames);
4084
        $userurl = new moodle_url('/user/view.php');
4085
        $userurl->params(['id' => $activity->user->id, 'course' => $courseid]);
4086
        $by = new stdClass();
4087
        $by->name = html_writer::link($userurl, $fullname);
4088
        $by->date = $timestamp;
4089
        $authornamedate = get_string('bynameondate', 'forum', $by);
4090
    }
4091
    $output .= html_writer::div($authornamedate, 'user');
4092
    $output .= html_writer::end_tag('td');
4093
    $output .= html_writer::end_tag('tr');
4094
    $output .= html_writer::end_tag('table');
4095
 
4096
    echo $output;
4097
}
4098
 
4099
/**
4100
 * recursively sets the discussion field to $discussionid on $postid and all its children
4101
 * used when pruning a post
4102
 *
4103
 * @global object
4104
 * @param int $postid
4105
 * @param int $discussionid
4106
 * @return bool
4107
 */
4108
function forum_change_discussionid($postid, $discussionid) {
4109
    global $DB;
4110
    $DB->set_field('forum_posts', 'discussion', $discussionid, array('id' => $postid));
4111
    if ($posts = $DB->get_records('forum_posts', array('parent' => $postid))) {
4112
        foreach ($posts as $post) {
4113
            forum_change_discussionid($post->id, $discussionid);
4114
        }
4115
    }
4116
    return true;
4117
}
4118
 
4119
// Functions to do with read tracking.
4120
 
4121
/**
4122
 * Mark posts as read.
4123
 *
4124
 * @global object
4125
 * @global object
4126
 * @param object $user object
4127
 * @param array $postids array of post ids
4128
 * @return boolean success
4129
 */
4130
function forum_tp_mark_posts_read($user, $postids) {
4131
    global $CFG, $DB;
4132
 
4133
    if (!forum_tp_can_track_forums(false, $user)) {
4134
        return true;
4135
    }
4136
 
4137
    $status = true;
4138
 
4139
    $now = time();
4140
    $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
4141
 
4142
    if (empty($postids)) {
4143
        return true;
4144
 
4145
    } else if (count($postids) > 200) {
4146
        while ($part = array_splice($postids, 0, 200)) {
4147
            $status = forum_tp_mark_posts_read($user, $part) && $status;
4148
        }
4149
        return $status;
4150
    }
4151
 
4152
    list($usql, $postidparams) = $DB->get_in_or_equal($postids, SQL_PARAMS_NAMED, 'postid');
4153
 
4154
    $insertparams = array(
4155
        'userid1' => $user->id,
4156
        'userid2' => $user->id,
4157
        'userid3' => $user->id,
4158
        'firstread' => $now,
4159
        'lastread' => $now,
4160
        'cutoffdate' => $cutoffdate,
4161
    );
4162
    $params = array_merge($postidparams, $insertparams);
4163
 
4164
    if ($CFG->forum_allowforcedreadtracking) {
4165
        $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_FORCED."
4166
                        OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND tf.id IS NULL))";
4167
    } else {
4168
        $trackingsql = "AND ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL."  OR f.trackingtype = ".FORUM_TRACKING_FORCED.")
4169
                            AND tf.id IS NULL)";
4170
    }
4171
 
4172
    // First insert any new entries.
4173
    $sql = "INSERT INTO {forum_read} (userid, postid, discussionid, forumid, firstread, lastread)
4174
 
4175
            SELECT :userid1, p.id, p.discussion, d.forum, :firstread, :lastread
4176
                FROM {forum_posts} p
4177
                    JOIN {forum_discussions} d       ON d.id = p.discussion
4178
                    JOIN {forum} f                   ON f.id = d.forum
4179
                    LEFT JOIN {forum_track_prefs} tf ON (tf.userid = :userid2 AND tf.forumid = f.id)
4180
                    LEFT JOIN {forum_read} fr        ON (
4181
                            fr.userid = :userid3
4182
                        AND fr.postid = p.id
4183
                        AND fr.discussionid = d.id
4184
                        AND fr.forumid = f.id
4185
                    )
4186
                WHERE p.id $usql
4187
                    AND p.modified >= :cutoffdate
4188
                    $trackingsql
4189
                    AND fr.id IS NULL";
4190
 
4191
    $status = $DB->execute($sql, $params) && $status;
4192
 
4193
    // Then update all records.
4194
    $updateparams = array(
4195
        'userid' => $user->id,
4196
        'lastread' => $now,
4197
    );
4198
    $params = array_merge($postidparams, $updateparams);
4199
    $status = $DB->set_field_select('forum_read', 'lastread', $now, '
4200
                userid      =  :userid
4201
            AND lastread    <> :lastread
4202
            AND postid      ' . $usql,
4203
            $params) && $status;
4204
 
4205
    return $status;
4206
}
4207
 
4208
/**
4209
 * Mark post as read.
4210
 * @global object
4211
 * @global object
4212
 * @param int $userid
4213
 * @param int $postid
4214
 */
4215
function forum_tp_add_read_record($userid, $postid) {
4216
    global $CFG, $DB;
4217
 
4218
    $now = time();
4219
    $cutoffdate = $now - ($CFG->forum_oldpostdays * 24 * 3600);
4220
 
4221
    if (!$DB->record_exists('forum_read', array('userid' => $userid, 'postid' => $postid))) {
4222
        $sql = "INSERT INTO {forum_read} (userid, postid, discussionid, forumid, firstread, lastread)
4223
 
4224
                SELECT ?, p.id, p.discussion, d.forum, ?, ?
4225
                  FROM {forum_posts} p
4226
                       JOIN {forum_discussions} d ON d.id = p.discussion
4227
                 WHERE p.id = ? AND p.modified >= ?";
4228
        return $DB->execute($sql, array($userid, $now, $now, $postid, $cutoffdate));
4229
 
4230
    } else {
4231
        $sql = "UPDATE {forum_read}
4232
                   SET lastread = ?
4233
                 WHERE userid = ? AND postid = ?";
4234
        return $DB->execute($sql, array($now, $userid, $userid));
4235
    }
4236
}
4237
 
4238
/**
4239
 * If its an old post, do nothing. If the record exists, the maintenance will clear it up later.
4240
 *
4241
 * @param   int     $userid The ID of the user to mark posts read for.
4242
 * @param   object  $post   The post record for the post to mark as read.
4243
 * @param   mixed   $unused
4244
 * @return bool
4245
 */
4246
function forum_tp_mark_post_read($userid, $post, $unused = null) {
4247
    if (!forum_tp_is_post_old($post)) {
4248
        return forum_tp_add_read_record($userid, $post->id);
4249
    } else {
4250
        return true;
4251
    }
4252
}
4253
 
4254
/**
4255
 * Marks a whole forum as read, for a given user
4256
 *
4257
 * @global object
4258
 * @global object
4259
 * @param object $user
4260
 * @param int $forumid
4261
 * @param int|bool $groupid
4262
 * @return bool
4263
 */
4264
function forum_tp_mark_forum_read($user, $forumid, $groupid=false) {
4265
    global $CFG, $DB;
4266
 
4267
    $cutoffdate = time() - ($CFG->forum_oldpostdays*24*60*60);
4268
 
4269
    $groupsel = "";
4270
    $params = array($user->id, $forumid, $cutoffdate);
4271
 
4272
    if ($groupid !== false) {
4273
        $groupsel = " AND (d.groupid = ? OR d.groupid = -1)";
4274
        $params[] = $groupid;
4275
    }
4276
 
4277
    $sql = "SELECT p.id
4278
              FROM {forum_posts} p
4279
                   LEFT JOIN {forum_discussions} d ON d.id = p.discussion
4280
                   LEFT JOIN {forum_read} r        ON (r.postid = p.id AND r.userid = ?)
4281
             WHERE d.forum = ?
4282
                   AND p.modified >= ? AND r.id is NULL
4283
                   $groupsel";
4284
 
4285
    if ($posts = $DB->get_records_sql($sql, $params)) {
4286
        $postids = array_keys($posts);
4287
        return forum_tp_mark_posts_read($user, $postids);
4288
    }
4289
 
4290
    return true;
4291
}
4292
 
4293
/**
4294
 * Marks a whole discussion as read, for a given user
4295
 *
4296
 * @global object
4297
 * @global object
4298
 * @param object $user
4299
 * @param int $discussionid
4300
 * @return bool
4301
 */
4302
function forum_tp_mark_discussion_read($user, $discussionid) {
4303
    global $CFG, $DB;
4304
 
4305
    $cutoffdate = time() - ($CFG->forum_oldpostdays*24*60*60);
4306
 
4307
    $sql = "SELECT p.id
4308
              FROM {forum_posts} p
4309
                   LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = ?)
4310
             WHERE p.discussion = ?
4311
                   AND p.modified >= ? AND r.id is NULL";
4312
 
4313
    if ($posts = $DB->get_records_sql($sql, array($user->id, $discussionid, $cutoffdate))) {
4314
        $postids = array_keys($posts);
4315
        return forum_tp_mark_posts_read($user, $postids);
4316
    }
4317
 
4318
    return true;
4319
}
4320
 
4321
/**
4322
 * @global object
4323
 * @param int $userid
4324
 * @param object $post
4325
 */
4326
function forum_tp_is_post_read($userid, $post) {
4327
    global $DB;
4328
    return (forum_tp_is_post_old($post) ||
4329
            $DB->record_exists('forum_read', array('userid' => $userid, 'postid' => $post->id)));
4330
}
4331
 
4332
/**
4333
 * @global object
4334
 * @param object $post
4335
 * @param int $time Defautls to time()
4336
 */
4337
function forum_tp_is_post_old($post, $time=null) {
4338
    global $CFG;
4339
 
4340
    if (is_null($time)) {
4341
        $time = time();
4342
    }
4343
    return ($post->modified < ($time - ($CFG->forum_oldpostdays * 24 * 3600)));
4344
}
4345
 
4346
/**
4347
 * Returns the count of records for the provided user and course.
4348
 * Please note that group access is ignored!
4349
 *
4350
 * @global object
4351
 * @global object
4352
 * @param int $userid
4353
 * @param int $courseid
4354
 * @return array
4355
 */
4356
function forum_tp_get_course_unread_posts($userid, $courseid) {
4357
    global $CFG, $DB;
4358
 
4359
    $modinfo = get_fast_modinfo($courseid);
4360
    $forumcms = $modinfo->get_instances_of('forum');
4361
    if (empty($forumcms)) {
4362
        // Return early if the course doesn't have any forum. Will save us a DB query.
4363
        return [];
4364
    }
4365
 
4366
    $now = floor(time() / MINSECS) * MINSECS; // DB cache friendliness.
4367
    $cutoffdate = $now - ($CFG->forum_oldpostdays * DAYSECS);
4368
    $params = [
4369
        'privatereplyto' => $userid,
4370
        'modified' => $cutoffdate,
4371
        'readuserid' => $userid,
4372
        'trackprefsuser' => $userid,
4373
        'courseid' => $courseid,
4374
        'trackforumuser' => $userid,
4375
    ];
4376
 
4377
    if (!empty($CFG->forum_enabletimedposts)) {
4378
        $timedsql = "AND d.timestart < :timestart AND (d.timeend = 0 OR d.timeend > :timeend)";
4379
        $params['timestart'] = $now;
4380
        $params['timeend'] = $now;
4381
    } else {
4382
        $timedsql = "";
4383
    }
4384
 
4385
    if ($CFG->forum_allowforcedreadtracking) {
4386
        $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_FORCED."
4387
                            OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND tf.id IS NULL
4388
                                AND (SELECT trackforums FROM {user} WHERE id = :trackforumuser) = 1))";
4389
    } else {
4390
        $trackingsql = "AND ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL." OR f.trackingtype = ".FORUM_TRACKING_FORCED.")
4391
                            AND tf.id IS NULL
4392
                            AND (SELECT trackforums FROM {user} WHERE id = :trackforumuser) = 1)";
4393
    }
4394
 
4395
    $sql = "SELECT f.id, COUNT(p.id) AS unread,
4396
                   COUNT(p.privatereply) as privatereplies,
4397
                   COUNT(p.privatereplytouser) as privaterepliestouser
4398
              FROM (
4399
                        SELECT
4400
                            id,
4401
                            discussion,
4402
                            CASE WHEN privatereplyto <> 0 THEN 1 END privatereply,
4403
                            CASE WHEN privatereplyto = :privatereplyto THEN 1 END privatereplytouser
4404
                        FROM {forum_posts}
4405
                        WHERE modified >= :modified
4406
                   ) p
4407
                   JOIN {forum_discussions} d       ON d.id = p.discussion
4408
                   JOIN {forum} f                   ON f.id = d.forum
4409
                   JOIN {course} c                  ON c.id = f.course
4410
                   LEFT JOIN {forum_read} r         ON (r.postid = p.id AND r.userid = :readuserid)
4411
                   LEFT JOIN {forum_track_prefs} tf ON (tf.userid = :trackprefsuser AND tf.forumid = f.id)
4412
             WHERE f.course = :courseid
4413
                   AND r.id is NULL
4414
                   $trackingsql
4415
                   $timedsql
4416
          GROUP BY f.id";
4417
 
4418
    $results = [];
4419
    if ($records = $DB->get_records_sql($sql, $params)) {
4420
        // Loop through each forum instance to check for capability and count the number of unread posts.
4421
        foreach ($forumcms as $cm) {
4422
            // Check that the forum instance exists in the query results.
4423
            if (!isset($records[$cm->instance])) {
4424
                continue;
4425
            }
4426
 
4427
            $record = $records[$cm->instance];
4428
            $unread = $record->unread;
4429
 
4430
            // Check if the user has the capability to read private replies for this forum instance.
4431
            $forumcontext = context_module::instance($cm->id);
4432
            if (!has_capability('mod/forum:readprivatereplies', $forumcontext, $userid)) {
4433
                // The real unread count would be the total of unread count minus the number of unread private replies plus
4434
                // the total unread private replies to the user.
4435
                $unread = $record->unread - $record->privatereplies + $record->privaterepliestouser;
4436
            }
4437
 
4438
            // Build and add the object to the array of results to be returned.
4439
            $results[$record->id] = (object)[
4440
                'id' => $record->id,
4441
                'unread' => $unread,
4442
            ];
4443
        }
4444
    }
4445
 
4446
    return $results;
4447
}
4448
 
4449
/**
4450
 * Returns the count of records for the provided user and forum and [optionally] group.
4451
 *
4452
 * @global object
4453
 * @global object
4454
 * @global object
4455
 * @param object $cm
4456
 * @param object $course
4457
 * @param bool   $resetreadcache optional, true to reset the function static $readcache var
4458
 * @return int
4459
 */
4460
function forum_tp_count_forum_unread_posts($cm, $course, $resetreadcache = false) {
4461
    global $CFG, $USER, $DB;
4462
 
4463
    static $readcache = array();
4464
 
4465
    if ($resetreadcache) {
4466
        $readcache = array();
4467
    }
4468
 
4469
    $forumid = $cm->instance;
4470
 
4471
    if (!isset($readcache[$course->id])) {
4472
        $readcache[$course->id] = array();
4473
        if ($counts = forum_tp_get_course_unread_posts($USER->id, $course->id)) {
4474
            foreach ($counts as $count) {
4475
                $readcache[$course->id][$count->id] = $count->unread;
4476
            }
4477
        }
4478
    }
4479
 
4480
    if (empty($readcache[$course->id][$forumid])) {
4481
        // no need to check group mode ;-)
4482
        return 0;
4483
    }
4484
 
4485
    $groupmode = groups_get_activity_groupmode($cm, $course);
4486
 
4487
    if ($groupmode != SEPARATEGROUPS) {
4488
        return $readcache[$course->id][$forumid];
4489
    }
4490
 
4491
    $forumcontext = context_module::instance($cm->id);
4492
    if (has_any_capability(['moodle/site:accessallgroups', 'mod/forum:readprivatereplies'], $forumcontext)) {
4493
        return $readcache[$course->id][$forumid];
4494
    }
4495
 
4496
    require_once($CFG->dirroot.'/course/lib.php');
4497
 
4498
    $modinfo = get_fast_modinfo($course);
4499
 
4500
    $mygroups = $modinfo->get_groups($cm->groupingid);
4501
 
4502
    // add all groups posts
4503
    $mygroups[-1] = -1;
4504
 
4505
    list ($groupssql, $groupsparams) = $DB->get_in_or_equal($mygroups, SQL_PARAMS_NAMED);
4506
 
4507
    $now = floor(time() / MINSECS) * MINSECS; // DB Cache friendliness.
4508
    $cutoffdate = $now - ($CFG->forum_oldpostdays * DAYSECS);
4509
    $params = [
4510
        'readuser' => $USER->id,
4511
        'forum' => $forumid,
4512
        'cutoffdate' => $cutoffdate,
4513
        'privatereplyto' => $USER->id,
4514
    ];
4515
 
4516
    if (!empty($CFG->forum_enabletimedposts)) {
4517
        $timedsql = "AND d.timestart < :timestart AND (d.timeend = 0 OR d.timeend > :timeend)";
4518
        $params['timestart'] = $now;
4519
        $params['timeend'] = $now;
4520
    } else {
4521
        $timedsql = "";
4522
    }
4523
 
4524
    $params = array_merge($params, $groupsparams);
4525
 
4526
    $sql = "SELECT COUNT(p.id)
4527
              FROM {forum_posts} p
4528
              JOIN {forum_discussions} d ON p.discussion = d.id
4529
         LEFT JOIN {forum_read} r ON (r.postid = p.id AND r.userid = :readuser)
4530
             WHERE d.forum = :forum
4531
                   AND p.modified >= :cutoffdate AND r.id is NULL
4532
                   $timedsql
4533
                   AND d.groupid $groupssql
4534
                   AND (p.privatereplyto = 0 OR p.privatereplyto = :privatereplyto)";
4535
 
4536
    return $DB->get_field_sql($sql, $params);
4537
}
4538
 
4539
/**
4540
 * Deletes read records for the specified index. At least one parameter must be specified.
4541
 *
4542
 * @global object
4543
 * @param int $userid
4544
 * @param int $postid
4545
 * @param int $discussionid
4546
 * @param int $forumid
4547
 * @return bool
4548
 */
4549
function forum_tp_delete_read_records($userid=-1, $postid=-1, $discussionid=-1, $forumid=-1) {
4550
    global $DB;
4551
    $params = array();
4552
 
4553
    $select = '';
4554
    if ($userid > -1) {
4555
        if ($select != '') $select .= ' AND ';
4556
        $select .= 'userid = ?';
4557
        $params[] = $userid;
4558
    }
4559
    if ($postid > -1) {
4560
        if ($select != '') $select .= ' AND ';
4561
        $select .= 'postid = ?';
4562
        $params[] = $postid;
4563
    }
4564
    if ($discussionid > -1) {
4565
        if ($select != '') $select .= ' AND ';
4566
        $select .= 'discussionid = ?';
4567
        $params[] = $discussionid;
4568
    }
4569
    if ($forumid > -1) {
4570
        if ($select != '') $select .= ' AND ';
4571
        $select .= 'forumid = ?';
4572
        $params[] = $forumid;
4573
    }
4574
    if ($select == '') {
4575
        return false;
4576
    }
4577
    else {
4578
        return $DB->delete_records_select('forum_read', $select, $params);
4579
    }
4580
}
4581
/**
4582
 * Get a list of forums not tracked by the user.
4583
 *
4584
 * @global object
4585
 * @global object
4586
 * @param int $userid The id of the user to use.
4587
 * @param int $courseid The id of the course being checked.
4588
 * @return mixed An array indexed by forum id, or false.
4589
 */
4590
function forum_tp_get_untracked_forums($userid, $courseid) {
4591
    global $CFG, $DB;
4592
 
4593
    if ($CFG->forum_allowforcedreadtracking) {
4594
        $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_OFF."
4595
                            OR (f.trackingtype = ".FORUM_TRACKING_OPTIONAL." AND (ft.id IS NOT NULL
4596
                                OR (SELECT trackforums FROM {user} WHERE id = ?) = 0)))";
4597
    } else {
4598
        $trackingsql = "AND (f.trackingtype = ".FORUM_TRACKING_OFF."
4599
                            OR ((f.trackingtype = ".FORUM_TRACKING_OPTIONAL." OR f.trackingtype = ".FORUM_TRACKING_FORCED.")
4600
                                AND (ft.id IS NOT NULL
4601
                                    OR (SELECT trackforums FROM {user} WHERE id = ?) = 0)))";
4602
    }
4603
 
4604
    $sql = "SELECT f.id
4605
              FROM {forum} f
4606
                   LEFT JOIN {forum_track_prefs} ft ON (ft.forumid = f.id AND ft.userid = ?)
4607
             WHERE f.course = ?
4608
                   $trackingsql";
4609
 
4610
    if ($forums = $DB->get_records_sql($sql, array($userid, $courseid, $userid))) {
4611
        foreach ($forums as $forum) {
4612
            $forums[$forum->id] = $forum;
4613
        }
4614
        return $forums;
4615
 
4616
    } else {
4617
        return array();
4618
    }
4619
}
4620
 
4621
/**
4622
 * Determine if a user can track forums and optionally a particular forum.
4623
 * Checks the site settings, the user settings and the forum settings (if
4624
 * requested).
4625
 *
4626
 * @global object
4627
 * @global object
4628
 * @global object
4629
 * @param mixed $forum The forum object to test, or the int id (optional).
4630
 * @param mixed $userid The user object to check for (optional).
4631
 * @return boolean
4632
 */
4633
function forum_tp_can_track_forums($forum=false, $user=false) {
4634
    global $USER, $CFG, $DB;
4635
 
4636
    // if possible, avoid expensive
4637
    // queries
4638
    if (empty($CFG->forum_trackreadposts)) {
4639
        return false;
4640
    }
4641
 
4642
    if ($user === false) {
4643
        $user = $USER;
4644
    }
4645
 
4646
    if (isguestuser($user) or empty($user->id)) {
4647
        return false;
4648
    }
4649
 
4650
    if ($forum === false) {
4651
        if ($CFG->forum_allowforcedreadtracking) {
4652
            // Since we can force tracking, assume yes without a specific forum.
4653
            return true;
4654
        } else {
4655
            return (bool)$user->trackforums;
4656
        }
4657
    }
4658
 
4659
    // Work toward always passing an object...
4660
    if (is_numeric($forum)) {
4661
        debugging('Better use proper forum object.', DEBUG_DEVELOPER);
4662
        $forum = $DB->get_record('forum', array('id' => $forum), '', 'id,trackingtype');
4663
    }
4664
 
4665
    $forumallows = ($forum->trackingtype == FORUM_TRACKING_OPTIONAL);
4666
    $forumforced = ($forum->trackingtype == FORUM_TRACKING_FORCED);
4667
 
4668
    if ($CFG->forum_allowforcedreadtracking) {
4669
        // If we allow forcing, then forced forums takes procidence over user setting.
4670
        return ($forumforced || ($forumallows  && (!empty($user->trackforums) && (bool)$user->trackforums)));
4671
    } else {
4672
        // If we don't allow forcing, user setting trumps.
4673
        return ($forumforced || $forumallows)  && !empty($user->trackforums);
4674
    }
4675
}
4676
 
4677
/**
4678
 * Tells whether a specific forum is tracked by the user. A user can optionally
4679
 * be specified. If not specified, the current user is assumed.
4680
 *
4681
 * @global object
4682
 * @global object
4683
 * @global object
4684
 * @param mixed $forum If int, the id of the forum being checked; if object, the forum object
4685
 * @param int $userid The id of the user being checked (optional).
4686
 * @return boolean
4687
 */
4688
function forum_tp_is_tracked($forum, $user=false) {
4689
    global $USER, $CFG, $DB;
4690
 
4691
    if ($user === false) {
4692
        $user = $USER;
4693
    }
4694
 
4695
    if (isguestuser($user) or empty($user->id)) {
4696
        return false;
4697
    }
4698
 
4699
    $cache = cache::make('mod_forum', 'forum_is_tracked');
4700
    $forumid = is_numeric($forum) ? $forum : $forum->id;
4701
    $key = $forumid . '_' . $user->id;
4702
    if ($cachedvalue = $cache->get($key)) {
4703
        return $cachedvalue == 'tracked';
4704
    }
4705
 
4706
    // Work toward always passing an object...
4707
    if (is_numeric($forum)) {
4708
        debugging('Better use proper forum object.', DEBUG_DEVELOPER);
4709
        $forum = $DB->get_record('forum', array('id' => $forum));
4710
    }
4711
 
4712
    if (!forum_tp_can_track_forums($forum, $user)) {
4713
        return false;
4714
    }
4715
 
4716
    $forumallows = ($forum->trackingtype == FORUM_TRACKING_OPTIONAL);
4717
    $forumforced = ($forum->trackingtype == FORUM_TRACKING_FORCED);
4718
    $userpref = $DB->get_record('forum_track_prefs', array('userid' => $user->id, 'forumid' => $forum->id));
4719
 
4720
    if ($CFG->forum_allowforcedreadtracking) {
4721
        $istracked = $forumforced || ($forumallows && $userpref === false);
4722
    } else {
4723
        $istracked = ($forumallows || $forumforced) && $userpref === false;
4724
    }
4725
 
4726
    // We have to store a string here because the cache API returns false
4727
    // when it can't find the key which would be confused with our legitimate
4728
    // false value. *sigh*.
4729
    $cache->set($key, $istracked ? 'tracked' : 'not');
4730
 
4731
    return $istracked;
4732
}
4733
 
4734
/**
4735
 * @global object
4736
 * @global object
4737
 * @param int $forumid
4738
 * @param int $userid
4739
 */
4740
function forum_tp_start_tracking($forumid, $userid=false) {
4741
    global $USER, $DB;
4742
 
4743
    if ($userid === false) {
4744
        $userid = $USER->id;
4745
    }
4746
 
4747
    return $DB->delete_records('forum_track_prefs', array('userid' => $userid, 'forumid' => $forumid));
4748
}
4749
 
4750
/**
4751
 * @global object
4752
 * @global object
4753
 * @param int $forumid
4754
 * @param int $userid
4755
 */
4756
function forum_tp_stop_tracking($forumid, $userid=false) {
4757
    global $USER, $DB;
4758
 
4759
    if ($userid === false) {
4760
        $userid = $USER->id;
4761
    }
4762
 
4763
    if (!$DB->record_exists('forum_track_prefs', array('userid' => $userid, 'forumid' => $forumid))) {
4764
        $track_prefs = new stdClass();
4765
        $track_prefs->userid = $userid;
4766
        $track_prefs->forumid = $forumid;
4767
        $DB->insert_record('forum_track_prefs', $track_prefs);
4768
    }
4769
 
4770
    return forum_tp_delete_read_records($userid, -1, -1, $forumid);
4771
}
4772
 
4773
 
4774
/**
4775
 * Clean old records from the forum_read table.
4776
 * @global object
4777
 * @global object
4778
 * @return void
4779
 */
4780
function forum_tp_clean_read_records() {
4781
    global $CFG, $DB;
4782
 
4783
    if (!isset($CFG->forum_oldpostdays)) {
4784
        return;
4785
    }
4786
// Look for records older than the cutoffdate that are still in the forum_read table.
4787
    $cutoffdate = time() - ($CFG->forum_oldpostdays*24*60*60);
4788
 
4789
    //first get the oldest tracking present - we need tis to speedup the next delete query
4790
    $sql = "SELECT MIN(fp.modified) AS first
4791
              FROM {forum_posts} fp
4792
                   JOIN {forum_read} fr ON fr.postid=fp.id";
4793
    if (!$first = $DB->get_field_sql($sql)) {
4794
        // nothing to delete;
4795
        return;
4796
    }
4797
 
4798
    // now delete old tracking info
4799
    $sql = "DELETE
4800
              FROM {forum_read}
4801
             WHERE postid IN (SELECT fp.id
4802
                                FROM {forum_posts} fp
4803
                               WHERE fp.modified >= ? AND fp.modified < ?)";
4804
    $DB->execute($sql, array($first, $cutoffdate));
4805
}
4806
 
4807
/**
4808
 * Sets the last post for a given discussion
4809
 *
4810
 * @global object
4811
 * @global object
4812
 * @param into $discussionid
4813
 * @return bool|int
4814
 **/
4815
function forum_discussion_update_last_post($discussionid) {
4816
    global $CFG, $DB;
4817
 
4818
// Check the given discussion exists
4819
    if (!$DB->record_exists('forum_discussions', array('id' => $discussionid))) {
4820
        return false;
4821
    }
4822
 
4823
// Use SQL to find the last post for this discussion
4824
    $sql = "SELECT id, userid, modified
4825
              FROM {forum_posts}
4826
             WHERE discussion=?
4827
             ORDER BY modified DESC";
4828
 
4829
// Lets go find the last post
4830
    if (($lastposts = $DB->get_records_sql($sql, array($discussionid), 0, 1))) {
4831
        $lastpost = reset($lastposts);
4832
        $discussionobject = new stdClass();
4833
        $discussionobject->id           = $discussionid;
4834
        $discussionobject->usermodified = $lastpost->userid;
4835
        $discussionobject->timemodified = $lastpost->modified;
4836
        $DB->update_record('forum_discussions', $discussionobject);
4837
        return $lastpost->id;
4838
    }
4839
 
4840
// To get here either we couldn't find a post for the discussion (weird)
4841
// or we couldn't update the discussion record (weird x2)
4842
    return false;
4843
}
4844
 
4845
 
4846
/**
4847
 * List the actions that correspond to a view of this module.
4848
 * This is used by the participation report.
4849
 *
4850
 * Note: This is not used by new logging system. Event with
4851
 *       crud = 'r' and edulevel = LEVEL_PARTICIPATING will
4852
 *       be considered as view action.
4853
 *
4854
 * @return array
4855
 */
4856
function forum_get_view_actions() {
4857
    return array('view discussion', 'search', 'forum', 'forums', 'subscribers', 'view forum');
4858
}
4859
 
4860
/**
4861
 * List the options for forum subscription modes.
4862
 * This is used by the settings page and by the mod_form page.
4863
 *
4864
 * @return array
4865
 */
4866
function forum_get_subscriptionmode_options() {
4867
    $options = array();
4868
    $options[FORUM_CHOOSESUBSCRIBE] = get_string('subscriptionoptional', 'forum');
4869
    $options[FORUM_FORCESUBSCRIBE] = get_string('subscriptionforced', 'forum');
4870
    $options[FORUM_INITIALSUBSCRIBE] = get_string('subscriptionauto', 'forum');
4871
    $options[FORUM_DISALLOWSUBSCRIBE] = get_string('subscriptiondisabled', 'forum');
4872
    return $options;
4873
}
4874
 
4875
/**
4876
 * List the actions that correspond to a post of this module.
4877
 * This is used by the participation report.
4878
 *
4879
 * Note: This is not used by new logging system. Event with
4880
 *       crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING
4881
 *       will be considered as post action.
4882
 *
4883
 * @return array
4884
 */
4885
function forum_get_post_actions() {
4886
    return array('add discussion','add post','delete discussion','delete post','move discussion','prune post','update post');
4887
}
4888
 
4889
/**
4890
 * Returns a warning object if a user has reached the number of posts equal to
4891
 * the warning/blocking setting, or false if there is no warning to show.
4892
 *
4893
 * @param int|stdClass $forum the forum id or the forum object
4894
 * @param stdClass $cm the course module
4895
 * @return stdClass|bool returns an object with the warning information, else
4896
 *         returns false if no warning is required.
4897
 */
4898
function forum_check_throttling($forum, $cm = null) {
4899
    global $CFG, $DB, $USER;
4900
 
4901
    if (is_numeric($forum)) {
4902
        $forum = $DB->get_record('forum', ['id' => $forum], 'id, course, blockperiod, blockafter, warnafter', MUST_EXIST);
4903
    }
4904
 
4905
    if (!is_object($forum) || !isset($forum->id) || !isset($forum->course)) {
4906
        // The passed forum parameter is invalid. This can happen if:
4907
        // - a non-object and non-numeric forum is passed; or
4908
        // - the forum object does not have an ID or course attributes.
4909
        // This is unlikely to happen with properly formed forum record fetched from the database,
4910
        // so it's most likely a dev error if we hit such this case.
4911
        throw new coding_exception('Invalid forum parameter passed');
4912
    }
4913
 
4914
    if (empty($forum->blockafter)) {
4915
        return false;
4916
    }
4917
 
4918
    if (empty($forum->blockperiod)) {
4919
        return false;
4920
    }
4921
 
4922
    if (!$cm) {
4923
        // Try to fetch the $cm object via get_fast_modinfo() so we don't incur DB reads.
4924
        $modinfo = get_fast_modinfo($forum->course);
4925
        $forumcms = $modinfo->get_instances_of('forum');
4926
        foreach ($forumcms as $tmpcm) {
4927
            if ($tmpcm->instance == $forum->id) {
4928
                $cm = $tmpcm;
4929
                break;
4930
            }
4931
        }
4932
        // Last resort. Try to fetch via get_coursemodule_from_instance().
4933
        if (!$cm) {
4934
            $cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course, false, MUST_EXIST);
4935
        }
4936
    }
4937
 
4938
    $modcontext = context_module::instance($cm->id);
4939
    if (has_capability('mod/forum:postwithoutthrottling', $modcontext)) {
4940
        return false;
4941
    }
4942
 
4943
    // Get the number of posts in the last period we care about.
4944
    $timenow = time();
4945
    $timeafter = $timenow - $forum->blockperiod;
4946
    $numposts = $DB->count_records_sql('SELECT COUNT(p.id) FROM {forum_posts} p
4947
                                        JOIN {forum_discussions} d
4948
                                        ON p.discussion = d.id WHERE d.forum = ?
4949
                                        AND p.userid = ? AND p.created > ?', array($forum->id, $USER->id, $timeafter));
4950
 
4951
    $a = new stdClass();
4952
    $a->blockafter = $forum->blockafter;
4953
    $a->numposts = $numposts;
4954
    $a->blockperiod = get_string('secondstotime'.$forum->blockperiod);
4955
 
4956
    if ($forum->blockafter <= $numposts) {
4957
        $warning = new stdClass();
4958
        $warning->canpost = false;
4959
        $warning->errorcode = 'forumblockingtoomanyposts';
4960
        $warning->module = 'error';
4961
        $warning->additional = $a;
4962
        $warning->link = $CFG->wwwroot . '/mod/forum/view.php?f=' . $forum->id;
4963
 
4964
        return $warning;
4965
    }
4966
 
4967
    if ($forum->warnafter <= $numposts) {
4968
        $warning = new stdClass();
4969
        $warning->canpost = true;
4970
        $warning->errorcode = 'forumblockingalmosttoomanyposts';
4971
        $warning->module = 'forum';
4972
        $warning->additional = $a;
4973
        $warning->link = null;
4974
 
4975
        return $warning;
4976
    }
4977
 
4978
    // No warning needs to be shown yet.
4979
    return false;
4980
}
4981
 
4982
/**
4983
 * Throws an error if the user is no longer allowed to post due to having reached
4984
 * or exceeded the number of posts specified in 'Post threshold for blocking'
4985
 * setting.
4986
 *
4987
 * @since Moodle 2.5
4988
 * @param stdClass $thresholdwarning the warning information returned
4989
 *        from the function forum_check_throttling.
4990
 */
4991
function forum_check_blocking_threshold($thresholdwarning) {
4992
    if (!empty($thresholdwarning) && !$thresholdwarning->canpost) {
4993
        throw new \moodle_exception($thresholdwarning->errorcode,
4994
                    $thresholdwarning->module,
4995
                    $thresholdwarning->link,
4996
                    $thresholdwarning->additional);
4997
    }
4998
}
4999
 
5000
 
5001
/**
5002
 * Removes all grades from gradebook
5003
 *
5004
 * @global object
5005
 * @global object
5006
 * @param int $courseid
5007
 * @param string $type optional
5008
 */
5009
function forum_reset_gradebook($courseid, $type='') {
5010
    global $CFG, $DB;
5011
 
5012
    $wheresql = '';
5013
    $params = array($courseid);
5014
    if ($type) {
5015
        $wheresql = "AND f.type=?";
5016
        $params[] = $type;
5017
    }
5018
 
5019
    $sql = "SELECT f.*, cm.idnumber as cmidnumber, f.course as courseid
5020
              FROM {forum} f, {course_modules} cm, {modules} m
5021
             WHERE m.name='forum' AND m.id=cm.module AND cm.instance=f.id AND f.course=? $wheresql";
5022
 
5023
    if ($forums = $DB->get_records_sql($sql, $params)) {
5024
        foreach ($forums as $forum) {
5025
            forum_grade_item_update($forum, 'reset', 'reset');
5026
        }
5027
    }
5028
}
5029
 
5030
/**
5031
 * This function is used by the reset_course_userdata function in moodlelib.
5032
 * This function will remove all posts from the specified forum
5033
 * and clean up any related data.
5034
 *
5035
 * @global object
5036
 * @global object
5037
 * @param $data the data submitted from the reset course.
5038
 * @return array status array
5039
 */
5040
function forum_reset_userdata($data) {
5041
    global $CFG, $DB;
5042
    require_once($CFG->dirroot.'/rating/lib.php');
5043
 
5044
    $componentstr = get_string('modulenameplural', 'forum');
5045
    $status = array();
5046
 
5047
    $params = array($data->courseid);
5048
 
5049
    $removeposts = false;
5050
    $typesql     = "";
5051
    if (!empty($data->reset_forum_all)) {
5052
        $removeposts = true;
5053
        $typesstr    = get_string('resetforumsall', 'forum');
5054
        $types       = array();
5055
    } else if (!empty($data->reset_forum_types)){
5056
        $removeposts = true;
5057
        $types       = array();
5058
        $sqltypes    = array();
5059
        $forum_types_all = forum_get_forum_types_all();
5060
        foreach ($data->reset_forum_types as $type) {
5061
            if (!array_key_exists($type, $forum_types_all)) {
5062
                continue;
5063
            }
5064
            $types[] = $forum_types_all[$type];
5065
            $sqltypes[] = $type;
5066
        }
5067
        if (!empty($sqltypes)) {
5068
            list($typesql, $typeparams) = $DB->get_in_or_equal($sqltypes);
5069
            $typesql = " AND f.type " . $typesql;
5070
            $params = array_merge($params, $typeparams);
5071
        }
5072
        $typesstr = get_string('resetforums', 'forum').': '.implode(', ', $types);
5073
    }
5074
    $alldiscussionssql = "SELECT fd.id
5075
                            FROM {forum_discussions} fd, {forum} f
5076
                           WHERE f.course=? AND f.id=fd.forum";
5077
 
5078
    $allforumssql      = "SELECT f.id
5079
                            FROM {forum} f
5080
                           WHERE f.course=?";
5081
 
5082
    $allpostssql       = "SELECT fp.id
5083
                            FROM {forum_posts} fp, {forum_discussions} fd, {forum} f
5084
                           WHERE f.course=? AND f.id=fd.forum AND fd.id=fp.discussion";
5085
 
5086
    $forumssql = $forums = $rm = null;
5087
 
5088
    // Check if we need to get additional data.
5089
    if ($removeposts || !empty($data->reset_forum_ratings) || !empty($data->reset_forum_tags)) {
5090
        // Set this up if we have to remove ratings.
5091
        $rm = new rating_manager();
5092
        $ratingdeloptions = new stdClass;
5093
        $ratingdeloptions->component = 'mod_forum';
5094
        $ratingdeloptions->ratingarea = 'post';
5095
 
5096
        // Get the forums for actions that require it.
5097
        $forumssql = "$allforumssql $typesql";
5098
        $forums = $DB->get_records_sql($forumssql, $params);
5099
    }
5100
 
5101
    if ($removeposts) {
5102
        $discussionssql = "$alldiscussionssql $typesql";
5103
        $postssql       = "$allpostssql $typesql";
5104
 
5105
        // now get rid of all attachments
5106
        $fs = get_file_storage();
5107
        if ($forums) {
5108
            foreach ($forums as $forumid=>$unused) {
5109
                if (!$cm = get_coursemodule_from_instance('forum', $forumid)) {
5110
                    continue;
5111
                }
5112
                $context = context_module::instance($cm->id);
5113
                $fs->delete_area_files($context->id, 'mod_forum', 'attachment');
5114
                $fs->delete_area_files($context->id, 'mod_forum', 'post');
5115
 
5116
                //remove ratings
5117
                $ratingdeloptions->contextid = $context->id;
5118
                $rm->delete_ratings($ratingdeloptions);
5119
 
5120
                core_tag_tag::delete_instances('mod_forum', null, $context->id);
5121
            }
5122
        }
5123
 
5124
        // first delete all read flags
5125
        $DB->delete_records_select('forum_read', "forumid IN ($forumssql)", $params);
5126
 
5127
        // remove tracking prefs
5128
        $DB->delete_records_select('forum_track_prefs', "forumid IN ($forumssql)", $params);
5129
 
5130
        // remove posts from queue
5131
        $DB->delete_records_select('forum_queue', "discussionid IN ($discussionssql)", $params);
5132
 
5133
        // all posts - initial posts must be kept in single simple discussion forums
5134
        $DB->delete_records_select('forum_posts', "discussion IN ($discussionssql) AND parent <> 0", $params); // first all children
5135
        $DB->delete_records_select('forum_posts', "discussion IN ($discussionssql AND f.type <> 'single') AND parent = 0", $params); // now the initial posts for non single simple
5136
 
5137
        // finally all discussions except single simple forums
5138
        $DB->delete_records_select('forum_discussions', "forum IN ($forumssql AND f.type <> 'single')", $params);
5139
 
5140
        // remove all grades from gradebook
5141
        if (empty($data->reset_gradebook_grades)) {
5142
            if (empty($types)) {
5143
                forum_reset_gradebook($data->courseid);
5144
            } else {
5145
                foreach ($types as $type) {
5146
                    forum_reset_gradebook($data->courseid, $type);
5147
                }
5148
            }
5149
        }
5150
 
5151
        $status[] = array('component'=>$componentstr, 'item'=>$typesstr, 'error'=>false);
5152
    }
5153
 
5154
    // remove all ratings in this course's forums
5155
    if (!empty($data->reset_forum_ratings)) {
5156
        if ($forums) {
5157
            foreach ($forums as $forumid=>$unused) {
5158
                if (!$cm = get_coursemodule_from_instance('forum', $forumid)) {
5159
                    continue;
5160
                }
5161
                $context = context_module::instance($cm->id);
5162
 
5163
                //remove ratings
5164
                $ratingdeloptions->contextid = $context->id;
5165
                $rm->delete_ratings($ratingdeloptions);
5166
            }
5167
        }
5168
 
5169
        // remove all grades from gradebook
5170
        if (empty($data->reset_gradebook_grades)) {
5171
            forum_reset_gradebook($data->courseid);
5172
        }
5173
    }
5174
 
5175
    // Remove all the tags.
5176
    if (!empty($data->reset_forum_tags)) {
5177
        if ($forums) {
5178
            foreach ($forums as $forumid => $unused) {
5179
                if (!$cm = get_coursemodule_from_instance('forum', $forumid)) {
5180
                    continue;
5181
                }
5182
 
5183
                $context = context_module::instance($cm->id);
5184
                core_tag_tag::delete_instances('mod_forum', null, $context->id);
5185
            }
5186
        }
5187
 
5188
        $status[] = array('component' => $componentstr, 'item' => get_string('tagsdeleted', 'forum'), 'error' => false);
5189
    }
5190
 
5191
    // remove all digest settings unconditionally - even for users still enrolled in course.
5192
    if (!empty($data->reset_forum_digests)) {
5193
        $DB->delete_records_select('forum_digests', "forum IN ($allforumssql)", $params);
5194
        $status[] = array('component' => $componentstr, 'item' => get_string('resetdigests', 'forum'), 'error' => false);
5195
    }
5196
 
5197
    // remove all subscriptions unconditionally - even for users still enrolled in course
5198
    if (!empty($data->reset_forum_subscriptions)) {
5199
        $DB->delete_records_select('forum_subscriptions', "forum IN ($allforumssql)", $params);
5200
        $DB->delete_records_select('forum_discussion_subs', "forum IN ($allforumssql)", $params);
5201
        $status[] = array('component' => $componentstr, 'item' => get_string('resetsubscriptions', 'forum'), 'error' => false);
5202
    }
5203
 
5204
    // remove all tracking prefs unconditionally - even for users still enrolled in course
5205
    if (!empty($data->reset_forum_track_prefs)) {
5206
        $DB->delete_records_select('forum_track_prefs', "forumid IN ($allforumssql)", $params);
5207
        $status[] = array('component'=>$componentstr, 'item'=>get_string('resettrackprefs','forum'), 'error'=>false);
5208
    }
5209
 
5210
    /// updating dates - shift may be negative too
5211
    if ($data->timeshift) {
5212
        // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
5213
        // See MDL-9367.
5214
        shift_course_mod_dates('forum', array('assesstimestart', 'assesstimefinish'), $data->timeshift, $data->courseid);
5215
        $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
5216
    }
5217
 
5218
    return $status;
5219
}
5220
 
5221
/**
5222
 * Called by course/reset.php
5223
 *
5224
 * @param MoodleQuickForm $mform form passed by reference
5225
 */
5226
function forum_reset_course_form_definition(&$mform) {
5227
    $mform->addElement('header', 'forumheader', get_string('modulenameplural', 'forum'));
5228
 
5229
    $mform->addElement('checkbox', 'reset_forum_all', get_string('resetforumsall','forum'));
5230
 
5231
    $mform->addElement('select', 'reset_forum_types', get_string('resetforums', 'forum'), forum_get_forum_types_all(), array('multiple' => 'multiple'));
5232
    $mform->setAdvanced('reset_forum_types');
5233
    $mform->disabledIf('reset_forum_types', 'reset_forum_all', 'checked');
5234
 
5235
    $mform->addElement('checkbox', 'reset_forum_digests', get_string('resetdigests','forum'));
5236
    $mform->setAdvanced('reset_forum_digests');
5237
 
5238
    $mform->addElement('checkbox', 'reset_forum_subscriptions', get_string('resetsubscriptions','forum'));
5239
    $mform->setAdvanced('reset_forum_subscriptions');
5240
 
5241
    $mform->addElement('checkbox', 'reset_forum_track_prefs', get_string('resettrackprefs','forum'));
5242
    $mform->setAdvanced('reset_forum_track_prefs');
5243
    $mform->disabledIf('reset_forum_track_prefs', 'reset_forum_all', 'checked');
5244
 
5245
    $mform->addElement('checkbox', 'reset_forum_ratings', get_string('deleteallratings'));
5246
    $mform->disabledIf('reset_forum_ratings', 'reset_forum_all', 'checked');
5247
 
5248
    $mform->addElement('checkbox', 'reset_forum_tags', get_string('removeallforumtags', 'forum'));
5249
    $mform->disabledIf('reset_forum_tags', 'reset_forum_all', 'checked');
5250
}
5251
 
5252
/**
5253
 * Course reset form defaults.
5254
 * @return array
5255
 */
5256
function forum_reset_course_form_defaults($course) {
5257
    return array('reset_forum_all'=>1, 'reset_forum_digests' => 0, 'reset_forum_subscriptions'=>0, 'reset_forum_track_prefs'=>0, 'reset_forum_ratings'=>1);
5258
}
5259
 
5260
/**
5261
 * Returns array of forum layout modes
5262
 *
5263
 * @param bool $useexperimentalui use experimental layout modes or not
5264
 * @return array
5265
 */
5266
function forum_get_layout_modes(bool $useexperimentalui = false) {
5267
    $modes = [
5268
        FORUM_MODE_FLATOLDEST => get_string('modeflatoldestfirst', 'forum'),
5269
        FORUM_MODE_FLATNEWEST => get_string('modeflatnewestfirst', 'forum'),
5270
        FORUM_MODE_THREADED   => get_string('modethreaded', 'forum')
5271
    ];
5272
 
5273
    if ($useexperimentalui) {
5274
        $modes[FORUM_MODE_NESTED_V2] = get_string('modenestedv2', 'forum');
5275
    } else {
5276
        $modes[FORUM_MODE_NESTED] = get_string('modenested', 'forum');
5277
    }
5278
 
5279
    return $modes;
5280
}
5281
 
5282
/**
5283
 * Returns array of forum types chooseable on the forum editing form
5284
 *
5285
 * @return array
5286
 */
5287
function forum_get_forum_types() {
5288
    return array ('general'  => get_string('generalforum', 'forum'),
5289
                  'eachuser' => get_string('eachuserforum', 'forum'),
5290
                  'single'   => get_string('singleforum', 'forum'),
5291
                  'qanda'    => get_string('qandaforum', 'forum'),
5292
                  'blog'     => get_string('blogforum', 'forum'));
5293
}
5294
 
5295
/**
5296
 * Returns array of all forum layout modes
5297
 *
5298
 * @return array
5299
 */
5300
function forum_get_forum_types_all() {
5301
    return array ('news'     => get_string('namenews','forum'),
5302
                  'social'   => get_string('namesocial','forum'),
5303
                  'general'  => get_string('generalforum', 'forum'),
5304
                  'eachuser' => get_string('eachuserforum', 'forum'),
5305
                  'single'   => get_string('singleforum', 'forum'),
5306
                  'qanda'    => get_string('qandaforum', 'forum'),
5307
                  'blog'     => get_string('blogforum', 'forum'));
5308
}
5309
 
5310
/**
5311
 * Returns all other caps used in module
5312
 *
5313
 * @return array
5314
 */
5315
function forum_get_extra_capabilities() {
5316
    return ['moodle/rating:view', 'moodle/rating:viewany', 'moodle/rating:viewall', 'moodle/rating:rate'];
5317
}
5318
 
5319
/**
5320
 * Adds module specific settings to the settings block
5321
 *
5322
 * @param settings_navigation $settings The settings navigation object
5323
 * @param navigation_node $forumnode The node to add module settings to
5324
 */
5325
function forum_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $forumnode) {
5326
    global $USER, $CFG;
5327
 
5328
    if (empty($settingsnav->get_page()->cm->context)) {
5329
        $settingsnav->get_page()->cm->context = context_module::instance($settingsnav->get_page()->cm->instance);
5330
    }
5331
 
5332
    $vaultfactory = mod_forum\local\container::get_vault_factory();
5333
    $managerfactory = mod_forum\local\container::get_manager_factory();
5334
    $legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
5335
    $forumvault = $vaultfactory->get_forum_vault();
5336
    $forumentity = $forumvault->get_from_id($settingsnav->get_page()->cm->instance);
5337
    $forumobject = $legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($forumentity);
5338
 
5339
    $params = $settingsnav->get_page()->url->params();
5340
    if (!empty($params['d'])) {
5341
        $discussionid = $params['d'];
5342
    }
5343
 
5344
    // For some actions you need to be enrolled, being admin is not enough sometimes here.
5345
    $enrolled = is_enrolled($settingsnav->get_page()->context, $USER, '', false);
5346
    $activeenrolled = is_enrolled($settingsnav->get_page()->context, $USER, '', true);
5347
 
5348
    $canmanage  = has_capability('mod/forum:managesubscriptions', $settingsnav->get_page()->context);
5349
    $subscriptionmode = \mod_forum\subscriptions::get_subscription_mode($forumobject);
5350
    $cansubscribe = $activeenrolled && !\mod_forum\subscriptions::is_forcesubscribed($forumobject) &&
5351
            (!\mod_forum\subscriptions::subscription_disabled($forumobject) || $canmanage);
5352
 
5353
    if ($canmanage) {
5354
        $mode = $forumnode->add(get_string('subscriptionmode', 'forum'), null, navigation_node::TYPE_CONTAINER);
5355
        $mode->add_class('subscriptionmode');
5356
        $mode->set_show_in_secondary_navigation(false);
5357
 
5358
        // Optional subscription mode.
5359
        $allowchoicestring = get_string('subscriptionoptional', 'forum');
5360
        $allowchoiceaction = new action_link(
5361
            new moodle_url('/mod/forum/subscribe.php', [
5362
                'id' => $forumobject->id,
5363
                'mode' => FORUM_CHOOSESUBSCRIBE,
5364
                'sesskey' => sesskey(),
5365
            ]),
5366
            $allowchoicestring,
5367
            new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $allowchoicestring))
5368
        );
5369
        $allowchoice = $mode->add($allowchoicestring, $allowchoiceaction, navigation_node::TYPE_SETTING);
5370
 
5371
        // Forced subscription mode.
5372
        $forceforeverstring = get_string('subscriptionforced', 'forum');
5373
        $forceforeveraction = new action_link(
5374
            new moodle_url('/mod/forum/subscribe.php', [
5375
                'id' => $forumobject->id,
5376
                'mode' => FORUM_FORCESUBSCRIBE,
5377
                'sesskey' => sesskey(),
5378
            ]),
5379
            $forceforeverstring,
5380
            new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $forceforeverstring))
5381
        );
5382
        $forceforever = $mode->add($forceforeverstring, $forceforeveraction, navigation_node::TYPE_SETTING);
5383
 
5384
        // Initial subscription mode.
5385
        $forceinitiallystring = get_string('subscriptionauto', 'forum');
5386
        $forceinitiallyaction = new action_link(
5387
            new moodle_url('/mod/forum/subscribe.php', [
5388
                'id' => $forumobject->id,
5389
                'mode' => FORUM_INITIALSUBSCRIBE,
5390
                'sesskey' => sesskey(),
5391
            ]),
5392
            $forceinitiallystring,
5393
            new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $forceinitiallystring))
5394
        );
5395
        $forceinitially = $mode->add($forceinitiallystring, $forceinitiallyaction, navigation_node::TYPE_SETTING);
5396
 
5397
        // Disabled subscription mode.
5398
        $disallowchoicestring = get_string('subscriptiondisabled', 'forum');
5399
        $disallowchoiceaction = new action_link(
5400
            new moodle_url('/mod/forum/subscribe.php', [
5401
                'id' => $forumobject->id,
5402
                'mode' => FORUM_DISALLOWSUBSCRIBE,
5403
                'sesskey' => sesskey(),
5404
            ]),
5405
            $disallowchoicestring,
5406
            new confirm_action(get_string('subscriptionmodeconfirm', 'mod_forum', $disallowchoicestring))
5407
        );
5408
        $disallowchoice = $mode->add($disallowchoicestring, $disallowchoiceaction, navigation_node::TYPE_SETTING);
5409
 
5410
        switch ($subscriptionmode) {
5411
            case FORUM_CHOOSESUBSCRIBE : // 0
5412
                $allowchoice->action = null;
5413
                $allowchoice->add_class('activesetting');
5414
                $allowchoice->icon = new pix_icon('t/selected', '', 'mod_forum');
5415
                break;
5416
            case FORUM_FORCESUBSCRIBE : // 1
5417
                $forceforever->action = null;
5418
                $forceforever->add_class('activesetting');
5419
                $forceforever->icon = new pix_icon('t/selected', '', 'mod_forum');
5420
                break;
5421
            case FORUM_INITIALSUBSCRIBE : // 2
5422
                $forceinitially->action = null;
5423
                $forceinitially->add_class('activesetting');
5424
                $forceinitially->icon = new pix_icon('t/selected', '', 'mod_forum');
5425
                break;
5426
            case FORUM_DISALLOWSUBSCRIBE : // 3
5427
                $disallowchoice->action = null;
5428
                $disallowchoice->add_class('activesetting');
5429
                $disallowchoice->icon = new pix_icon('t/selected', '', 'mod_forum');
5430
                break;
5431
        }
5432
 
5433
    } else if ($activeenrolled) {
5434
 
5435
        switch ($subscriptionmode) {
5436
            case FORUM_CHOOSESUBSCRIBE : // 0
5437
                $notenode = $forumnode->add(get_string('subscriptionoptional', 'forum'));
5438
                break;
5439
            case FORUM_FORCESUBSCRIBE : // 1
5440
                $notenode = $forumnode->add(get_string('subscriptionforced', 'forum'));
5441
                break;
5442
            case FORUM_INITIALSUBSCRIBE : // 2
5443
                $notenode = $forumnode->add(get_string('subscriptionauto', 'forum'));
5444
                break;
5445
            case FORUM_DISALLOWSUBSCRIBE : // 3
5446
                $notenode = $forumnode->add(get_string('subscriptiondisabled', 'forum'));
5447
                break;
5448
        }
5449
    }
5450
 
5451
    if (has_capability('mod/forum:viewsubscribers', $settingsnav->get_page()->context)) {
5452
        $url = new moodle_url('/mod/forum/subscribers.php', ['id' => $forumobject->id, 'edit' => 'off']);
5453
        $forumnode->add(get_string('subscriptions', 'forum'), $url, navigation_node::TYPE_SETTING, null, 'forumsubscriptions');
5454
    }
5455
 
5456
    // Display all forum reports user has access to.
5457
    if (isloggedin() && !isguestuser()) {
5458
        $reportnames = array_keys(core_component::get_plugin_list('forumreport'));
5459
 
5460
        foreach ($reportnames as $reportname) {
5461
            if (has_capability("forumreport/{$reportname}:view", $settingsnav->get_page()->context)) {
5462
                $reportlinkparams = [
5463
                    'courseid' => $forumobject->course,
5464
                    'forumid' => $forumobject->id,
5465
                ];
5466
                $reportlink = new moodle_url("/mod/forum/report/{$reportname}/index.php", $reportlinkparams);
5467
                $forumnode->add(get_string('reports'), $reportlink, navigation_node::TYPE_CONTAINER);
5468
            }
5469
        }
5470
    }
5471
 
5472
    if ($enrolled && forum_tp_can_track_forums($forumobject)) { // keep tracking info for users with suspended enrolments
5473
        if ($forumobject->trackingtype == FORUM_TRACKING_OPTIONAL
5474
                || ((!$CFG->forum_allowforcedreadtracking) && $forumobject->trackingtype == FORUM_TRACKING_FORCED)) {
5475
            if (forum_tp_is_tracked($forumobject)) {
5476
                $linktext = get_string('notrackforum', 'forum');
5477
            } else {
5478
                $linktext = get_string('trackforum', 'forum');
5479
            }
5480
            $url = new moodle_url('/mod/forum/settracking.php', array(
5481
                    'id' => $forumobject->id,
5482
                    'sesskey' => sesskey(),
5483
                ));
5484
            $forumnode->add($linktext, $url, navigation_node::TYPE_SETTING);
5485
        }
5486
    }
5487
 
5488
    if (!isloggedin() && $settingsnav->get_page()->course->id == SITEID) {
5489
        $userid = guest_user()->id;
5490
    } else {
5491
        $userid = $USER->id;
5492
    }
5493
 
5494
    $hascourseaccess = ($settingsnav->get_page()->course->id == SITEID) ||
5495
        can_access_course($settingsnav->get_page()->course, $userid);
5496
    $enablerssfeeds = !empty($CFG->enablerssfeeds) && !empty($CFG->forum_enablerssfeeds);
5497
 
5498
    if ($enablerssfeeds && $forumobject->rsstype && $forumobject->rssarticles && $hascourseaccess) {
5499
 
5500
        if (!function_exists('rss_get_url')) {
5501
            require_once("$CFG->libdir/rsslib.php");
5502
        }
5503
 
5504
        if ($forumobject->rsstype == 1) {
5505
            $string = get_string('rsssubscriberssdiscussions','forum');
5506
        } else {
5507
            $string = get_string('rsssubscriberssposts','forum');
5508
        }
5509
 
5510
        $url = new moodle_url(rss_get_url($settingsnav->get_page()->cm->context->id, $userid, "mod_forum",
5511
            $forumobject->id));
5512
        $forumnode->add($string, $url, settings_navigation::TYPE_SETTING, null, null, new pix_icon('i/rss', ''));
5513
    }
5514
 
5515
    $capabilitymanager = $managerfactory->get_capability_manager($forumentity);
5516
    if ($capabilitymanager->can_export_forum($USER)) {
5517
        $url = new moodle_url('/mod/forum/export.php', ['id' => $forumobject->id]);
5518
        $forumnode->add(get_string('export', 'mod_forum'), $url, navigation_node::TYPE_SETTING);
5519
    }
5520
}
5521
 
5522
/**
5523
 * Return a list of page types
5524
 * @param string $pagetype current page type
5525
 * @param stdClass $parentcontext Block's parent context
5526
 * @param stdClass $currentcontext Current context of block
5527
 */
5528
function forum_page_type_list($pagetype, $parentcontext, $currentcontext) {
5529
    $forum_pagetype = array(
5530
        'mod-forum-*'=>get_string('page-mod-forum-x', 'forum'),
5531
        'mod-forum-view'=>get_string('page-mod-forum-view', 'forum'),
5532
        'mod-forum-discuss'=>get_string('page-mod-forum-discuss', 'forum')
5533
    );
5534
    return $forum_pagetype;
5535
}
5536
 
5537
/**
5538
 * Gets all of the courses where the provided user has posted in a forum.
5539
 *
5540
 * @global moodle_database $DB The database connection
5541
 * @param stdClass $user The user who's posts we are looking for
5542
 * @param bool $discussionsonly If true only look for discussions started by the user
5543
 * @param bool $includecontexts If set to trye contexts for the courses will be preloaded
5544
 * @param int $limitfrom The offset of records to return
5545
 * @param int $limitnum The number of records to return
5546
 * @return array An array of courses
5547
 */
5548
function forum_get_courses_user_posted_in($user, $discussionsonly = false, $includecontexts = true, $limitfrom = null, $limitnum = null) {
5549
    global $DB;
5550
 
5551
    // If we are only after discussions we need only look at the forum_discussions
5552
    // table and join to the userid there. If we are looking for posts then we need
5553
    // to join to the forum_posts table.
5554
    if (!$discussionsonly) {
5555
        $subquery = "(SELECT DISTINCT fd.course
5556
                         FROM {forum_discussions} fd
5557
                         JOIN {forum_posts} fp ON fp.discussion = fd.id
5558
                        WHERE fp.userid = :userid )";
5559
    } else {
5560
        $subquery= "(SELECT DISTINCT fd.course
5561
                         FROM {forum_discussions} fd
5562
                        WHERE fd.userid = :userid )";
5563
    }
5564
 
5565
    $params = array('userid' => $user->id);
5566
 
5567
    // Join to the context table so that we can preload contexts if required.
5568
    if ($includecontexts) {
5569
        $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
5570
        $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
5571
        $params['contextlevel'] = CONTEXT_COURSE;
5572
    } else {
5573
        $ctxselect = '';
5574
        $ctxjoin = '';
5575
    }
5576
 
5577
    // Now we need to get all of the courses to search.
5578
    // All courses where the user has posted within a forum will be returned.
5579
    $sql = "SELECT c.* $ctxselect
5580
            FROM {course} c
5581
            $ctxjoin
5582
            WHERE c.id IN ($subquery)";
5583
    $courses = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
5584
    if ($includecontexts) {
5585
        array_map('context_helper::preload_from_record', $courses);
5586
    }
5587
    return $courses;
5588
}
5589
 
5590
/**
5591
 * Gets all of the forums a user has posted in for one or more courses.
5592
 *
5593
 * @global moodle_database $DB
5594
 * @param stdClass $user
5595
 * @param array $courseids An array of courseids to search or if not provided
5596
 *                       all courses the user has posted within
5597
 * @param bool $discussionsonly If true then only forums where the user has started
5598
 *                       a discussion will be returned.
5599
 * @param int $limitfrom The offset of records to return
5600
 * @param int $limitnum The number of records to return
5601
 * @return array An array of forums the user has posted within in the provided courses
5602
 */
5603
function forum_get_forums_user_posted_in($user, array $courseids = null, $discussionsonly = false, $limitfrom = null, $limitnum = null) {
5604
    global $DB;
5605
 
5606
    if (!is_null($courseids)) {
5607
        list($coursewhere, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, 'courseid');
5608
        $coursewhere = ' AND f.course '.$coursewhere;
5609
    } else {
5610
        $coursewhere = '';
5611
        $params = array();
5612
    }
5613
    $params['userid'] = $user->id;
5614
    $params['forum'] = 'forum';
5615
 
5616
    if ($discussionsonly) {
5617
        $join = 'JOIN {forum_discussions} ff ON ff.forum = f.id';
5618
    } else {
5619
        $join = 'JOIN {forum_discussions} fd ON fd.forum = f.id
5620
                 JOIN {forum_posts} ff ON ff.discussion = fd.id';
5621
    }
5622
 
5623
    $sql = "SELECT f.*, cm.id AS cmid
5624
              FROM {forum} f
5625
              JOIN {course_modules} cm ON cm.instance = f.id
5626
              JOIN {modules} m ON m.id = cm.module
5627
              JOIN (
5628
                  SELECT f.id
5629
                    FROM {forum} f
5630
                    {$join}
5631
                   WHERE ff.userid = :userid
5632
                GROUP BY f.id
5633
                   ) j ON j.id = f.id
5634
             WHERE m.name = :forum
5635
                 {$coursewhere}";
5636
 
5637
    $courseforums = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
5638
    return $courseforums;
5639
}
5640
 
5641
/**
5642
 * Returns posts made by the selected user in the requested courses.
5643
 *
5644
 * This method can be used to return all of the posts made by the requested user
5645
 * within the given courses.
5646
 * For each course the access of the current user and requested user is checked
5647
 * and then for each post access to the post and forum is checked as well.
5648
 *
5649
 * This function is safe to use with usercapabilities.
5650
 *
5651
 * @global moodle_database $DB
5652
 * @param stdClass $user The user whose posts we want to get
5653
 * @param array $courses The courses to search
5654
 * @param bool $musthaveaccess If set to true errors will be thrown if the user
5655
 *                             cannot access one or more of the courses to search
5656
 * @param bool $discussionsonly If set to true only discussion starting posts
5657
 *                              will be returned.
5658
 * @param int $limitfrom The offset of records to return
5659
 * @param int $limitnum The number of records to return
5660
 * @return stdClass An object the following properties
5661
 *               ->totalcount: the total number of posts made by the requested user
5662
 *                             that the current user can see.
5663
 *               ->courses: An array of courses the current user can see that the
5664
 *                          requested user has posted in.
5665
 *               ->forums: An array of forums relating to the posts returned in the
5666
 *                         property below.
5667
 *               ->posts: An array containing the posts to show for this request.
5668
 */
5669
function forum_get_posts_by_user($user, array $courses, $musthaveaccess = false, $discussionsonly = false, $limitfrom = 0, $limitnum = 50) {
5670
    global $DB, $USER, $CFG;
5671
 
5672
    $return = new stdClass;
5673
    $return->totalcount = 0;    // The total number of posts that the current user is able to view
5674
    $return->courses = array(); // The courses the current user can access
5675
    $return->forums = array();  // The forums that the current user can access that contain posts
5676
    $return->posts = array();   // The posts to display
5677
 
5678
    // First up a small sanity check. If there are no courses to check we can
5679
    // return immediately, there is obviously nothing to search.
5680
    if (empty($courses)) {
5681
        return $return;
5682
    }
5683
 
5684
    // A couple of quick setups
5685
    $isloggedin = isloggedin();
5686
    $isguestuser = $isloggedin && isguestuser();
5687
    $iscurrentuser = $isloggedin && $USER->id == $user->id;
5688
 
5689
    // Checkout whether or not the current user has capabilities over the requested
5690
    // user and if so they have the capabilities required to view the requested
5691
    // users content.
5692
    $usercontext = context_user::instance($user->id, MUST_EXIST);
5693
    $hascapsonuser = !$iscurrentuser && $DB->record_exists('role_assignments', array('userid' => $USER->id, 'contextid' => $usercontext->id));
5694
    $hascapsonuser = $hascapsonuser && has_all_capabilities(array('moodle/user:viewdetails', 'moodle/user:readuserposts'), $usercontext);
5695
 
5696
    // Before we actually search each course we need to check the user's access to the
5697
    // course. If the user doesn't have the appropraite access then we either throw an
5698
    // error if a particular course was requested or we just skip over the course.
5699
    foreach ($courses as $course) {
5700
        $coursecontext = context_course::instance($course->id, MUST_EXIST);
5701
        if ($iscurrentuser || $hascapsonuser) {
5702
            // If it is the current user, or the current user has capabilities to the
5703
            // requested user then all we need to do is check the requested users
5704
            // current access to the course.
5705
            // Note: There is no need to check group access or anything of the like
5706
            // as either the current user is the requested user, or has granted
5707
            // capabilities on the requested user. Either way they can see what the
5708
            // requested user posted, although its VERY unlikely in the `parent` situation
5709
            // that the current user will be able to view the posts in context.
5710
            if (!is_viewing($coursecontext, $user) && !is_enrolled($coursecontext, $user)) {
5711
                // Need to have full access to a course to see the rest of own info
5712
                if ($musthaveaccess) {
5713
                    throw new \moodle_exception('errorenrolmentrequired', 'forum');
5714
                }
5715
                continue;
5716
            }
5717
        } else {
5718
            // Check whether the current user is enrolled or has access to view the course
5719
            // if they don't we immediately have a problem.
5720
            if (!can_access_course($course)) {
5721
                if ($musthaveaccess) {
5722
                    throw new \moodle_exception('errorenrolmentrequired', 'forum');
5723
                }
5724
                continue;
5725
            }
5726
 
5727
            // If groups are in use and enforced throughout the course then make sure
5728
            // we can meet in at least one course level group.
5729
            // Note that we check if either the current user or the requested user have
5730
            // the capability to access all groups. This is because with that capability
5731
            // a user in group A could post in the group B forum. Grrrr.
5732
            if (groups_get_course_groupmode($course) == SEPARATEGROUPS && $course->groupmodeforce
5733
              && !has_capability('moodle/site:accessallgroups', $coursecontext) && !has_capability('moodle/site:accessallgroups', $coursecontext, $user->id)) {
5734
                // If its the guest user to bad... the guest user cannot access groups
5735
                if (!$isloggedin or $isguestuser) {
5736
                    // do not use require_login() here because we might have already used require_login($course)
5737
                    if ($musthaveaccess) {
5738
                        redirect(get_login_url());
5739
                    }
5740
                    continue;
5741
                }
5742
                // Get the groups of the current user
5743
                $mygroups = array_keys(groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid, 'g.id, g.name'));
5744
                // Get the groups the requested user is a member of
5745
                $usergroups = array_keys(groups_get_all_groups($course->id, $user->id, $course->defaultgroupingid, 'g.id, g.name'));
5746
                // Check whether they are members of the same group. If they are great.
5747
                $intersect = array_intersect($mygroups, $usergroups);
5748
                if (empty($intersect)) {
5749
                    // But they're not... if it was a specific course throw an error otherwise
5750
                    // just skip this course so that it is not searched.
5751
                    if ($musthaveaccess) {
5752
                        throw new \moodle_exception("groupnotamember", '', $CFG->wwwroot."/course/view.php?id=$course->id");
5753
                    }
5754
                    continue;
5755
                }
5756
            }
5757
        }
5758
        // Woo hoo we got this far which means the current user can search this
5759
        // this course for the requested user. Although this is only the course accessibility
5760
        // handling that is complete, the forum accessibility tests are yet to come.
5761
        $return->courses[$course->id] = $course;
5762
    }
5763
    // No longer beed $courses array - lose it not it may be big
5764
    unset($courses);
5765
 
5766
    // Make sure that we have some courses to search
5767
    if (empty($return->courses)) {
5768
        // If we don't have any courses to search then the reality is that the current
5769
        // user doesn't have access to any courses is which the requested user has posted.
5770
        // Although we do know at this point that the requested user has posts.
5771
        if ($musthaveaccess) {
5772
            throw new \moodle_exception('permissiondenied');
5773
        } else {
5774
            return $return;
5775
        }
5776
    }
5777
 
5778
    // Next step: Collect all of the forums that we will want to search.
5779
    // It is important to note that this step isn't actually about searching, it is
5780
    // about determining which forums we can search by testing accessibility.
5781
    $forums = forum_get_forums_user_posted_in($user, array_keys($return->courses), $discussionsonly);
5782
 
5783
    // Will be used to build the where conditions for the search
5784
    $forumsearchwhere = array();
5785
    // Will be used to store the where condition params for the search
5786
    $forumsearchparams = array();
5787
    // Will record forums where the user can freely access everything
5788
    $forumsearchfullaccess = array();
5789
    // DB caching friendly
5790
    $now = floor(time() / 60) * 60;
5791
    // For each course to search we want to find the forums the user has posted in
5792
    // and providing the current user can access the forum create a search condition
5793
    // for the forum to get the requested users posts.
5794
    foreach ($return->courses as $course) {
5795
        // Now we need to get the forums
5796
        $modinfo = get_fast_modinfo($course);
5797
        if (empty($modinfo->instances['forum'])) {
5798
            // hmmm, no forums? well at least its easy... skip!
5799
            continue;
5800
        }
5801
        // Iterate
5802
        foreach ($modinfo->get_instances_of('forum') as $forumid => $cm) {
5803
            if (!$cm->uservisible or !isset($forums[$forumid])) {
5804
                continue;
5805
            }
5806
            // Get the forum in question
5807
            $forum = $forums[$forumid];
5808
 
5809
            // This is needed for functionality later on in the forum code. It is converted to an object
5810
            // because the cm_info is readonly from 2.6. This is a dirty hack because some other parts of the
5811
            // code were expecting an writeable object. See {@link forum_print_post()}.
5812
            $forum->cm = new stdClass();
5813
            foreach ($cm as $key => $value) {
5814
                $forum->cm->$key = $value;
5815
            }
5816
 
5817
            // Check that either the current user can view the forum, or that the
5818
            // current user has capabilities over the requested user and the requested
5819
            // user can view the discussion
5820
            if (!has_capability('mod/forum:viewdiscussion', $cm->context) && !($hascapsonuser && has_capability('mod/forum:viewdiscussion', $cm->context, $user->id))) {
5821
                continue;
5822
            }
5823
 
5824
            // This will contain forum specific where clauses
5825
            $forumsearchselect = array();
5826
            if (!$iscurrentuser && !$hascapsonuser) {
5827
                // Make sure we check group access
5828
                if (groups_get_activity_groupmode($cm, $course) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $cm->context)) {
5829
                    $groups = $modinfo->get_groups($cm->groupingid);
5830
                    $groups[] = -1;
5831
                    list($groupid_sql, $groupid_params) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grps'.$forumid.'_');
5832
                    $forumsearchparams = array_merge($forumsearchparams, $groupid_params);
5833
                    $forumsearchselect[] = "d.groupid $groupid_sql";
5834
                }
5835
 
5836
                // hidden timed discussions
5837
                if (!empty($CFG->forum_enabletimedposts) && !has_capability('mod/forum:viewhiddentimedposts', $cm->context)) {
5838
                    $forumsearchselect[] = "(d.userid = :userid{$forumid} OR (d.timestart < :timestart{$forumid} AND (d.timeend = 0 OR d.timeend > :timeend{$forumid})))";
5839
                    $forumsearchparams['userid'.$forumid] = $user->id;
5840
                    $forumsearchparams['timestart'.$forumid] = $now;
5841
                    $forumsearchparams['timeend'.$forumid] = $now;
5842
                }
5843
 
5844
                // qanda access
5845
                if ($forum->type == 'qanda' && !has_capability('mod/forum:viewqandawithoutposting', $cm->context)) {
5846
                    // We need to check whether the user has posted in the qanda forum.
5847
                    $discussionspostedin = forum_discussions_user_has_posted_in($forum->id, $user->id);
5848
                    if (!empty($discussionspostedin)) {
5849
                        $forumonlydiscussions = array();  // Holds discussion ids for the discussions the user is allowed to see in this forum.
5850
                        foreach ($discussionspostedin as $d) {
5851
                            $forumonlydiscussions[] = $d->id;
5852
                        }
5853
                        list($discussionid_sql, $discussionid_params) = $DB->get_in_or_equal($forumonlydiscussions, SQL_PARAMS_NAMED, 'qanda'.$forumid.'_');
5854
                        $forumsearchparams = array_merge($forumsearchparams, $discussionid_params);
5855
                        $forumsearchselect[] = "(d.id $discussionid_sql OR p.parent = 0)";
5856
                    } else {
5857
                        $forumsearchselect[] = "p.parent = 0";
5858
                    }
5859
 
5860
                }
5861
 
5862
                if (count($forumsearchselect) > 0) {
5863
                    $forumsearchwhere[] = "(d.forum = :forum{$forumid} AND ".implode(" AND ", $forumsearchselect).")";
5864
                    $forumsearchparams['forum'.$forumid] = $forumid;
5865
                } else {
5866
                    $forumsearchfullaccess[] = $forumid;
5867
                }
5868
            } else {
5869
                // The current user/parent can see all of their own posts
5870
                $forumsearchfullaccess[] = $forumid;
5871
            }
5872
        }
5873
    }
5874
 
5875
    // If we dont have any search conditions, and we don't have any forums where
5876
    // the user has full access then we just return the default.
5877
    if (empty($forumsearchwhere) && empty($forumsearchfullaccess)) {
5878
        return $return;
5879
    }
5880
 
5881
    // Prepare a where condition for the full access forums.
5882
    if (count($forumsearchfullaccess) > 0) {
5883
        list($fullidsql, $fullidparams) = $DB->get_in_or_equal($forumsearchfullaccess, SQL_PARAMS_NAMED, 'fula');
5884
        $forumsearchparams = array_merge($forumsearchparams, $fullidparams);
5885
        $forumsearchwhere[] = "(d.forum $fullidsql)";
5886
    }
5887
 
5888
    // Prepare SQL to both count and search.
5889
    // We alias user.id to useridx because we forum_posts already has a userid field and not aliasing this would break
5890
    // oracle and mssql.
5891
    $userfieldsapi = \core_user\fields::for_userpic();
5892
    $userfields = $userfieldsapi->get_sql('u', false, '', 'useridx', false)->selects;
5893
    $countsql = 'SELECT COUNT(*) ';
5894
    $selectsql = 'SELECT p.*, d.forum, d.name AS discussionname, '.$userfields.' ';
5895
    $wheresql = implode(" OR ", $forumsearchwhere);
5896
 
5897
    if ($discussionsonly) {
5898
        if ($wheresql == '') {
5899
            $wheresql = 'p.parent = 0';
5900
        } else {
5901
            $wheresql = 'p.parent = 0 AND ('.$wheresql.')';
5902
        }
5903
    }
5904
 
5905
    $sql = "FROM {forum_posts} p
5906
            JOIN {forum_discussions} d ON d.id = p.discussion
5907
            JOIN {user} u ON u.id = p.userid
5908
           WHERE ($wheresql)
5909
             AND p.userid = :userid ";
5910
    $orderby = "ORDER BY p.modified DESC";
5911
    $forumsearchparams['userid'] = $user->id;
5912
 
5913
    // Set the total number posts made by the requested user that the current user can see
5914
    $return->totalcount = $DB->count_records_sql($countsql.$sql, $forumsearchparams);
5915
    // Set the collection of posts that has been requested
5916
    $return->posts = $DB->get_records_sql($selectsql.$sql.$orderby, $forumsearchparams, $limitfrom, $limitnum);
5917
 
5918
    // We need to build an array of forums for which posts will be displayed.
5919
    // We do this here to save the caller needing to retrieve them themselves before
5920
    // printing these forums posts. Given we have the forums already there is
5921
    // practically no overhead here.
5922
    foreach ($return->posts as $post) {
5923
        if (!array_key_exists($post->forum, $return->forums)) {
5924
            $return->forums[$post->forum] = $forums[$post->forum];
5925
        }
5926
    }
5927
 
5928
    return $return;
5929
}
5930
 
5931
/**
5932
 * Set the per-forum maildigest option for the specified user.
5933
 *
5934
 * @param stdClass $forum The forum to set the option for.
5935
 * @param int $maildigest The maildigest option.
5936
 * @param stdClass $user The user object. This defaults to the global $USER object.
5937
 * @throws invalid_digest_setting thrown if an invalid maildigest option is provided.
5938
 */
5939
function forum_set_user_maildigest($forum, $maildigest, $user = null) {
5940
    global $DB, $USER;
5941
 
5942
    if (is_number($forum)) {
5943
        $forum = $DB->get_record('forum', array('id' => $forum));
5944
    }
5945
 
5946
    if ($user === null) {
5947
        $user = $USER;
5948
    }
5949
 
5950
    $course  = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
5951
    $cm      = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST);
5952
    $context = context_module::instance($cm->id);
5953
 
5954
    // User must be allowed to see this forum.
5955
    require_capability('mod/forum:viewdiscussion', $context, $user->id);
5956
 
5957
    // Validate the maildigest setting.
5958
    $digestoptions = forum_get_user_digest_options($user);
5959
 
5960
    if (!isset($digestoptions[$maildigest])) {
5961
        throw new moodle_exception('invaliddigestsetting', 'mod_forum');
5962
    }
5963
 
5964
    // Attempt to retrieve any existing forum digest record.
5965
    $subscription = $DB->get_record('forum_digests', array(
5966
        'userid' => $user->id,
5967
        'forum' => $forum->id,
5968
    ));
5969
 
5970
    // Create or Update the existing maildigest setting.
5971
    if ($subscription) {
5972
        if ($maildigest == -1) {
5973
            $DB->delete_records('forum_digests', array('forum' => $forum->id, 'userid' => $user->id));
5974
        } else if ($maildigest !== $subscription->maildigest) {
5975
            // Only update the maildigest setting if it's changed.
5976
 
5977
            $subscription->maildigest = $maildigest;
5978
            $DB->update_record('forum_digests', $subscription);
5979
        }
5980
    } else {
5981
        if ($maildigest != -1) {
5982
            // Only insert the maildigest setting if it's non-default.
5983
 
5984
            $subscription = new stdClass();
5985
            $subscription->forum = $forum->id;
5986
            $subscription->userid = $user->id;
5987
            $subscription->maildigest = $maildigest;
5988
            $subscription->id = $DB->insert_record('forum_digests', $subscription);
5989
        }
5990
    }
5991
}
5992
 
5993
/**
5994
 * Determine the maildigest setting for the specified user against the
5995
 * specified forum.
5996
 *
5997
 * @param Array $digests An array of forums and user digest settings.
5998
 * @param stdClass $user The user object containing the id and maildigest default.
5999
 * @param int $forumid The ID of the forum to check.
6000
 * @return int The calculated maildigest setting for this user and forum.
6001
 */
6002
function forum_get_user_maildigest_bulk($digests, $user, $forumid) {
6003
    if (isset($digests[$forumid]) && isset($digests[$forumid][$user->id])) {
6004
        $maildigest = $digests[$forumid][$user->id];
6005
        if ($maildigest === -1) {
6006
            $maildigest = $user->maildigest;
6007
        }
6008
    } else {
6009
        $maildigest = $user->maildigest;
6010
    }
6011
    return $maildigest;
6012
}
6013
 
6014
/**
6015
 * Retrieve the list of available user digest options.
6016
 *
6017
 * @param stdClass $user The user object. This defaults to the global $USER object.
6018
 * @return array The mapping of values to digest options.
6019
 */
6020
function forum_get_user_digest_options($user = null) {
6021
    global $USER;
6022
 
6023
    // Revert to the global user object.
6024
    if ($user === null) {
6025
        $user = $USER;
6026
    }
6027
 
6028
    $digestoptions = array();
6029
    $digestoptions['0']  = get_string('emaildigestoffshort', 'mod_forum');
6030
    $digestoptions['1']  = get_string('emaildigestcompleteshort', 'mod_forum');
6031
    $digestoptions['2']  = get_string('emaildigestsubjectsshort', 'mod_forum');
6032
 
6033
    // We need to add the default digest option at the end - it relies on
6034
    // the contents of the existing values.
6035
    $digestoptions['-1'] = get_string('emaildigestdefault', 'mod_forum',
6036
            $digestoptions[$user->maildigest]);
6037
 
6038
    // Resort the options to be in a sensible order.
6039
    ksort($digestoptions);
6040
 
6041
    return $digestoptions;
6042
}
6043
 
6044
/**
6045
 * Determine the current context if one was not already specified.
6046
 *
6047
 * If a context of type context_module is specified, it is immediately
6048
 * returned and not checked.
6049
 *
6050
 * @param int $forumid The ID of the forum
6051
 * @param context_module $context The current context.
6052
 * @return context_module The context determined
6053
 */
6054
function forum_get_context($forumid, $context = null) {
6055
    global $PAGE;
6056
 
6057
    if (!$context || !($context instanceof context_module)) {
6058
        // Find out forum context. First try to take current page context to save on DB query.
6059
        if ($PAGE->cm && $PAGE->cm->modname === 'forum' && $PAGE->cm->instance == $forumid
6060
                && $PAGE->context->contextlevel == CONTEXT_MODULE && $PAGE->context->instanceid == $PAGE->cm->id) {
6061
            $context = $PAGE->context;
6062
        } else {
6063
            $cm = get_coursemodule_from_instance('forum', $forumid);
6064
            $context = \context_module::instance($cm->id);
6065
        }
6066
    }
6067
 
6068
    return $context;
6069
}
6070
 
6071
/**
6072
 * Mark the activity completed (if required) and trigger the course_module_viewed event.
6073
 *
6074
 * @param  stdClass $forum   forum object
6075
 * @param  stdClass $course  course object
6076
 * @param  stdClass $cm      course module object
6077
 * @param  stdClass $context context object
6078
 * @since Moodle 2.9
6079
 */
6080
function forum_view($forum, $course, $cm, $context) {
6081
 
6082
    // Completion.
6083
    $completion = new completion_info($course);
6084
    $completion->set_module_viewed($cm);
6085
 
6086
    // Trigger course_module_viewed event.
6087
 
6088
    $params = array(
6089
        'context' => $context,
6090
        'objectid' => $forum->id
6091
    );
6092
 
6093
    $event = \mod_forum\event\course_module_viewed::create($params);
6094
    $event->add_record_snapshot('course_modules', $cm);
6095
    $event->add_record_snapshot('course', $course);
6096
    $event->add_record_snapshot('forum', $forum);
6097
    $event->trigger();
6098
}
6099
 
6100
/**
6101
 * Trigger the discussion viewed event
6102
 *
6103
 * @param  stdClass $modcontext module context object
6104
 * @param  stdClass $forum      forum object
6105
 * @param  stdClass $discussion discussion object
6106
 * @since Moodle 2.9
6107
 */
6108
function forum_discussion_view($modcontext, $forum, $discussion) {
6109
    $params = array(
6110
        'context' => $modcontext,
6111
        'objectid' => $discussion->id,
6112
    );
6113
 
6114
    $event = \mod_forum\event\discussion_viewed::create($params);
6115
    $event->add_record_snapshot('forum_discussions', $discussion);
6116
    $event->add_record_snapshot('forum', $forum);
6117
    $event->trigger();
6118
}
6119
 
6120
/**
6121
 * Set the discussion to pinned and trigger the discussion pinned event
6122
 *
6123
 * @param  stdClass $modcontext module context object
6124
 * @param  stdClass $forum      forum object
6125
 * @param  stdClass $discussion discussion object
6126
 * @since Moodle 3.1
6127
 */
6128
function forum_discussion_pin($modcontext, $forum, $discussion) {
6129
    global $DB;
6130
 
6131
    $DB->set_field('forum_discussions', 'pinned', FORUM_DISCUSSION_PINNED, array('id' => $discussion->id));
6132
 
6133
    $params = array(
6134
        'context' => $modcontext,
6135
        'objectid' => $discussion->id,
6136
        'other' => array('forumid' => $forum->id)
6137
    );
6138
 
6139
    $event = \mod_forum\event\discussion_pinned::create($params);
6140
    $event->add_record_snapshot('forum_discussions', $discussion);
6141
    $event->trigger();
6142
}
6143
 
6144
/**
6145
 * Set discussion to unpinned and trigger the discussion unpin event
6146
 *
6147
 * @param  stdClass $modcontext module context object
6148
 * @param  stdClass $forum      forum object
6149
 * @param  stdClass $discussion discussion object
6150
 * @since Moodle 3.1
6151
 */
6152
function forum_discussion_unpin($modcontext, $forum, $discussion) {
6153
    global $DB;
6154
 
6155
    $DB->set_field('forum_discussions', 'pinned', FORUM_DISCUSSION_UNPINNED, array('id' => $discussion->id));
6156
 
6157
    $params = array(
6158
        'context' => $modcontext,
6159
        'objectid' => $discussion->id,
6160
        'other' => array('forumid' => $forum->id)
6161
    );
6162
 
6163
    $event = \mod_forum\event\discussion_unpinned::create($params);
6164
    $event->add_record_snapshot('forum_discussions', $discussion);
6165
    $event->trigger();
6166
}
6167
 
6168
/**
6169
 * Add nodes to myprofile page.
6170
 *
6171
 * @param \core_user\output\myprofile\tree $tree Tree object
6172
 * @param stdClass $user user object
6173
 * @param bool $iscurrentuser
6174
 * @param stdClass $course Course object
6175
 *
6176
 * @return bool
6177
 */
6178
function mod_forum_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) {
6179
    if (isguestuser($user)) {
6180
        // The guest user cannot post, so it is not possible to view any posts.
6181
        // May as well just bail aggressively here.
6182
        return false;
6183
    }
6184
    $postsurl = new moodle_url('/mod/forum/user.php', array('id' => $user->id));
6185
    if (!empty($course)) {
6186
        $postsurl->param('course', $course->id);
6187
    }
6188
    $string = get_string('forumposts', 'mod_forum');
6189
    $node = new core_user\output\myprofile\node('miscellaneous', 'forumposts', $string, null, $postsurl);
6190
    $tree->add_node($node);
6191
 
6192
    $discussionssurl = new moodle_url('/mod/forum/user.php', array('id' => $user->id, 'mode' => 'discussions'));
6193
    if (!empty($course)) {
6194
        $discussionssurl->param('course', $course->id);
6195
    }
6196
    $string = get_string('myprofileotherdis', 'mod_forum');
6197
    $node = new core_user\output\myprofile\node('miscellaneous', 'forumdiscussions', $string, null,
6198
        $discussionssurl);
6199
    $tree->add_node($node);
6200
 
6201
    return true;
6202
}
6203
 
6204
/**
6205
 * Checks whether the author's name and picture for a given post should be hidden or not.
6206
 *
6207
 * @param object $post The forum post.
6208
 * @param object $forum The forum object.
6209
 * @return bool
6210
 * @throws coding_exception
6211
 */
6212
function forum_is_author_hidden($post, $forum) {
6213
    if (!isset($post->parent)) {
6214
        throw new coding_exception('$post->parent must be set.');
6215
    }
6216
    if (!isset($forum->type)) {
6217
        throw new coding_exception('$forum->type must be set.');
6218
    }
6219
    if ($forum->type === 'single' && empty($post->parent)) {
6220
        return true;
6221
    }
6222
    return false;
6223
}
6224
 
6225
/**
6226
 * Manage inplace editable saves.
6227
 *
6228
 * @param   string      $itemtype       The type of item.
6229
 * @param   int         $itemid         The ID of the item.
6230
 * @param   mixed       $newvalue       The new value
6231
 * @return  string
6232
 */
6233
function mod_forum_inplace_editable($itemtype, $itemid, $newvalue) {
6234
    global $DB, $PAGE;
6235
 
6236
    if ($itemtype === 'digestoptions') {
6237
        // The itemid is the forumid.
6238
        $forum   = $DB->get_record('forum', array('id' => $itemid), '*', MUST_EXIST);
6239
        $course  = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
6240
        $cm      = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST);
6241
        $context = context_module::instance($cm->id);
6242
 
6243
        $PAGE->set_context($context);
6244
        require_login($course, false, $cm);
6245
        forum_set_user_maildigest($forum, $newvalue);
6246
 
6247
        $renderer = $PAGE->get_renderer('mod_forum');
6248
        return $renderer->render_digest_options($forum, $newvalue);
6249
    }
6250
}
6251
 
6252
/**
6253
 * Determine whether the specified forum's cutoff date is reached.
6254
 *
6255
 * @param stdClass $forum The forum
6256
 * @return bool
6257
 */
6258
function forum_is_cutoff_date_reached($forum) {
6259
    $entityfactory = \mod_forum\local\container::get_entity_factory();
6260
    $coursemoduleinfo = get_fast_modinfo($forum->course);
6261
    $cminfo = $coursemoduleinfo->instances['forum'][$forum->id];
6262
    $forumentity = $entityfactory->get_forum_from_stdclass(
6263
            $forum,
6264
            context_module::instance($cminfo->id),
6265
            $cminfo->get_course_module_record(),
6266
            $cminfo->get_course()
6267
    );
6268
 
6269
    return $forumentity->is_cutoff_date_reached();
6270
}
6271
 
6272
/**
6273
 * Determine whether the specified forum's due date is reached.
6274
 *
6275
 * @param stdClass $forum The forum
6276
 * @return bool
6277
 */
6278
function forum_is_due_date_reached($forum) {
6279
    $entityfactory = \mod_forum\local\container::get_entity_factory();
6280
    $coursemoduleinfo = get_fast_modinfo($forum->course);
6281
    $cminfo = $coursemoduleinfo->instances['forum'][$forum->id];
6282
    $forumentity = $entityfactory->get_forum_from_stdclass(
6283
            $forum,
6284
            context_module::instance($cminfo->id),
6285
            $cminfo->get_course_module_record(),
6286
            $cminfo->get_course()
6287
    );
6288
 
6289
    return $forumentity->is_due_date_reached();
6290
}
6291
 
6292
/**
6293
 * Determine whether the specified discussion is time-locked.
6294
 *
6295
 * @param   stdClass    $forum          The forum that the discussion belongs to
6296
 * @param   stdClass    $discussion     The discussion to test
6297
 * @return  bool
6298
 */
6299
function forum_discussion_is_locked($forum, $discussion) {
6300
    $entityfactory = \mod_forum\local\container::get_entity_factory();
6301
    $coursemoduleinfo = get_fast_modinfo($forum->course);
6302
    $cminfo = $coursemoduleinfo->instances['forum'][$forum->id];
6303
    $forumentity = $entityfactory->get_forum_from_stdclass(
6304
        $forum,
6305
        context_module::instance($cminfo->id),
6306
        $cminfo->get_course_module_record(),
6307
        $cminfo->get_course()
6308
    );
6309
    $discussionentity = $entityfactory->get_discussion_from_stdclass($discussion);
6310
 
6311
    return $forumentity->is_discussion_locked($discussionentity);
6312
}
6313
 
6314
/**
6315
 * Check if the module has any update that affects the current user since a given time.
6316
 *
6317
 * @param  cm_info $cm course module data
6318
 * @param  int $from the time to check updates from
6319
 * @param  array $filter  if we need to check only specific updates
6320
 * @return stdClass an object with the different type of areas indicating if they were updated or not
6321
 * @since Moodle 3.2
6322
 */
6323
function forum_check_updates_since(cm_info $cm, $from, $filter = array()) {
6324
 
6325
    $context = $cm->context;
6326
    $updates = new stdClass();
6327
    if (!has_capability('mod/forum:viewdiscussion', $context)) {
6328
        return $updates;
6329
    }
6330
 
6331
    $updates = course_check_module_updates_since($cm, $from, array(), $filter);
6332
 
6333
    // Check if there are new discussions in the forum.
6334
    $updates->discussions = (object) array('updated' => false);
6335
    $discussions = forum_get_discussions($cm, '', false, -1, -1, true, -1, 0, FORUM_POSTS_ALL_USER_GROUPS, $from);
6336
    if (!empty($discussions)) {
6337
        $updates->discussions->updated = true;
6338
        $updates->discussions->itemids = array_keys($discussions);
6339
    }
6340
 
6341
    return $updates;
6342
}
6343
 
6344
/**
6345
 * Check if the user can create attachments in a forum.
6346
 * @param  stdClass $forum   forum object
6347
 * @param  stdClass $context context object
6348
 * @return bool true if the user can create attachments, false otherwise
6349
 * @since  Moodle 3.3
6350
 */
6351
function forum_can_create_attachment($forum, $context) {
6352
    // If maxbytes == 1 it means no attachments at all.
6353
    if (empty($forum->maxattachments) || $forum->maxbytes == 1 ||
6354
            !has_capability('mod/forum:createattachment', $context)) {
6355
        return false;
6356
    }
6357
    return true;
6358
}
6359
 
6360
/**
6361
 * Get icon mapping for font-awesome.
6362
 *
6363
 * @return  array
6364
 */
6365
function mod_forum_get_fontawesome_icon_map() {
6366
    return [
6367
        'mod_forum:i/pinned' => 'fa-map-pin',
6368
        'mod_forum:t/selected' => 'fa-check',
6369
        'mod_forum:t/subscribed' => 'fa-envelope-o',
6370
        'mod_forum:t/unsubscribed' => 'fa-envelope-open-o',
6371
        'mod_forum:t/star' => 'fa-star',
6372
    ];
6373
}
6374
 
6375
/**
6376
 * Callback function that determines whether an action event should be showing its item count
6377
 * based on the event type and the item count.
6378
 *
6379
 * @param calendar_event $event The calendar event.
6380
 * @param int $itemcount The item count associated with the action event.
6381
 * @return bool
6382
 */
6383
function mod_forum_core_calendar_event_action_shows_item_count(calendar_event $event, $itemcount = 0) {
6384
    // Always show item count for forums if item count is greater than 1.
6385
    // If only one action is required than it is obvious and we don't show it for other modules.
6386
    return $itemcount > 1;
6387
}
6388
 
6389
/**
6390
 * This function receives a calendar event and returns the action associated with it, or null if there is none.
6391
 *
6392
 * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
6393
 * is not displayed on the block.
6394
 *
6395
 * @param calendar_event $event
6396
 * @param \core_calendar\action_factory $factory
6397
 * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
6398
 * @return \core_calendar\local\event\entities\action_interface|null
6399
 */
6400
function mod_forum_core_calendar_provide_event_action(calendar_event $event,
6401
                                                      \core_calendar\action_factory $factory,
6402
                                                      int $userid = 0) {
6403
    global $DB, $USER;
6404
 
6405
    if (!$userid) {
6406
        $userid = $USER->id;
6407
    }
6408
 
6409
    $cm = get_fast_modinfo($event->courseid, $userid)->instances['forum'][$event->instance];
6410
 
6411
    if (!$cm->uservisible) {
6412
        // The module is not visible to the user for any reason.
6413
        return null;
6414
    }
6415
 
6416
    $context = context_module::instance($cm->id);
6417
 
6418
    if (!has_capability('mod/forum:viewdiscussion', $context, $userid)) {
6419
        return null;
6420
    }
6421
 
6422
    $completion = new \completion_info($cm->get_course());
6423
 
6424
    $completiondata = $completion->get_data($cm, false, $userid);
6425
 
6426
    if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
6427
        return null;
6428
    }
6429
 
6430
    // Get action itemcount.
6431
    $itemcount = 0;
6432
    $forum = $DB->get_record('forum', array('id' => $cm->instance));
6433
    $postcountsql = "
6434
                SELECT
6435
                    COUNT(1)
6436
                  FROM
6437
                    {forum_posts} fp
6438
                    INNER JOIN {forum_discussions} fd ON fp.discussion=fd.id
6439
                 WHERE
6440
                    fp.userid=:userid AND fd.forum=:forumid";
6441
    $postcountparams = array('userid' => $userid, 'forumid' => $forum->id);
6442
 
6443
    if ($forum->completiondiscussions) {
6444
        $count = $DB->count_records('forum_discussions', array('forum' => $forum->id, 'userid' => $userid));
6445
        $itemcount += ($forum->completiondiscussions >= $count) ? ($forum->completiondiscussions - $count) : 0;
6446
    }
6447
 
6448
    if ($forum->completionreplies) {
6449
        $count = $DB->get_field_sql( $postcountsql.' AND fp.parent<>0', $postcountparams);
6450
        $itemcount += ($forum->completionreplies >= $count) ? ($forum->completionreplies - $count) : 0;
6451
    }
6452
 
6453
    if ($forum->completionposts) {
6454
        $count = $DB->get_field_sql($postcountsql, $postcountparams);
6455
        $itemcount += ($forum->completionposts >= $count) ? ($forum->completionposts - $count) : 0;
6456
    }
6457
 
6458
    // Well there is always atleast one actionable item (view forum, etc).
6459
    $itemcount = $itemcount > 0 ? $itemcount : 1;
6460
 
6461
    return $factory->create_instance(
6462
        get_string('view'),
6463
        new \moodle_url('/mod/forum/view.php', ['id' => $cm->id]),
6464
        $itemcount,
6465
        true
6466
    );
6467
}
6468
 
6469
/**
6470
 * Add a get_coursemodule_info function in case any forum type wants to add 'extra' information
6471
 * for the course (see resource).
6472
 *
6473
 * Given a course_module object, this function returns any "extra" information that may be needed
6474
 * when printing this activity in a course listing.  See get_array_of_activities() in course/lib.php.
6475
 *
6476
 * @param stdClass $coursemodule The coursemodule object (record).
6477
 * @return cached_cm_info An object on information that the courses
6478
 *                        will know about (most noticeably, an icon).
6479
 */
6480
function forum_get_coursemodule_info($coursemodule) {
6481
    global $DB;
6482
 
6483
    $dbparams = ['id' => $coursemodule->instance];
6484
    $fields = 'id, name, intro, introformat, completionposts, completiondiscussions, completionreplies, duedate, cutoffdate, trackingtype';
6485
    if (!$forum = $DB->get_record('forum', $dbparams, $fields)) {
6486
        return false;
6487
    }
6488
 
6489
    $result = new cached_cm_info();
6490
    $result->name = $forum->name;
6491
 
6492
    if ($coursemodule->showdescription) {
6493
        // Convert intro to html. Do not filter cached version, filters run at display time.
6494
        $result->content = format_module_intro('forum', $forum, $coursemodule->id, false);
6495
    }
6496
 
6497
    // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'.
6498
    if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
6499
        $result->customdata['customcompletionrules']['completiondiscussions'] = $forum->completiondiscussions;
6500
        $result->customdata['customcompletionrules']['completionreplies'] = $forum->completionreplies;
6501
        $result->customdata['customcompletionrules']['completionposts'] = $forum->completionposts;
6502
    }
6503
 
6504
    // Populate some other values that can be used in calendar or on dashboard.
6505
    if ($forum->duedate) {
6506
        $result->customdata['duedate'] = $forum->duedate;
6507
    }
6508
    if ($forum->cutoffdate) {
6509
        $result->customdata['cutoffdate'] = $forum->cutoffdate;
6510
    }
6511
    // Add the forum type to the custom data for Web Services (core_course_get_contents).
6512
    $result->customdata['trackingtype'] = $forum->trackingtype;
6513
 
6514
    return $result;
6515
}
6516
 
6517
/**
6518
 * Callback which returns human-readable strings describing the active completion custom rules for the module instance.
6519
 *
6520
 * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules']
6521
 * @return array $descriptions the array of descriptions for the custom rules.
6522
 */
6523
function mod_forum_get_completion_active_rule_descriptions($cm) {
6524
    // Values will be present in cm_info, and we assume these are up to date.
6525
    if (empty($cm->customdata['customcompletionrules'])
6526
        || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) {
6527
        return [];
6528
    }
6529
 
6530
    $descriptions = [];
6531
    foreach ($cm->customdata['customcompletionrules'] as $key => $val) {
6532
        switch ($key) {
6533
            case 'completiondiscussions':
6534
                if (!empty($val)) {
6535
                    $descriptions[] = get_string('completiondiscussionsdesc', 'forum', $val);
6536
                }
6537
                break;
6538
            case 'completionreplies':
6539
                if (!empty($val)) {
6540
                    $descriptions[] = get_string('completionrepliesdesc', 'forum', $val);
6541
                }
6542
                break;
6543
            case 'completionposts':
6544
                if (!empty($val)) {
6545
                    $descriptions[] = get_string('completionpostsdesc', 'forum', $val);
6546
                }
6547
                break;
6548
            default:
6549
                break;
6550
        }
6551
    }
6552
    return $descriptions;
6553
}
6554
 
6555
/**
6556
 * Check whether the forum post is a private reply visible to this user.
6557
 *
6558
 * @param   stdClass    $post   The post to check.
6559
 * @param   cm_info     $cm     The context module instance.
6560
 * @return  bool                Whether the post is visible in terms of private reply configuration.
6561
 */
6562
function forum_post_is_visible_privately($post, $cm) {
6563
    global $USER;
6564
 
6565
    if (!empty($post->privatereplyto)) {
6566
        // Allow the user to see the private reply if:
6567
        // * they hold the permission;
6568
        // * they are the author; or
6569
        // * they are the intended recipient.
6570
        $cansee = false;
6571
        $cansee = $cansee || ($post->userid == $USER->id);
6572
        $cansee = $cansee || ($post->privatereplyto == $USER->id);
6573
        $cansee = $cansee || has_capability('mod/forum:readprivatereplies', context_module::instance($cm->id));
6574
        return $cansee;
6575
    }
6576
 
6577
    return true;
6578
}
6579
 
6580
/**
6581
 * Check whether the user can reply privately to the parent post.
6582
 *
6583
 * @param   \context_module $context
6584
 * @param   \stdClass   $parent
6585
 * @return  bool
6586
 */
6587
function forum_user_can_reply_privately(\context_module $context, \stdClass $parent): bool {
6588
    if ($parent->privatereplyto) {
6589
        // You cannot reply privately to a post which is, itself, a private reply.
6590
        return false;
6591
    }
6592
 
6593
    return has_capability('mod/forum:postprivatereply', $context);
6594
}
6595
 
6596
/**
6597
 * This function calculates the minimum and maximum cutoff values for the timestart of
6598
 * the given event.
6599
 *
6600
 * It will return an array with two values, the first being the minimum cutoff value and
6601
 * the second being the maximum cutoff value. Either or both values can be null, which
6602
 * indicates there is no minimum or maximum, respectively.
6603
 *
6604
 * If a cutoff is required then the function must return an array containing the cutoff
6605
 * timestamp and error string to display to the user if the cutoff value is violated.
6606
 *
6607
 * A minimum and maximum cutoff return value will look like:
6608
 * [
6609
 *     [1505704373, 'The date must be after this date'],
6610
 *     [1506741172, 'The date must be before this date']
6611
 * ]
6612
 *
6613
 * @param calendar_event $event The calendar event to get the time range for
6614
 * @param stdClass $forum The module instance to get the range from
6615
 * @return array Returns an array with min and max date.
6616
 */
6617
function mod_forum_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $forum) {
6618
    global $CFG;
6619
 
6620
    require_once($CFG->dirroot . '/mod/forum/locallib.php');
6621
 
6622
    $mindate = null;
6623
    $maxdate = null;
6624
 
6625
    if ($event->eventtype == FORUM_EVENT_TYPE_DUE) {
6626
        if (!empty($forum->cutoffdate)) {
6627
            $maxdate = [
6628
                $forum->cutoffdate,
6629
                get_string('cutoffdatevalidation', 'forum'),
6630
            ];
6631
        }
6632
    }
6633
 
6634
    return [$mindate, $maxdate];
6635
}
6636
 
6637
/**
6638
 * This function will update the forum module according to the
6639
 * event that has been modified.
6640
 *
6641
 * It will set the timeclose value of the forum instance
6642
 * according to the type of event provided.
6643
 *
6644
 * @throws \moodle_exception
6645
 * @param \calendar_event $event
6646
 * @param stdClass $forum The module instance to get the range from
6647
 */
6648
function mod_forum_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $forum) {
6649
    global $CFG, $DB;
6650
 
6651
    require_once($CFG->dirroot . '/mod/forum/locallib.php');
6652
 
6653
    if ($event->eventtype != FORUM_EVENT_TYPE_DUE) {
6654
        return;
6655
    }
6656
 
6657
    $courseid = $event->courseid;
6658
    $modulename = $event->modulename;
6659
    $instanceid = $event->instance;
6660
 
6661
    // Something weird going on. The event is for a different module so
6662
    // we should ignore it.
6663
    if ($modulename != 'forum') {
6664
        return;
6665
    }
6666
 
6667
    if ($forum->id != $instanceid) {
6668
        return;
6669
    }
6670
 
6671
    $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
6672
    $context = context_module::instance($coursemodule->id);
6673
 
6674
    // The user does not have the capability to modify this activity.
6675
    if (!has_capability('moodle/course:manageactivities', $context)) {
6676
        return;
6677
    }
6678
 
6679
    if ($event->eventtype == FORUM_EVENT_TYPE_DUE) {
6680
        if ($forum->duedate != $event->timestart) {
6681
            $forum->duedate = $event->timestart;
6682
            $forum->timemodified = time();
6683
            // Persist the instance changes.
6684
            $DB->update_record('forum', $forum);
6685
            $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
6686
            $event->trigger();
6687
        }
6688
    }
6689
}
6690
 
6691
/**
6692
 * Fetch the data used to display the discussions on the current page.
6693
 *
6694
 * @param   \mod_forum\local\entities\forum  $forum The forum entity
6695
 * @param   stdClass                         $user The user to render for
6696
 * @param   int[]|null                       $groupid The group to render
6697
 * @param   int|null                         $sortorder The sort order to use when selecting the discussions in the list
6698
 * @param   int|null                         $pageno The zero-indexed page number to use
6699
 * @param   int|null                         $pagesize The number of discussions to show on the page
6700
 * @return  array                            The data to use for display
6701
 */
6702
function mod_forum_get_discussion_summaries(\mod_forum\local\entities\forum $forum, stdClass $user, ?int $groupid, ?int $sortorder,
6703
        ?int $pageno = 0, ?int $pagesize = 0) {
6704
 
6705
    $vaultfactory = mod_forum\local\container::get_vault_factory();
6706
    $discussionvault = $vaultfactory->get_discussions_in_forum_vault();
6707
    $managerfactory = mod_forum\local\container::get_manager_factory();
6708
    $capabilitymanager = $managerfactory->get_capability_manager($forum);
6709
 
6710
    $groupids = mod_forum_get_groups_from_groupid($forum, $user, $groupid);
6711
 
6712
    if (null === $groupids) {
6713
        return $discussions = $discussionvault->get_from_forum_id(
6714
            $forum->get_id(),
6715
            $capabilitymanager->can_view_hidden_posts($user),
6716
            $user->id,
6717
            $sortorder,
6718
            $pagesize,
6719
            $pageno * $pagesize);
6720
    } else {
6721
        return $discussions = $discussionvault->get_from_forum_id_and_group_id(
6722
            $forum->get_id(),
6723
            $groupids,
6724
            $capabilitymanager->can_view_hidden_posts($user),
6725
            $user->id,
6726
            $sortorder,
6727
            $pagesize,
6728
            $pageno * $pagesize);
6729
    }
6730
}
6731
 
6732
/**
6733
 * Get a count of all discussions in a forum.
6734
 *
6735
 * @param   \mod_forum\local\entities\forum  $forum The forum entity
6736
 * @param   stdClass                         $user The user to render for
6737
 * @param   int                              $groupid The group to render
6738
 * @return  int                              The number of discussions in a forum
6739
 */
6740
function mod_forum_count_all_discussions(\mod_forum\local\entities\forum $forum, stdClass $user, ?int $groupid) {
6741
 
6742
    $managerfactory = mod_forum\local\container::get_manager_factory();
6743
    $capabilitymanager = $managerfactory->get_capability_manager($forum);
6744
    $vaultfactory = mod_forum\local\container::get_vault_factory();
6745
    $discussionvault = $vaultfactory->get_discussions_in_forum_vault();
6746
 
6747
    $groupids = mod_forum_get_groups_from_groupid($forum, $user, $groupid);
6748
 
6749
    if (null === $groupids) {
6750
        return $discussionvault->get_total_discussion_count_from_forum_id(
6751
            $forum->get_id(),
6752
            $capabilitymanager->can_view_hidden_posts($user),
6753
            $user->id);
6754
    } else {
6755
        return $discussionvault->get_total_discussion_count_from_forum_id_and_group_id(
6756
            $forum->get_id(),
6757
            $groupids,
6758
            $capabilitymanager->can_view_hidden_posts($user),
6759
            $user->id);
6760
    }
6761
}
6762
 
6763
/**
6764
 * Get the list of groups to show based on the current user and requested groupid.
6765
 *
6766
 * @param   \mod_forum\local\entities\forum  $forum The forum entity
6767
 * @param   stdClass                         $user The user viewing
6768
 * @param   int                              $groupid The groupid requested
6769
 * @return  array                            The list of groups to show
6770
 */
6771
function mod_forum_get_groups_from_groupid(\mod_forum\local\entities\forum $forum, stdClass $user, ?int $groupid): ?array {
6772
 
6773
    $effectivegroupmode = $forum->get_effective_group_mode();
6774
    if (empty($effectivegroupmode)) {
6775
        // This forum is not in a group mode. Show all posts always.
6776
        return null;
6777
    }
6778
 
6779
    if (null == $groupid) {
6780
        $managerfactory = mod_forum\local\container::get_manager_factory();
6781
        $capabilitymanager = $managerfactory->get_capability_manager($forum);
6782
        // No group was specified.
6783
        $showallgroups = (VISIBLEGROUPS == $effectivegroupmode);
6784
        $showallgroups = $showallgroups || $capabilitymanager->can_access_all_groups($user);
6785
        if ($showallgroups) {
6786
            // Return null to show all groups.
6787
            return null;
6788
        } else {
6789
            // No group was specified. Only show the users current groups.
6790
            return array_keys(
6791
                groups_get_all_groups(
6792
                    $forum->get_course_id(),
6793
                    $user->id,
6794
                    $forum->get_course_module_record()->groupingid
6795
                )
6796
            );
6797
        }
6798
    } else {
6799
        // A group was specified. Just show that group.
6800
        return [$groupid];
6801
    }
6802
}
6803
 
6804
/**
6805
 * Return a list of all the user preferences used by mod_forum.
6806
 *
6807
 * @return array
6808
 */
6809
function mod_forum_user_preferences() {
6810
    $vaultfactory = \mod_forum\local\container::get_vault_factory();
6811
    $discussionlistvault = $vaultfactory->get_discussions_in_forum_vault();
6812
 
6813
    $preferences = array();
6814
    $preferences['forum_discussionlistsortorder'] = array(
6815
        'null' => NULL_NOT_ALLOWED,
6816
        'default' => $discussionlistvault::SORTORDER_LASTPOST_DESC,
6817
        'type' => PARAM_INT,
6818
        'choices' => array(
6819
            $discussionlistvault::SORTORDER_LASTPOST_DESC,
6820
            $discussionlistvault::SORTORDER_LASTPOST_ASC,
6821
            $discussionlistvault::SORTORDER_CREATED_DESC,
6822
            $discussionlistvault::SORTORDER_CREATED_ASC,
6823
            $discussionlistvault::SORTORDER_REPLIES_DESC,
6824
            $discussionlistvault::SORTORDER_REPLIES_ASC
6825
        )
6826
    );
6827
    $preferences['forum_useexperimentalui'] = [
6828
        'null' => NULL_NOT_ALLOWED,
6829
        'default' => false,
6830
        'type' => PARAM_BOOL
6831
    ];
6832
 
6833
    return $preferences;
6834
}
6835
 
6836
/**
6837
 * Lists all gradable areas for the advanced grading methods gramework.
6838
 *
6839
 * @return array('string'=>'string') An array with area names as keys and descriptions as values
6840
 */
6841
function forum_grading_areas_list() {
6842
    return [
6843
        'forum' => get_string('grade_forum_header', 'forum'),
6844
    ];
6845
}
6846
 
6847
/**
6848
 * Callback to fetch the activity event type lang string.
6849
 *
6850
 * @param string $eventtype The event type.
6851
 * @return lang_string The event type lang string.
6852
 */
6853
function mod_forum_core_calendar_get_event_action_string(string $eventtype): string {
6854
    global $CFG;
6855
    require_once($CFG->dirroot . '/mod/forum/locallib.php');
6856
 
6857
    $modulename = get_string('modulename', 'forum');
6858
 
6859
    if ($eventtype == FORUM_EVENT_TYPE_DUE) {
6860
        return get_string('calendardue', 'forum', $modulename);
6861
    } else {
6862
        return get_string('requiresaction', 'calendar', $modulename);
6863
    }
6864
}
6865
 
6866
/**
6867
 * This callback will check the provided instance of this module
6868
 * and make sure there are up-to-date events created for it.
6869
 *
6870
 * @param int $courseid Not used.
6871
 * @param stdClass $instance Forum module instance.
6872
 * @param stdClass $cm Course module object.
6873
 */
6874
function forum_refresh_events(int $courseid, stdClass $instance, stdClass $cm): void {
6875
    global $CFG;
6876
 
6877
    // This function is called by cron and we need to include the locallib for calls further down.
6878
    require_once($CFG->dirroot . '/mod/forum/locallib.php');
6879
 
6880
    forum_update_calendar($instance, $cm->id);
6881
}