Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * A scheduled task for forum cron.
  19   *
  20   * @package    mod_forum
  21   * @copyright  2014 Dan Poltawski <dan@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace mod_forum\task;
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once($CFG->dirroot . '/mod/forum/lib.php');
  29  
  30  /**
  31   * The main scheduled task for the forum.
  32   *
  33   * @package    mod_forum
  34   * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class cron_task extends \core\task\scheduled_task {
  38  
  39      // Use the logging trait to get some nice, juicy, logging.
  40      use \core\task\logging_trait;
  41  
  42      /**
  43       * @var The list of courses which contain posts to be sent.
  44       */
  45      protected $courses = [];
  46  
  47      /**
  48       * @var The list of forums which contain posts to be sent.
  49       */
  50      protected $forums = [];
  51  
  52      /**
  53       * @var The list of discussions which contain posts to be sent.
  54       */
  55      protected $discussions = [];
  56  
  57      /**
  58       * @var The list of posts to be sent.
  59       */
  60      protected $posts = [];
  61  
  62      /**
  63       * @var The list of post authors.
  64       */
  65      protected $users = [];
  66  
  67      /**
  68       * @var The list of subscribed users.
  69       */
  70      protected $subscribedusers = [];
  71  
  72      /**
  73       * @var The list of digest users.
  74       */
  75      protected $digestusers = [];
  76  
  77      /**
  78       * @var The list of adhoc data for sending.
  79       */
  80      protected $adhocdata = [];
  81  
  82      /**
  83       * Get a descriptive name for this task (shown to admins).
  84       *
  85       * @return string
  86       */
  87      public function get_name() {
  88          return get_string('crontask', 'mod_forum');
  89      }
  90  
  91      /**
  92       * Execute the scheduled task.
  93       */
  94      public function execute() {
  95          global $CFG, $DB;
  96  
  97          $timenow = time();
  98  
  99          // Delete any really old posts in the digest queue.
 100          $weekago = $timenow - (7 * 24 * 3600);
 101          $this->log_start("Removing old digest records from 7 days ago.");
 102          $DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
 103          $this->log_finish("Removed all old digest records.");
 104  
 105          $endtime   = $timenow - $CFG->maxeditingtime;
 106          $starttime = $endtime - (2 * DAYSECS);
 107          $this->log_start("Fetching unmailed posts.");
 108          if (!$posts = $this->get_unmailed_posts($starttime, $endtime, $timenow)) {
 109              $this->log_finish("No posts found.", 1);
 110              return false;
 111          }
 112          $this->log_finish("Done");
 113  
 114          // Process post data and turn into adhoc tasks.
 115          $this->process_post_data($posts);
 116  
 117          // Mark posts as read.
 118          list($in, $params) = $DB->get_in_or_equal(array_keys($posts));
 119          $DB->set_field_select('forum_posts', 'mailed', 1, "id {$in}", $params);
 120      }
 121  
 122      /**
 123       * Process all posts and convert to appropriated hoc tasks.
 124       *
 125       * @param   \stdClass[] $posts
 126       */
 127      protected function process_post_data($posts) {
 128          $discussionids = [];
 129          $forumids = [];
 130          $courseids = [];
 131  
 132          $this->log_start("Processing post information");
 133  
 134          $start = microtime(true);
 135          foreach ($posts as $id => $post) {
 136              $discussionids[$post->discussion] = true;
 137              $forumids[$post->forum] = true;
 138              $courseids[$post->course] = true;
 139              $this->add_data_for_post($post);
 140              $this->posts[$id] = $post;
 141          }
 142          $this->log_finish(sprintf("Processed %s posts", count($this->posts)));
 143  
 144          if (empty($this->posts)) {
 145              $this->log("No posts found. Returning early.");
 146              return;
 147          }
 148  
 149          // Please note, this order is intentional.
 150          // The forum cache makes use of the course.
 151          $this->log_start("Filling caches");
 152  
 153          $start = microtime(true);
 154          $this->log_start("Filling course cache", 1);
 155          $this->fill_course_cache(array_keys($courseids));
 156          $this->log_finish("Done", 1);
 157  
 158          $this->log_start("Filling forum cache", 1);
 159          $this->fill_forum_cache(array_keys($forumids));
 160          $this->log_finish("Done", 1);
 161  
 162          $this->log_start("Filling discussion cache", 1);
 163          $this->fill_discussion_cache(array_keys($discussionids));
 164          $this->log_finish("Done", 1);
 165  
 166          $this->log_start("Filling user subscription cache", 1);
 167          $this->fill_user_subscription_cache();
 168          $this->log_finish("Done", 1);
 169  
 170          $this->log_start("Filling digest cache", 1);
 171          $this->fill_digest_cache();
 172          $this->log_finish("Done", 1);
 173  
 174          $this->log_finish("All caches filled");
 175  
 176          $this->log_start("Queueing user tasks.");
 177          $this->queue_user_tasks();
 178          $this->log_finish("All tasks queued.");
 179      }
 180  
 181      /**
 182       * Fill the course cache.
 183       *
 184       * @param   int[]       $courseids
 185       */
 186      protected function fill_course_cache($courseids) {
 187          global $DB;
 188  
 189          list($in, $params) = $DB->get_in_or_equal($courseids);
 190          $this->courses = $DB->get_records_select('course', "id $in", $params);
 191      }
 192  
 193      /**
 194       * Fill the forum cache.
 195       *
 196       * @param   int[]       $forumids
 197       */
 198      protected function fill_forum_cache($forumids) {
 199          global $DB;
 200  
 201          $requiredfields = [
 202                  'id',
 203                  'course',
 204                  'forcesubscribe',
 205                  'type',
 206              ];
 207          list($in, $params) = $DB->get_in_or_equal($forumids);
 208          $this->forums = $DB->get_records_select('forum', "id $in", $params, '', implode(', ', $requiredfields));
 209          foreach ($this->forums as $id => $forum) {
 210              \mod_forum\subscriptions::fill_subscription_cache($id);
 211              \mod_forum\subscriptions::fill_discussion_subscription_cache($id);
 212          }
 213      }
 214  
 215      /**
 216       * Fill the discussion cache.
 217       *
 218       * @param   int[]       $discussionids
 219       */
 220      protected function fill_discussion_cache($discussionids) {
 221          global $DB;
 222  
 223          if (empty($discussionids)) {
 224              $this->discussions = [];
 225          } else {
 226  
 227              $requiredfields = [
 228                      'id',
 229                      'groupid',
 230                      'firstpost',
 231                      'timestart',
 232                      'timeend',
 233                  ];
 234  
 235              list($in, $params) = $DB->get_in_or_equal($discussionids);
 236              $this->discussions = $DB->get_records_select(
 237                      'forum_discussions', "id $in", $params, '', implode(', ', $requiredfields));
 238          }
 239      }
 240  
 241      /**
 242       * Fill the cache of user digest preferences.
 243       */
 244      protected function fill_digest_cache() {
 245          global $DB;
 246  
 247          if (empty($this->users)) {
 248              return;
 249          }
 250          // Get the list of forum subscriptions for per-user per-forum maildigest settings.
 251          list($in, $params) = $DB->get_in_or_equal(array_keys($this->users));
 252          $digestspreferences = $DB->get_recordset_select(
 253                  'forum_digests', "userid $in", $params, '', 'id, userid, forum, maildigest');
 254          foreach ($digestspreferences as $digestpreference) {
 255              if (!isset($this->digestusers[$digestpreference->forum])) {
 256                  $this->digestusers[$digestpreference->forum] = [];
 257              }
 258              $this->digestusers[$digestpreference->forum][$digestpreference->userid] = $digestpreference->maildigest;
 259          }
 260          $digestspreferences->close();
 261      }
 262  
 263      /**
 264       * Add dsta for the current forum post to the structure of adhoc data.
 265       *
 266       * @param   \stdClass   $post
 267       */
 268      protected function add_data_for_post($post) {
 269          if (!isset($this->adhocdata[$post->course])) {
 270              $this->adhocdata[$post->course] = [];
 271          }
 272  
 273          if (!isset($this->adhocdata[$post->course][$post->forum])) {
 274              $this->adhocdata[$post->course][$post->forum] = [];
 275          }
 276  
 277          if (!isset($this->adhocdata[$post->course][$post->forum][$post->discussion])) {
 278              $this->adhocdata[$post->course][$post->forum][$post->discussion] = [];
 279          }
 280  
 281          $this->adhocdata[$post->course][$post->forum][$post->discussion][$post->id] = $post->id;
 282      }
 283  
 284      /**
 285       * Fill the cache of user subscriptions.
 286       */
 287      protected function fill_user_subscription_cache() {
 288          foreach ($this->forums as $forum) {
 289              $cm = get_fast_modinfo($this->courses[$forum->course])->instances['forum'][$forum->id];
 290              $modcontext = \context_module::instance($cm->id);
 291  
 292              $this->subscribedusers[$forum->id] = [];
 293              if ($users = \mod_forum\subscriptions::fetch_subscribed_users($forum, 0, $modcontext, 'u.id, u.maildigest', true)) {
 294                  foreach ($users as $user) {
 295                      // This user is subscribed to this forum.
 296                      $this->subscribedusers[$forum->id][$user->id] = $user->id;
 297                      if (!isset($this->users[$user->id])) {
 298                          // Store minimal user info.
 299                          $this->users[$user->id] = $user;
 300                      }
 301                  }
 302                  // Release memory.
 303                  unset($users);
 304              }
 305          }
 306      }
 307  
 308      /**
 309       * Queue the user tasks.
 310       */
 311      protected function queue_user_tasks() {
 312          global $CFG, $DB;
 313  
 314          $timenow = time();
 315          $sitetimezone = \core_date::get_server_timezone();
 316          $counts = [
 317              'digests' => 0,
 318              'individuals' => 0,
 319              'users' => 0,
 320              'ignored' => 0,
 321              'messages' => 0,
 322          ];
 323          $this->log("Processing " . count($this->users) . " users", 1);
 324          foreach ($this->users as $user) {
 325              $usercounts = [
 326                  'digests' => 0,
 327                  'messages' => 0,
 328              ];
 329  
 330              $send = false;
 331              // Setup this user so that the capabilities are cached, and environment matches receiving user.
 332              \core\cron::setup_user($user);
 333  
 334              list($individualpostdata, $digestpostdata) = $this->fetch_posts_for_user($user);
 335  
 336              if (!empty($digestpostdata)) {
 337                  // Insert all of the records for the digest.
 338                  $DB->insert_records('forum_queue', $digestpostdata);
 339                  $servermidnight = usergetmidnight($timenow, $sitetimezone);
 340                  $digesttime = $servermidnight + ($CFG->digestmailtime * 3600);
 341  
 342                  if ($digesttime < $timenow) {
 343                      // Digest time is in the past. Get a new time for tomorrow.
 344                      $servermidnight = usergetmidnight($timenow + DAYSECS, $sitetimezone);
 345                      $digesttime = $servermidnight + ($CFG->digestmailtime * 3600);
 346                  }
 347  
 348                  $task = new \mod_forum\task\send_user_digests();
 349                  $task->set_userid($user->id);
 350                  $task->set_component('mod_forum');
 351                  $task->set_custom_data(['servermidnight' => $servermidnight]);
 352                  $task->set_next_run_time($digesttime);
 353                  \core\task\manager::reschedule_or_queue_adhoc_task($task);
 354                  $usercounts['digests']++;
 355                  $send = true;
 356              }
 357  
 358              if (!empty($individualpostdata)) {
 359                  $usercounts['messages'] += count($individualpostdata);
 360  
 361                  $task = new \mod_forum\task\send_user_notifications();
 362                  $task->set_userid($user->id);
 363                  $task->set_custom_data($individualpostdata);
 364                  $task->set_component('mod_forum');
 365                  \core\task\manager::queue_adhoc_task($task);
 366                  $counts['individuals']++;
 367                  $send = true;
 368              }
 369  
 370              if ($send) {
 371                  $counts['users']++;
 372                  $counts['messages'] += $usercounts['messages'];
 373                  $counts['digests'] += $usercounts['digests'];
 374              } else {
 375                  $counts['ignored']++;
 376              }
 377  
 378              $this->log(sprintf("Queued %d digests and %d messages for %s",
 379                      $usercounts['digests'],
 380                      $usercounts['messages'],
 381                      $user->id
 382                  ), 2);
 383          }
 384          $this->log(
 385              sprintf(
 386                  "Queued %d digests, and %d individual tasks for %d post mails. " .
 387                  "Unique users: %d (%d ignored)",
 388                  $counts['digests'],
 389                  $counts['individuals'],
 390                  $counts['messages'],
 391                  $counts['users'],
 392                  $counts['ignored']
 393              ), 1);
 394      }
 395  
 396      /**
 397       * Fetch posts for this user.
 398       *
 399       * @param   \stdClass   $user The user to fetch posts for.
 400       */
 401      protected function fetch_posts_for_user($user) {
 402          // We maintain a mapping of user groups for each forum.
 403          $usergroups = [];
 404          $digeststructure = [];
 405  
 406          $poststructure = $this->adhocdata;
 407          $poststosend = [];
 408          foreach ($poststructure as $courseid => $forumids) {
 409              $course = $this->courses[$courseid];
 410              foreach ($forumids as $forumid => $discussionids) {
 411                  $forum = $this->forums[$forumid];
 412                  $maildigest = forum_get_user_maildigest_bulk($this->digestusers, $user, $forumid);
 413  
 414                  if (!isset($this->subscribedusers[$forumid][$user->id])) {
 415                      // This user has no subscription of any kind to this forum.
 416                      // Do not send them any posts at all.
 417                      unset($poststructure[$courseid][$forumid]);
 418                      continue;
 419                  }
 420  
 421                  $subscriptiontime = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $user->id);
 422  
 423                  $cm = get_fast_modinfo($course)->instances['forum'][$forumid];
 424                  foreach ($discussionids as $discussionid => $postids) {
 425                      $discussion = $this->discussions[$discussionid];
 426                      if (!\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussionid, $cm)) {
 427                          // The user does not subscribe to this forum as a whole, or to this specific discussion.
 428                          unset($poststructure[$courseid][$forumid][$discussionid]);
 429                          continue;
 430                      }
 431  
 432                      if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {
 433                          // This discussion has a groupmode set (SEPARATEGROUPS or VISIBLEGROUPS).
 434                          // Check whether the user can view it based on their groups.
 435                          if (!isset($usergroups[$forum->id])) {
 436                              $usergroups[$forum->id] = groups_get_all_groups($courseid, $user->id, $cm->groupingid);
 437                          }
 438  
 439                          if (!isset($usergroups[$forum->id][$discussion->groupid])) {
 440                              // This user is not a member of this group, or the group no longer exists.
 441  
 442                              $modcontext = \context_module::instance($cm->id);
 443                              if (!has_capability('moodle/site:accessallgroups', $modcontext, $user)) {
 444                                  // This user does not have the accessallgroups and is not a member of the group.
 445                                  // Do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS.
 446                                  unset($poststructure[$courseid][$forumid][$discussionid]);
 447                                  continue;
 448                              }
 449                          }
 450                      }
 451  
 452                      foreach ($postids as $postid) {
 453                          $post = $this->posts[$postid];
 454                          if ($subscriptiontime) {
 455                              // Skip posts if the user subscribed to the discussion after it was created.
 456                              $subscribedafter = isset($subscriptiontime[$post->discussion]);
 457                              $subscribedafter = $subscribedafter && ($subscriptiontime[$post->discussion] > $post->created);
 458                              if ($subscribedafter) {
 459                                  // The user subscribed to the discussion/forum after this post was created.
 460                                  unset($poststructure[$courseid][$forumid][$discussionid][$postid]);
 461                                  continue;
 462                              }
 463                          }
 464  
 465                          if ($maildigest > 0) {
 466                              // This user wants the mails to be in digest form.
 467                              $digeststructure[] = (object) [
 468                                  'userid' => $user->id,
 469                                  'discussionid' => $discussion->id,
 470                                  'postid' => $post->id,
 471                                  'timemodified' => $post->created,
 472                              ];
 473                              unset($poststructure[$courseid][$forumid][$discussionid][$postid]);
 474                              continue;
 475                          } else {
 476                              // Add this post to the list of postids to be sent.
 477                              $poststosend[] = $postid;
 478                          }
 479                      }
 480                  }
 481  
 482                  if (empty($poststructure[$courseid][$forumid])) {
 483                      // This user is not subscribed to any discussions in this forum at all.
 484                      unset($poststructure[$courseid][$forumid]);
 485                      continue;
 486                  }
 487              }
 488              if (empty($poststructure[$courseid])) {
 489                  // This user is not subscribed to any forums in this course.
 490                  unset($poststructure[$courseid]);
 491              }
 492          }
 493  
 494          return [$poststosend, $digeststructure];
 495      }
 496  
 497      /**
 498       * Returns a list of all new posts that have not been mailed yet
 499       *
 500       * @param int $starttime posts created after this time
 501       * @param int $endtime posts created before this
 502       * @param int $now used for timed discussions only
 503       * @return array
 504       */
 505      protected function get_unmailed_posts($starttime, $endtime, $now = null) {
 506          global $CFG, $DB;
 507  
 508          $params = array();
 509          $params['mailed'] = FORUM_MAILED_PENDING;
 510          $params['ptimestart'] = $starttime;
 511          $params['ptimeend'] = $endtime;
 512          $params['mailnow'] = 1;
 513  
 514          if (!empty($CFG->forum_enabletimedposts)) {
 515              if (empty($now)) {
 516                  $now = time();
 517              }
 518              $selectsql = "AND (p.created >= :ptimestart OR d.timestart >= :pptimestart)";
 519              $params['pptimestart'] = $starttime;
 520              $timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))";
 521              $params['dtimestart'] = $now;
 522              $params['dtimeend'] = $now;
 523          } else {
 524              $timedsql = "";
 525              $selectsql = "AND p.created >= :ptimestart";
 526          }
 527  
 528          return $DB->get_records_sql(
 529                 "SELECT
 530                      p.id,
 531                      p.discussion,
 532                      d.forum,
 533                      d.course,
 534                      p.created,
 535                      p.parent,
 536                      p.userid
 537                    FROM {forum_posts} p
 538                    JOIN {forum_discussions} d ON d.id = p.discussion
 539                   WHERE p.mailed = :mailed
 540                  $selectsql
 541                     AND (p.created < :ptimeend OR p.mailnow = :mailnow)
 542                  $timedsql
 543                   ORDER BY p.modified ASC",
 544               $params);
 545      }
 546  }