1<?php
2/* Copyright (c) 1998-2009 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4/**
5 * Forum notifications
6 *
7 * @author Michael Jansen <mjansen@databay.de>
8 * @author Nadia Matuschek <nmatuschek@databay.de>
9 */
10class ilForumCronNotification extends ilCronJob
11{
12    const KEEP_ALIVE_CHUNK_SIZE = 25;
13
14    /**
15     * @var ilSetting
16     */
17    protected $settings;
18
19    /**
20     * @var \ilLogger
21     */
22    protected $logger;
23
24    /**
25     * @var \ilForumCronNotificationDataProvider[]
26     */
27    public static $providerObject = array();
28
29    /**
30     * @var array frm_posts_deleted.deleted_id
31     */
32    protected static $deleted_ids_cache = array();
33
34    /**
35     * @var array
36     */
37    protected static $ref_ids_by_obj_id = array();
38
39    /**
40     * @var array
41     */
42    protected static $accessible_ref_ids_by_user = array();
43
44    /**
45     * @var int
46     */
47    protected $num_sent_messages = 0;
48
49    /** @var \ilDBInterface */
50    private $ilDB;
51
52    /** @var \ilForumNotificationCache|null */
53    private $notificationCache;
54
55    /**
56     * @param ilDBInterface|null $database
57     * @param ilForumNotificationCache|null $notificationCache
58     */
59    public function __construct(\ilDBInterface $database = null, \ilForumNotificationCache $notificationCache = null)
60    {
61        $this->settings = new ilSetting('frma');
62
63        if ($database === null) {
64            global $DIC;
65            $ilDB = $DIC->database();
66        }
67        $this->ilDB = $ilDB;
68
69        if ($notificationCache === null) {
70            $notificationCache = new \ilForumNotificationCache();
71        }
72        $this->notificationCache = $notificationCache;
73    }
74
75    public function getId()
76    {
77        return "frm_notification";
78    }
79
80    public function getTitle()
81    {
82        global $DIC;
83
84        return $DIC->language()->txt("cron_forum_notification");
85    }
86
87    public function getDescription()
88    {
89        global $DIC;
90
91        return $DIC->language()->txt("cron_forum_notification_crob_desc");
92    }
93
94    public function getDefaultScheduleType()
95    {
96        return self::SCHEDULE_TYPE_IN_HOURS;
97    }
98
99    public function getDefaultScheduleValue()
100    {
101        return 1;
102    }
103
104    public function hasAutoActivation()
105    {
106        return false;
107    }
108
109    public function hasFlexibleSchedule()
110    {
111        return true;
112    }
113
114    /**
115     * @return bool
116     */
117    public function hasCustomSettings()
118    {
119        return true;
120    }
121
122    /**
123     *
124     */
125    public function keepAlive()
126    {
127        $this->logger->debug('Sending ping to cron manager ...');
128        \ilCronManager::ping($this->getId());
129        $this->logger->debug(sprintf('Current memory usage: %s', memory_get_usage(true)));
130    }
131
132    /**
133     * @return ilCronJobResult
134     */
135    public function run()
136    {
137        global $DIC;
138
139        $ilSetting = $DIC->settings();
140        $lng = $DIC->language();
141
142        $this->logger = $DIC->logger()->frm();
143
144        $status = ilCronJobResult::STATUS_NO_ACTION;
145
146        $lng->loadLanguageModule('forum');
147
148        $this->logger->info('Started forum notification job ...');
149
150        if (!($last_run_datetime = $ilSetting->get('cron_forum_notification_last_date'))) {
151            $last_run_datetime = null;
152        }
153
154        $this->num_sent_messages = 0;
155        $cj_start_date = date('Y-m-d H:i:s');
156
157        if ($last_run_datetime != null &&
158            checkDate(date('m', strtotime($last_run_datetime)), date('d', strtotime($last_run_datetime)), date('Y', strtotime($last_run_datetime)))) {
159            $threshold = max(strtotime($last_run_datetime), strtotime('-' . (int) $this->settings->get('max_notification_age', 30) . ' days', time()));
160        } else {
161            $threshold = strtotime('-' . (int) $this->settings->get('max_notification_age', 30) . ' days', time());
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
176        $this->sendNotificationForDeletedThreads();
177
178        $this->sendNotifcationForDeletedPosts();
179
180        $ilSetting->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 ilCronJobResult();
188        if ($this->num_sent_messages) {
189            $status = ilCronJobResult::STATUS_OK;
190            $result->setMessage($mess);
191        };
192        $result->setStatus($status);
193        return $result;
194    }
195
196    /**
197     * @param int $a_obj_id
198     * @return array
199     */
200    protected function getRefIdsByObjId($a_obj_id)
201    {
202        if (!array_key_exists($a_obj_id, self::$ref_ids_by_obj_id)) {
203            self::$ref_ids_by_obj_id[$a_obj_id] = ilObject::_getAllReferences($a_obj_id);
204        }
205
206        return (array) self::$ref_ids_by_obj_id[$a_obj_id];
207    }
208
209    /**
210     * @param int $a_user_id
211     * @param int $a_obj_id
212     * @return int
213     */
214    protected function getFirstAccessibleRefIdBUserAndObjId($a_user_id, $a_obj_id)
215    {
216        global $DIC;
217        $ilAccess = $DIC->access();
218
219        if (!array_key_exists($a_user_id, self::$accessible_ref_ids_by_user)) {
220            self::$accessible_ref_ids_by_user[$a_user_id] = array();
221        }
222
223        if (!array_key_exists($a_obj_id, self::$accessible_ref_ids_by_user[$a_user_id])) {
224            $accessible_ref_id = 0;
225            foreach ($this->getRefIdsByObjId($a_obj_id) as $ref_id) {
226                if ($ilAccess->checkAccessOfUser($a_user_id, 'read', '', $ref_id)) {
227                    $accessible_ref_id = $ref_id;
228                    break;
229                }
230            }
231            self::$accessible_ref_ids_by_user[$a_user_id][$a_obj_id] = $accessible_ref_id;
232        }
233
234        return (int) self::$accessible_ref_ids_by_user[$a_user_id][$a_obj_id];
235    }
236
237    /**
238     * @param $res
239     * @param $notification_type
240     */
241    public function sendCronForumNotification($res, $notification_type)
242    {
243        global $DIC;
244        $ilDB = $DIC->database();
245
246        while ($row = $ilDB->fetchAssoc($res)) {
247            if ($notification_type == ilForumMailNotification::TYPE_POST_DELETED
248                || $notification_type == ilForumMailNotification::TYPE_THREAD_DELETED) {
249                // important! save the deleted_id to cache before proceeding getFirstAccessibleRefIdBUserAndObjId !
250                self::$deleted_ids_cache[$row['deleted_id']] = $row['deleted_id'];
251            }
252
253            $ref_id = $this->getFirstAccessibleRefIdBUserAndObjId($row['user_id'], $row['obj_id']);
254            if ($ref_id < 1) {
255                $this->logger->debug(sprintf(
256                    'The recipient with id %s has no "read" permission for object with id %s',
257                    $row['user_id'],
258                    $row['obj_id']
259                ));
260                continue;
261            }
262
263            $row['ref_id'] = $ref_id;
264
265            if ($this->existsProviderObject($row['pos_pk'])) {
266                self::$providerObject[$row['pos_pk']]->addRecipient($row['user_id']);
267            } else {
268                $this->addProviderObject($row);
269            }
270        }
271
272        $usrIdsToPreload = array();
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(sprintf(
296                'Trying to send forum notifications for posting id "%s", type "%s" and recipients: %s',
297                $provider->getPostId(),
298                $notification_type,
299                implode(', ', $recipients)
300            ));
301
302            $mailNotification = new ilForumMailNotification($provider, $this->logger);
303            $mailNotification->setIsCronjob(true);
304            $mailNotification->setType($notification_type);
305            $mailNotification->setRecipients($recipients);
306
307            $mailNotification->send();
308
309            $this->num_sent_messages += count($provider->getCronRecipients());
310            $this->logger->info(sprintf("Sent notifications ... "));
311
312            ++$i;
313        }
314
315        $this->resetProviderCache();
316    }
317
318    /**
319     * @param $post_id
320     * @return bool
321     */
322    public function existsProviderObject($post_id)
323    {
324        if (isset(self::$providerObject[$post_id])) {
325            return true;
326        }
327        return false;
328    }
329
330    /**
331     * @param $row
332     */
333    private function addProviderObject($row)
334    {
335        $tmp_provider = new ilForumCronNotificationDataProvider($row, $this->notificationCache);
336
337        self::$providerObject[$row['pos_pk']] = $tmp_provider;
338        self::$providerObject[$row['pos_pk']]->addRecipient($row['user_id']);
339    }
340
341    /**
342     *
343     */
344    private function resetProviderCache()
345    {
346        self::$providerObject = array();
347    }
348
349    /**
350     * @param int   $a_form_id
351     * @param array $a_fields
352     * @param bool  $a_is_active
353     */
354    public function addToExternalSettingsForm($a_form_id, array &$a_fields, $a_is_active)
355    {
356        global $DIC;
357        $lng = $DIC->language();
358
359        switch ($a_form_id) {
360            case ilAdministrationSettingsFormHandler::FORM_FORUM:
361                $a_fields['cron_forum_notification'] = $a_is_active ?
362                    $lng->txt('enabled') :
363                    $lng->txt('disabled');
364                break;
365        }
366    }
367
368    /**
369     * @param bool $a_currently_active
370     */
371    public function activationWasToggled($a_currently_active)
372    {
373        global $DIC;
374
375        $value = 1;
376        // propagate cron-job setting to object setting
377        if ((bool) $a_currently_active) {
378            $value = 2;
379        }
380        $DIC->settings()->set('forum_notification', $value);
381    }
382
383    /**
384     * @param ilPropertyFormGUI $a_form
385     */
386    public function addCustomSettingsToForm(ilPropertyFormGUI $a_form)
387    {
388        global $DIC;
389        $lng = $DIC->language();
390
391        $lng->loadLanguageModule('forum');
392
393        $max_notification_age = new ilNumberInputGUI($lng->txt('frm_max_notification_age'), 'max_notification_age');
394        $max_notification_age->setSize(5);
395        $max_notification_age->setSuffix($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($lng->txt('frm_max_notification_age_info'));
400        $max_notification_age->setValue($this->settings->get('max_notification_age', 30));
401
402        $a_form->addItem($max_notification_age);
403    }
404
405    /**
406     * @param ilPropertyFormGUI $a_form
407     * @return bool
408     */
409    public function saveCustomSettings(ilPropertyFormGUI $a_form)
410    {
411        $this->settings->set('max_notification_age', $a_form->getInput('max_notification_age'));
412        return true;
413    }
414
415    /**
416     * @param $threshold_date
417     */
418    private function sendNotificationForNewPosts(string $threshold_date)
419    {
420        $condition = '
421			frm_posts.pos_status = %s AND (
422				(frm_posts.pos_date >= %s AND frm_posts.pos_date = frm_posts.pos_activation_date) OR
423				(frm_posts.pos_activation_date >= %s AND frm_posts.pos_date < frm_posts.pos_activation_date)
424			) ';
425        $types = array('integer', 'timestamp', 'timestamp');
426        $values = array(1, $threshold_date, $threshold_date);
427
428        $res = $this->ilDB->queryf(
429            $this->createForumPostSql($condition),
430            $types,
431            $values
432        );
433
434        $this->sendNotification(
435            $res,
436            'new posting',
437            ilForumMailNotification::TYPE_POST_NEW
438        );
439    }
440
441    /**
442     * @param $threshold_date
443     */
444    private function sendNotificationForUpdatedPosts(string $threshold_date)
445    {
446        $condition = '
447			frm_posts.pos_cens = %s AND frm_posts.pos_status = %s AND
448			(frm_posts.pos_update > frm_posts.pos_date AND frm_posts.pos_update >= %s) ';
449        $types = array('integer', 'integer', 'timestamp');
450        $values = array(0, 1, $threshold_date);
451
452        $res = $this->ilDB->queryf(
453            $this->createForumPostSql($condition),
454            $types,
455            $values
456        );
457
458        $this->sendNotification(
459            $res,
460            'updated posting',
461            ilForumMailNotification::TYPE_POST_UPDATED
462        );
463    }
464
465    /**
466     * @param $threshold_date
467     */
468    private function sendNotificationForCensoredPosts(string $threshold_date)
469    {
470        $condition = '
471			frm_posts.pos_cens = %s AND frm_posts.pos_status = %s AND
472            (frm_posts.pos_cens_date >= %s AND frm_posts.pos_cens_date > frm_posts.pos_activation_date ) ';
473        $types = array('integer', 'integer', 'timestamp');
474        $values = array(1, 1, $threshold_date);
475
476        $res = $this->ilDB->queryf(
477            $this->createForumPostSql($condition),
478            $types,
479            $values
480        );
481
482        $this->sendNotification(
483            $res,
484            'censored posting',
485            ilForumMailNotification::TYPE_POST_CENSORED
486        );
487    }
488
489    /**
490     * @param $threshold_date
491     */
492    private function sendNotificationForUncensoredPosts(string $threshold_date)
493    {
494        $condition = '
495			frm_posts.pos_cens = %s AND frm_posts.pos_status = %s AND
496            (frm_posts.pos_cens_date >= %s AND frm_posts.pos_cens_date > frm_posts.pos_activation_date ) ';
497        $types = array('integer', 'integer', 'timestamp');
498        $values = array(0, 1, $threshold_date);
499
500        $res = $this->ilDB->queryf(
501            $this->createForumPostSql($condition),
502            $types,
503            $values
504        );
505
506        $this->sendNotification(
507            $res,
508            'uncensored posting',
509            ilForumMailNotification::TYPE_POST_UNCENSORED
510        );
511    }
512
513    private function sendNotificationForDeletedThreads()
514    {
515        $res = $this->ilDB->queryF(
516            $this->createSelectOfDeletionNotificationsSql(),
517            array('integer'),
518            array(1)
519        );
520
521        $this->sendDeleteNotifcations(
522            $res,
523            'frm_threads_deleted',
524            'deleted threads',
525            ilForumMailNotification::TYPE_THREAD_DELETED
526        );
527    }
528
529    private function sendNotifcationForDeletedPosts()
530    {
531        $res = $this->ilDB->queryF(
532            $this->createSelectOfDeletionNotificationsSql(),
533            array('integer'),
534            array(0)
535        );
536
537        $this->sendDeleteNotifcations(
538            $res,
539            'frm_posts_deleted',
540            'deleted postings',
541            ilForumMailNotification::TYPE_POST_DELETED
542        );
543    }
544
545    /**
546     * @param $res
547     * @param $actionName
548     * @param $notificationType
549     */
550    private function sendNotification(\ilPDOStatement $res, string $actionName, int $notificationType)
551    {
552        $numRows = $this->ilDB->numRows($res);
553        if ($numRows > 0) {
554            $this->logger->info(sprintf('Sending notifications for %s "%s" events ...', $numRows, $actionName));
555            $this->sendCronForumNotification($res, $notificationType);
556            $this->logger->info(sprintf('Sent notifications for %s ...', $actionName));
557        }
558
559        $this->keepAlive();
560    }
561
562    /**
563     * @param $res
564     * @param $action
565     * @param $actionDescription
566     * @param $notificationType
567     */
568    private function sendDeleteNotifcations(\ilPDOStatement $res, string $action, string $actionDescription, int $notificationType)
569    {
570        $numRows = $this->ilDB->numRows($res);
571        if ($numRows > 0) {
572            $this->logger->info(sprintf('Sending notifications for %s "%s" events ...', $numRows, $actionDescription));
573            $this->sendCronForumNotification($res, $notificationType);
574            if (count(self::$deleted_ids_cache) > 0) {
575                $this->ilDB->manipulate('DELETE FROM frm_posts_deleted WHERE ' . $this->ilDB->in('deleted_id', self::$deleted_ids_cache, false, 'integer'));
576                $this->logger->info('Deleted obsolete entries of table "' . $action . '" ...');
577            }
578            $this->logger->info(sprintf('Sent notifications for %s ...', $actionDescription));
579        }
580
581        $this->keepAlive();
582    }
583
584    /**
585     * @param $condition
586     * @return string
587     */
588    private function createForumPostSql($condition) : string
589    {
590        return '
591			SELECT 	frm_threads.thr_subject thr_subject,
592					frm_data.top_name top_name,
593					frm_data.top_frm_fk obj_id,
594					frm_notification.user_id user_id,
595					frm_threads.thr_pk thread_id,
596					frm_posts.*
597			FROM 	frm_notification, frm_posts, frm_threads, frm_data, frm_posts_tree
598			WHERE	frm_posts.pos_thr_fk = frm_threads.thr_pk AND ' . $condition . '
599			AND 	((frm_threads.thr_top_fk = frm_data.top_pk AND 	frm_data.top_frm_fk = frm_notification.frm_id)
600					OR (frm_threads.thr_pk = frm_notification.thread_id
601			AND 	frm_data.top_pk = frm_threads.thr_top_fk) )
602			AND 	frm_posts.pos_display_user_id != frm_notification.user_id
603			AND     frm_posts_tree.pos_fk = frm_posts.pos_pk AND frm_posts_tree.parent_pos != 0
604			ORDER BY frm_posts.pos_date ASC';
605    }
606
607    /**
608     * @return string
609     */
610    private function createSelectOfDeletionNotificationsSql() : string
611    {
612        return '
613			SELECT 	frm_posts_deleted.thread_title thr_subject,
614					frm_posts_deleted.forum_title  top_name,
615					frm_posts_deleted.obj_id obj_id,
616					frm_notification.user_id user_id,
617					frm_posts_deleted.pos_display_user_id,
618					frm_posts_deleted.pos_usr_alias,
619					frm_posts_deleted.deleted_id,
620					frm_posts_deleted.post_date pos_date,
621					frm_posts_deleted.post_title pos_subject,
622					frm_posts_deleted.post_message pos_message,
623					frm_posts_deleted.deleted_by
624
625			FROM 	frm_notification, frm_posts_deleted
626
627			WHERE 	( frm_posts_deleted.obj_id = frm_notification.frm_id
628					OR frm_posts_deleted.thread_id = frm_notification.thread_id)
629			AND 	frm_posts_deleted.pos_display_user_id != frm_notification.user_id
630			AND 	frm_posts_deleted.is_thread_deleted = %s
631			ORDER BY frm_posts_deleted.post_date ASC';
632    }
633}
634