MDL-46881 mod_forum: Move cron around

This commit is contained in:
Andrew Nicols 2018-06-01 11:39:39 +08:00
parent 28fd710f84
commit 4459ad290d
12 changed files with 3166 additions and 1401 deletions

View File

@ -17,16 +17,68 @@
/**
* A scheduled task for forum cron.
*
* @todo MDL-44734 This job will be split up properly.
*
* @package mod_forum
* @copyright 2014 Dan Poltawski <dan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\task;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* The main scheduled task for the forum.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cron_task extends \core\task\scheduled_task {
// Use the logging trait to get some nice, juicy, logging.
use \core\task\logging_trait;
/**
* @var The list of courses which contain posts to be sent.
*/
protected $courses = [];
/**
* @var The list of forums which contain posts to be sent.
*/
protected $forums = [];
/**
* @var The list of discussions which contain posts to be sent.
*/
protected $discussions = [];
/**
* @var The list of posts to be sent.
*/
protected $posts = [];
/**
* @var The list of post authors.
*/
protected $users = [];
/**
* @var The list of subscribed users.
*/
protected $subscribedusers = [];
/**
* @var The list of digest users.
*/
protected $digestusers = [];
/**
* @var The list of adhoc data for sending.
*/
protected $adhocdata = [];
/**
* Get a descriptive name for this task (shown to admins).
*
@ -37,12 +89,512 @@ class cron_task extends \core\task\scheduled_task {
}
/**
* Run forum cron.
* Execute the scheduled task.
*/
public function execute() {
global $CFG;
require_once($CFG->dirroot . '/mod/forum/lib.php');
forum_cron();
global $CFG, $DB;
$timenow = time();
// Delete any really old posts in the digest queue.
$weekago = $timenow - (7 * 24 * 3600);
$this->log_start("Removing old digest records from 7 days ago.");
$DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
$this->log_finish("Removed all old digest records.");
$endtime = $timenow - $CFG->maxeditingtime;
$starttime = $endtime - (2 * DAYSECS);
$this->log_start("Fetching unmailed posts.");
if (!$posts = $this->get_unmailed_posts($starttime, $endtime, $timenow)) {
$this->log_finish("No posts found.", 1);
return false;
}
$this->log_finish("Done");
// Process post data and turn into adhoc tasks.
$this->process_post_data($posts);
// Mark posts as read.
list($in, $params) = $DB->get_in_or_equal(array_keys($posts));
$DB->set_field_select('forum_posts', 'mailed', 1, "id {$in}", $params);
}
/**
* Process all posts and convert to appropriated hoc tasks.
*
* @param \stdClass[] $posts
*/
protected function process_post_data($posts) {
$trace = $this->get_trace();
$discussionids = [];
$forumids = [];
$courseids = [];
$this->log_start("Processing post information");
$start = microtime(true);
foreach ($posts as $id => $post) {
$discussionids[$post->discussion] = true;
$forumids[$post->forum] = true;
$courseids[$post->course] = true;
$this->add_data_for_post($post);
$this->posts[$id] = $post;
}
$this->log_finish(sprintf("Processed %s posts", count($this->posts)));
if (empty($this->posts)) {
$this->log("No posts found. Returning early.");
return;
}
// Please note, this order is intentional.
// The forum cache makes use of the course.
$this->log_start("Filling caches");
$start = microtime(true);
$this->log_start("Filling course cache", 1);
$this->fill_course_cache(array_keys($courseids));
$this->log_finish("Done", 1);
$this->log_start("Filling forum cache", 1);
$this->fill_forum_cache(array_keys($forumids));
$this->log_finish("Done", 1);
$this->log_start("Filling discussion cache", 1);
$this->fill_discussion_cache(array_keys($discussionids));
$this->log_finish("Done", 1);
$this->log_start("Filling user subscription cache", 1);
$this->fill_user_subscription_cache();
$this->log_finish("Done", 1);
$this->log_start("Filling digest cache", 1);
$this->fill_digest_cache();
$this->log_finish("Done", 1);
$this->log_start("Filling user posted cache for Q&A forums", 1);
$this->fetch_user_has_posted();
$this->log_finish("Done", 1);
$this->log_start("Queueing user tasks.");
$this->queue_user_tasks();
$this->log_finish("All tasks queued.", 1);
$this->log_finish("All caches filled");
}
/**
* Fill the course cache.
*
* @param int[] $courseids
*/
protected function fill_course_cache($courseids) {
global $DB;
list($in, $params) = $DB->get_in_or_equal($courseids);
$this->courses = $DB->get_records_select('course', "id $in", $params);
}
/**
* Fill the forum cache.
*
* @param int[] $forumids
*/
protected function fill_forum_cache($forumids) {
global $DB;
$requiredfields = [
'id',
'course',
'forcesubscribe',
'type',
];
list($in, $params) = $DB->get_in_or_equal($forumids);
$this->forums = $DB->get_records_select('forum', "id $in", $params, '', implode(', ', $requiredfields));
foreach ($this->forums as $id => $forum) {
\mod_forum\subscriptions::fill_subscription_cache($id);
\mod_forum\subscriptions::fill_discussion_subscription_cache($id);
}
}
/**
* Fill the discussion cache.
*
* @param int[] $discussionids
*/
protected function fill_discussion_cache($discussionids) {
global $DB;
if (empty($discussionids)) {
$this->discussion = [];
} else {
$requiredfields = [
'id',
'groupid',
'firstpost',
'timestart',
'timeend',
];
list($in, $params) = $DB->get_in_or_equal($discussionids);
$this->discussions = $DB->get_records_select(
'forum_discussions', "id $in", $params, '', implode(', ', $requiredfields));
}
}
/**
* Fill the cache of user digest preferences.
*/
protected function fill_digest_cache() {
global $DB;
if (empty($this->users)) {
return;
}
// Get the list of forum subscriptions for per-user per-forum maildigest settings.
list($in, $params) = $DB->get_in_or_equal(array_keys($this->users));
$digestspreferences = $DB->get_recordset_select(
'forum_digests', "userid $in", $params, '', 'id, userid, forum, maildigest');
foreach ($digestspreferences as $digestpreference) {
if (!isset($this->digestusers[$digestpreference->forum])) {
$this->digestusers[$digestpreference->forum] = [];
}
$this->digestusers[$digestpreference->forum][$digestpreference->userid] = $digestpreference->maildigest;
}
$digestspreferences->close();
}
/**
* Add dsta for the current forum post to the structure of adhoc data.
*
* @param \stdClass $post
*/
protected function add_data_for_post($post) {
if (!isset($this->adhocdata[$post->course])) {
$this->adhocdata[$post->course] = [];
}
if (!isset($this->adhocdata[$post->course][$post->forum])) {
$this->adhocdata[$post->course][$post->forum] = [];
}
if (!isset($this->adhocdata[$post->course][$post->forum][$post->discussion])) {
$this->adhocdata[$post->course][$post->forum][$post->discussion] = [];
}
$this->adhocdata[$post->course][$post->forum][$post->discussion][$post->id] = $post->id;
}
/**
* Fill the cache of user subscriptions.
*/
protected function fill_user_subscription_cache() {
foreach ($this->forums as $forum) {
$cm = get_fast_modinfo($this->courses[$forum->course])->instances['forum'][$forum->id];
$modcontext = \context_module::instance($cm->id);
$this->subscribedusers[$forum->id] = [];
if ($users = \mod_forum\subscriptions::fetch_subscribed_users($forum, 0, $modcontext, 'u.id, u.maildigest', true)) {
foreach ($users as $user) {
// This user is subscribed to this forum.
$this->subscribedusers[$forum->id][$user->id] = $user->id;
if (!isset($this->users[$user->id])) {
// Store minimal user info.
$this->users[$user->id] = $user;
}
}
// Release memory.
unset($users);
}
}
}
/**
* Fill the cache for Q&A forums.
*/
protected function fetch_user_has_posted() {
global $DB;
$forums = array_filter($this->forums, function($forum) {
return $forum->type === 'qanda';
});
if (empty($forums)) {
return;
}
list($in, $params) = $DB->get_in_or_equal(array_keys($forums));
$sql = "SELECT d.forum, d.firstpost, p.userid
FROM {forum} f
INNER JOIN {forum_discussions} d ON d.forum = f.id
LEFT JOIN {forum_posts} p ON p.discussion = d.id
WHERE f.type = 'qanda'
AND f.id {$in}
GROUP BY p.userid, d.forum, d.firstpost";
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $row) {
if (empty($this->qandametadata[$row->forum])) {
$this->qandametadata[$row->forum] = (object) [
'users' => [],
'firstpost' => [$row->firstpost],
];
}
$this->qandametadata[$row->forum]->users[$row->userid] = true;
}
$rs->close();
}
/**
* Queue the user tasks.
*/
protected function queue_user_tasks() {
global $CFG, $DB;
$trace = $this->get_trace();
$timenow = time();
$sitetimezone = \core_date::get_server_timezone();
$counts = [
'digests' => 0,
'individuals' => 0,
'users' => 0,
'ignored' => 0,
'messages' => 0,
];
$this->log("Processing " . count($this->users) . " users", 1);
foreach ($this->users as $user) {
$usercounts = [
'digests' => 0,
'messages' => 0,
];
$send = false;
// Setup this user so that the capabilities are cached, and environment matches receiving user.
cron_setup_user($user);
list($individualpostdata, $digestpostdata) = $this->fetch_posts_for_user($user);
if (!empty($digestpostdata)) {
// Insert all of the records for the digest.
$DB->insert_records('forum_queue', $digestpostdata);
$digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
$task = new \mod_forum\task\send_user_digests();
$task->set_userid($user->id);
$task->set_component('mod_forum');
$task->set_next_run_time($digesttime);
\core\task\manager::queue_adhoc_task($task, true);
$usercounts['digests']++;
$send = true;
}
if (!empty($individualpostdata)) {
$usercounts['messages'] += count($individualpostdata);
$task = new \mod_forum\task\send_user_notifications();
$task->set_userid($user->id);
$task->set_custom_data($individualpostdata);
$task->set_component('mod_forum');
\core\task\manager::queue_adhoc_task($task);
$counts['individuals']++;
$send = true;
}
if ($send) {
$counts['users']++;
$counts['messages'] += $usercounts['messages'];
$counts['digests'] += $usercounts['digests'];
} else {
$counts['ignored']++;
}
$this->log(sprintf("Queued %d digests and %d messages for %s",
$usercounts['digests'],
$usercounts['messages'],
$user->id
), 2);
}
$this->log(
sprintf(
"Queued %d digests, and %d individual tasks for %d post mails. " .
"Unique users: %d (%d ignored)",
$counts['digests'],
$counts['individuals'],
$counts['messages'],
$counts['users'],
$counts['ignored']
), 1);
}
/**
* Fetch posts for this user.
*
* @param \stdClass $user The user to fetch posts for.
*/
protected function fetch_posts_for_user($user) {
// We maintain a mapping of user groups for each forum.
$usergroups = [];
$digeststructure = [];
$poststructure = $this->adhocdata;
$poststosend = [];
foreach ($poststructure as $courseid => $forumids) {
$course = $this->courses[$courseid];
foreach ($forumids as $forumid => $discussionids) {
$forum = $this->forums[$forumid];
$maildigest = forum_get_user_maildigest_bulk($this->digestusers, $user, $forumid);
if (!isset($this->subscribedusers[$forumid][$user->id])) {
// This user has no subscription of any kind to this forum.
// Do not send them any posts at all.
unset($poststructure[$courseid][$forumid]);
continue;
}
$subscriptiontime = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $user->id);
$cm = get_fast_modinfo($course)->instances['forum'][$forumid];
foreach ($discussionids as $discussionid => $postids) {
$discussion = $this->discussions[$discussionid];
if (!\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussionid, $cm)) {
// The user does not subscribe to this forum as a whole, or to this specific discussion.
unset($poststructure[$courseid][$forumid][$discussionid]);
continue;
}
if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {
// This discussion has a groupmode set (SEPARATEGROUPS or VISIBLEGROUPS).
// Check whether the user can view it based on their groups.
if (!isset($usergroups[$forum->id])) {
$usergroups[$forum->id] = groups_get_all_groups($courseid, $user->id, $cm->groupingid);
}
if (!isset($usergroups[$forum->id][$discussion->groupid])) {
// This user is not a member of this group, or the group no longer exists.
$modcontext = \context_module::instance($cm->id);
if (!has_capability('moodle/site:accessallgroups', $modcontext, $user)) {
// This user does not have the accessallgroups and is not a member of the group.
// Do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS.
unset($poststructure[$courseid][$forumid][$discussionid]);
continue;
}
}
}
foreach ($postids as $postid) {
$post = $this->posts[$postid];
if ($subscriptiontime) {
// Skip posts if the user subscribed to the discussion after it was created.
$subscribedafter = isset($subscriptiontime[$post->discussion]);
$subscribedafter = $subscribedafter && ($subscriptiontime[$post->discussion] > $post->created);
if ($subscribedafter) {
// The user subscribed to the discussion/forum after this post was created.
unset($poststructure[$courseid][$forumid][$discussionid]);
continue;
}
}
if ($forum->type === 'qanda' && $postid != $discussion->firstpost) {
$hasqandapost = isset($this->qandametadata[$forumid]);
$hasqandapost = $hasqandapost && isset($this->qandametadata[$forumid]->users[$user->id]);
if (!$hasqandapost) {
// The user has not posted to this qanda forum.
unset($poststructure[$courseid][$forumid][$discussionid]);
continue;
}
}
if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm)) {
// The user is not allowed to see the post for some other reason.
unset($poststructure[$courseid][$forumid][$discussionid][$postid]);
continue;
}
if ($maildigest > 0) {
// This user wants the mails to be in digest form.
$digeststructure[] = (object) [
'userid' => $user->id,
'discussionid' => $discussion->id,
'postid' => $post->id,
'timemodified' => $post->created,
];
unset($poststructure[$courseid][$forumid][$discussionid][$postid]);
continue;
} else {
// Add this post to the list of postids to be sent.
$poststosend[] = $postid;
}
}
}
if (empty($poststructure[$courseid][$forumid])) {
// This user is not subscribed to any discussions in this forum at all.
unset($poststructure[$courseid][$forumid]);
continue;
}
}
if (empty($poststructure[$courseid])) {
// This user is not subscribed to any forums in this course.
unset($poststructure[$courseid]);
}
}
return [$poststosend, $digeststructure];
}
/**
* Returns a list of all new posts that have not been mailed yet
*
* @param int $starttime posts created after this time
* @param int $endtime posts created before this
* @param int $now used for timed discussions only
* @return array
*/
protected function get_unmailed_posts($starttime, $endtime, $now = null) {
global $CFG, $DB;
$params = array();
$params['mailed'] = FORUM_MAILED_PENDING;
$params['ptimestart'] = $starttime;
$params['ptimeend'] = $endtime;
$params['mailnow'] = 1;
if (!empty($CFG->forum_enabletimedposts)) {
if (empty($now)) {
$now = time();
}
$selectsql = "AND (p.created >= :ptimestart OR d.timestart >= :pptimestart)";
$params['pptimestart'] = $starttime;
$timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))";
$params['dtimestart'] = $now;
$params['dtimeend'] = $now;
} else {
$timedsql = "";
$selectsql = "AND p.created >= :ptimestart";
}
return $DB->get_records_sql(
"SELECT
p.id,
p.discussion,
d.forum,
d.course,
p.created,
p.parent,
p.userid
FROM {forum_posts} p
JOIN {forum_discussions} d ON d.id = p.discussion
WHERE p.mailed = :mailed
$selectsql
AND (p.created < :ptimeend OR p.mailnow = :mailnow)
$timedsql
ORDER BY p.modified ASC",
$params);
}
}

