Proyectos de Subversion Moodle

Rev

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