Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * This file defines an adhoc task to send notifications.
19
 *
20
 * @package    mod_forum
21
 * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace mod_forum\task;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
use html_writer;
30
require_once($CFG->dirroot . '/mod/forum/lib.php');
31
 
32
/**
33
 * Adhoc task to send moodle forum digests for the specified user.
34
 *
35
 * @package    mod_forum
36
 * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
37
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38
 */
39
class send_user_digests extends \core\task\adhoc_task {
40
 
41
    // Use the logging trait to get some nice, juicy, logging.
42
    use \core\task\logging_trait;
43
 
44
    /**
45
     * @var \stdClass   A shortcut to $USER.
46
     */
47
    protected $recipient;
48
 
49
    /**
50
     * @var bool[]  Whether the user can view fullnames for each forum.
51
     */
52
    protected $viewfullnames = [];
53
 
54
    /**
55
     * @var bool[]  Whether the user can post in each forum.
56
     */
57
    protected $canpostto = [];
58
 
59
    /**
60
     * @var \stdClass[] Courses with posts them.
61
     */
62
    protected $courses = [];
63
 
64
    /**
65
     * @var \stdClass[] Forums with posts them.
66
     */
67
    protected $forums = [];
68
 
69
    /**
70
     * @var \stdClass[] Discussions with posts them.
71
     */
72
    protected $discussions = [];
73
 
74
    /**
75
     * @var \stdClass[] The posts to be sent.
76
     */
77
    protected $posts = [];
78
 
79
    /**
80
     * @var \stdClass[] The various authors.
81
     */
82
    protected $users = [];
83
 
84
    /**
85
     * @var \stdClass[] A list of any per-forum digest preference that this user holds.
86
     */
87
    protected $forumdigesttypes = [];
88
 
89
    /**
90
     * @var bool    Whether the user has requested HTML or not.
91
     */
92
    protected $allowhtml = true;
93
 
94
    /**
95
     * @var string  The subject of the message.
96
     */
97
    protected $postsubject = '';
98
 
99
    /**
100
     * @var string  The plaintext content of the whole message.
101
     */
102
    protected $notificationtext = '';
103
 
104
    /**
105
     * @var string  The HTML content of the whole message.
106
     */
107
    protected $notificationhtml = '';
108
 
109
    /**
110
     * @var string  The plaintext content for the current discussion being processed.
111
     */
112
    protected $discussiontext = '';
113
 
114
    /**
115
     * @var string  The HTML content for the current discussion being processed.
116
     */
117
    protected $discussionhtml = '';
118
 
119
    /**
120
     * @var int     The number of messages sent in this digest.
121
     */
122
    protected $sentcount = 0;
123
 
124
    /**
125
     * @var \renderer[][] A cache of the different types of renderer, stored both by target (HTML, or Text), and type.
126
     */
127
    protected $renderers = [
128
        'html' => [],
129
        'text' => [],
130
    ];
131
 
132
    /**
133
     * @var int[] A list of post IDs to be marked as read for this user.
134
     */
135
    protected $markpostsasread = [];
136
 
137
    /**
138
     * Send out messages.
139
     * @throws \moodle_exception
140
     */
141
    public function execute() {
142
        $starttime = time();
143
 
144
        $this->recipient = \core_user::get_user($this->get_userid());
145
        $this->log_start("Sending forum digests for {$this->recipient->username} ({$this->recipient->id})");
146
 
147
        if (empty($this->recipient->mailformat) || $this->recipient->mailformat != 1) {
148
            // This user does not want to receive HTML.
149
            $this->allowhtml = false;
150
        }
151
 
152
        // Fetch all of the data we need to mail these posts.
153
        $this->prepare_data($starttime);
154
 
155
        if (empty($this->posts) || empty($this->discussions) || empty($this->forums)) {
156
            $this->log_finish("No messages found to send.");
157
            return;
158
        }
159
 
160
        // Add the message headers.
161
        $this->add_message_header();
162
 
163
        foreach ($this->discussions as $discussion) {
164
            // Raise the time limit for each discussion.
165
            \core_php_time_limit::raise(120);
166
 
167
            // Grab the data pertaining to this discussion.
168
            $forum = $this->forums[$discussion->forum];
169
            $course = $this->courses[$forum->course];
170
            $cm = get_fast_modinfo($course)->instances['forum'][$forum->id];
171
            $modcontext = \context_module::instance($cm->id);
172
            $coursecontext = \context_course::instance($course->id);
173
 
174
            if (empty($this->posts[$discussion->id])) {
175
                // Somehow there are no posts.
176
                // This should not happen but better safe than sorry.
177
                continue;
178
            }
179
 
180
            if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
181
                // The course is hidden and the user does not have access to it.
182
                // Permissions may have changed since it was queued.
183
                continue;
184
            }
185
 
186
            if (!forum_user_can_see_discussion($forum, $discussion, $modcontext, $this->recipient)) {
187
                // User cannot see this discussion.
188
                // Permissions may have changed since it was queued.
189
                continue;
190
            }
191
 
192
            if (!\mod_forum\subscriptions::is_subscribed($this->recipient->id, $forum, $discussion->id, $cm)) {
193
                // The user does not subscribe to this forum as a whole, or to this specific discussion.
194
                continue;
195
            }
196
 
197
            // Fetch additional values relating to this forum.
198
            if (!isset($this->canpostto[$discussion->id])) {
199
                $this->canpostto[$discussion->id] = forum_user_can_post(
200
                        $forum, $discussion, $this->recipient, $cm, $course, $modcontext);
201
            }
202
 
203
            if (!isset($this->viewfullnames[$forum->id])) {
204
                $this->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext, $this->recipient->id);
205
            }
206
 
207
            // Set the discussion storage values.
208
            $discussionpostcount = 0;
209
            $this->discussiontext = '';
210
            $this->discussionhtml = '';
211
 
212
            // Add the header for this discussion.
213
            $this->add_discussion_header($discussion, $forum, $course);
214
            $this->log_start("Adding messages in discussion {$discussion->id} (forum {$forum->id})", 1);
215
 
216
            // Add all posts in this forum.
217
            foreach ($this->posts[$discussion->id] as $post) {
218
                $author = $this->get_post_author($post->userid, $course, $forum, $cm, $modcontext);
219
                if (empty($author)) {
220
                    // Unable to find the author. Skip to avoid errors.
221
                    continue;
222
                }
223
 
224
                if (!forum_user_can_see_post($forum, $discussion, $post, $this->recipient, $cm)) {
225
                    // User cannot see this post.
226
                    // Permissions may have changed since it was queued.
227
                    continue;
228
                }
229
 
230
                $this->add_post_body($author, $post, $discussion, $forum, $cm, $course);
231
                $discussionpostcount++;
232
            }
233
 
234
            // Add the forum footer.
235
            $this->add_discussion_footer($discussion, $forum, $course);
236
 
237
            // Add the data for this discussion to the notification body.
238
            if ($discussionpostcount) {
239
                $this->sentcount += $discussionpostcount;
240
                $this->notificationtext .= $this->discussiontext;
241
                $this->notificationhtml .= $this->discussionhtml;
242
                $this->log_finish("Added {$discussionpostcount} messages to discussion {$discussion->id}", 1);
243
            } else {
244
                $this->log_finish("No messages found in discussion {$discussion->id} - skipped.", 1);
245
            }
246
        }