View File

@ -0,0 +1,599 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file defines an adhoc task to send notifications.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\task;
defined('MOODLE_INTERNAL') || die();
use html_writer;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Adhoc task to send moodle forum digests for the specified user.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class send_user_digests extends \core\task\adhoc_task {
// Use the logging trait to get some nice, juicy, logging.
use \core\task\logging_trait;
/**
* @var \stdClass A shortcut to $USER.
*/
protected $recipient;
/**
* @var bool[] Whether the user can view fullnames for each forum.
*/
protected $viewfullnames = [];
/**
* @var bool[] Whether the user can post in each forum.
*/
protected $canpostto = [];
/**
* @var \stdClass[] Courses with posts them.
*/
protected $courses = [];
/**
* @var \stdClass[] Forums with posts them.
*/
protected $forums = [];
/**
* @var \stdClass[] Discussions with posts them.
*/
protected $discussions = [];
/**
* @var \stdClass[] The posts to be sent.
*/
protected $posts = [];
/**
* @var \stdClass[] The various authors.
*/
protected $users = [];
/**
* @var \stdClass[] A list of any per-forum digest preference that this user holds.
*/
protected $forumdigesttypes = [];
/**
* @var bool Whether the user has requested HTML or not.
*/
protected $allowhtml = true;
/**
* @var string The subject of the message.
*/
protected $postsubject = '';
/**
* @var string The plaintext content of the whole message.
*/
protected $notificationtext = '';
/**
* @var string The HTML content of the whole message.
*/
protected $notificationhtml = '';
/**
* @var string The plaintext content for the current discussion being processed.
*/
protected $discussiontext = '';
/**
* @var string The HTML content for the current discussion being processed.
*/
protected $discussionhtml = '';
/**
* @var int The number of messages sent in this digest.
*/
protected $sentcount = 0;
/**
* @var \renderer[][] A cache of the different types of renderer, stored both by target (HTML, or Text), and type.
*/
protected $renderers = [
'html' => [],
'text' => [],
];
/**
* @var int[] A list of post IDs to be marked as read for this user.
*/
protected $markpostsasread = [];
/**
* Send out messages.
*/
public function execute() {
// Terminate if not able to fetch all digests in 5 minutes.
\core_php_time_limit::raise(300);
$starttime = time();
$this->recipient = \core_user::get_user($this->get_userid());
$this->log_start("Sending forum digests for {$this->recipient->username} ({$this->recipient->id})");
if (empty($this->recipient->mailformat) || $this->recipient->mailformat != 1) {
// This user does not want to receive HTML.
$this->allowhtml = false;
}
// Fetch all of the data we need to mail these posts.
$this->prepare_data($starttime);
if (empty($this->posts) || empty($this->discussions) || empty($this->forums)) {
$this->log_finish("No messages found to send.");
return;
}
// Add the message headers.
$this->add_message_header();
foreach ($this->discussions as $discussion) {
// Raise the time limit for each discussion.
\core_php_time_limit::raise(120);
// Grab the data pertaining to this discussion.
$forum = $this->forums[$discussion->forum];
$course = $this->courses[$forum->course];
$cm = get_fast_modinfo($course)->instances['forum'][$forum->id];
$modcontext = \context_module::instance($cm->id);
$coursecontext = \context_course::instance($course->id);
if (empty($this->posts[$discussion->id])) {
// Somehow there are no posts.
// This should not happen but better safe than sorry.
continue;
}
if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
// The course is hidden and the user does not have access to it.
// Permissions may have changed since it was queued.
continue;
}
if (!forum_user_can_see_discussion($forum, $discussion, $modcontext, $this->recipient)) {
// User cannot see this discussion.
// Permissions may have changed since it was queued.
continue;
}
if (!\mod_forum\subscriptions::is_subscribed($this->recipient->id, $forum, $discussion->id, $cm)) {
// The user does not subscribe to this forum as a whole, or to this specific discussion.
continue;
}
// Fetch additional values relating to this forum.
if (!isset($this->canpostto[$discussion->id])) {
$this->canpostto[$discussion->id] = forum_user_can_post(
$forum, $discussion, $this->recipient, $cm, $course, $modcontext);
}
if (!isset($this->viewfullnames[$forum->id])) {
$this->viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $this->recipient->id);
}
// Set the discussion storage values.
$discussionpostcount = 0;
$this->discussiontext = '';
$this->discussionhtml = '';
// Add the header for this discussion.
$this->add_discussion_header($discussion, $forum, $course);
$this->log_start("Adding messages in discussion {$discussion->id} (forum {$forum->id})", 1);
// Add all posts in this forum.
foreach ($this->posts[$discussion->id] as $post) {
$author = $this->get_post_author($post->userid, $course, $forum, $cm, $modcontext);
if (empty($author)) {
// Unable to find the author. Skip to avoid errors.
continue;
}
if (!forum_user_can_see_post($forum, $discussion, $post, $this->recipient, $cm)) {
// User cannot see this post.
// Permissions may have changed since it was queued.
continue;
}
$this->add_post_body($author, $post, $discussion, $forum, $cm, $course);
$discussionpostcount++;
}
// Add the forum footer.
$this->add_discussion_footer($discussion, $forum, $course);
// Add the data for this discussion to the notification body.
if ($discussionpostcount) {
$this->sentcount += $discussionpostcount;
$this->notificationtext .= $this->discussiontext;
$this->notificationhtml .= $this->discussionhtml;
$this->log_finish("Added {$discussionpostcount} messages to discussion {$discussion->id}", 1);
} else {
$this->log_finish("No messages found in discussion {$discussion->id} - skipped.", 1);
}
}
if ($this->sentcount) {
// This digest has at least one post and should therefore be sent.
if ($this->send_mail()) {
$this->log_finish("Digest sent with {$this->sentcount} messages.");
if (get_user_preferences('forum_markasreadonnotification', 1, $this->recipient->id) == 1) {
forum_tp_mark_posts_read($this->recipient, $this->markpostsasread);
}
} else {
$this->log_finish("Issue sending digest. Skipping.");
}
} else {
$this->log_finish("No messages found to send.");
}
// We have finishied all digest emails, update $CFG->digestmailtimelast.
set_config('digestmailtimelast', $starttime);
}
/**
* Prepare the data for this run.
*
* Note: This will also remove posts from the queue.
*
* @param int $timenow
*/
protected function prepare_data(int $timenow) {
global $DB;
$sql = "SELECT p.*, f.id AS forum, f.course
FROM {forum_queue} q
INNER JOIN {forum_posts} p ON p.id = q.postid
INNER JOIN {forum_discussions} d ON d.id = p.discussion
INNER JOIN {forum} f ON f.id = d.forum
WHERE q.userid = :userid
AND q.timemodified < :timemodified
ORDER BY d.id, q.timemodified ASC";
$queueparams = [
'userid' => $this->recipient->id,
'timemodified' => $timenow,
];
$posts = $DB->get_recordset_sql($sql, $queueparams);
$discussionids = [];
$forumids = [];
$courseids = [];
$userids = [];
foreach ($posts as $post) {
$discussionids[] = $post->discussion;
$forumids[] = $post->forum;
$courseids[] = $post->course;
$userids[] = $post->userid;
unset($post->forum);
if (!isset($this->posts[$post->discussion])) {
$this->posts[$post->discussion] = [];
}
$this->posts[$post->discussion][$post->id] = $post;
}
$posts->close();
list($in, $params) = $DB->get_in_or_equal($discussionids);
$this->discussions = $DB->get_records_select('forum_discussions', "id {$in}", $params);
list($in, $params) = $DB->get_in_or_equal($forumids);
$this->forums = $DB->get_records_select('forum', "id {$in}", $params);
list($in, $params) = $DB->get_in_or_equal($courseids);
$this->courses = $DB->get_records_select('course', "id $in", $params);
list($in, $params) = $DB->get_in_or_equal($userids);
$this->users = $DB->get_records_select('user', "id $in", $params);
$this->fill_digest_cache();
$DB->delete_records_select('forum_queue', "userid = :userid AND timemodified < :timemodified", $queueparams);
}
/**
* Fill the cron digest cache.
*/
protected function fill_digest_cache() {
global $DB;
$this->forumdigesttypes = $DB->get_records_menu('forum_digests', [
'userid' => $this->recipient->id,
], '', 'forum, maildigest');
}
/**
* Fetch and initialise the post author.
*
* @param int $userid The id of the user to fetch
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $cm
* @param \context $context
* @return \stdClass
*/
protected function get_post_author($userid, $course, $forum, $cm, $context) {
if (!isset($this->users[$userid])) {
// This user no longer exists.
return false;
}
$user = $this->users[$userid];
if (!isset($user->groups)) {
// Initialise the groups list.
$user->groups = [];
}
if (!isset($user->groups[$forum->id])) {
$user->groups[$forum->id] = groups_get_all_groups($course->id, $user->id, $cm->groupingid);
}
// Clone the user object to prevent leaks between messages.
return (object) (array) $user;
}
/**
* Add the header to this message.
*/
protected function add_message_header() {
$site = get_site();
// Set the subject of the message.
$this->postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
// And the content of the header in body.
$headerdata = (object) [
'sitename' => format_string($site->fullname, true),
'userprefs' => (new \moodle_url('/user/forum.php', [
'id' => $this->recipient->id,
'course' => $site->id,
]))->out(false),
];
$this->notificationtext .= get_string('digestmailheader', 'forum', $headerdata) . "\n";
if ($this->allowhtml) {
$headerdata->userprefs = html_writer::link($headerdata->userprefs, get_string('digestmailprefs', 'forum'), [
'target' => '_blank',
]);
$this->notificationhtml .= html_writer::tag('p', get_string('digestmailheader', 'forum', $headerdata));
$this->notificationhtml .= html_writer::empty_tag('br');
$this->notificationhtml .= html_writer::empty_tag('hr', [
'size' => 1,
'noshade' => 'noshade',
]);
}
}
/**
* Add the header for this discussion.
*
* @param \stdClass $discussion The discussion to add the footer for
* @param \stdClass $forum The forum that the discussion belongs to
* @param \stdClass $course The course that the forum belongs to
*/
protected function add_discussion_header($discussion, $forum, $course) {
global $CFG;
$shortname = format_string($course->shortname, true, [
'context' => \context_course::instance($course->id),
]);
$strforums = get_string('forums', 'forum');
$this->discussiontext .= "\n=====================================================================\n\n";
$this->discussiontext .= "$shortname -> $strforums -> " . format_string($forum->name, true);
if ($discussion->name != $forum->name) {
$this->discussiontext .= " -> " . format_string($discussion->name, true);
}
$this->discussiontext .= "\n";
$this->discussiontext .= new \moodle_url('/mod/forum/discuss.php', [
'd' => $discussion->id,
]);
$this->discussiontext .= "\n";
if ($this->allowhtml) {
$this->discussionhtml .= "<p><font face=\"sans-serif\">".
"<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
"<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
"<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">" .
format_string($forum->name, true)."</a>";
if ($discussion->name == $forum->name) {
$this->discussionhtml .= "</font></p>";
} else {
$this->discussionhtml .=
" -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">" .
format_string($discussion->name, true)."</a></font></p>";
}
$this->discussionhtml .= '<p>';
}
}
/**
* Add the body of this post.
*
* @param \stdClass $author The author of the post
* @param \stdClass $post The post being sent
* @param \stdClass $discussion The discussion that the post is in
* @param \stdClass $forum The forum that the discussion belongs to
* @param \cminfo $cm The cminfo object for the forum
* @param \stdClass $course The course that the forum belongs to
*/
protected function add_post_body($author, $post, $discussion, $forum, $cm, $course) {
global $CFG;
$canreply = $this->canpostto[$discussion->id];
$data = new \mod_forum\output\forum_post_email(
$course,
$cm,
$forum,
$discussion,
$post,
$author,
$this->recipient,
$canreply
);
// Override the viewfullnames value.
$data->viewfullnames = $this->viewfullnames[$forum->id];
// Determine the type of digest being sent.
$maildigest = $this->get_maildigest($forum->id);
$textrenderer = $this->get_renderer($maildigest);
$this->discussiontext .= $textrenderer->render($data);
$this->discussiontext .= "\n";
if ($this->allowhtml) {
$htmlrenderer = $this->get_renderer($maildigest, true);
$this->discussionhtml .= $htmlrenderer->render($data);
$this->log("Adding post {$post->id} in format {$maildigest} with HTML", 2);
} else {
$this->log("Adding post {$post->id} in format {$maildigest} without HTML", 2);
}
if ($maildigest == 1 && $CFG->forum_usermarksread) {
// Create an array of postid's for this user to mark as read.
$this->markpostsasread[] = $post->id;
}
}
/**
* Add the footer for this discussion.
*
* @param \stdClass $discussion The discussion to add the footer for
*/
protected function add_discussion_footer($discussion) {
global $CFG;
if ($this->allowhtml) {
$footerlinks = [];
$forum = $this->forums[$discussion->forum];
if (\mod_forum\subscriptions::is_forcesubscribed($forum)) {
// This forum is force subscribed. The user cannot unsubscribe.
$footerlinks[] = get_string("everyoneissubscribed", "forum");
} else {
$footerlinks[] = "<a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">" .
get_string("unsubscribe", "forum") . "</a>";
}
$footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" .
get_string("digestmailpost", "forum") . '</a>';
$this->discussionhtml .= "\n<div class='mdl-right'><font size=\"1\">" .
implode('&nbsp;', $footerlinks) . '</font></div>';
$this->discussionhtml .= '<hr size="1" noshade="noshade" /></p>';
}
}
/**
* Get the forum digest type for the specified forum, failing back to
* the default setting for the current user if not specified.
*
* @param int $forumid
* @return int
*/
protected function get_maildigest($forumid) {
$maildigest = -1;
if (isset($this->forumdigesttypes[$forumid])) {
$maildigest = $this->forumdigesttypes[$forumid];
}
if ($maildigest === -1 && !empty($this->recipient->maildigest)) {
$maildigest = $this->recipient->maildigest;
}
if ($maildigest === -1) {
// There is no maildigest type right now.
$maildigest = 1;
}
return $maildigest;
}
/**
* Send the composed message to the user.
*/
protected function send_mail() {
// Headers to help prevent auto-responders.
$userfrom = \core_user::get_noreply_user();
$userfrom->customheaders = array(
"Precedence: Bulk",
'X-Auto-Response-Suppress: All',
'Auto-Submitted: auto-generated',
);
$eventdata = new \core\message\message();
$eventdata->courseid = SITEID;
$eventdata->component = 'mod_forum';
$eventdata->name = 'digests';
$eventdata->userfrom = $userfrom;
$eventdata->userto = $this->recipient;
$eventdata->subject = $this->postsubject;
$eventdata->fullmessage = $this->notificationtext;
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = $this->notificationhtml;
$eventdata->notification = 1;
$eventdata->smallmessage = get_string('smallmessagedigest', 'forum', $this->sentcount);
return message_send($eventdata);
}
/**
* Helper to fetch the required renderer, instantiating as required.
*
* @param int $maildigest The type of mail digest being sent
* @param bool $html Whether to fetch the HTML renderer
* @return \core_renderer
*/
protected function get_renderer($maildigest, $html = false) {
global $PAGE;
$type = $maildigest == 2 ? 'emaildigestbasic' : 'emaildigestfull';
$target = $html ? 'htmlemail' : 'textemail';
if (!isset($this->renderers[$target][$type])) {
$this->renderers[$target][$type] = $PAGE->get_renderer('mod_forum', $type, $target);
}
return $this->renderers[$target][$type];
}
}

