Differences Between: [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]
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 */ 140 public function execute() { 141 $starttime = time(); 142 143 $this->recipient = \core_user::get_user($this->get_userid()); 144 $this->log_start("Sending forum digests for {$this->recipient->username} ({$this->recipient->id})"); 145 146 if (empty($this->recipient->mailformat) || $this->recipient->mailformat != 1) { 147 // This user does not want to receive HTML. 148 $this->allowhtml = false; 149 } 150 151 // Fetch all of the data we need to mail these posts. 152 $this->prepare_data($starttime); 153 154 if (empty($this->posts) || empty($this->discussions) || empty($this->forums)) { 155 $this->log_finish("No messages found to send."); 156 return; 157 } 158 159 // Add the message headers. 160 $this->add_message_header(); 161 162 foreach ($this->discussions as $discussion) { 163 // Raise the time limit for each discussion. 164 \core_php_time_limit::raise(120); 165 166 // Grab the data pertaining to this discussion. 167 $forum = $this->forums[$discussion->forum]; 168 $course = $this->courses[$forum->course]; 169 $cm = get_fast_modinfo($course)->instances['forum'][$forum->id]; 170 $modcontext = \context_module::instance($cm->id); 171 $coursecontext = \context_course::instance($course->id); 172 173 if (empty($this->posts[$discussion->id])) { 174 // Somehow there are no posts. 175 // This should not happen but better safe than sorry. 176 continue; 177 } 178 179 if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { 180 // The course is hidden and the user does not have access to it. 181 // Permissions may have changed since it was queued. 182 continue; 183 } 184 185 if (!forum_user_can_see_discussion($forum, $discussion, $modcontext, $this->recipient)) { 186 // User cannot see this discussion. 187 // Permissions may have changed since it was queued. 188 continue; 189 } 190 191 if (!\mod_forum\subscriptions::is_subscribed($this->recipient->id, $forum, $discussion->id, $cm)) { 192 // The user does not subscribe to this forum as a whole, or to this specific discussion. 193 continue; 194 } 195 196 // Fetch additional values relating to this forum. 197 if (!isset($this->canpostto[$discussion->id])) { 198 $this->canpostto[$discussion->id] = forum_user_can_post( 199 $forum, $discussion, $this->recipient, $cm, $course, $modcontext); 200 } 201 202 if (!isset($this->viewfullnames[$forum->id])) { 203 $this->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext, $this->recipient->id); 204 } 205 206 // Set the discussion storage values. 207 $discussionpostcount = 0; 208 $this->discussiontext = ''; 209 $this->discussionhtml = ''; 210 211 // Add the header for this discussion. 212 $this->add_discussion_header($discussion, $forum, $course); 213 $this->log_start("Adding messages in discussion {$discussion->id} (forum {$forum->id})", 1); 214 215 // Add all posts in this forum. 216 foreach ($this->posts[$discussion->id] as $post) { 217 $author = $this->get_post_author($post->userid, $course, $forum, $cm, $modcontext); 218 if (empty($author)) { 219 // Unable to find the author. Skip to avoid errors. 220 continue; 221 } 222 223 if (!forum_user_can_see_post($forum, $discussion, $post, $this->recipient, $cm)) { 224 // User cannot see this post. 225 // Permissions may have changed since it was queued. 226 continue; 227 } 228 229 $this->add_post_body($author, $post, $discussion, $forum, $cm, $course); 230 $discussionpostcount++; 231 } 232 233 // Add the forum footer. 234 $this->add_discussion_footer($discussion, $forum, $course); 235 236 // Add the data for this discussion to the notification body. 237 if ($discussionpostcount) { 238 $this->sentcount += $discussionpostcount; 239 $this->notificationtext .= $this->discussiontext; 240 $this->notificationhtml .= $this->discussionhtml; 241 $this->log_finish("Added {$discussionpostcount} messages to discussion {$discussion->id}", 1); 242 } else { 243 $this->log_finish("No messages found in discussion {$discussion->id} - skipped.", 1); 244 } 245 } 246 247 if ($this->sentcount) { 248 // This digest has at least one post and should therefore be sent. 249 if ($this->send_mail()) { 250 $this->log_finish("Digest sent with {$this->sentcount} messages."); 251 if (get_user_preferences('forum_markasreadonnotification', 1, $this->recipient->id) == 1) { 252 forum_tp_mark_posts_read($this->recipient, $this->markpostsasread); 253 } 254 } else { 255 $this->log_finish("Issue sending digest. Skipping."); 256 } 257 } else { 258 $this->log_finish("No messages found to send."); 259 } 260 261 // We have finishied all digest emails, update $CFG->digestmailtimelast. 262 set_config('digestmailtimelast', $starttime); 263 } 264 265 /** 266 * Prepare the data for this run. 267 * 268 * Note: This will also remove posts from the queue. 269 * 270 * @param int $timenow 271 */ 272 protected function prepare_data(int $timenow) { 273 global $DB; 274 275 $sql = "SELECT p.*, f.id AS forum, f.course 276 FROM {forum_queue} q 277 INNER JOIN {forum_posts} p ON p.id = q.postid 278 INNER JOIN {forum_discussions} d ON d.id = p.discussion 279 INNER JOIN {forum} f ON f.id = d.forum 280 WHERE q.userid = :userid 281 AND q.timemodified < :timemodified 282 ORDER BY d.id, q.timemodified ASC"; 283 284 $queueparams = [ 285 'userid' => $this->recipient->id, 286 'timemodified' => $timenow, 287 ]; 288 289 $posts = $DB->get_recordset_sql($sql, $queueparams); 290 $discussionids = []; 291 $forumids = []; 292 $courseids = []; 293 $userids = []; 294 foreach ($posts as $post) { 295 $discussionids[] = $post->discussion; 296 $forumids[] = $post->forum; 297 $courseids[] = $post->course; 298 $userids[] = $post->userid; 299 unset($post->forum); 300 if (!isset($this->posts[$post->discussion])) { 301 $this->posts[$post->discussion] = []; 302 } 303 $this->posts[$post->discussion][$post->id] = $post; 304 } 305 $posts->close(); 306 307 if (empty($discussionids)) { 308 // All posts have been removed since the task was queued. 309 $this->empty_queue($this->recipient->id, $timenow); 310 return; 311 } 312 313 list($in, $params) = $DB->get_in_or_equal($discussionids); 314 $this->discussions = $DB->get_records_select('forum_discussions', "id {$in}", $params); 315 316 list($in, $params) = $DB->get_in_or_equal($forumids); 317 $this->forums = $DB->get_records_select('forum', "id {$in}", $params); 318 319 list($in, $params) = $DB->get_in_or_equal($courseids); 320 $this->courses = $DB->get_records_select('course', "id $in", $params); 321 322 list($in, $params) = $DB->get_in_or_equal($userids); 323 $this->users = $DB->get_records_select('user', "id $in", $params); 324 325 $this->fill_digest_cache(); 326 327 $this->empty_queue($this->recipient->id, $timenow); 328 } 329 330 /** 331 * Empty the queue of posts for this user. 332 * 333 * @param int $userid user id which queue elements are going to be removed. 334 * @param int $timemodified up time limit of the queue elements to be removed. 335 */ 336 protected function empty_queue(int $userid, int $timemodified) : void { 337 global $DB; 338 339 $DB->delete_records_select('forum_queue', "userid = :userid AND timemodified < :timemodified", [ 340 'userid' => $userid, 341 'timemodified' => $timemodified, 342 ]); 343 } 344 345 /** 346 * Fill the cron digest cache. 347 */ 348 protected function fill_digest_cache() { 349 global $DB; 350 351 $this->forumdigesttypes = $DB->get_records_menu('forum_digests', [ 352 'userid' => $this->recipient->id, 353 ], '', 'forum, maildigest'); 354 } 355 356 /** 357 * Fetch and initialise the post author. 358 * 359 * @param int $userid The id of the user to fetch 360 * @param \stdClass $course 361 * @param \stdClass $forum 362 * @param \stdClass $cm 363 * @param \context $context 364 * @return \stdClass 365 */ 366 protected function get_post_author($userid, $course, $forum, $cm, $context) { 367 if (!isset($this->users[$userid])) { 368 // This user no longer exists. 369 return false; 370 } 371 372 $user = $this->users[$userid]; 373 374 if (!isset($user->groups)) { 375 // Initialise the groups list. 376 $user->groups = []; 377 } 378 379 if (!isset($user->groups[$forum->id])) { 380 $user->groups[$forum->id] = groups_get_all_groups($course->id, $user->id, $cm->groupingid); 381 } 382 383 // Clone the user object to prevent leaks between messages. 384 return (object) (array) $user; 385 } 386 387 /** 388 * Add the header to this message. 389 */ 390 protected function add_message_header() { 391 $site = get_site(); 392 393 // Set the subject of the message. 394 $this->postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true)); 395 396 // And the content of the header in body. 397 $headerdata = (object) [ 398 'sitename' => format_string($site->fullname, true), 399 'userprefs' => (new \moodle_url('/user/forum.php', [ 400 'id' => $this->recipient->id, 401 'course' => $site->id, 402 ]))->out(false), 403 ]; 404 405 $this->notificationtext .= get_string('digestmailheader', 'forum', $headerdata) . "\n"; 406 407 if ($this->allowhtml) { 408 $headerdata->userprefs = html_writer::link($headerdata->userprefs, get_string('digestmailprefs', 'forum'), [ 409 'target' => '_blank', 410 ]); 411 412 $this->notificationhtml .= html_writer::tag('p', get_string('digestmailheader', 'forum', $headerdata)); 413 $this->notificationhtml .= html_writer::empty_tag('br'); 414 $this->notificationhtml .= html_writer::empty_tag('hr', [ 415 'size' => 1, 416 'noshade' => 'noshade', 417 ]); 418 } 419 } 420 421 /** 422 * Add the header for this discussion. 423 * 424 * @param \stdClass $discussion The discussion to add the footer for 425 * @param \stdClass $forum The forum that the discussion belongs to 426 * @param \stdClass $course The course that the forum belongs to 427 */ 428 protected function add_discussion_header($discussion, $forum, $course) { 429 global $CFG; 430 431 $shortname = format_string($course->shortname, true, [ 432 'context' => \context_course::instance($course->id), 433 ]); 434 435 $strforums = get_string('forums', 'forum'); 436 437 $this->discussiontext .= "\n=====================================================================\n\n"; 438 $this->discussiontext .= "$shortname -> $strforums -> " . format_string($forum->name, true); 439 if ($discussion->name != $forum->name) { 440 $this->discussiontext .= " -> " . format_string($discussion->name, true); 441 } 442 $this->discussiontext .= "\n"; 443 $this->discussiontext .= new \moodle_url('/mod/forum/discuss.php', [ 444 'd' => $discussion->id, 445 ]); 446 $this->discussiontext .= "\n"; 447 448 if ($this->allowhtml) { 449 $this->discussionhtml .= "<p><font face=\"sans-serif\">". 450 "<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ". 451 "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ". 452 "<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">" . 453 format_string($forum->name, true)."</a>"; 454 if ($discussion->name == $forum->name) { 455 $this->discussionhtml .= "</font></p>"; 456 } else { 457 $this->discussionhtml .= 458 " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">" . 459 format_string($discussion->name, true)."</a></font></p>"; 460 } 461 $this->discussionhtml .= '<p>'; 462 } 463 464 } 465 466 /** 467 * Add the body of this post. 468 * 469 * @param \stdClass $author The author of the post 470 * @param \stdClass $post The post being sent 471 * @param \stdClass $discussion The discussion that the post is in 472 * @param \stdClass $forum The forum that the discussion belongs to 473 * @param \cminfo $cm The cminfo object for the forum 474 * @param \stdClass $course The course that the forum belongs to 475 */ 476 protected function add_post_body($author, $post, $discussion, $forum, $cm, $course) { 477 global $CFG; 478 479 $canreply = $this->canpostto[$discussion->id]; 480 481 $data = new \mod_forum\output\forum_post_email( 482 $course, 483 $cm, 484 $forum, 485 $discussion, 486 $post, 487 $author, 488 $this->recipient, 489 $canreply 490 ); 491 492 // Override the viewfullnames value. 493 $data->viewfullnames = $this->viewfullnames[$forum->id]; 494 495 // Determine the type of digest being sent. 496 $maildigest = $this->get_maildigest($forum->id); 497 498 $textrenderer = $this->get_renderer($maildigest); 499 $this->discussiontext .= $textrenderer->render($data); 500 $this->discussiontext .= "\n"; 501 if ($this->allowhtml) { 502 $htmlrenderer = $this->get_renderer($maildigest, true); 503 $this->discussionhtml .= $htmlrenderer->render($data); 504 $this->log("Adding post {$post->id} in format {$maildigest} with HTML", 2); 505 } else { 506 $this->log("Adding post {$post->id} in format {$maildigest} without HTML", 2); 507 } 508 509 if ($maildigest == 1 && !$CFG->forum_usermarksread) { 510 // Create an array of postid's for this user to mark as read. 511 $this->markpostsasread[] = $post->id; 512 } 513 514 } 515 516 /** 517 * Add the footer for this discussion. 518 * 519 * @param \stdClass $discussion The discussion to add the footer for 520 */ 521 protected function add_discussion_footer($discussion) { 522 global $CFG; 523 524 if ($this->allowhtml) { 525 $footerlinks = []; 526 527 $forum = $this->forums[$discussion->forum]; 528 if (\mod_forum\subscriptions::is_forcesubscribed($forum)) { 529 // This forum is force subscribed. The user cannot unsubscribe. 530 $footerlinks[] = get_string("everyoneissubscribed", "forum"); 531 } else { 532 $footerlinks[] = "<a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">" . 533 get_string("unsubscribe", "forum") . "</a>"; 534 } 535 $footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . 536 get_string("digestmailpost", "forum") . '</a>'; 537 538 $this->discussionhtml .= "\n<div class='mdl-right'><font size=\"1\">" . 539 implode(' ', $footerlinks) . '</font></div>'; 540 $this->discussionhtml .= '<hr size="1" noshade="noshade" /></p>'; 541 } 542 } 543 544 /** 545 * Get the forum digest type for the specified forum, failing back to 546 * the default setting for the current user if not specified. 547 * 548 * @param int $forumid 549 * @return int 550 */ 551 protected function get_maildigest($forumid) { 552 $maildigest = -1; 553 554 if (isset($this->forumdigesttypes[$forumid])) { 555 $maildigest = $this->forumdigesttypes[$forumid]; 556 } 557 558 if ($maildigest === -1 && !empty($this->recipient->maildigest)) { 559 $maildigest = $this->recipient->maildigest; 560 } 561 562 if ($maildigest === -1) { 563 // There is no maildigest type right now. 564 $maildigest = 1; 565 } 566 567 return $maildigest; 568 } 569 570 /** 571 * Send the composed message to the user. 572 */ 573 protected function send_mail() { 574 // Headers to help prevent auto-responders. 575 $userfrom = \core_user::get_noreply_user(); 576 $userfrom->customheaders = array( 577 "Precedence: Bulk", 578 'X-Auto-Response-Suppress: All', 579 'Auto-Submitted: auto-generated', 580 ); 581 582 $eventdata = new \core\message\message(); 583 $eventdata->courseid = SITEID; 584 $eventdata->component = 'mod_forum'; 585 $eventdata->name = 'digests'; 586 $eventdata->userfrom = $userfrom; 587 $eventdata->userto = $this->recipient; 588 $eventdata->subject = $this->postsubject; 589 $eventdata->fullmessage = $this->notificationtext; 590 $eventdata->fullmessageformat = FORMAT_PLAIN; 591 $eventdata->fullmessagehtml = $this->notificationhtml; 592 $eventdata->notification = 1; 593 $eventdata->smallmessage = get_string('smallmessagedigest', 'forum', $this->sentcount); 594 595 return message_send($eventdata); 596 } 597 598 /** 599 * Helper to fetch the required renderer, instantiating as required. 600 * 601 * @param int $maildigest The type of mail digest being sent 602 * @param bool $html Whether to fetch the HTML renderer 603 * @return \core_renderer 604 */ 605 protected function get_renderer($maildigest, $html = false) { 606 global $PAGE; 607 608 $type = $maildigest == 2 ? 'emaildigestbasic' : 'emaildigestfull'; 609 $target = $html ? 'htmlemail' : 'textemail'; 610 611 if (!isset($this->renderers[$target][$type])) { 612 $this->renderers[$target][$type] = $PAGE->get_renderer('mod_forum', $type, $target); 613 } 614 615 return $this->renderers[$target][$type]; 616 } 617 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body