247
 
248
        if ($this->sentcount) {
249
            // This digest has at least one post and should therefore be sent.
250
            if ($this->send_mail()) {
251
                $this->log_finish("Digest sent with {$this->sentcount} messages.");
252
                if (get_user_preferences('forum_markasreadonnotification', 1, $this->recipient->id) == 1) {
253
                    forum_tp_mark_posts_read($this->recipient, $this->markpostsasread);
254
                }
255
            } else {
256
                $this->log_finish("Issue sending digest. Skipping.");
257
                throw new \moodle_exception("Issue sending digest. Skipping.");
258
            }
259
        } else {
260
            $this->log_finish("No messages found to send.");
261
        }
262
 
263
        // Empty the queue only if successful.
264
        $this->empty_queue($this->recipient->id, $starttime);
265
 
266
        // We have finishied all digest emails, update $CFG->digestmailtimelast.
267
        set_config('digestmailtimelast', $starttime);
268
    }
269
 
270
    /**
271
     * Prepare the data for this run.
272
     *
273
     * Note: This will also remove posts from the queue.
274
     *
275
     * @param   int     $timenow
276
     */
277
    protected function prepare_data(int $timenow) {
278
        global $DB;
279
 
280
        $sql = "SELECT p.*, f.id AS forum, f.course
281
                  FROM {forum_queue} q
282
            INNER JOIN {forum_posts} p ON p.id = q.postid
283
            INNER JOIN {forum_discussions} d ON d.id = p.discussion
284
            INNER JOIN {forum} f ON f.id = d.forum
285
                 WHERE q.userid = :userid
286
                   AND q.timemodified < :timemodified
287
              ORDER BY d.id, q.timemodified ASC";
288
 
289
        $queueparams = [
290
                'userid' => $this->recipient->id,
291
                'timemodified' => $timenow,
292
            ];
293
 
294
        $posts = $DB->get_recordset_sql($sql, $queueparams);
295
        $discussionids = [];
296
        $forumids = [];
297
        $courseids = [];
298
        $userids = [];
299
        foreach ($posts as $post) {
300
            $discussionids[] = $post->discussion;
301
            $forumids[] = $post->forum;
302
            $courseids[] = $post->course;
303
            $userids[] = $post->userid;
304
            unset($post->forum);
305
            if (!isset($this->posts[$post->discussion])) {
306
                $this->posts[$post->discussion] = [];
307
            }
308
            $this->posts[$post->discussion][$post->id] = $post;
309
        }
310
        $posts->close();
311
 
312
        if (empty($discussionids)) {
313
            // All posts have been removed since the task was queued.
314
            $this->empty_queue($this->recipient->id, $timenow);
315
            return;
316
        }
317
 
318
        list($in, $params) = $DB->get_in_or_equal($discussionids);
319
        $this->discussions = $DB->get_records_select('forum_discussions', "id {$in}", $params);
320
 
321
        list($in, $params) = $DB->get_in_or_equal($forumids);
322
        $this->forums = $DB->get_records_select('forum', "id {$in}", $params);
323
 
324
        list($in, $params) = $DB->get_in_or_equal($courseids);
325
        $this->courses = $DB->get_records_select('course', "id $in", $params);
326
 
327
        list($in, $params) = $DB->get_in_or_equal($userids);
328
        $this->users = $DB->get_records_select('user', "id $in", $params);
329
 
330
        $this->fill_digest_cache();
331
    }
