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