View File

@ -0,0 +1,534 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file defines an adhoc task to send notifications.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\task;
defined('MOODLE_INTERNAL') || die();
/**
* Adhoc task to send user forum notifications.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class send_user_notifications extends \core\task\adhoc_task {
// Use the logging trait to get some nice, juicy, logging.
use \core\task\logging_trait;
/**
* @var \stdClass A shortcut to $USER.
*/
protected $recipient;
/**
* @var \stdClass[] List of courses the messages are in, indexed by courseid.
*/
protected $courses = [];
/**
* @var \stdClass[] List of forums the messages are in, indexed by courseid.
*/
protected $forums = [];
/**
* @var int[] List of IDs for forums in each course.
*/
protected $courseforums = [];
/**
* @var \stdClass[] List of discussions the messages are in, indexed by forumid.
*/
protected $discussions = [];
/**
* @var \stdClass[] List of IDs for discussions in each forum.
*/
protected $forumdiscussions = [];
/**
* @var \stdClass[] List of posts the messages are in, indexed by discussionid.
*/
protected $posts = [];
/**
* @var bool[] Whether the user can view fullnames for each forum.
*/
protected $viewfullnames = [];
/**
* @var bool[] Whether the user can post in each discussion.
*/
protected $canpostto = [];
/**
* @var \renderer[] The renderers.
*/
protected $renderers = [];
/**
* @var \core\message\inbound\address_manager The inbound message address manager.
*/
protected $inboundmanager;
/**
* Send out messages.
*/
public function execute() {
global $CFG;
$this->recipient = \core_user::get_user($this->get_userid());
// Create the generic messageinboundgenerator.
$this->inboundmanager = new \core\message\inbound\address_manager();
$this->inboundmanager->set_handler('\mod_forum\message\inbound\reply_handler');
$data = $this->get_custom_data();
$this->prepare_data((array) $data);
$markposts = [];
$errorcount = 0;
$sentcount = 0;
$this->log_start("Sending messages to {$this->recipient->username} ({$this->recipient->id})");
foreach ($this->courses as $course) {
$coursecontext = \context_course::instance($course->id);
if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
// The course is hidden and the user does not have access to it.
// Permissions may have changed since it was queued.
continue;
}
foreach ($this->courseforums[$course->id] as $forumid) {
$forum = $this->forums[$forumid];
$cm = get_fast_modinfo($course)->instances['forum'][$forumid];
$modcontext = \context_module::instance($cm->id);
foreach (array_values($this->forumdiscussions[$forumid]) as $discussionid) {
$discussion = $this->discussions[$discussionid];
if (!forum_user_can_see_discussion($forum, $discussion, $modcontext, $this->recipient)) {
// User cannot see this discussion.
// Permissions may have changed since it was queued.
continue;
}
if (!\mod_forum\subscriptions::is_subscribed($this->recipient->id, $forum, $discussionid, $cm)) {
// The user does not subscribe to this forum as a whole, or to this specific discussion.
continue;
}
foreach ($this->posts[$discussionid] as $post) {
if (!forum_user_can_see_post($forum, $discussion, $post, $this->recipient, $cm)) {
// User cannot see this post.
// Permissions may have changed since it was queued.
continue;
}
if ($this->send_post($course, $forum, $discussion, $post, $cm, $modcontext)) {
$this->log("Post {$post->id} sent", 1);
// Mark post as read if forum_usermarksread is set off.
if (!$CFG->forum_usermarksread) {
$markposts[$post->id] = true;
}
$sentcount++;
} else {
$this->log("Failed to send post {$post->id}", 1);
$errorcount++;
}
}
}
}
}
$this->log_finish("Sent {$sentcount} messages with {$errorcount} failures");
if (!empty($markposts)) {
if (get_user_preferences('forum_markasreadonnotification', 1, $this->recipient->id) == 1) {
$this->log_start("Marking posts as read");
$count = count($markposts);
forum_tp_mark_posts_read($this->recipient, $markposts);
$this->log_finish("Marked {$count} posts as read");
}
}
}
/**
* Prepare all data for this run.
*
* Take all post ids, and fetch the relevant authors, discussions, forums, and courses for them.
*
* @param int[] $postids The list of post IDs
*/
protected function prepare_data(array $postids) {
global $DB;
if (empty($postids)) {
return;
}
list($in, $params) = $DB->get_in_or_equal(array_values($postids));
$sql = "SELECT p.*, f.id AS forum, f.course
FROM {forum_posts} p
INNER JOIN {forum_discussions} d ON d.id = p.discussion
INNER JOIN {forum} f ON f.id = d.forum
WHERE p.id {$in}";
$posts = $DB->get_recordset_sql($sql, $params);
$discussionids = [];
$forumids = [];
$courseids = [];
$userids = [];
foreach ($posts as $post) {
$discussionids[] = $post->discussion;
$forumids[] = $post->forum;
$courseids[] = $post->course;
$userids[] = $post->userid;
unset($post->forum);
if (!isset($this->posts[$post->discussion])) {
$this->posts[$post->discussion] = [];
}
$this->posts[$post->discussion][$post->id] = $post;
}
$posts->close();
// Fetch all discussions.
list($in, $params) = $DB->get_in_or_equal(array_values($discussionids));
$this->discussions = $DB->get_records_select('forum_discussions', "id {$in}", $params);
foreach ($this->discussions as $discussion) {
if (empty($this->forumdiscussions[$discussion->forum])) {
$this->forumdiscussions[$discussion->forum] = [];
}
$this->forumdiscussions[$discussion->forum][] = $discussion->id;
}
// Fetch all forums.
list($in, $params) = $DB->get_in_or_equal(array_values($forumids));
$this->forums = $DB->get_records_select('forum', "id {$in}", $params);
foreach ($this->forums as $forum) {
if (empty($this->courseforums[$forum->course])) {
$this->courseforums[$forum->course] = [];
}
$this->courseforums[$forum->course][] = $forum->id;
}
// Fetch all courses.
list($in, $params) = $DB->get_in_or_equal(array_values($courseids));
$this->courses = $DB->get_records_select('course', "id $in", $params);
// Fetch all authors.
list($in, $params) = $DB->get_in_or_equal(array_values($userids));
$users = $DB->get_recordset_select('user', "id $in", $params);
foreach ($users as $user) {
$this->minimise_user_record($user);
$this->users[$user->id] = $user;
}
$users->close();
// Fill subscription caches for each forum.
// These are per-user.
foreach (array_values($forumids) as $id) {
\mod_forum\subscriptions::fill_subscription_cache($id);
\mod_forum\subscriptions::fill_discussion_subscription_cache($id);
}
}
/**
* Send the specified post for the current user.
*
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $discussion
* @param \stdClass $post
* @param \stdClass $cm
* @param \context $context
*/
protected function send_post($course, $forum, $discussion, $post, $cm, $context) {
global $CFG;
$author = $this->get_post_author($post->userid, $course, $forum, $cm, $context);
if (empty($author)) {
return false;
}
// Prepare to actually send the post now, and build up the content.
$cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
$shortname = format_string($course->shortname, true, [
'context' => \context_course::instance($course->id),
]);
// Generate a reply-to address from using the Inbound Message handler.
$replyaddress = $this->get_reply_address($course, $forum, $discussion, $post, $cm, $context);
$data = new \mod_forum\output\forum_post_email(
$course,
$cm,
$forum,
$discussion,
$post,
$author,
$this->recipient,
$this->can_post($course, $forum, $discussion, $post, $cm, $context)
);
$data->viewfullnames = $this->can_view_fullnames($course, $forum, $discussion, $post, $cm, $context);
// Not all of these variables are used in the default string but are made available to support custom subjects.
$site = get_site();
$a = (object) [
'subject' => $data->get_subject(),
'forumname' => $cleanforumname,
'sitefullname' => format_string($site->fullname),
'siteshortname' => format_string($site->shortname),
'courseidnumber' => $data->get_courseidnumber(),
'coursefullname' => $data->get_coursefullname(),
'courseshortname' => $data->get_coursename(),
];
$postsubject = html_to_text(get_string('postmailsubject', 'forum', $a), 0);
// Message headers are stored against the message author.
$author->customheaders = $this->get_message_headers($course, $forum, $discussion, $post, $a, $data);
$eventdata = new \core\message\message();
$eventdata->courseid = $course->id;
$eventdata->component = 'mod_forum';
$eventdata->name = 'posts';
$eventdata->userfrom = $author;
$eventdata->userto = $this->recipient;
$eventdata->subject = $postsubject;
$eventdata->fullmessage = $this->get_renderer()->render($data);
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = $this->get_renderer(true)->render($data);
$eventdata->notification = 1;
$eventdata->replyto = $replyaddress;
if (!empty($replyaddress)) {
// Add extra text to email messages if they can reply back.
$eventdata->set_additional_content('email', [
'fullmessage' => [
'footer' => "\n\n" . get_string('replytopostbyemail', 'mod_forum'),
],
'fullmessagehtml' => [
'footer' => \html_writer::tag('p', get_string('replytopostbyemail', 'mod_forum')),
]
]);
}
$eventdata->smallmessage = get_string('smallmessage', 'forum', (object) [
'user' => fullname($author),
'forumname' => "$shortname: " . format_string($forum->name, true) . ": " . $discussion->name,
'message' => $post->message,
]);
$contexturl = new \moodle_url('/mod/forum/discuss.php', ['d' => $discussion->id], "p{$post->id}");
$eventdata->contexturl = $contexturl->out();
$eventdata->contexturlname = $discussion->name;
return message_send($eventdata);
}
/**
* Fetch and initialise the post author.
*
* @param int $userid The id of the user to fetch
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $cm
* @param \context $context
* @return \stdClass
*/
protected function get_post_author($userid, $course, $forum, $cm, $context) {
if (!isset($this->users[$userid])) {
// This user no longer exists.
return false;
}
$user = $this->users[$userid];
if (!isset($user->groups)) {
// Initialise the groups list.
$user->groups = [];
}
if (!isset($user->groups[$forum->id])) {
$user->groups[$forum->id] = groups_get_all_groups($course->id, $user->id, $cm->groupingid);
}
// Clone the user object to prevent leaks between messages.
return (object) (array) $user;
}
/**
* Helper to fetch the required renderer, instantiating as required.
*
* @param bool $html Whether to fetch the HTML renderer
* @return \core_renderer
*/
protected function get_renderer($html = false) {
global $PAGE;
$target = $html ? 'htmlemail' : 'textemail';
if (!isset($this->renderers[$target])) {
$this->renderers[$target] = $PAGE->get_renderer('mod_forum', 'email', $target);
}
return $this->renderers[$target];
}
/**
* Get the list of message headers.
*
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $discussion
* @param \stdClass $post
* @param \stdClass $a The list of strings for this post
* @param \core\message\message $message The message to be sent
* @return \stdClass
*/
protected function get_message_headers($course, $forum, $discussion, $post, $a, $message) {
$cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
$viewurl = new \moodle_url('/mod/forum/view.php', ['f' => $forum->id]);
$headers = [
// Headers to make emails easier to track.
'List-Id: "' . $cleanforumname . '" ' . generate_email_messageid('moodleforum' . $forum->id),
'List-Help: ' . $viewurl->out(),
'Message-ID: ' . forum_get_email_message_id($post->id, $this->recipient->id),
'X-Course-Id: ' . $course->id,
'X-Course-Name: '. format_string($course->fullname, true),
// Headers to help prevent auto-responders.
'Precedence: Bulk',
'X-Auto-Response-Suppress: All',
'Auto-Submitted: auto-generated',
'List-Unsubscribe: <' . $message->get_unsubscribediscussionlink() . '>',
];
$rootid = forum_get_email_message_id($discussion->firstpost, $this->recipient->id);
if ($post->parent) {
// This post is a reply, so add reply header (RFC 2822).
$parentid = forum_get_email_message_id($post->parent, $this->recipient->id);
$headers[] = "In-Reply-To: $parentid";
// If the post is deeply nested we also reference the parent message id and
// the root message id (if different) to aid threading when parts of the email
// conversation have been deleted (RFC1036).
if ($post->parent != $discussion->firstpost) {
$headers[] = "References: $rootid $parentid";
} else {
$headers[] = "References: $parentid";
}
}
// MS Outlook / Office uses poorly documented and non standard headers, including
// Thread-Topic which overrides the Subject and shouldn't contain Re: or Fwd: etc.
$aclone = (object) (array) $a;
$aclone->subject = $discussion->name;
$threadtopic = html_to_text(get_string('postmailsubject', 'forum', $aclone), 0);
$headers[] = "Thread-Topic: $threadtopic";
$headers[] = "Thread-Index: " . substr($rootid, 1, 28);
return $headers;
}
/**
* Get a no-reply address for this user to reply to the current post.
*
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $discussion
* @param \stdClass $post
* @param \stdClass $cm
* @param \context $context
* @return string
*/
protected function get_reply_address($course, $forum, $discussion, $post, $cm, $context) {
if ($this->can_post($course, $forum, $discussion, $post, $cm, $context)) {
// Generate a reply-to address from using the Inbound Message handler.
$this->inboundmanager->set_data($post->id);
return $this->inboundmanager->generate($this->recipient->id);
}
// TODO Check if we can return a string.
// This will be controlled by the event.
return null;
}
/**
* Check whether the user can post.
*
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $discussion
* @param \stdClass $post
* @param \stdClass $cm
* @param \context $context
* @return bool
*/
protected function can_post($course, $forum, $discussion, $post, $cm, $context) {
if (!isset($this->canpostto[$discussion->id])) {
$this->canpostto[$discussion->id] = forum_user_can_post($forum, $discussion, $this->recipient, $cm, $course, $context);
}
return $this->canpostto[$discussion->id];
}
/**
* Check whether the user can view full names of other users.
*
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $discussion
* @param \stdClass $post
* @param \stdClass $cm
* @param \context $context
* @return bool
*/
protected function can_view_fullnames($course, $forum, $discussion, $post, $cm, $context) {
if (!isset($this->viewfullnames[$forum->id])) {
$this->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $context, $this->recipient->id);
}
return $this->viewfullnames[$forum->id];
}
/**
* Removes properties from user record that are not necessary for sending post notifications.
*
* @param \stdClass $user
*/
protected function minimise_user_record(\stdClass $user) {
// We store large amount of users in one huge array, make sure we do not store info there we do not actually
// need in mail generation code or messaging.
unset($user->institution);
unset($user->department);
unset($user->address);
unset($user->city);
unset($user->url);
unset($user->currentlogin);
unset($user->description);
unset($user->descriptionformat);
}
}