332
 
333
    /**
334
     * Empty the queue of posts for this user.
335
     *
336
     * @param int $userid user id which queue elements are going to be removed.
337
     * @param int $timemodified up time limit of the queue elements to be removed.
338
     */
339
    protected function empty_queue(int $userid, int $timemodified): void {
340
        global $DB;
341
 
342
        $DB->delete_records_select('forum_queue', "userid = :userid AND timemodified < :timemodified", [
343
                'userid' => $userid,
344
                'timemodified' => $timemodified,
345
            ]);
346
    }
347
 
348
    /**
349
     * Fill the cron digest cache.
350
     */
351
    protected function fill_digest_cache() {
352
        global $DB;
353
 
354
        $this->forumdigesttypes = $DB->get_records_menu('forum_digests', [
355
                'userid' => $this->recipient->id,
356
            ], '', 'forum, maildigest');
357
    }
358
 
359
    /**
360
     * Fetch and initialise the post author.
361
     *
362
     * @param   int         $userid The id of the user to fetch
363
     * @param   \stdClass   $course
364
     * @param   \stdClass   $forum
365
     * @param   \stdClass   $cm
366
     * @param   \context    $context
367
     * @return  \stdClass
368
     */
369
    protected function get_post_author($userid, $course, $forum, $cm, $context) {
370
        if (!isset($this->users[$userid])) {
371
            // This user no longer exists.
372
            return false;
373
        }
374
 
375
        $user = $this->users[$userid];
376
 
377
        if (!isset($user->groups)) {
378
            // Initialise the groups list.
379
            $user->groups = [];
380
        }
381
 
382
        if (!isset($user->groups[$forum->id])) {
383
            $user->groups[$forum->id] = groups_get_all_groups($course->id, $user->id, $cm->groupingid);
384
        }
385
 
386
        // Clone the user object to prevent leaks between messages.
387
        return (object) (array) $user;
388
    }
389
 
390
    /**
391
     * Add the header to this message.
392
     */
