ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
class.ilForumCronNotification.php
Go to the documentation of this file.
1 <?php
2 
19 declare(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 
42  private ilLanguage $lng;
44  private ilLogger $logger;
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,
54  ilForumNotificationCache $notificationCache = null,
55  ilLanguage $lng = null,
56  ilSetting $settings = null,
57  \ILIAS\Refinery\Factory $refinery = null,
58  ilCronManager $cronManager = 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  {
87  return self::SCHEDULE_TYPE_IN_HOURS;
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) {
184  $status = ilCronJobResult::STATUS_OK;
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 }
$res
Definition: ltiservices.php:69
const ANONYMOUS_USER_ID
Definition: constants.php:27
existsProviderObject(int $provider_id, int $notification_type)
fetchAssoc(ilDBStatement $statement)
Class ChatMainBarProvider .
__construct(ilDBInterface $database=null, ilForumNotificationCache $notificationCache=null, ilLanguage $lng=null, ilSetting $settings=null, \ILIAS\Refinery\Factory $refinery=null, ilCronManager $cronManager=null)
set(string $a_key, string $a_val)
static _getAllReferences(int $id)
get all reference ids for object ID
sendNotificationForUncensoredPosts(string $threshold_date)
sendDeleteNotifications(ilDBStatement $res, string $action, string $actionDescription, int $notificationType)
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-...
$container
Definition: wac.php:14
addProviderObject(int $provider_id, array $row, int $notification_type)
global $DIC
Definition: feed.php:28
addCustomSettingsToForm(ilPropertyFormGUI $a_form)
$provider
Definition: ltitoken.php:83
$ref_id
Definition: ltiauth.php:67
sendNotificationForCensoredPosts(string $threshold_date)
ilForumNotificationCache $notificationCache
This class represents a number property in a property form.
static getInstanceByRefId(int $ref_id, bool $stop_on_error=true)
get an instance of an Ilias object by reference id
activationWasToggled(ilDBInterface $db, ilSetting $setting, bool $a_currently_active)
Class ilForumNotificationCache.
sendNotificationForNewPosts(string $threshold_date)
saveCustomSettings(ilPropertyFormGUI $a_form)
addToExternalSettingsForm(int $a_form_id, array &$a_fields, bool $a_is_active)
sendCronForumNotification(ilDBStatement $res, int $notification_type)
getFirstAccessibleRefIdBUserAndObjId(int $a_user_id, int $a_obj_id)
Class ilObjGroup.
sendNotificationForUpdatedPosts(string $threshold_date)
sendNotification(ilDBStatement $res, string $actionName, int $notificationType)
$i
Definition: metadata.php:41