ILIAS  release_8 Revision v8.24
class.ilForumCronNotification.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
27{
28 private const KEEP_ALIVE_CHUNK_SIZE = 25;
30
32 private static array $providerObject = [];
34 private static array $deleted_ids_cache = [];
36 private static array $ref_ids_by_obj_id = [];
38 private static array $accessible_ref_ids_by_user = [];
40 private static array $container_by_frm_ref_id = [];
41
45 private ilTree $tree;
46 private int $num_sent_messages = 0;
49 private \ILIAS\Refinery\Factory $refinery;
51
52 public function __construct(
53 ilDBInterface $database = null,
55 ilLanguage $lng = null,
56 ilSetting $settings = null,
57 \ILIAS\Refinery\Factory $refinery = null,
59 ) {
60 global $DIC;
61
62 $this->settings = $settings ?? new ilSetting('frma');
63 $this->lng = $lng ?? $DIC->language();
64 $this->ilDB = $database ?? $DIC->database();
65 $this->notificationCache = $notificationCache ?? new ilForumNotificationCache();
66 $this->refinery = $refinery ?? $DIC->refinery();
67 $this->cronManager = $cronManager ?? $DIC->cron()->manager();
68 }
69
70 public function getId(): string
71 {
72 return 'frm_notification';
73 }
74
75 public function getTitle(): string
76 {
77 return $this->lng->txt('cron_forum_notification');
78 }
79
80 public function getDescription(): string
81 {
82 return $this->lng->txt('cron_forum_notification_crob_desc');
83 }
84
85 public function getDefaultScheduleType(): int
86 {
88 }
89
90 public function getDefaultScheduleValue(): ?int
91 {
92 return 1;
93 }
94
95 public function hasAutoActivation(): bool
96 {
97 return false;
98 }
99
100 public function hasFlexibleSchedule(): bool
101 {
102 return true;
103 }
104
105 public function hasCustomSettings(): bool
106 {
107 return true;
108 }
109
110 public function keepAlive(): void
111 {
112 $this->logger->debug('Sending ping to cron manager ...');
113 $this->cronManager->ping($this->getId());
114 $this->logger->debug(sprintf('Current memory usage: %s', memory_get_usage(true)));
115 }
116
117 public function run(): ilCronJobResult
118 {
119 global $DIC;
120
121 $this->logger = $DIC->logger()->frm();
122 $this->tree = $DIC->repositoryTree();
123
125
126 $this->lng->loadLanguageModule('forum');
127
128 $this->logger->info('Started forum notification job ...');
129
130 if (!($last_run_datetime = $this->settings->get('cron_forum_notification_last_date'))) {
131 $last_run_datetime = null;
132 }
133
134 $this->num_sent_messages = 0;
135 $cj_start_date = date('Y-m-d H:i:s');
136
137 if ((string) $this->settings->get('max_notification_age', '') === '') {
138 $this->logger->info(sprintf(
139 'No maximum notification age set, %s days will be used to determine the ' .
140 'left interval when querying the relevant forum events.',
141 self::DEFAULT_MAX_NOTIFICATION_AGE_IN_DAYS
142 ));
143 }
144
145 if ($last_run_datetime !== null &&
146 checkdate(
147 (int) date('m', strtotime($last_run_datetime)),
148 (int) date('d', strtotime($last_run_datetime)),
149 (int) date('Y', strtotime($last_run_datetime))
150 )) {
151 $threshold = max(
152 strtotime($last_run_datetime),
153 strtotime('-' . (int) $this->settings->get('max_notification_age', (string) self::DEFAULT_MAX_NOTIFICATION_AGE_IN_DAYS) . ' days')
154 );
155 } else {
156 $threshold = strtotime('-' . (int) $this->settings->get('max_notification_age', (string) self::DEFAULT_MAX_NOTIFICATION_AGE_IN_DAYS) . ' days');
157 }
158
159 $this->logger->info(sprintf('Threshold for forum event determination is: %s', date('Y-m-d H:i:s', $threshold)));
160
161 $threshold_date = date('Y-m-d H:i:s', $threshold);
162
163 $this->sendNotificationForNewPosts($threshold_date);
164
165 $this->sendNotificationForUpdatedPosts($threshold_date);
166
167 $this->sendNotificationForCensoredPosts($threshold_date);
168
169 $this->sendNotificationForUncensoredPosts($threshold_date);
170
172
174
175 $this->settings->set('cron_forum_notification_last_date', $cj_start_date);
176
177 $mess = 'Sent ' . $this->num_sent_messages . ' messages.';
178
179 $this->logger->info($mess);
180 $this->logger->info('Finished forum notification job');
181
182 $result = new ilCronJobResult();
183 if ($this->num_sent_messages) {
185 $result->setMessage($mess);
186 }
187
188 $result->setStatus($status);
189
190 return $result;
191 }
192
196 protected function getRefIdsByObjId(int $a_obj_id): array
197 {
198 if (!array_key_exists($a_obj_id, self::$ref_ids_by_obj_id)) {
199 self::$ref_ids_by_obj_id[$a_obj_id] = array_values(ilObject::_getAllReferences($a_obj_id));
200 }
201
202 return self::$ref_ids_by_obj_id[$a_obj_id];
203 }
204
205 protected function getFirstAccessibleRefIdBUserAndObjId(int $a_user_id, int $a_obj_id): int
206 {
207 global $DIC;
208 $ilAccess = $DIC->access();
209
210 if (!array_key_exists($a_user_id, self::$accessible_ref_ids_by_user)) {
211 self::$accessible_ref_ids_by_user[$a_user_id] = [];
212 }
213
214 if (!array_key_exists($a_obj_id, self::$accessible_ref_ids_by_user[$a_user_id])) {
215 $accessible_ref_id = 0;
216 foreach ($this->getRefIdsByObjId($a_obj_id) as $ref_id) {
217 if ($ilAccess->checkAccessOfUser($a_user_id, 'read', '', $ref_id)) {
218 $accessible_ref_id = $ref_id;
219 break;
220 }
221 }
222 self::$accessible_ref_ids_by_user[$a_user_id][$a_obj_id] = $accessible_ref_id;
223 }
224
225 return (int) self::$accessible_ref_ids_by_user[$a_user_id][$a_obj_id];
226 }
227
228 public function sendCronForumNotification(ilDBStatement $res, int $notification_type): void
229 {
230 global $DIC;
231 $ilDB = $DIC->database();
232
233 while ($row = $ilDB->fetchAssoc($res)) {
234 if ($notification_type === ilForumMailNotification::TYPE_POST_DELETED
235 || $notification_type === ilForumMailNotification::TYPE_THREAD_DELETED) {
236 // important! save the deleted_id to cache before proceeding getFirstAccessibleRefIdBUserAndObjId !
237 self::$deleted_ids_cache[$row['deleted_id']] = $row['deleted_id'];
238 }
239
240 if (defined('ANONYMOUS_USER_ID') && (int) $row['user_id'] === ANONYMOUS_USER_ID) {
241 continue;
242 }
243
244 $ref_id = $this->getFirstAccessibleRefIdBUserAndObjId((int) $row['user_id'], (int) $row['obj_id']);
245 if ($ref_id < 1) {
246 $this->logger->debug(
247 sprintf(
248 'The recipient with id %s has no "read" permission for object with id %s',
249 $row['user_id'],
250 $row['obj_id']
251 )
252 );
253 continue;
254 }
255
256 $row['ref_id'] = $ref_id;
257
258 $container = $this->determineClosestContainer($ref_id);
259 $row['closest_container'] = null;
260 if ($container instanceof ilObjCourse || $container instanceof ilObjGroup) {
261 $row['closest_container'] = $container;
262 }
263
264 $provider_id = isset($row['deleted_id']) ? -((int) $row['deleted_id']) : (int) $row['pos_pk'];
265 if ($this->existsProviderObject($provider_id, $notification_type)) {
266 self::$providerObject[$provider_id . '_' . $notification_type]->addRecipient((int) $row['user_id']);
267 } else {
268 $this->addProviderObject($provider_id, $row, $notification_type);
269 }
270 }
271
272 $usrIdsToPreload = [];
273 foreach (self::$providerObject as $provider) {
274 if ($provider->getPosAuthorId()) {
275 $usrIdsToPreload[$provider->getPosAuthorId()] = $provider->getPosAuthorId();
276 }
277 if ($provider->getPosDisplayUserId()) {
278 $usrIdsToPreload[$provider->getPosDisplayUserId()] = $provider->getPosDisplayUserId();
279 }
280 if ($provider->getPostUpdateUserId()) {
281 $usrIdsToPreload[$provider->getPostUpdateUserId()] = $provider->getPostUpdateUserId();
282 }
283 }
284
285 ilForumAuthorInformationCache::preloadUserObjects(array_unique($usrIdsToPreload));
286
287 $i = 0;
288 foreach (self::$providerObject as $provider) {
289 if ($i > 0 && ($i % self::KEEP_ALIVE_CHUNK_SIZE) === 0) {
290 $this->keepAlive();
291 }
292
293 $recipients = array_unique($provider->getCronRecipients());
294
295 $this->logger->info(
296 sprintf(
297 'Trying to send forum notifications for posting id "%s", type "%s" and recipients: %s',
298 $provider->getPostId(),
299 $notification_type,
300 implode(', ', $recipients)
301 )
302 );
303
304 $mailNotification = new ilForumMailNotification($provider, $this->logger);
305 $mailNotification->setIsCronjob(true);
306 $mailNotification->setType($notification_type);
307 $mailNotification->setRecipients($recipients);
308
309 $mailNotification->send();
310
311 $this->num_sent_messages += count($provider->getCronRecipients());
312 $this->logger->info('Sent notifications ... ');
313
314 ++$i;
315 }
316
317 $this->resetProviderCache();
318 }
319
324 public function determineClosestContainer(int $frm_ref_id): ?ilObject
325 {
326 if (isset(self::$container_by_frm_ref_id[$frm_ref_id])) {
327 return self::$container_by_frm_ref_id[$frm_ref_id];
328 }
329
330 $ref_id = $this->tree->checkForParentType($frm_ref_id, 'crs');
331 if (!($ref_id > 0)) {
332 $ref_id = $this->tree->checkForParentType($frm_ref_id, 'grp');
333 }
334
335 if ($ref_id > 0) {
338 self::$container_by_frm_ref_id[$frm_ref_id] = $container;
339 return $container;
340 }
341
342 return null;
343 }
344
345 public function existsProviderObject(int $provider_id, int $notification_type): bool
346 {
347 return isset(self::$providerObject[$provider_id . '_' . $notification_type]);
348 }
349
353 private function addProviderObject(int $provider_id, array $row, int $notification_type): void
354 {
355 $tmp_provider = new ilForumCronNotificationDataProvider($row, $notification_type, $this->notificationCache);
356 self::$providerObject[$provider_id . '_' . $notification_type] = $tmp_provider;
357 self::$providerObject[$provider_id . '_' . $notification_type]->addRecipient((int) $row['user_id']);
358 }
359
360 private function resetProviderCache(): void
361 {
362 self::$providerObject = [];
363 }
364
365 public function addToExternalSettingsForm(int $a_form_id, array &$a_fields, bool $a_is_active): void
366 {
367 switch ($a_form_id) {
369 $a_fields['cron_forum_notification'] = $a_is_active ?
370 $this->lng->txt('enabled') :
371 $this->lng->txt('disabled');
372 break;
373 }
374 }
375
376 public function activationWasToggled(ilDBInterface $db, ilSetting $setting, bool $a_currently_active): void
377 {
378 $value = 1;
379 // propagate cron-job setting to object setting
380 if ($a_currently_active) {
381 $value = 2;
382 }
383
384 $setting->set('forum_notification', (string) $value);
385 }
386
387 public function addCustomSettingsToForm(ilPropertyFormGUI $a_form): void
388 {
389 $this->lng->loadLanguageModule('forum');
390
391 $max_notification_age = new ilNumberInputGUI(
392 $this->lng->txt('frm_max_notification_age'),
393 'max_notification_age'
394 );
395 $max_notification_age->setSize(5);
396 $max_notification_age->setSuffix($this->lng->txt('frm_max_notification_age_unit'));
397 $max_notification_age->setRequired(true);
398 $max_notification_age->allowDecimals(false);
399 $max_notification_age->setMinValue(1);
400 $max_notification_age->setInfo($this->lng->txt('frm_max_notification_age_info'));
401 $max_notification_age->setValue(
402 $this->settings->get(
403 'max_notification_age',
404 (string) self::DEFAULT_MAX_NOTIFICATION_AGE_IN_DAYS
405 )
406 );
407
408 $a_form->addItem($max_notification_age);
409 }
410
411 public function saveCustomSettings(ilPropertyFormGUI $a_form): bool
412 {
413 $this->settings->set(
414 'max_notification_age',
415 $this->refinery->in()->series([
416 $this->refinery->byTrying([
417 $this->refinery->kindlyTo()->int(),
418 $this->refinery->in()->series([
419 $this->refinery->kindlyTo()->float(),
420 $this->refinery->kindlyTo()->int()
421 ])
422 ]),
423 $this->refinery->kindlyTo()->string()
424 ])->transform($a_form->getInput('max_notification_age'))
425 );
426 return true;
427 }
428
429 private function sendNotificationForNewPosts(string $threshold_date): void
430 {
431 $condition = '
432
433 frm_posts.pos_status = %s AND (
434 (frm_posts.pos_date >= %s AND frm_posts.pos_date = frm_posts.pos_activation_date) OR
435 (frm_posts.pos_activation_date >= %s AND frm_posts.pos_date < frm_posts.pos_activation_date)
436 ) ';
438 $values = [1, $threshold_date, $threshold_date];
439
440 $res = $this->ilDB->queryF(
441 $this->createForumPostSql($condition),
442 $types,
443 $values
444 );
445
446 $this->sendNotification(
447 $res,
448 'new posting',
450 );
451 }
452
453 private function sendNotificationForUpdatedPosts(string $threshold_date): void
454 {
455 $condition = '
456 frm_notification.interested_events & %s AND
457 frm_posts.pos_cens = %s AND frm_posts.pos_status = %s AND
458 (frm_posts.pos_update > frm_posts.pos_date AND frm_posts.pos_update >= %s) ';
459 $types = [
464 ];
465 $values = [ilForumNotificationEvents::UPDATED, 0, 1, $threshold_date];
466
467 $res = $this->ilDB->queryF(
468 $this->createForumPostSql($condition),
469 $types,
470 $values
471 );
472
473 $this->sendNotification(
474 $res,
475 'updated posting',
477 );
478 }
479
480 private function sendNotificationForCensoredPosts(string $threshold_date): void
481 {
482 $condition = '
483 frm_notification.interested_events & %s AND
484 frm_posts.pos_cens = %s AND frm_posts.pos_status = %s AND
485 (frm_posts.pos_cens_date >= %s AND frm_posts.pos_cens_date > frm_posts.pos_activation_date ) ';
486 $types = [
491 ];
492 $values = [ilForumNotificationEvents::CENSORED, 1, 1, $threshold_date];
493
494 $res = $this->ilDB->queryF(
495 $this->createForumPostSql($condition),
496 $types,
497 $values
498 );
499
500 $this->sendNotification(
501 $res,
502 'censored posting',
504 );
505 }
506
507 private function sendNotificationForUncensoredPosts(string $threshold_date): void
508 {
509 $condition = '
510 frm_notification.interested_events & %s AND
511 frm_posts.pos_cens = %s AND frm_posts.pos_status = %s AND
512 (frm_posts.pos_cens_date >= %s AND frm_posts.pos_cens_date > frm_posts.pos_activation_date ) ';
513 $types = [
518 ];
519 $values = [ilForumNotificationEvents::UNCENSORED, 0, 1, $threshold_date];
520
521 $res = $this->ilDB->queryF(
522 $this->createForumPostSql($condition),
523 $types,
524 $values
525 );
526
527 $this->sendNotification(
528 $res,
529 'uncensored posting',
531 );
532 }
533
534 private function sendNotificationForDeletedThreads(): void
535 {
536 $res = $this->ilDB->queryF(
540 );
541
543 $res,
544 'frm_threads_deleted',
545 'deleted threads',
547 );
548 }
549
550 private function sendNotificationForDeletedPosts(): void
551 {
552 $res = $this->ilDB->queryF(
556 );
557
559 $res,
560 'frm_posts_deleted',
561 'deleted postings',
563 );
564 }
565
566 private function sendNotification(ilDBStatement $res, string $actionName, int $notificationType): void
567 {
568 $numRows = $this->ilDB->numRows($res);
569 if ($numRows > 0) {
570 $this->logger->info(sprintf('Sending notifications for %s "%s" events ...', $numRows, $actionName));
571 $this->sendCronForumNotification($res, $notificationType);
572 $this->logger->info(sprintf('Sent notifications for %s ...', $actionName));
573 }
574
575 $this->keepAlive();
576 }
577
578 private function sendDeleteNotifications(
580 string $action,
581 string $actionDescription,
582 int $notificationType
583 ): void {
584 $numRows = $this->ilDB->numRows($res);
585 if ($numRows > 0) {
586 $this->logger->info(sprintf('Sending notifications for %s "%s" events ...', $numRows, $actionDescription));
587 $this->sendCronForumNotification($res, $notificationType);
588 if (count(self::$deleted_ids_cache) > 0) {
589 $this->ilDB->manipulate(
590 'DELETE FROM frm_posts_deleted WHERE ' . $this->ilDB->in(
591 'deleted_id',
592 self::$deleted_ids_cache,
593 false,
595 )
596 );
597 $this->logger->info('Deleted obsolete entries of table "' . $action . '" ...');
598 }
599 $this->logger->info(sprintf('Sent notifications for %s ...', $actionDescription));
600 }
601
602 $this->keepAlive();
603 }
604
605 private function createForumPostSql(string $condition): string
606 {
607 return '
608 SELECT frm_threads.thr_subject thr_subject,
609 frm_data.top_name top_name,
610 frm_data.top_frm_fk obj_id,
611 frm_notification.user_id user_id,
612 frm_threads.thr_pk thread_id,
613 frm_posts.*
614 FROM frm_notification, frm_posts, frm_threads, frm_data, frm_posts_tree
615 WHERE frm_posts.pos_thr_fk = frm_threads.thr_pk AND ' . $condition . '
616 AND ((frm_threads.thr_top_fk = frm_data.top_pk AND frm_data.top_frm_fk = frm_notification.frm_id)
617 OR (frm_threads.thr_pk = frm_notification.thread_id
618 AND frm_data.top_pk = frm_threads.thr_top_fk) )
619 AND frm_posts.pos_author_id != frm_notification.user_id
620 AND frm_posts_tree.pos_fk = frm_posts.pos_pk AND frm_posts_tree.parent_pos != 0
621 ORDER BY frm_posts.pos_date ASC';
622 }
623
624 private function createSelectOfDeletionNotificationsSql(): string
625 {
626 return '
627 SELECT frm_posts_deleted.thread_title thr_subject,
628 frm_posts_deleted.forum_title top_name,
629 frm_posts_deleted.obj_id obj_id,
630 frm_notification.user_id user_id,
631 frm_posts_deleted.pos_display_user_id,
632 frm_posts_deleted.pos_usr_alias,
633 frm_posts_deleted.deleted_id,
634 frm_posts_deleted.post_date pos_date,
635 frm_posts_deleted.post_title pos_subject,
636 frm_posts_deleted.post_message pos_message,
637 frm_posts_deleted.deleted_by
638
639 FROM frm_notification, frm_posts_deleted
640
641 WHERE ( frm_posts_deleted.obj_id = frm_notification.frm_id
642 OR frm_posts_deleted.thread_id = frm_notification.thread_id)
643 AND frm_posts_deleted.pos_display_user_id != frm_notification.user_id
644 AND frm_posts_deleted.is_thread_deleted = %s
645 AND frm_notification.interested_events & %s
646 ORDER BY frm_posts_deleted.post_date ASC';
647 }
648}
const SCHEDULE_TYPE_IN_HOURS
@depracated This will be replaced with an ENUM in ILIAS 9
__construct(ilDBInterface $database=null, ilForumNotificationCache $notificationCache=null, ilLanguage $lng=null, ilSetting $settings=null, \ILIAS\Refinery\Factory $refinery=null, ilCronManager $cronManager=null)
saveCustomSettings(ilPropertyFormGUI $a_form)
addCustomSettingsToForm(ilPropertyFormGUI $a_form)
existsProviderObject(int $provider_id, int $notification_type)
ilForumNotificationCache $notificationCache
getFirstAccessibleRefIdBUserAndObjId(int $a_user_id, int $a_obj_id)
sendNotificationForUncensoredPosts(string $threshold_date)
addProviderObject(int $provider_id, array $row, int $notification_type)
sendDeleteNotifications(ilDBStatement $res, string $action, string $actionDescription, int $notificationType)
activationWasToggled(ilDBInterface $db, ilSetting $setting, bool $a_currently_active)
Important: This method is (also) called from the setup process, where the constructor of an ilCronJob...
sendNotificationForUpdatedPosts(string $threshold_date)
sendCronForumNotification(ilDBStatement $res, int $notification_type)
addToExternalSettingsForm(int $a_form_id, array &$a_fields, bool $a_is_active)
hasAutoActivation()
Is to be activated on "installation", does only work for ILIAS core cron jobs.
sendNotificationForCensoredPosts(string $threshold_date)
sendNotification(ilDBStatement $res, string $actionName, int $notificationType)
sendNotificationForNewPosts(string $threshold_date)
Class ilForumNotificationCache.
language handling
Component logger with individual log levels by component id.
This class represents a number property in a property form.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Class ilObjGroup.
static getInstanceByRefId(int $ref_id, bool $stop_on_error=true)
get an instance of an Ilias object by reference id
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _getAllReferences(int $id)
get all reference ids for object ID
This class represents a property form user interface.
getInput(string $a_post_var, bool $ensureValidation=true)
Returns the input of an item, if item provides getInput method and as fallback the value of the HTTP-...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
set(string $a_key, string $a_val)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
const ANONYMOUS_USER_ID
Definition: constants.php:27
global $DIC
Definition: feed.php:28
Interface ilDBInterface.
fetchAssoc(ilDBStatement $statement)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$ref_id
Definition: ltiauth.php:67
$res
Definition: ltiservices.php:69
$provider
Definition: ltitoken.php:83
$i
Definition: metadata.php:41
Class ChatMainBarProvider \MainMenu\Provider.
$container
@noRector
Definition: wac.php:14