393
    protected function add_message_header() {
394
        $site = get_site();
395
 
396
        // Set the subject of the message.
397
        $this->postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
398
 
399
        // And the content of the header in body.
400
        $headerdata = (object) [
401
            'sitename' => format_string($site->fullname, true),
402
            'userprefs' => (new \moodle_url('/user/forum.php', [
403
                    'id' => $this->recipient->id,
404
                    'course' => $site->id,
405
                ]))->out(false),
406
            ];
407
 
408
        $this->notificationtext .= get_string('digestmailheader', 'forum', $headerdata) . "\n";
409
 
410
        if ($this->allowhtml) {
411
            $headerdata->userprefs = html_writer::link($headerdata->userprefs, get_string('digestmailprefs', 'forum'), [
412
                    'target' => '_blank',
413
                ]);
414
 
415
            $this->notificationhtml .= html_writer::tag('p', get_string('digestmailheader', 'forum', $headerdata));
416
            $this->notificationhtml .= html_writer::empty_tag('br');
417
            $this->notificationhtml .= html_writer::empty_tag('hr', [
418
                    'size' => 1,
419
                    'noshade' => 'noshade',
420
                ]);
421
        }
422
    }
423
 
424
    /**
425
     * Add the header for this discussion.
426
     *
427
     * @param   \stdClass   $discussion The discussion to add the footer for
428
     * @param   \stdClass   $forum The forum that the discussion belongs to
429
     * @param   \stdClass   $course The course that the forum belongs to
430
     */
431
    protected function add_discussion_header($discussion, $forum, $course) {
432
        global $CFG;
433
 
434
        $shortname = format_string($course->shortname, true, [
435
                'context' => \context_course::instance($course->id),
436
            ]);
437
 
438
        $strforums = get_string('forums', 'forum');
439
 
440
        $this->discussiontext .= "\n=====================================================================\n\n";
441
        $this->discussiontext .= "$shortname -> $strforums -> " . format_string($forum->name, true);
442
        if ($discussion->name != $forum->name) {
443
            $this->discussiontext  .= " -> " . format_string($discussion->name, true);
444
        }
445
        $this->discussiontext .= "\n";
446
        $this->discussiontext .= new \moodle_url('/mod/forum/discuss.php', [
447
                'd' => $discussion->id,
448
            ]);
449
        $this->discussiontext .= "\n";
450
 
451
        if ($this->allowhtml) {
452
            $this->discussionhtml .= "<p><font face=\"sans-serif\">".
453
                "<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
454
                "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
455
                "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">" .
456
                        format_string($forum->name, true)."</a>";
457
            if ($discussion->name == $forum->name) {
458
                $this->discussionhtml .= "</font></p>";
459
            } else {
460
                $this->discussionhtml .=
461
                        " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">" .
462
                        format_string($discussion->name, true)."</a></font></p>";
463
            }
464
            $this->discussionhtml .= '<p>';
465
        }
466
 
467
    }
468
 
469
    /**
470
     * Add the body of this post.
471
     *
472
     * @param   \stdClass   $author The author of the post
473
     * @param   \stdClass   $post The post being sent
474
     * @param   \stdClass   $discussion The discussion that the post is in
475
     * @param   \stdClass   $forum The forum that the discussion belongs to
476
     * @param   \cminfo     $cm The cminfo object for the forum
477
     * @param   \stdClass   $course The course that the forum belongs to
478
     */
479
    protected function add_post_body($author, $post, $discussion, $forum, $cm, $course) {
480
        global $CFG;
481
 
482
        $canreply = $this->canpostto[$discussion->id];
483
 
484
        $data = new \mod_forum\output\forum_post_email(
485
            $course,
486
            $cm,
487
            $forum,
488
            $discussion,
489
            $post,
490
            $author,
491
            $this->recipient,
492
            $canreply
493
        );
494
 
495
        // Override the viewfullnames value.
496
        $data->viewfullnames = $this->viewfullnames[$forum->id];
497
 
498
        // Determine the type of digest being sent.
499
        $maildigest = $this->get_maildigest($forum->id);
500
 
501
        $textrenderer = $this->get_renderer($maildigest);
502
        $this->discussiontext .= $textrenderer->render($data);
503
        $this->discussiontext .= "\n";
504
        if ($this->allowhtml) {
505
            $htmlrenderer = $this->get_renderer($maildigest, true);
506
            $this->discussionhtml .= $htmlrenderer->render($data);
507
            $this->log("Adding post {$post->id} in format {$maildigest} with HTML", 2);
508
        } else {
509
            $this->log("Adding post {$post->id} in format {$maildigest} without HTML", 2);
510
        }
511
 
512
        if ($maildigest == 1 && !$CFG->forum_usermarksread) {
513
            // Create an array of postid's for this user to mark as read.
514
            $this->markpostsasread[] = $post->id;
515
        }
516
 
517
    }
