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