View File

@ -372,3 +372,41 @@ function forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfro
return $renderer->render($renderable);
}
/**
* Removes properties from user record that are not necessary for sending post notifications.
*
* @param stdClass $user
* @return void, $user parameter is modified
* @deprecated since Moodle 3.7
*/
function forum_cron_minimise_user_record(stdClass $user) {
debugging("forum_cron_minimise_user_record() has been deprecated and has not been replaced.",
DEBUG_DEVELOPER);
// We store large amount of users in one huge array,
// make sure we do not store info there we do not actually need
// in mail generation code or messaging.
unset($user->institution);
unset($user->department);
unset($user->address);
unset($user->city);
unset($user->url);
unset($user->currentlogin);
unset($user->description);
unset($user->descriptionformat);
}
/**
* Function to be run periodically according to the scheduled task.
*
* Finds all posts that have yet to be mailed out, and mails them out to all subscribers as well as other maintance
* tasks.
*
* @deprecated since Moodle 3.7
*/
function forum_cron() {
debugging("forum_cron() has been deprecated and replaced with new tasks. Please uses these instead.",
DEBUG_DEVELOPER);
}

View File

@ -423,829 +423,6 @@ function forum_get_email_message_id($postid, $usertoid) {
return generate_email_messageid(hash('sha256', $postid . 'to' . $usertoid));
}
/**
* Removes properties from user record that are not necessary
* for sending post notifications.
* @param stdClass $user
* @return void, $user parameter is modified
*/
function forum_cron_minimise_user_record(stdClass $user) {
// We store large amount of users in one huge array,
// make sure we do not store info there we do not actually need
// in mail generation code or messaging.
unset($user->institution);
unset($user->department);
unset($user->address);
unset($user->city);
unset($user->url);
unset($user->currentlogin);
unset($user->description);
unset($user->descriptionformat);
}
/**
* Function to be run periodically according to the scheduled task.
*
* Finds all posts that have yet to be mailed out, and mails them
* out to all subscribers as well as other maintance tasks.
*
* NOTE: Since 2.7.2 this function is run by scheduled task rather
* than standard cron.
*
* @todo MDL-44734 The function will be split up into seperate tasks.
*/
function forum_cron() {
global $CFG, $USER, $DB, $PAGE;
$site = get_site();
// The main renderers.
$htmlout = $PAGE->get_renderer('mod_forum', 'email', 'htmlemail');
$textout = $PAGE->get_renderer('mod_forum', 'email', 'textemail');
$htmldigestfullout = $PAGE->get_renderer('mod_forum', 'emaildigestfull', 'htmlemail');
$textdigestfullout = $PAGE->get_renderer('mod_forum', 'emaildigestfull', 'textemail');
$htmldigestbasicout = $PAGE->get_renderer('mod_forum', 'emaildigestbasic', 'htmlemail');
$textdigestbasicout = $PAGE->get_renderer('mod_forum', 'emaildigestbasic', 'textemail');
// All users that are subscribed to any post that needs sending,
// please increase $CFG->extramemorylimit on large sites that
// send notifications to a large number of users.
$users = array();
$userscount = 0; // Cached user counter - count($users) in PHP is horribly slow!!!
// Status arrays.
$mailcount = array();
$errorcount = array();
// caches
$discussions = array();
$forums = array();
$courses = array();
$coursemodules = array();
$subscribedusers = array();
$messageinboundhandlers = array();
// Posts older than 2 days will not be mailed. This is to avoid the problem where
// cron has not been running for a long time, and then suddenly people are flooded
// with mail from the past few weeks or months
$timenow = time();
$endtime = $timenow - $CFG->maxeditingtime;
$starttime = $endtime - 48 * 3600; // Two days earlier
// Get the list of forum subscriptions for per-user per-forum maildigest settings.
$digestsset = $DB->get_recordset('forum_digests', null, '', 'id, userid, forum, maildigest');
$digests = array();
foreach ($digestsset as $thisrow) {
if (!isset($digests[$thisrow->forum])) {
$digests[$thisrow->forum] = array();
}
$digests[$thisrow->forum][$thisrow->userid] = $thisrow->maildigest;
}
$digestsset->close();
// Create the generic messageinboundgenerator.
$messageinboundgenerator = new \core\message\inbound\address_manager();
$messageinboundgenerator->set_handler('\mod_forum\message\inbound\reply_handler');
if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) {
// Mark them all now as being mailed. It's unlikely but possible there
// might be an error later so that a post is NOT actually mailed out,
// but since mail isn't crucial, we can accept this risk. Doing it now
// prevents the risk of duplicated mails, which is a worse problem.
if (!forum_mark_old_posts_as_mailed($endtime)) {
mtrace('Errors occurred while trying to mark some posts as being mailed.');
return false; // Don't continue trying to mail them, in case we are in a cron loop
}
// checking post validity, and adding users to loop through later
foreach ($posts as $pid => $post) {
$discussionid = $post->discussion;
if (!isset($discussions[$discussionid])) {
if ($discussion = $DB->get_record('forum_discussions', array('id'=> $post->discussion))) {
$discussions[$discussionid] = $discussion;
\mod_forum\subscriptions::fill_subscription_cache($discussion->forum);
\mod_forum\subscriptions::fill_discussion_subscription_cache($discussion->forum);
} else {
mtrace('Could not find discussion ' . $discussionid);
unset($posts[$pid]);
continue;
}
}
$forumid = $discussions[$discussionid]->forum;
if (!isset($forums[$forumid])) {
if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
$forums[$forumid] = $forum;
} else {
mtrace('Could not find forum '.$forumid);
unset($posts[$pid]);
continue;
}
}
$courseid = $forums[$forumid]->course;
if (!isset($courses[$courseid])) {
if ($course = $DB->get_record('course', array('id' => $courseid))) {
$courses[$courseid] = $course;
} else {
mtrace('Could not find course '.$courseid);
unset($posts[$pid]);
continue;
}
}
if (!isset($coursemodules[$forumid])) {
if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
$coursemodules[$forumid] = $cm;
} else {
mtrace('Could not find course module for forum '.$forumid);
unset($posts[$pid]);
continue;
}
}
$modcontext = context_module::instance($coursemodules[$forumid]->id);
// Save the Inbound Message datakey here to reduce DB queries later.
$messageinboundgenerator->set_data($pid);
$messageinboundhandlers[$pid] = $messageinboundgenerator->fetch_data_key();
// Caching subscribed users of each forum.
if (!isset($subscribedusers[$forumid])) {
if ($subusers = \mod_forum\subscriptions::fetch_subscribed_users($forums[$forumid], 0, $modcontext, 'u.*', true)) {
foreach ($subusers as $postuser) {
// this user is subscribed to this forum
$subscribedusers[$forumid][$postuser->id] = $postuser->id;
$userscount++;
if ($userscount > FORUM_CRON_USER_CACHE) {
// Store minimal user info.
$minuser = new stdClass();
$minuser->id = $postuser->id;
$users[$postuser->id] = $minuser;
} else {
// Cache full user record.
forum_cron_minimise_user_record($postuser);
$users[$postuser->id] = $postuser;
}
}
// Release memory.
unset($subusers);
unset($postuser);
}
}
$mailcount[$pid] = 0;
$errorcount[$pid] = 0;
}
}
if ($users && $posts) {
foreach ($users as $userto) {
// Terminate if processing of any account takes longer than 2 minutes.
core_php_time_limit::raise(120);
mtrace('Processing user ' . $userto->id);
// Init user caches - we keep the cache for one cycle only, otherwise it could consume too much memory.
if (isset($userto->username)) {
$userto = clone($userto);
} else {
$userto = $DB->get_record('user', array('id' => $userto->id));
forum_cron_minimise_user_record($userto);
}
$userto->viewfullnames = array();
$userto->canpost = array();
$userto->markposts = array();
// Setup this user so that the capabilities are cached, and environment matches receiving user.
cron_setup_user($userto);
// Reset the caches.
foreach ($coursemodules as $forumid => $unused) {
$coursemodules[$forumid]->cache = new stdClass();
$coursemodules[$forumid]->cache->caps = array();
unset($coursemodules[$forumid]->uservisible);
}
foreach ($posts as $pid => $post) {
$discussion = $discussions[$post->discussion];
$forum = $forums[$discussion->forum];
$course = $courses[$forum->course];
$cm =& $coursemodules[$forum->id];
// Do some checks to see if we can bail out now.
// Only active enrolled users are in the list of subscribers.
// This does not necessarily mean that the user is subscribed to the forum or to the discussion though.
if (!isset($subscribedusers[$forum->id][$userto->id])) {
// The user does not subscribe to this forum.
continue;
}
if (!\mod_forum\subscriptions::is_subscribed($userto->id, $forum, $post->discussion, $coursemodules[$forum->id])) {
// The user does not subscribe to this forum, or to this specific discussion.
continue;
}
if ($subscriptiontime = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $userto->id)) {
// Skip posts if the user subscribed to the discussion after it was created.
if (isset($subscriptiontime[$post->discussion]) && ($subscriptiontime[$post->discussion] > $post->created)) {
continue;
}
}
$coursecontext = context_course::instance($course->id);
if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userto->id)) {
// The course is hidden and the user does not have access to it.
continue;
}
// Don't send email if the forum is Q&A and the user has not posted.
// Initial topics are still mailed.
if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
mtrace('Did not email ' . $userto->id.' because user has not posted in discussion');
continue;
}
// Get info about the sending user.
if (array_key_exists($post->userid, $users)) {
// We might know the user already.
$userfrom = $users[$post->userid];
if (!isset($userfrom->idnumber)) {
// Minimalised user info, fetch full record.
$userfrom = $DB->get_record('user', array('id' => $userfrom->id));
forum_cron_minimise_user_record($userfrom);
}
} else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
forum_cron_minimise_user_record($userfrom);
// Fetch only once if possible, we can add it to user list, it will be skipped anyway.
if ($userscount <= FORUM_CRON_USER_CACHE) {
$userscount++;
$users[$userfrom->id] = $userfrom;
}
} else {
mtrace('Could not find user ' . $post->userid . ', author of post ' . $post->id . '. Unable to send message.');
continue;
}
// Note: If we want to check that userto and userfrom are not the same person this is probably the spot to do it.
// Setup global $COURSE properly - needed for roles and languages.
cron_setup_user($userto, $course);
// Fill caches.
if (!isset($userto->viewfullnames[$forum->id])) {
$modcontext = context_module::instance($cm->id);
$userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
}
if (!isset($userto->canpost[$discussion->id])) {
$modcontext = context_module::instance($cm->id);
$userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
}
if (!isset($userfrom->groups[$forum->id])) {
if (!isset($userfrom->groups)) {
$userfrom->groups = array();
if (isset($users[$userfrom->id])) {
$users[$userfrom->id]->groups = array();
}
}
$userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
if (isset($users[$userfrom->id])) {
$users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
}
}
// Make sure groups allow this user to see this email.
if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {
// Groups are being used.
if (!groups_group_exists($discussion->groupid)) {
// Can't find group - be safe and don't this message.
continue;
}
if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) {
// Do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS.
continue;
}
}
// Make sure we're allowed to see the post.
if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm)) {
mtrace('User ' . $userto->id .' can not see ' . $post->id . '. Not sending message.');
continue;
}
// OK so we need to send the email.
// Does the user want this post in a digest? If so postpone it for now.
$maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
if ($maildigest > 0) {
// This user wants the mails to be in digest form.
$queue = new stdClass();
$queue->userid = $userto->id;
$queue->discussionid = $discussion->id;
$queue->postid = $post->id;
$queue->timemodified = $post->created;
$DB->insert_record('forum_queue', $queue);
continue;
}
// Prepare to actually send the post now, and build up the content.
$cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
$userfrom->customheaders = array (
// Headers to make emails easier to track.
'List-Id: "' . $cleanforumname . '" ' . generate_email_messageid('moodleforum' . $forum->id),
'List-Help: ' . $CFG->wwwroot . '/mod/forum/view.php?f=' . $forum->id,
'Message-ID: ' . forum_get_email_message_id($post->id, $userto->id),
'X-Course-Id: ' . $course->id,
'X-Course-Name: ' . format_string($course->fullname, true),
// Headers to help prevent auto-responders.
'Precedence: Bulk',
'X-Auto-Response-Suppress: All',
'Auto-Submitted: auto-generated',
);
$shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
// Generate a reply-to address from using the Inbound Message handler.
$replyaddress = null;
if ($userto->canpost[$discussion->id] && array_key_exists($post->id, $messageinboundhandlers)) {
$messageinboundgenerator->set_data($post->id, $messageinboundhandlers[$post->id]);
$replyaddress = $messageinboundgenerator->generate($userto->id);
}
if (!isset($userto->canpost[$discussion->id])) {
$canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
} else {
$canreply = $userto->canpost[$discussion->id];
}
$data = new \mod_forum\output\forum_post_email(
$course,
$cm,
$forum,
$discussion,
$post,
$userfrom,
$userto,
$canreply
);
$userfrom->customheaders[] = sprintf('List-Unsubscribe: <%s>',
$data->get_unsubscribediscussionlink());
if (!isset($userto->viewfullnames[$forum->id])) {
$data->viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
} else {
$data->viewfullnames = $userto->viewfullnames[$forum->id];
}
// Not all of these variables are used in the default language
// string but are made available to support custom subjects.
$a = new stdClass();
$a->subject = $data->get_subject();
$a->forumname = $cleanforumname;
$a->sitefullname = format_string($site->fullname);
$a->siteshortname = format_string($site->shortname);
$a->courseidnumber = $data->get_courseidnumber();
$a->coursefullname = $data->get_coursefullname();
$a->courseshortname = $data->get_coursename();
$postsubject = html_to_text(get_string('postmailsubject', 'forum', $a), 0);
$rootid = forum_get_email_message_id($discussion->firstpost, $userto->id);
if ($post->parent) {
// This post is a reply, so add reply header (RFC 2822).
$parentid = forum_get_email_message_id($post->parent, $userto->id);
$userfrom->customheaders[] = "In-Reply-To: $parentid";
// If the post is deeply nested we also reference the parent message id and
// the root message id (if different) to aid threading when parts of the email
// conversation have been deleted (RFC1036).
if ($post->parent != $discussion->firstpost) {
$userfrom->customheaders[] = "References: $rootid $parentid";
} else {
$userfrom->customheaders[] = "References: $parentid";
}
}
// MS Outlook / Office uses poorly documented and non standard headers, including
// Thread-Topic which overrides the Subject and shouldn't contain Re: or Fwd: etc.
$a->subject = $discussion->name;
$threadtopic = html_to_text(get_string('postmailsubject', 'forum', $a), 0);
$userfrom->customheaders[] = "Thread-Topic: $threadtopic";
$userfrom->customheaders[] = "Thread-Index: " . substr($rootid, 1, 28);
// Send the post now!
mtrace('Sending ', '');
$eventdata = new \core\message\message();
$eventdata->courseid = $course->id;
$eventdata->component = 'mod_forum';
$eventdata->name = 'posts';
$eventdata->userfrom = $userfrom;
$eventdata->userto = $userto;
$eventdata->subject = $postsubject;
$eventdata->fullmessage = $textout->render($data);
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = $htmlout->render($data);
$eventdata->notification = 1;
$eventdata->replyto = $replyaddress;
if (!empty($replyaddress)) {
// Add extra text to email messages if they can reply back.
$textfooter = "\n\n" . get_string('replytopostbyemail', 'mod_forum');
$htmlfooter = html_writer::tag('p', get_string('replytopostbyemail', 'mod_forum'));
$additionalcontent = array('fullmessage' => array('footer' => $textfooter),
'fullmessagehtml' => array('footer' => $htmlfooter));
$eventdata->set_additional_content('email', $additionalcontent);
}
$smallmessagestrings = new stdClass();
$smallmessagestrings->user = fullname($userfrom);
$smallmessagestrings->forumname = "$shortname: " . format_string($forum->name, true) . ": " . $discussion->name;
$smallmessagestrings->message = $post->message;
// Make sure strings are in message recipients language.
$eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'forum', $smallmessagestrings, $userto->lang);
$contexturl = new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id), 'p' . $post->id);
$eventdata->contexturl = $contexturl->out();
$eventdata->contexturlname = $discussion->name;
$mailresult = message_send($eventdata);
if (!$mailresult) {
mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id $post->id to user $userto->id".
" ($userto->email) .. not trying again.");
$errorcount[$post->id]++;
} else {
$mailcount[$post->id]++;
// Mark post as read if forum_usermarksread is set off.
if (!$CFG->forum_usermarksread) {
$userto->markposts[$post->id] = $post->id;
}
}
mtrace('post ' . $post->id . ': ' . $post->subject);
}
// Mark processed posts as read.
if (get_user_preferences('forum_markasreadonnotification', 1, $userto->id) == 1) {
forum_tp_mark_posts_read($userto, $userto->markposts);
}
unset($userto);
}
}
if ($posts) {
foreach ($posts as $post) {
mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'");
if ($errorcount[$post->id]) {
$DB->set_field('forum_posts', 'mailed', FORUM_MAILED_ERROR, array('id' => $post->id));
}
}
}
// release some memory
unset($subscribedusers);
unset($mailcount);
unset($errorcount);
cron_setup_user();
$sitetimezone = core_date::get_server_timezone();
// Now see if there are any digest mails waiting to be sent, and if we should send them
mtrace('Starting digest processing...');
core_php_time_limit::raise(300); // terminate if not able to fetch all digests in 5 minutes
if (!isset($CFG->digestmailtimelast)) { // To catch the first time
set_config('digestmailtimelast', 0);
}
$timenow = time();
$digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
// Delete any really old ones (normally there shouldn't be any)
$weekago = $timenow - (7 * 24 * 3600);
$DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
mtrace ('Cleaned old digest records');
if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) {
mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone));
$digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime));
if ($digestposts_rs->valid()) {
// We have work to do
$usermailcount = 0;
//caches - reuse the those filled before too
$discussionposts = array();
$userdiscussions = array();
foreach ($digestposts_rs as $digestpost) {
if (!isset($posts[$digestpost->postid])) {
if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) {
$posts[$digestpost->postid] = $post;
} else {
continue;
}
}
$discussionid = $digestpost->discussionid;
if (!isset($discussions[$discussionid])) {
if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) {
$discussions[$discussionid] = $discussion;
} else {
continue;
}
}
$forumid = $discussions[$discussionid]->forum;
if (!isset($forums[$forumid])) {
if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
$forums[$forumid] = $forum;
} else {
continue;
}
}
$courseid = $forums[$forumid]->course;
if (!isset($courses[$courseid])) {
if ($course = $DB->get_record('course', array('id' => $courseid))) {
$courses[$courseid] = $course;
} else {
continue;
}
}
if (!isset($coursemodules[$forumid])) {
if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
$coursemodules[$forumid] = $cm;
} else {
continue;
}
}
$userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid;
$discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid;
}
$digestposts_rs->close(); /// Finished iteration, let's close the resultset
// Data collected, start sending out emails to each user
foreach ($userdiscussions as $userid => $thesediscussions) {
core_php_time_limit::raise(120); // terminate if processing of any account takes longer than 2 minutes
cron_setup_user();
mtrace(get_string('processingdigest', 'forum', $userid), '... ');
// First of all delete all the queue entries for this user
$DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime));
// Init user caches - we keep the cache for one cycle only,
// otherwise it would unnecessarily consume memory.
if (array_key_exists($userid, $users) and isset($users[$userid]->username)) {
$userto = clone($users[$userid]);
} else {
$userto = $DB->get_record('user', array('id' => $userid));
forum_cron_minimise_user_record($userto);
}
$userto->viewfullnames = array();
$userto->canpost = array();
$userto->markposts = array();
// Override the language and timezone of the "current" user, so that
// mail is customised for the receiver.
cron_setup_user($userto);
$postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
$headerdata = new stdClass();
$headerdata->sitename = format_string($site->fullname, true);
$headerdata->userprefs = $CFG->wwwroot.'/user/forum.php?id='.$userid.'&amp;course='.$site->id;
$posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
$headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
$posthtml = '<p>'.get_string('digestmailheader', 'forum', $headerdata).'</p>'
. '<br /><hr size="1" noshade="noshade" />';
foreach ($thesediscussions as $discussionid) {
core_php_time_limit::raise(120); // to be reset for each post
$discussion = $discussions[$discussionid];
$forum = $forums[$discussion->forum];
$course = $courses[$forum->course];
$cm = $coursemodules[$forum->id];
//override language
cron_setup_user($userto, $course);
// Fill caches
if (!isset($userto->viewfullnames[$forum->id])) {
$modcontext = context_module::instance($cm->id);
$userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
}
if (!isset($userto->canpost[$discussion->id])) {
$modcontext = context_module::instance($cm->id);
$userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
}
$strforums = get_string('forums', 'forum');
$canunsubscribe = ! \mod_forum\subscriptions::is_forcesubscribed($forum);
$canreply = $userto->canpost[$discussion->id];
$shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
$posttext .= "\n \n";
$posttext .= '=====================================================================';
$posttext .= "\n \n";
$posttext .= "$shortname -> $strforums -> ".format_string($forum->name,true);
if ($discussion->name != $forum->name) {
$posttext .= " -> ".format_string($discussion->name,true);
}
$posttext .= "\n";
$posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
$posttext .= "\n";
$posthtml .= "<p><font face=\"sans-serif\">".
"<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
"<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
"<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
if ($discussion->name == $forum->name) {
$posthtml .= "</font></p>";
} else {
$posthtml .= " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a></font></p>";
}
$posthtml .= '<p>';
$postsarray = $discussionposts[$discussionid];
sort($postsarray);
$sentcount = 0;
foreach ($postsarray as $postid) {
$post = $posts[$postid];
if (array_key_exists($post->userid, $users)) { // we might know him/her already
$userfrom = $users[$post->userid];
if (!isset($userfrom->idnumber)) {
$userfrom = $DB->get_record('user', array('id' => $userfrom->id));
forum_cron_minimise_user_record($userfrom);
}
} else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
forum_cron_minimise_user_record($userfrom);
if ($userscount <= FORUM_CRON_USER_CACHE) {
$userscount++;
$users[$userfrom->id] = $userfrom;
}
} else {
mtrace('Could not find user '.$post->userid);
continue;
}
if (!isset($userfrom->groups[$forum->id])) {
if (!isset($userfrom->groups)) {
$userfrom->groups = array();
if (isset($users[$userfrom->id])) {
$users[$userfrom->id]->groups = array();
}
}
$userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
if (isset($users[$userfrom->id])) {
$users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
}
}
// Headers to help prevent auto-responders.
$userfrom->customheaders = array(
"Precedence: Bulk",
'X-Auto-Response-Suppress: All',
'Auto-Submitted: auto-generated',
);
$maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
if (!isset($userto->canpost[$discussion->id])) {
$canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
} else {
$canreply = $userto->canpost[$discussion->id];
}
$data = new \mod_forum\output\forum_post_email(
$course,
$cm,
$forum,
$discussion,
$post,
$userfrom,
$userto,
$canreply
);
if (!isset($userto->viewfullnames[$forum->id])) {
$data->viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
} else {
$data->viewfullnames = $userto->viewfullnames[$forum->id];
}
if ($maildigest == 2) {
// Subjects and link only.
$posttext .= $textdigestbasicout->render($data);
$posthtml .= $htmldigestbasicout->render($data);
} else {
// The full treatment.
$posttext .= $textdigestfullout->render($data);
$posthtml .= $htmldigestfullout->render($data);
// Create an array of postid's for this user to mark as read.
if (!$CFG->forum_usermarksread) {
$userto->markposts[$post->id] = $post->id;
}
}
$sentcount++;
}
$footerlinks = array();
if ($canunsubscribe) {
$footerlinks[] = "<a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">" . get_string("unsubscribe", "forum") . "</a>";
} else {
$footerlinks[] = get_string("everyoneissubscribed", "forum");
}
$footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . get_string("digestmailpost", "forum") . '</a>';
$posthtml .= "\n<div class='mdl-right'><font size=\"1\">" . implode('&nbsp;', $footerlinks) . '</font></div>';
$posthtml .= '<hr size="1" noshade="noshade" /></p>';
}
if (empty($userto->mailformat) || $userto->mailformat != 1) {
// This user DOESN'T want to receive HTML
$posthtml = '';
}
$eventdata = new \core\message\message();
$eventdata->courseid = SITEID;
$eventdata->component = 'mod_forum';
$eventdata->name = 'digests';
$eventdata->userfrom = core_user::get_noreply_user();
$eventdata->userto = $userto;
$eventdata->subject = $postsubject;
$eventdata->fullmessage = $posttext;
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = $posthtml;
$eventdata->notification = 1;
$eventdata->smallmessage = get_string('smallmessagedigest', 'forum', $sentcount);
$mailresult = message_send($eventdata);
if (!$mailresult) {
mtrace("ERROR: mod/forum/cron.php: Could not send out digest mail to user $userto->id ".
"($userto->email)... not trying again.");
} else {
mtrace("success.");
$usermailcount++;
// Mark post as read if forum_usermarksread is set off
if (get_user_preferences('forum_markasreadonnotification', 1, $userto->id) == 1) {
forum_tp_mark_posts_read($userto, $userto->markposts);
}
}
}
}
/// We have finishied all digest emails, update $CFG->digestmailtimelast
set_config('digestmailtimelast', $timenow);
}
cron_setup_user();
if (!empty($usermailcount)) {
mtrace(get_string('digestsentusers', 'forum', $usermailcount));
}
if (!empty($CFG->forum_lastreadclean)) {
$timenow = time();
if ($CFG->forum_lastreadclean + (24*3600) < $timenow) {
set_config('forum_lastreadclean', $timenow);
mtrace('Removing old forum read tracking info...');
forum_tp_clean_read_records();
}
} else {
set_config('forum_lastreadclean', time());
}
return true;
}
/**
*
* @param object $course

View File

@ -44,7 +44,8 @@
} {{/ str }}
---------------------------------------------------------------------
{{{ message }}}
{{# attachments }}
{{{ attachments }}}
{{/ attachments }}
---------------------------------------------------------------------
{{# str }} digestmailpostlink, forum, {{{ forumindexlink }}} {{/ str }}

View File

@ -0,0 +1,129 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The forum module cron trait.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
trait mod_forum_tests_cron_trait {
/**
* Run the main cron task to queue all tasks, and ensure that posts
* were sent to the correct users.
*
* @param \stdClass[] $expectations The list of users, along with their expected count of messages and digests.
*/
protected function queue_tasks_and_assert($expectations = []) {
global $DB;
// Note, we cannot use expectOutputRegex because it only allows for a single RegExp.
ob_start();
cron_setup_user();
$cron = new \mod_forum\task\cron_task();
$cron->execute();
$output = ob_get_contents();
ob_end_clean();
$uniqueusers = 0;
foreach ($expectations as $expect) {
$expect->digests = isset($expect->digests) ? $expect->digests : 0;
$expect->messages = isset($expect->messages) ? $expect->messages : 0;
$expect->mentioned = isset($expect->mentioned) ? $expect->mentioned : false;
if ($expect->digests || $expect->messages) {
$expect->mentioned = true;
}
if (!$expect->mentioned) {
$this->assertNotRegExp("/Queued 0 for {$expect->userid}/", $output);
} else {
$uniqueusers++;
$this->assertRegExp(
"/Queued {$expect->digests} digests and {$expect->messages} messages for {$expect->userid}/",
$output
);
}
}
if (empty($expectations)) {
$this->assertRegExp("/No posts found./", $output);
} else {
$this->assertRegExp("/Unique users: {$uniqueusers}/", $output);
}
// Update the forum queue for digests.
$DB->execute("UPDATE {forum_queue} SET timemodified = timemodified - 1");
}
/**
* Run any send_user_notifications tasks for the specified user, and
* ensure that the posts specified were sent.
*
* @param \stdClass $user
* @param \stdClass[] $posts
* @param bool $ignoreemptyposts
*/
protected function send_notifications_and_assert($user, $posts = [], $ignoreemptyposts = false) {
ob_start();
$this->runAdhocTasks(\mod_forum\task\send_user_notifications::class, $user->id);
$output = ob_get_contents();
ob_end_clean();
if (empty($posts) && !$ignoreemptyposts) {
$this->assertEquals('', $output);
} else {
$this->assertRegExp("/Sending messages to {$user->username}/", $output);
foreach ($posts as $post) {
$this->assertRegExp("/Post {$post->id} sent/", $output);
}
$count = count($posts);
$this->assertRegExp("/Sent {$count} messages with 0 failures/", $output);
}
}
/**
* Run any send_user_digests tasks for the specified user, and
* ensure that the posts specified were sent.
*
* @param \stdClass $user
* @param \stdClass[] $fullposts
* @param \stdClass[] $shortposts
*/
protected function send_digests_and_assert($user, $fullposts = [], $shortposts = []) {
ob_start();
$this->runAdhocTasks(\mod_forum\task\send_user_digests::class, $user->id);
$output = ob_get_contents();
ob_end_clean();
if (empty($shortposts) && empty($fullposts)) {
$this->assertEquals('', $output);
$this->assertRegExp("/Digest sent with 0 messages./", $output);
} else {
$this->assertRegExp("/Sending forum digests for {$user->username}/", $output);
foreach ($fullposts as $post) {
$this->assertRegExp("/Adding post {$post->id} in format 1/", $output);
}
foreach ($shortposts as $post) {
$this->assertRegExp("/Adding post {$post->id} in format 2/", $output);
}
$count = count($fullposts) + count($shortposts);
$this->assertRegExp("/Digest sent with {$count} messages./", $output);
}
}
}