518
 
519
    /**
520
     * Add the footer for this discussion.
521
     *
522
     * @param   \stdClass   $discussion The discussion to add the footer for
523
     */
524
    protected function add_discussion_footer($discussion) {
525
        global $CFG;
526
 
527
        if ($this->allowhtml) {
528
            $footerlinks = [];
529
 
530
            $forum = $this->forums[$discussion->forum];
531
            if (\mod_forum\subscriptions::is_forcesubscribed($forum)) {
532
                // This forum is force subscribed. The user cannot unsubscribe.
533
                $footerlinks[] = get_string("everyoneissubscribed", "forum");
534
            } else {
535
                $footerlinks[] = "<a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">" .
536
                    get_string("unsubscribe", "forum") . "</a>";
537
            }
538
            $footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" .
539
                    get_string("digestmailpost", "forum") . '</a>';
540
 
541
            $this->discussionhtml .= "\n<div class='mdl-right'><font size=\"1\">" .
542
                    implode('&nbsp;', $footerlinks) . '</font></div>';
543
            $this->discussionhtml .= '<hr size="1" noshade="noshade" /></p>';
544
        }
545
    }
546
 
547
    /**
548
     * Get the forum digest type for the specified forum, failing back to
549
     * the default setting for the current user if not specified.
550
     *
551
     * @param   int     $forumid
552
     * @return  int
553
     */
554
    protected function get_maildigest($forumid) {
555
        $maildigest = -1;
556
 
557
        if (isset($this->forumdigesttypes[$forumid])) {
558
            $maildigest = $this->forumdigesttypes[$forumid];
559
        }
560
 
561
        if ($maildigest === -1 && !empty($this->recipient->maildigest)) {
562
            $maildigest = $this->recipient->maildigest;
563
        }
564
 
565
        if ($maildigest === -1) {
566
            // There is no maildigest type right now.
567
            $maildigest = 1;
568
        }
569
 
570
        return $maildigest;
571
    }
572
 
573
    /**
574
     * Send the composed message to the user.
575
     */
576
    protected function send_mail() {
577
        // Headers to help prevent auto-responders.
578
        $userfrom = \core_user::get_noreply_user();
579
        $userfrom->customheaders = array(
580
            "Precedence: Bulk",
581
            'X-Auto-Response-Suppress: All',
582
            'Auto-Submitted: auto-generated',
583
        );
584
 
585
        $eventdata = new \core\message\message();
586
        $eventdata->courseid = SITEID;
587
        $eventdata->component = 'mod_forum';
588
        $eventdata->name = 'digests';
589
        $eventdata->userfrom = $userfrom;
590
        $eventdata->userto = $this->recipient;
591
        $eventdata->subject = $this->postsubject;
592
        $eventdata->fullmessage = $this->notificationtext;
593
        $eventdata->fullmessageformat = FORMAT_PLAIN;
594
        $eventdata->fullmessagehtml = $this->notificationhtml;
595
        $eventdata->notification = 1;
596
        $eventdata->smallmessage = get_string('smallmessagedigest', 'forum', $this->sentcount);
597
 
598
        return message_send($eventdata);
599
    }
600
 
601
    /**
602
     * Helper to fetch the required renderer, instantiating as required.
603
     *
604
     * @param   int     $maildigest The type of mail digest being sent
605
     * @param   bool    $html Whether to fetch the HTML renderer
606
     * @return  \core_renderer
607
     */
608
    protected function get_renderer($maildigest, $html = false) {
609
        global $PAGE;
610
 
611
        $type = $maildigest == 2 ? 'emaildigestbasic' : 'emaildigestfull';
612
        $target = $html ? 'htmlemail' : 'textemail';
613
 
614
        if (!isset($this->renderers[$target][$type])) {
615
            $this->renderers[$target][$type] = $PAGE->get_renderer('mod_forum', $type, $target);
616
        }
617
 
618
        return $this->renderers[$target][$type];
619
    }
620
}