Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
<?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 = [];
> $this->discussions = [];
} 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); } }