View File

@ -0,0 +1,159 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The forum module trait with additional generator helpers.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
trait mod_forum_tests_generator_trait {
/**
* Helper to create the required number of users in the specified course.
* Users are enrolled as students by default.
*
* @param stdClass $course The course object
* @param integer $count The number of users to create
* @param string $role The role to assign users as
* @return array The users created
*/
protected function helper_create_users($course, $count, $role = null) {
$users = array();
for ($i = 0; $i < $count; $i++) {
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
$users[] = $user;
}
return $users;
}
/**
* Create a new discussion and post within the specified forum, as the
* specified author.
*
* @param stdClass $forum The forum to post in
* @param stdClass $author The author to post as
* @param array $fields any other fields in discussion (name, message, messageformat, ...)
* @return array An array containing the discussion object, and the post object
*/
protected function helper_post_to_forum($forum, $author, $fields = array()) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Create a discussion in the forum, and then add a post to that discussion.
$record = (object)$fields;
$record->course = $forum->course;
$record->userid = $author->id;
$record->forum = $forum->id;
$discussion = $generator->create_discussion($record);
// Retrieve the post which was created by create_discussion.
$post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
return array($discussion, $post);
}
/**
* Update the post time for the specified post by $factor.
*
* @param stdClass $post The post to update
* @param int $factor The amount to update by
*/
protected function helper_update_post_time($post, $factor) {
global $DB;
// Update the post to have a created in the past.
$DB->set_field('forum_posts', 'created', $post->created + $factor, array('id' => $post->id));
}
/**
* Update the subscription time for the specified user/discussion by $factor.
*
* @param stdClass $user The user to update
* @param stdClass $discussion The discussion to update for this user
* @param int $factor The amount to update by
*/
protected function helper_update_subscription_time($user, $discussion, $factor) {
global $DB;
$sub = $DB->get_record('forum_discussion_subs', array('userid' => $user->id, 'discussion' => $discussion->id));
// Update the subscription to have a preference in the past.
$DB->set_field('forum_discussion_subs', 'preference', $sub->preference + $factor, array('id' => $sub->id));
}
/**
* Create a new post within an existing discussion, as the specified author.
*
* @param stdClass $forum The forum to post in
* @param stdClass $discussion The discussion to post in
* @param stdClass $author The author to post as
* @return stdClass The forum post
*/
protected function helper_post_to_discussion($forum, $discussion, $author) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Add a post to the discussion.
$record = new stdClass();
$record->course = $forum->course;
$strre = get_string('re', 'forum');
$record->subject = $strre . ' ' . $discussion->subject;
$record->userid = $author->id;
$record->forum = $forum->id;
$record->discussion = $discussion->id;
$record->mailnow = 1;
$post = $generator->create_post($record);
return $post;
}
/**
* Create a new post within an existing discussion, as the specified author.
*
* @param stdClass $parent The post being replied to
* @param stdClass $author The author to post as
* @return stdClass The forum post
*/
protected function helper_reply_to_post($parent, $author) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Add a post to the discussion.
$strre = get_string('re', 'forum');
$record = (object) [
'discussion' => $parent->discussion,
'parent' => $parent->id,
'userid' => $author->id,
'mailnow' => 1,
'subject' => $strre . ' ' . $parent->subject,
];
$post = $generator->create_post($record);
return $post;
}
}

