Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 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 * @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(' ', $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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body