See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [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 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body