View File

@ -0,0 +1,246 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The forum module mail generation tests for groups.
*
* @package mod_forum
* @copyright 2013 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/forum/lib.php');
require_once(__DIR__ . '/cron_trait.php');
require_once(__DIR__ . '/generator_trait.php');
/**
* The forum module mail generation tests for groups.
*
* @copyright 2013 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_forum_mail_group_testcase extends advanced_testcase {
// Make use of the cron tester trait.
use mod_forum_tests_cron_trait;
// Make use of the test generator trait.
use mod_forum_tests_generator_trait;
/**
* @var \phpunit_message_sink
*/
protected $messagesink;
/**
* @var \phpunit_mailer_sink
*/
protected $mailsink;
public function setUp() {
global $CFG;
// We must clear the subscription caches. This has to be done both before each test, and after in case of other
// tests using these functions.
\mod_forum\subscriptions::reset_forum_cache();
\mod_forum\subscriptions::reset_discussion_cache();
// Messaging is not compatible with transactions...
$this->preventResetByRollback();
// Catch all messages.
$this->messagesink = $this->redirectMessages();
$this->mailsink = $this->redirectEmails();
// Forcibly reduce the maxeditingtime to a second in the past to
// ensure that messages are sent out.
$CFG->maxeditingtime = -1;
}
public function tearDown() {
// We must clear the subscription caches. This has to be done both before each test, and after in case of other
// tests using these functions.
\mod_forum\subscriptions::reset_forum_cache();
$this->messagesink->clear();
$this->messagesink->close();
unset($this->messagesink);
$this->mailsink->clear();
$this->mailsink->close();
unset($this->mailsink);
}
/**
* Ensure that posts written in a forum marked for separate groups includes notifications for the members of that
* group, and any user with accessallgroups.
*/
public function test_separate_group() {
global $CFG, $DB;
$this->resetAfterTest(true);
// Create a course, with a forum.
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
'forcesubscribe' => FORUM_INITIALSUBSCRIBE,
'groupmode' => SEPARATEGROUPS,
]);
// Create three students:
// - author, enrolled in group A; and
// - recipient, enrolled in group B; and
// - other, enrolled in the course, but no groups.
list($author, $recipient, $otheruser) = $this->helper_create_users($course, 3);
// Create one teacher, not in any group and no accessallgroups capability.
list($teacher) = $this->helper_create_users($course, 1, 'teacher');
// Create one editing teacher, not in any group but with accessallgroups capability.
list($editingteacher) = $this->helper_create_users($course, 1, 'editingteacher');
$groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$this->getDataGenerator()->create_group_member([
'groupid' => $groupa->id,
'userid' => $author->id,
]);
$this->getDataGenerator()->create_group_member([
'groupid' => $groupb->id,
'userid' => $recipient->id,
]);
// Post a discussion to the forum.
list($discussion, $post) = $this->helper_post_to_forum($forum, $author, [
'groupid' => $groupa->id,
]);
// Only the author should receive.
$expect = [
'author' => (object) [
'userid' => $author->id,
'messages' => 1,
],
'recipient' => (object) [
'userid' => $recipient->id,
'messages' => 0,
],
'otheruser' => (object) [
'userid' => $otheruser->id,
'messages' => 0,
],
'teacher' => (object) [
'userid' => $teacher->id,
'messages' => 0,
],
'editingteacher' => (object) [
'userid' => $editingteacher->id,
'messages' => 1,
],
];
$this->queue_tasks_and_assert($expect);
// No notifications should be queued.
$this->send_notifications_and_assert($author, [$post]);
$this->send_notifications_and_assert($recipient, []);
$this->send_notifications_and_assert($otheruser, []);
$this->send_notifications_and_assert($teacher, []);
$this->send_notifications_and_assert($editingteacher, [$post]);
}
/**
* Ensure that posts written in a forum marked for visible groups includes notifications for the members of that
* group, and any user with accessallgroups.
*/
public function test_visible_group() {
global $CFG, $DB;
$this->resetAfterTest(true);
// Create a course, with a forum.
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
'forcesubscribe' => FORUM_INITIALSUBSCRIBE,
'groupmode' => VISIBLEGROUPS,
]);
// Create three students:
// - author, enrolled in group A; and
// - recipient, enrolled in group B; and
// - other, enrolled in the course, but no groups.
list($author, $recipient, $otheruser) = $this->helper_create_users($course, 3);
// Create one teacher, not in any group and no accessallgroups capability.
list($teacher) = $this->helper_create_users($course, 1, 'teacher');
// Create one editing teacher, not in any group but with accessallgroups capability.
list($editingteacher) = $this->helper_create_users($course, 1, 'editingteacher');
$groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$this->getDataGenerator()->create_group_member([
'groupid' => $groupa->id,
'userid' => $author->id,
]);
$this->getDataGenerator()->create_group_member([
'groupid' => $groupb->id,
'userid' => $recipient->id,
]);
// Post a discussion to the forum.
list($discussion, $post) = $this->helper_post_to_forum($forum, $author, [
'groupid' => $groupa->id,
]);
// Only the author should receive.
$expect = [
'author' => (object) [
'userid' => $author->id,
'messages' => 1,
],
'recipient' => (object) [
'userid' => $recipient->id,
'messages' => 0,
],
'otheruser' => (object) [
'userid' => $otheruser->id,
'messages' => 0,
],
'teacher' => (object) [
'userid' => $teacher->id,
'messages' => 0,
],
'editingteacher' => (object) [
'userid' => $editingteacher->id,
'messages' => 1,
],
];
$this->queue_tasks_and_assert($expect);
// No notifications should be queued.
$this->send_notifications_and_assert($author, [$post]);
$this->send_notifications_and_assert($recipient, []);
$this->send_notifications_and_assert($otheruser, []);
$this->send_notifications_and_assert($teacher, []);
$this->send_notifications_and_assert($editingteacher, [$post]);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -26,17 +26,16 @@
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/cron_trait.php');
require_once(__DIR__ . '/generator_trait.php');
class mod_forum_maildigest_testcase extends advanced_testcase {
/**
* Keep track of the message and mail sinks that we set up for each
* test.
*
* @var stdClass $helper
*/
protected $helper;
// Make use of the cron tester trait.
use mod_forum_tests_cron_trait;
// Make use of the test generator trait.
use mod_forum_tests_generator_trait;
/**
* Set up message and mail sinks, and set up other requirements for the
@ -45,20 +44,18 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
public function setUp() {
global $CFG;
$this->helper = new stdClass();
// Messaging is not compatible with transactions...
$this->preventResetByRollback();
// Catch all messages
$this->helper->messagesink = $this->redirectMessages();
$this->helper->mailsink = $this->redirectEmails();
$this->messagesink = $this->redirectMessages();
$this->mailsink = $this->redirectEmails();
// Confirm that we have an empty message sink so far.
$messages = $this->helper->messagesink->get_messages();
$messages = $this->messagesink->get_messages();
$this->assertEquals(0, count($messages));
$messages = $this->helper->mailsink->get_messages();
$messages = $this->mailsink->get_messages();
$this->assertEquals(0, count($messages));
// Tell Moodle that we've not sent any digest messages out recently.
@ -82,11 +79,11 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
* Clear the message sinks set up in this test.
*/
public function tearDown() {
$this->helper->messagesink->clear();
$this->helper->messagesink->close();
$this->messagesink->clear();
$this->messagesink->close();
$this->helper->mailsink->clear();
$this->helper->mailsink->close();
$this->mailsink->clear();
$this->mailsink->close();
}
/**
@ -131,52 +128,6 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
return $return;
}
/**
* Helper to falsify all forum post records for a digest run.
*/
protected function helper_force_digest_mail_times() {
global $CFG, $DB;
// Fake all of the post editing times because digests aren't sent until
// the start of an hour where the modification time on the message is before
// the start of that hour
$sitetimezone = core_date::get_server_timezone();
$digesttime = usergetmidnight(time(), $sitetimezone) + ($CFG->digestmailtime * 3600) - (60 * 60);
$DB->set_field('forum_posts', 'modified', $digesttime, array('mailed' => 0));
$DB->set_field('forum_posts', 'created', $digesttime, array('mailed' => 0));
}
/**
* Run the forum cron, and check that the specified post was sent the
* specified number of times.
*
* @param integer $expected The number of times that the post should have been sent
* @param integer $individualcount The number of individual messages sent
* @param integer $digestcount The number of digest messages sent
*/
protected function helper_run_cron_check_count($expected, $individualcount, $digestcount) {
if ($expected === 0) {
$this->expectOutputRegex('/(Email digests successfully sent to .* users.){0}/');
} else {
$this->expectOutputRegex("/Email digests successfully sent to {$expected} users/");
}
forum_cron();
// Now check the results in the message sink.
$messages = $this->helper->messagesink->get_messages();
$counts = (object) array('digest' => 0, 'individual' => 0);
foreach ($messages as $message) {
if (strpos($message->subject, 'forum digest') !== false) {
$counts->digest++;
} else {
$counts->individual++;
}
}
$this->assertEquals($digestcount, $counts->digest);
$this->assertEquals($individualcount, $counts->individual);
}
public function test_set_maildigest() {
global $DB;
@ -302,10 +253,9 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$this->resetAfterTest(true);
$this->helper_force_digest_mail_times();
// Initially the forum cron should generate no messages as we've made no posts.
$this->helper_run_cron_check_count(0, 0, 0);
$expect = [];
$this->queue_tasks_and_assert($expect);
}
/**
@ -324,27 +274,19 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$forum1 = $userhelper->forums->forum1;
$forum2 = $userhelper->forums->forum2;
// Add some discussions to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user->id;
$record->mailnow = 1;
// Add 5 discussions to forum 1.
$record->forum = $forum1->id;
$posts = [];
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Add 5 discussions to forum 2.
$record->forum = $forum2->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Ensure that the creation times mean that the messages will be sent.
$this->helper_force_digest_mail_times();
// Set the tested user's default maildigest setting.
$DB->set_field('user', 'maildigest', 0, array('id' => $user->id));
@ -355,7 +297,16 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
forum_set_user_maildigest($forum2, -1, $user);
// No digests mails should be sent, but 10 forum mails will be sent.
$this->helper_run_cron_check_count(0, 10, 0);
$expect = [
(object) [
'userid' => $user->id,
'messages' => 10,
'digests' => 0,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_notifications_and_assert($user, $posts);
}
/**
@ -373,28 +324,20 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$course1 = $userhelper->courses->course1;
$forum1 = $userhelper->forums->forum1;
$forum2 = $userhelper->forums->forum2;
// Add a discussion to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user->id;
$record->mailnow = 1;
$posts = [];
// Add 5 discussions to forum 1.
$record->forum = $forum1->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Add 5 discussions to forum 2.
$record->forum = $forum2->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Ensure that the creation times mean that the messages will be sent.
$this->helper_force_digest_mail_times();
// Set the tested user's default maildigest setting.
$DB->set_field('user', 'maildigest', 1, array('id' => $user->id));
@ -404,8 +347,17 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
// Set the maildigest preference for forum2 to default.
forum_set_user_maildigest($forum2, -1, $user);
// One digest mail should be sent, with no notifications, and one e-mail.
$this->helper_run_cron_check_count(1, 0, 1);
// No digests mails should be sent, but 10 forum mails will be sent.
$expect = [
(object) [
'userid' => $user->id,
'messages' => 0,
'digests' => 1,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_digests_and_assert($user, $posts);
}
/**
@ -424,28 +376,21 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$course1 = $userhelper->courses->course1;
$forum1 = $userhelper->forums->forum1;
$forum2 = $userhelper->forums->forum2;
// Add a discussion to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user->id;
$record->mailnow = 1;
$posts = [];
$digests = [];
// Add 5 discussions to forum 1.
$record->forum = $forum1->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
$digests[] = $post;
}
// Add 5 discussions to forum 2.
$record->forum = $forum2->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Ensure that the creation times mean that the messages will be sent.
$this->helper_force_digest_mail_times();
// Set the tested user's default maildigest setting.
$DB->set_field('user', 'maildigest', 0, array('id' => $user->id));
@ -456,7 +401,17 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
forum_set_user_maildigest($forum2, -1, $user);
// One digest e-mail should be sent, and five individual notifications.
$this->helper_run_cron_check_count(1, 5, 1);
$expect = [
(object) [
'userid' => $user->id,
'messages' => 5,
'digests' => 1,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_notifications_and_assert($user, $posts);
$this->send_digests_and_assert($user, $digests);
}
/**
@ -475,28 +430,21 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$course1 = $userhelper->courses->course1;
$forum1 = $userhelper->forums->forum1;
$forum2 = $userhelper->forums->forum2;
// Add a discussion to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user->id;
$record->mailnow = 1;
$posts = [];
$digests = [];
// Add 5 discussions to forum 1.
$record->forum = $forum1->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
$digests[] = $post;
}
// Add 5 discussions to forum 2.
$record->forum = $forum2->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Ensure that the creation times mean that the messages will be sent.
$this->helper_force_digest_mail_times();
// Set the tested user's default maildigest setting.
$DB->set_field('user', 'maildigest', 1, array('id' => $user->id));
@ -507,7 +455,17 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
forum_set_user_maildigest($forum2, 0, $user);
// One digest e-mail should be sent, and five individual notifications.
$this->helper_run_cron_check_count(1, 5, 1);
$expect = [
(object) [
'userid' => $user->id,
'messages' => 5,
'digests' => 1,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_notifications_and_assert($user, $posts);
$this->send_digests_and_assert($user, $digests);
}
/**
@ -525,28 +483,21 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$course1 = $userhelper->courses->course1;
$forum1 = $userhelper->forums->forum1;
$forum2 = $userhelper->forums->forum2;
// Add a discussion to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user->id;
$record->mailnow = 1;
$fulldigests = [];
$shortdigests = [];
// Add 5 discussions to forum 1.
$record->forum = $forum1->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
$fulldigests[] = $post;
}
// Add 5 discussions to forum 2.
$record->forum = $forum2->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]);
$shortdigests[] = $post;
}
// Ensure that the creation times mean that the messages will be sent.
$this->helper_force_digest_mail_times();
// Set the tested user's default maildigest setting.
$DB->set_field('user', 'maildigest', 0, array('id' => $user->id));
@ -557,7 +508,14 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
forum_set_user_maildigest($forum2, 2, $user);
// One digest e-mail should be sent, and no individual notifications.
$this->helper_run_cron_check_count(1, 0, 1);
}
$expect = [
(object) [
'userid' => $user->id,
'digests' => 1,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_digests_and_assert($user, $fulldigests, $shortdigests);
}
}

View File

@ -0,0 +1,153 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The forum module mail generation tests for groups.
*
* @package mod_forum
* @copyright 2013 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/forum/lib.php');
require_once(__DIR__ . '/cron_trait.php');
require_once(__DIR__ . '/generator_trait.php');
/**
* The forum module mail generation tests for groups.
*
* @copyright 2013 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_forum_qanda_testcase extends advanced_testcase {
// Make use of the cron tester trait.
use mod_forum_tests_cron_trait;
// Make use of the test generator trait.
use mod_forum_tests_generator_trait;
/**
* @var \phpunit_message_sink
*/
protected $messagesink;
/**
* @var \phpunit_mailer_sink
*/
protected $mailsink;
public function setUp() {
global $CFG;
// We must clear the subscription caches. This has to be done both before each test, and after in case of other
// tests using these functions.
\mod_forum\subscriptions::reset_forum_cache();
\mod_forum\subscriptions::reset_discussion_cache();
// Messaging is not compatible with transactions...
$this->preventResetByRollback();
// Catch all messages.
$this->messagesink = $this->redirectMessages();
$this->mailsink = $this->redirectEmails();
// Forcibly reduce the maxeditingtime to a second in the past to
// ensure that messages are sent out.
$CFG->maxeditingtime = -1;
}
public function tearDown() {
// We must clear the subscription caches. This has to be done both before each test, and after in case of other
// tests using these functions.
\mod_forum\subscriptions::reset_forum_cache();
$this->messagesink->clear();
$this->messagesink->close();
unset($this->messagesink);
$this->mailsink->clear();
$this->mailsink->close();
unset($this->mailsink);
}
/**
* Test that a user who has not posted in a q&a forum does not receive
* notificatinos.
*/
public function test_user_has_not_posted() {
global $CFG, $DB;
$this->resetAfterTest(true);
// Create a course, with a forum.
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
'forcesubscribe' => FORUM_INITIALSUBSCRIBE,
'groupmode' => SEPARATEGROUPS,
'type' => 'qanda',
]);
// Create three students:
// - author, enrolled in group A; and
// - recipient, enrolled in group B; and
// - other, enrolled in the course, but no groups.
list($author, $recipient, $otheruser) = $this->helper_create_users($course, 3);
// Create one editing teacher, not in any group but with accessallgroups capability.
list($editingteacher) = $this->helper_create_users($course, 1, 'editingteacher');
// Post a discussion to the forum.
list($discussion, $post) = $this->helper_post_to_forum($forum, $editingteacher);
$reply = $this->helper_reply_to_post($post, $author);
$otherreply = $this->helper_reply_to_post($post, $recipient);
$DB->execute("UPDATE {forum_posts} SET modified = modified - 1");
$DB->execute("UPDATE {forum_posts} SET created = created - 1");
$DB->execute("UPDATE {forum_discussions} SET timemodified = timemodified - 1");
// Only the author, recipient, and teachers should receive.
$expect = [
'author' => (object) [
'userid' => $author->id,
'messages' => 3,
],
'recipient' => (object) [
'userid' => $recipient->id,
'messages' => 3,
],
'otheruser' => (object) [
'userid' => $otheruser->id,
'messages' => 1,
],
'editingteacher' => (object) [
'userid' => $editingteacher->id,
'messages' => 3,
],
];
$this->queue_tasks_and_assert($expect);
$posts = [$post, $reply, $otherreply];
// No notifications should be queued.
$this->send_notifications_and_assert($author, $posts);
$this->send_notifications_and_assert($recipient, $posts);
$this->send_notifications_and_assert($otheruser, [$post]);
$this->send_notifications_and_assert($editingteacher, $posts);
}
}