Files
moodle/mod/forum/classes/task/cron_task.php
T
Neill Magill 42171eb435 MDL-72767 forum: Ensure digests are sent
Before this change if a new post was made after the digest time but
before todays daily digest had been sent the run time on the digest
would be set to the next day resulting in no digest being sent to
the user.

By adding the server midnight to the custom data on the task we
ensure that in this case a new adhoc task is created for the next
day leaving the current one to get processed.

Server midnight is used so that if the digest time setting is
changed we would not get two tasks queued for the same day.
2021-11-01 08:49:46 +00:00

547 lines
20 KiB
PHP

<?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/>.
/**
* A scheduled task for forum cron.
*
* @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).
*
* @return string
*/
public function get_name() {
return get_string('crontask', 'mod_forum');
}
/**
* Execute the scheduled task.
*/
public function execute() {
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) {
$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_finish("All caches filled");
$this->log_start("Queueing user tasks.");
$this->queue_user_tasks();
$this->log_finish("All tasks queued.");
}
/**
* 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);
}
}
}
/**
* Queue the user tasks.
*/
protected function queue_user_tasks() {
global $CFG, $DB;
$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);
$servermidnight = usergetmidnight($timenow, $sitetimezone);
$digesttime = $servermidnight + ($CFG->digestmailtime * 3600);
if ($digesttime < $timenow) {
// Digest time is in the past. Get a new time for tomorrow.
$servermidnight = usergetmidnight($timenow + DAYSECS, $sitetimezone);
$digesttime = $servermidnight + ($CFG->digestmailtime * 3600);
}
$task = new \mod_forum\task\send_user_digests();
$task->set_userid($user->id);
$task->set_component('mod_forum');
$task->set_custom_data(['servermidnight' => $servermidnight]);
$task->set_next_run_time($digesttime);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
$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][$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);
}
}