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