MDL-46881 core: Allow adhoc tasks to be rescheduled
[moodle.git] / mod / forum / classes / task / cron_task.php
CommitLineData
cae945d2
DP
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 *
cae945d2
DP
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 */
24namespace mod_forum\task;
25
4459ad29
AN
26defined('MOODLE_INTERNAL') || die();
27
28require_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 */
cae945d2
DP
37class cron_task extends \core\task\scheduled_task {
38
4459ad29
AN
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
cae945d2
DP
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 /**
4459ad29 92 * Execute the scheduled task.
cae945d2
DP
93 */
94 public function execute() {
4459ad29
AN
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) {
4459ad29
AN
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
4129d898
AN
174 $this->log_finish("All caches filled");
175
4459ad29
AN
176 $this->log_start("Queueing user tasks.");
177 $this->queue_user_tasks();
4129d898 178 $this->log_finish("All tasks queued.");
4459ad29
AN
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 }
cae945d2
DP
213 }
214
4459ad29
AN
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->discussion = [];
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
4459ad29
AN
308 /**
309 * Queue the user tasks.
310 */
311 protected function queue_user_tasks() {
312 global $CFG, $DB;
313
4459ad29
AN
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 $digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
340
341 $task = new \mod_forum\task\send_user_digests();
342 $task->set_userid($user->id);
343 $task->set_component('mod_forum');
344 $task->set_next_run_time($digesttime);
b51b6969 345 \core\task\manager::reschedule_or_queue_adhoc_task($task);
4459ad29
AN
346 $usercounts['digests']++;
347 $send = true;
348 }
349
350 if (!empty($individualpostdata)) {
351 $usercounts['messages'] += count($individualpostdata);
352
353 $task = new \mod_forum\task\send_user_notifications();
354 $task->set_userid($user->id);
355 $task->set_custom_data($individualpostdata);
356 $task->set_component('mod_forum');
357 \core\task\manager::queue_adhoc_task($task);
358 $counts['individuals']++;
359 $send = true;
360 }
361
362 if ($send) {
363 $counts['users']++;
364 $counts['messages'] += $usercounts['messages'];
365 $counts['digests'] += $usercounts['digests'];
366 } else {
367 $counts['ignored']++;
368 }
369
370 $this->log(sprintf("Queued %d digests and %d messages for %s",
371 $usercounts['digests'],
372 $usercounts['messages'],
373 $user->id
374 ), 2);
375 }
376 $this->log(
377 sprintf(
378 "Queued %d digests, and %d individual tasks for %d post mails. " .
379 "Unique users: %d (%d ignored)",
380 $counts['digests'],
381 $counts['individuals'],
382 $counts['messages'],
383 $counts['users'],
384 $counts['ignored']
385 ), 1);
386 }
387
388 /**
389 * Fetch posts for this user.
390 *
391 * @param \stdClass $user The user to fetch posts for.
392 */
393 protected function fetch_posts_for_user($user) {
394 // We maintain a mapping of user groups for each forum.
395 $usergroups = [];
396 $digeststructure = [];
397
398 $poststructure = $this->adhocdata;
399 $poststosend = [];
400 foreach ($poststructure as $courseid => $forumids) {
401 $course = $this->courses[$courseid];
402 foreach ($forumids as $forumid => $discussionids) {
403 $forum = $this->forums[$forumid];
404 $maildigest = forum_get_user_maildigest_bulk($this->digestusers, $user, $forumid);
405
406 if (!isset($this->subscribedusers[$forumid][$user->id])) {
407 // This user has no subscription of any kind to this forum.
408 // Do not send them any posts at all.
409 unset($poststructure[$courseid][$forumid]);
410 continue;
411 }
412
413 $subscriptiontime = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $user->id);
414
415 $cm = get_fast_modinfo($course)->instances['forum'][$forumid];
416 foreach ($discussionids as $discussionid => $postids) {
417 $discussion = $this->discussions[$discussionid];
418 if (!\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussionid, $cm)) {
419 // The user does not subscribe to this forum as a whole, or to this specific discussion.
420 unset($poststructure[$courseid][$forumid][$discussionid]);
421 continue;
422 }
423
424 if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {
425 // This discussion has a groupmode set (SEPARATEGROUPS or VISIBLEGROUPS).
426 // Check whether the user can view it based on their groups.
427 if (!isset($usergroups[$forum->id])) {
428 $usergroups[$forum->id] = groups_get_all_groups($courseid, $user->id, $cm->groupingid);
429 }
430
431 if (!isset($usergroups[$forum->id][$discussion->groupid])) {
432 // This user is not a member of this group, or the group no longer exists.
433
434 $modcontext = \context_module::instance($cm->id);
435 if (!has_capability('moodle/site:accessallgroups', $modcontext, $user)) {
436 // This user does not have the accessallgroups and is not a member of the group.
437 // Do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS.
438 unset($poststructure[$courseid][$forumid][$discussionid]);
439 continue;
440 }
441 }
442 }
443
444 foreach ($postids as $postid) {
445 $post = $this->posts[$postid];
446 if ($subscriptiontime) {
447 // Skip posts if the user subscribed to the discussion after it was created.
448 $subscribedafter = isset($subscriptiontime[$post->discussion]);
449 $subscribedafter = $subscribedafter && ($subscriptiontime[$post->discussion] > $post->created);
450 if ($subscribedafter) {
451 // The user subscribed to the discussion/forum after this post was created.
452 unset($poststructure[$courseid][$forumid][$discussionid]);
453 continue;
454 }
455 }
456
4459ad29
AN
457 if ($maildigest > 0) {
458 // This user wants the mails to be in digest form.
459 $digeststructure[] = (object) [
460 'userid' => $user->id,
461 'discussionid' => $discussion->id,
462 'postid' => $post->id,
463 'timemodified' => $post->created,
464 ];
465 unset($poststructure[$courseid][$forumid][$discussionid][$postid]);
466 continue;
467 } else {
468 // Add this post to the list of postids to be sent.
469 $poststosend[] = $postid;
470 }
471 }
472 }
473
474 if (empty($poststructure[$courseid][$forumid])) {
475 // This user is not subscribed to any discussions in this forum at all.
476 unset($poststructure[$courseid][$forumid]);
477 continue;
478 }
479 }
480 if (empty($poststructure[$courseid])) {
481 // This user is not subscribed to any forums in this course.
482 unset($poststructure[$courseid]);
483 }
484 }
485
486 return [$poststosend, $digeststructure];
487 }
488
489 /**
490 * Returns a list of all new posts that have not been mailed yet
491 *
492 * @param int $starttime posts created after this time
493 * @param int $endtime posts created before this
494 * @param int $now used for timed discussions only
495 * @return array
496 */
497 protected function get_unmailed_posts($starttime, $endtime, $now = null) {
498 global $CFG, $DB;
499
500 $params = array();
501 $params['mailed'] = FORUM_MAILED_PENDING;
502 $params['ptimestart'] = $starttime;
503 $params['ptimeend'] = $endtime;
504 $params['mailnow'] = 1;
505
506 if (!empty($CFG->forum_enabletimedposts)) {
507 if (empty($now)) {
508 $now = time();
509 }
510 $selectsql = "AND (p.created >= :ptimestart OR d.timestart >= :pptimestart)";
511 $params['pptimestart'] = $starttime;
512 $timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))";
513 $params['dtimestart'] = $now;
514 $params['dtimeend'] = $now;
515 } else {
516 $timedsql = "";
517 $selectsql = "AND p.created >= :ptimestart";
518 }
519
520 return $DB->get_records_sql(
521 "SELECT
522 p.id,
523 p.discussion,
524 d.forum,
525 d.course,
526 p.created,
527 p.parent,
528 p.userid
529 FROM {forum_posts} p
530 JOIN {forum_discussions} d ON d.id = p.discussion
531 WHERE p.mailed = :mailed
532 $selectsql
533 AND (p.created < :ptimeend OR p.mailnow = :mailnow)
534 $timedsql
535 ORDER BY p.modified ASC",
536 $params);
537 }
cae945d2 538}