ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
class.ilForum.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
21 use ILIAS\User\Profile\Mode as PersonalProfileMode;
22 
29 class ilForum
30 {
31  private const int SORT_TITLE = 1;
32  private const int SORT_DATE = 2;
33  private const int DEFAULT_PAGE_HITS = 30;
34 
36  protected static array $moderators_by_ref_id_map = [];
37 
38  private readonly ilAppEventHandler $event;
39  private string $dbTable;
40  private string $className = 'ilForum';
41  private string $mdb2Query = '';
42  private array $mdb2DataValue = [];
43  private array $mdb2DataType = [];
44  private string $txtQuote1 = "[quote]";
45  private string $txtQuote2 = "[/quote]";
46  private string $replQuote1 = '<blockquote class="ilForumQuote">';
47  private string $replQuote2 = '</blockquote>';
48  private int $pageHits = self::DEFAULT_PAGE_HITS;
49  private int $id;
50  private int $ref_id;
51  private string $import_name = '';
52  public ilLanguage $lng;
55  public ilObjUser $user;
59 
60  public function __construct()
61  {
62  global $DIC;
63 
64  $this->error = $DIC['ilErr'];
65  $this->lng = $DIC->language();
66  $this->db = $DIC->database();
67  $this->user = $DIC->user();
68  $this->settings = $DIC->settings();
69  $this->event = $DIC->event();
70  $this->ui_factory = $DIC->ui()->factory();
71  $this->ui_renderer = $DIC->ui()->renderer();
72  }
73 
74  public function setForumId(int $a_obj_id): void
75  {
76  $this->id = $a_obj_id;
77  }
78 
79  public function setForumRefId(int $a_ref_id): void
80  {
81  $this->ref_id = $a_ref_id;
82  }
83 
84  public function getForumId(): int
85  {
86  return $this->id;
87  }
88 
89  public function getForumRefId(): int
90  {
91  return $this->ref_id;
92  }
93 
94  public function setDbTable(string $dbTable): void
95  {
96  if ($dbTable === '') {
97  die($this->className . '::setDbTable(): No database table given.');
98  }
99 
100  $this->dbTable = $dbTable;
101  }
102 
103  public function getDbTable(): string
104  {
105  return $this->dbTable;
106  }
107 
108  public function setMDB2WhereCondition(string $query_string, array $data_type, array $data_value): bool
109  {
110  $this->mdb2Query = $query_string;
111  $this->mdb2DataValue = $data_value;
112  $this->mdb2DataType = $data_type;
113 
114  return true;
115  }
116 
117  public function getMDB2Query(): string
118  {
119  return $this->mdb2Query ?: '';
120  }
121 
122  public function getMDB2DataValue(): array
123  {
124  return $this->mdb2DataValue ?: [];
125  }
126 
127  public function getMDB2DataType(): array
128  {
129  return $this->mdb2DataType ?: [];
130  }
131 
132  public function setPageHits(int $pageHits): bool
133  {
134  if ($pageHits < 1) {
135  $pageHits = 1;
136  }
137 
138  $this->pageHits = $pageHits;
139  return true;
140  }
141 
142  public function getPageHits(): int
143  {
144  return $this->pageHits;
145  }
146 
147  public function getOneTopic(): ForumDto
148  {
149  $data_type = [];
150  $data_value = [];
151 
152  $query = 'SELECT * FROM frm_data WHERE ';
153  if ($this->getMDB2Query() !== '' && $this->getMDB2DataType() !== [] && $this->getMDB2DataValue() !== []) {
154  $query .= ' ' . $this->getMDB2Query() . ' ';
155  $data_type += $this->getMDB2DataType();
156  $data_value += $this->getMDB2DataValue();
157  } else {
158  $query .= '1 = 1';
159  }
160 
161  $res = $this->db->queryF($query, $data_type, $data_value);
162  $row = $this->db->fetchAssoc($res);
163 
164  if (!is_array($row) || $row === []) {
166  }
167 
168  return ForumDto::getInstanceFromArray($row);
169  }
170 
171  public function getOneThread(): ilForumTopic
172  {
173  $data_type = [];
174  $data_value = [];
175 
176  $query = 'SELECT * FROM frm_threads WHERE ';
177  if ($this->getMDB2Query() !== '' && $this->getMDB2DataType() !== [] && $this->getMDB2DataValue() !== []) {
178  $query .= ' ' . $this->getMDB2Query() . ' ';
179  $data_type += $this->getMDB2DataType();
180  $data_value += $this->getMDB2DataValue();
181  } else {
182  $query .= '1 = 1';
183  }
184 
185  $sql_res = $this->db->queryF($query, $data_type, $data_value);
186  $result = $this->db->fetchAssoc($sql_res);
187  $result['thr_subject'] = trim((string) ($result['thr_subject'] ?? ''));
188 
189  $thread_obj = new ilForumTopic();
190  $thread_obj->assignData($result);
191 
192  return $thread_obj;
193  }
194 
195  public function generatePost(
196  int $forum_id,
197  int $thread_id,
198  int $author_id,
199  int $display_user_id,
200  string $message,
201  int $parent_pos,
202  bool $notify,
203  string $subject = '',
204  ?string $alias = null,
205  string $date = '',
206  bool $status = true,
207  bool $send_activation_mail = false
208  ): int {
209  $objNewPost = new ilForumPost();
210  $objNewPost->setForumId($forum_id);
211  $objNewPost->setThreadId($thread_id);
212  $objNewPost->setSubject($subject);
213  $objNewPost->setMessage($message);
214  $objNewPost->setDisplayUserId($display_user_id);
215  $objNewPost->setUserAlias($alias);
216  $objNewPost->setPosAuthorId($author_id);
217 
218  $frm_settings = ilForumProperties::getInstance($this->getForumId());
219 
220  $is_moderator = false;
221  if ($frm_settings->getMarkModeratorPosts() && self::_isModerator($this->getForumRefId(), $author_id)) {
222  $is_moderator = true;
223  }
224  $objNewPost->setIsAuthorModerator($is_moderator);
225 
226  if ($date === '') {
227  $objNewPost->setCreateDate(date('Y-m-d H:i:s'));
228  } elseif (strpos($date, '-') > 0) {
229  $objNewPost->setCreateDate($date);
230  }
231 
232  if ($status) {
233  $objNewPost->setPostActivationDate($objNewPost->getCreateDate());
234  }
235 
236  $objNewPost->setImportName($this->getImportName());
237  $objNewPost->setNotification($notify);
238  $objNewPost->setStatus($status);
239  $objNewPost->insert();
240 
241  if ($parent_pos === 0) {
242  $this->addPostTree($objNewPost->getThreadId(), $objNewPost->getId(), $objNewPost->getCreateDate());
243  } else {
244  $this->insertPostNode(
245  $objNewPost->getId(),
246  $parent_pos,
247  $objNewPost->getThreadId(),
248  $objNewPost->getCreateDate()
249  );
250  }
251 
252  $lastPost = $objNewPost->getForumId() . '#' . $objNewPost->getThreadId() . '#' . $objNewPost->getId();
253 
254  $this->db->manipulateF(
255  'UPDATE frm_threads SET thr_num_posts = thr_num_posts + 1, thr_last_post = %s WHERE thr_pk = %s',
256  ['text', 'integer'],
257  [$lastPost, $objNewPost->getThreadId()]
258  );
259 
260  $this->db->manipulateF(
261  'UPDATE frm_data SET top_num_posts = top_num_posts + 1, top_last_post = %s WHERE top_pk = %s',
262  ['text', 'integer'],
263  [$lastPost, $objNewPost->getForumId()]
264  );
265 
267  $forum_obj = ilObjectFactory::getInstanceByRefId($this->getForumRefId());
268  $forum_obj->markPostRead($objNewPost->getPosAuthorId(), $objNewPost->getThreadId(), $objNewPost->getId());
269 
270  if ($status && $parent_pos > 0) {
271  $news_item = new ilNewsItem();
272  $news_item->setContext($forum_obj->getId(), 'frm', $objNewPost->getId(), 'pos');
273  $news_item->setPriority(NEWS_NOTICE);
274  $news_item->setTitle($objNewPost->getSubject());
275  $news_item->setContent(ilRTE::_replaceMediaObjectImageSrc(
276  $this->prepareText($objNewPost->getMessage(), 0),
277  1
278  ));
279  if ($objNewPost->getMessage() !== strip_tags($objNewPost->getMessage())) {
280  $news_item->setContentHtml(true);
281  }
282 
283  $news_item->setUserId($display_user_id);
284  $news_item->setVisibility(NEWS_USERS);
285  $news_item->create();
286  }
287 
288  return $objNewPost->getId();
289  }
290 
291  public function generateThread(
292  ilForumTopic $thread,
293  string $message,
294  bool $notify,
295  bool $notify_posts,
296  bool $status = true,
297  bool $withFirstVisibleEntry = true
298  ): int {
299  if (!$thread->getCreateDate()) {
300  $thread->setCreateDate(date('Y-m-d H:i:s'));
301  }
302 
303  $thread->setImportName($this->getImportName());
304  $thread->insert();
305 
306  if ($notify_posts) {
307  $thread->enableNotification($thread->getThrAuthorId());
308  }
309 
310  $this->db->manipulateF(
311  'UPDATE frm_data SET top_num_threads = top_num_threads + 1 WHERE top_pk = %s',
312  ['integer'],
313  [$thread->getForumId()]
314  );
315 
316  $rootNodeId = $this->generatePost(
317  $thread->getForumId(),
318  $thread->getId(),
319  $thread->getThrAuthorId(),
320  $thread->getDisplayUserId(),
321  '',
322  0,
323  false,
324  $thread->getSubject(),
325  $thread->getUserAlias(),
326  $thread->getCreateDate(),
327  true,
328  false
329  );
330 
331  if (!$withFirstVisibleEntry) {
332  return $rootNodeId;
333  }
334 
335  return $this->generatePost(
336  $thread->getForumId(),
337  $thread->getId(),
338  $thread->getThrAuthorId(),
339  $thread->getDisplayUserId(),
340  $message,
341  $rootNodeId,
342  $notify,
343  $thread->getSubject(),
344  $thread->getUserAlias(),
345  $thread->getCreateDate(),
346  $status,
347  false
348  );
349  }
350 
355  public function moveThreads(array $thread_ids, ilObjForum $src_forum, int $target_obj_id): array
356  {
357  $errorMessages = [];
358 
359  if ($target_obj_id <= 0 || $src_forum->getId() <= 0) {
360  return $errorMessages;
361  }
362 
363  $this->setMDB2WhereCondition('top_frm_fk = %s ', ['integer'], [$src_forum->getId()]);
364  $oldFrmData = $this->getOneTopic();
365 
366  $this->setMDB2WhereCondition('top_frm_fk = %s ', ['integer'], [$target_obj_id]);
367  $newFrmData = $this->getOneTopic();
368 
369  if (!$oldFrmData->getTopPk() || !$newFrmData->getTopPk()) {
370  return $errorMessages;
371  }
372 
373  $num_moved_posts = 0;
374  $num_moved_threads = 0;
375  $num_visits = 0;
376 
377  foreach ($thread_ids as $id) {
378  $objTmpThread = new ilForumTopic($id);
379 
380  try {
381  $numPosts = $objTmpThread->movePosts(
382  $src_forum->getId(),
383  $oldFrmData->getTopPk(),
384  $target_obj_id,
385  $newFrmData->getTopPk()
386  );
387 
388  if (($last_post_string = ($objTmpThread->getLastPostString() ?? '')) !== '') {
389  $last_post_string = explode('#', $last_post_string);
390  $last_post_string[0] = $newFrmData->getTopPk();
391  $last_post_string = implode('#', $last_post_string);
392  $objTmpThread->setLastPostString($last_post_string);
393  }
394 
395  $num_visits += $objTmpThread->getVisits();
396  $num_moved_posts += $numPosts;
397  ++$num_moved_threads;
398 
399  $objTmpThread->setForumId($newFrmData->getTopPk());
400  $objTmpThread->update();
401  } catch (ilFileUtilsException) {
402  $errorMessages[] = sprintf($this->lng->txt('frm_move_invalid_file_type'), $objTmpThread->getSubject());
403  continue;
404  }
405  }
406 
407  if (0 === max($num_moved_threads, $num_moved_posts, $num_visits)) {
408  return $errorMessages;
409  }
410 
411  $this->db->setLimit(1, 0);
412  $res = $this->db->queryF(
413  'SELECT pos_thr_fk, pos_pk FROM frm_posts WHERE pos_top_fk = %s ORDER BY pos_date DESC',
414  ['integer'],
415  [$oldFrmData->getTopPk()]
416  );
417 
418  $last_post_src = '';
419  $row = $this->db->fetchObject($res);
420  if ($row !== null) {
421  $last_post_src = $oldFrmData->getTopPk() . '#' . $row->pos_thr_fk . '#' . $row->pos_pk;
422  }
423 
424  $this->db->manipulateF(
425  'UPDATE frm_data ' .
426  'SET top_num_posts = top_num_posts - %s, top_num_threads = top_num_threads - %s, visits = visits - %s, ' .
427  'top_last_post = %s WHERE top_pk = %s',
428  ['integer', 'integer', 'integer', 'text', 'integer'],
429  [
430  $num_moved_posts,
431  $num_moved_threads,
432  $num_visits,
433  $last_post_src,
434  $oldFrmData->getTopPk()
435  ]
436  );
437 
438  $this->db->setLimit(1, 0);
439  $res = $this->db->queryF(
440  'SELECT pos_thr_fk, pos_pk FROM frm_posts WHERE pos_top_fk = %s ORDER BY pos_date DESC',
441  ['integer'],
442  [$newFrmData->getTopPk()]
443  );
444 
445  $last_post_dest = '';
446  $row = $this->db->fetchObject($res);
447  if ($row !== null) {
448  $last_post_dest = $newFrmData->getTopPk() . '#' . $row->pos_thr_fk . '#' . $row->pos_pk;
449  }
450 
451  $this->db->manipulateF(
452  'UPDATE frm_data SET top_num_posts = top_num_posts + %s, top_num_threads = top_num_threads + %s, ' .
453  'visits = visits + %s, top_last_post = %s WHERE top_pk = %s',
454  ['integer', 'integer', 'integer', 'text', 'integer'],
455  [$num_moved_posts, $num_moved_threads, $num_visits, $last_post_dest, $newFrmData->getTopPk()]
456  );
457 
458  $this->event->raise(
459  'components/ILIAS/Forum',
460  'movedThreads',
461  [
462  'source_ref_id' => $src_forum->getId(),
463  'target_ref_id' => $src_forum->getId(),
464  'thread_ids' => $src_forum->getId(),
465  'source_frm_obj_id' => $src_forum->getId(),
466  'target_frm_obj_id' => $target_obj_id
467  ]
468  );
469 
470  return $errorMessages;
471  }
472 
473  public function postCensorship(ilObjForum $forum, string $message, int $pos_pk, int $cens = 0): void
474  {
475  $cens_date = date('Y-m-d H:i:s');
476 
477  $this->db->manipulateF(
478  'UPDATE frm_posts
479  SET pos_cens_com = %s,
480  pos_cens_date = %s,
481  pos_cens = %s,
482  update_user = %s
483  WHERE pos_pk = %s',
484  ['text', 'timestamp', 'integer', 'integer', 'integer'],
485  [$message,
486  $cens_date,
487  $cens,
488  $this->user->getId(),
489  $pos_pk
490  ]
491  );
492 
494  $this->id,
495  'frm',
496  $pos_pk,
497  'pos'
498  );
499  if ($news_id > 0) {
500  if ($cens > 0) {
501  $news_item = new ilNewsItem($news_id);
502  $news_item->setContent(nl2br($this->prepareText($message, 0)));
503  $news_item->setContentHtml(false);
504  if ($message !== strip_tags($message)) {
505  $news_item->setContentHtml(true);
506  }
507  } else {
508  $res = $this->db->queryF('SELECT pos_message FROM frm_posts WHERE pos_pk = %s', ['integer'], [$pos_pk]);
509  $rec = $this->db->fetchAssoc($res);
510 
511  $news_item = new ilNewsItem($news_id);
512  $news_item->setContent(nl2br($this->prepareText($rec['pos_message'], 0)));
513  $news_item->setContentHtml(false);
514  if ($rec['pos_message'] !== strip_tags($rec['pos_message'])) {
515  $news_item->setContentHtml(true);
516  }
517  }
518  $news_item->update();
519  }
520 
521  $this->event->raise(
522  'components/ILIAS/Forum',
523  'censoredPost',
524  [
525  'ref_id' => $this->getForumRefId(),
526  'post' => new ilForumPost($pos_pk),
527  'object' => $forum
528  ]
529  );
530  }
531 
535  public function deletePost($postIdOrRecord, bool $raiseEvents = true): int
536  {
537  $p_node = $postIdOrRecord;
538  if (is_numeric($postIdOrRecord)) {
539  $p_node = $this->getPostNode($postIdOrRecord);
540  }
541 
542  $post = new ilForumPost((int) $p_node['pos_pk']);
543  if ($raiseEvents) {
544  $is_deleted_thread = ($post->getParentId() == 0) ? true : false;
545  $num_visible_active_posts = 0;
546  if ($is_deleted_thread) {
547  $query = '
548  SELECT COUNT(*) AS cnt
549  FROM frm_posts
550  INNER JOIN frm_posts_tree ON pos_pk = pos_fk
551  WHERE frm_posts_tree.parent_pos != 0
552  AND pos_thr_fk = ' . $this->db->quote($post->getThreadId(), 'integer') . '
553  AND pos_status = ' . $this->db->quote(1, 'integer');
554  $res = $this->db->query($query);
555  $row = $this->db->fetchAssoc($res);
556  $num_visible_active_posts = (int) ($row['cnt'] ?? 0);
557  }
558 
559  $this->event->raise(
560  'components/ILIAS/Forum',
561  'beforePostDeletion',
562  [
563  'obj_id' => $this->getForumId(),
564  'ref_id' => $this->getForumRefId(),
565  'post' => $post,
566  'thread_deleted' => $is_deleted_thread,
567  'num_visible_active_posts' => $num_visible_active_posts
568  ]
569  );
570  }
571 
572  $this->deletePostFiles(array_merge(
573  $this->getSubPathIdsForNode($post),
574  [$post->getId()]
575  ));
576 
577  $affected_user_ids[] = $post->getPosAuthorId();
578  $deleted_post_ids = $this->deletePostTree($p_node);
579 
580  $obj_history = new ilForumDraftsHistory();
581  $obj_history->deleteHistoryByPostIds($deleted_post_ids);
582 
583  $obj_draft = new ilForumPostDraft();
584  $obj_draft->deleteDraftsByPostIds($deleted_post_ids);
585 
586  foreach ($deleted_post_ids as $post_id) {
588  }
589 
590  $dead_pos = count($deleted_post_ids);
591  $dead_thr = 0;
592 
593  if ((int) $post->getParentId() === 0) {
594  $dead_thr = $post->getThreadId();
595 
596  $this->db->manipulateF('DELETE FROM frm_threads WHERE thr_pk = %s', ['integer'], [$dead_thr]);
597  $this->db->manipulateF(
598  'UPDATE frm_data SET top_num_threads = top_num_threads - 1 WHERE top_frm_fk = %s',
599  ['integer'],
600  [$this->id]
601  );
602 
603  $posset = $this->db->queryF('SELECT * FROM frm_posts WHERE pos_thr_fk = %s', ['integer'], [$dead_thr]);
604  while ($posrec = $this->db->fetchAssoc($posset)) {
606  $this->id,
607  'frm',
608  (int) $posrec['pos_pk'],
609  'pos'
610  );
611  if ($news_id > 0) {
612  $news_item = new ilNewsItem($news_id);
613  $news_item->delete();
614  }
615 
616  try {
617  $mobs = ilObjMediaObject::_getMobsOfObject('frm:html', (int) $posrec['pos_pk']);
618  foreach ($mobs as $mob) {
619  if (ilObjMediaObject::_exists($mob)) {
620  ilObjMediaObject::_removeUsage($mob, 'frm:html', (int) $posrec['pos_pk']);
621  $mob_obj = new ilObjMediaObject($mob);
622  $mob_obj->delete();
623  }
624  }
625  } catch (Exception) {
626  }
627  $affected_user_ids[] = (int) $posrec['pos_author_id'];
628  }
629 
630  $this->db->manipulateF('DELETE FROM frm_posts WHERE pos_thr_fk = %s', ['integer'], [$post->getThreadId()]);
631  } else {
632  for ($i = 0; $i < $dead_pos; $i++) {
633  $this->db->manipulateF('DELETE FROM frm_posts WHERE pos_pk = %s', ['integer'], [$deleted_post_ids[$i]]);
634 
636  $this->id,
637  'frm',
638  $deleted_post_ids[$i],
639  'pos'
640  );
641  if ($news_id > 0) {
642  $news_item = new ilNewsItem($news_id);
643  $news_item->delete();
644  }
645 
646  try {
647  $mobs = ilObjMediaObject::_getMobsOfObject('frm:html', $deleted_post_ids[$i]);
648  foreach ($mobs as $mob) {
649  if (ilObjMediaObject::_exists($mob)) {
650  ilObjMediaObject::_removeUsage($mob, 'frm:html', $deleted_post_ids[$i]);
651  $mob_obj = new ilObjMediaObject($mob);
652  $mob_obj->delete();
653  }
654  }
655  } catch (Exception) {
656  }
657  }
658 
659  $this->db->manipulateF(
660  'UPDATE frm_threads SET thr_num_posts = thr_num_posts - %s WHERE thr_pk = %s',
661  ['integer', 'integer'],
662  [$dead_pos, $post->getTreeId()]
663  );
664 
665  $res1 = $this->db->queryF(
666  'SELECT * FROM frm_posts WHERE pos_thr_fk = %s ORDER BY pos_date DESC',
667  ['integer'],
668  [$post->getTreeId()]
669  );
670 
671  $lastPost_thr = '';
672  if ($res1->numRows() > 0) {
673  $z = 0;
674 
675  while ($selData = $this->db->fetchAssoc($res1)) {
676  if ($z > 0) {
677  break;
678  }
679 
680  $lastPost_thr = $selData['pos_top_fk'] . '#' . $selData['pos_thr_fk'] . '#' . $selData['pos_pk'];
681  $z++;
682  }
683  }
684 
685  $this->db->manipulateF(
686  'UPDATE frm_threads SET thr_last_post = %s WHERE thr_pk = %s',
687  ['text', 'integer'],
688  [$lastPost_thr, $post->getTreeId()]
689  );
690  }
691 
692  $this->db->manipulateF(
693  'UPDATE frm_data SET top_num_posts = top_num_posts - %s WHERE top_frm_fk = %s',
694  ['integer', 'integer'],
695  [$dead_pos, $this->id]
696  );
697 
698  $res2 = $this->db->queryF(
699  'SELECT * FROM frm_posts, frm_data WHERE pos_top_fk = top_pk AND top_frm_fk = %s ORDER BY pos_date DESC',
700  ['integer'],
701  [$this->id]
702  );
703 
704  $lastPost_top = '';
705  if ($res2->numRows() > 0) {
706  $z = 0;
707 
708  while ($selData = $this->db->fetchAssoc($res2)) {
709  if ($z > 0) {
710  break;
711  }
712 
713  $lastPost_top = $selData['pos_top_fk'] . '#' . $selData['pos_thr_fk'] . '#' . $selData['pos_pk'];
714  $z++;
715  }
716  }
717 
718  $this->db->manipulateF(
719  'UPDATE frm_data SET top_last_post = %s WHERE top_frm_fk = %s',
720  ['text', 'integer'],
721  [$lastPost_top, $this->id]
722  );
723 
724  if ($raiseEvents) {
725  $this->event->raise(
726  'components/ILIAS/Forum',
727  'afterPostDeletion',
728  [
729  'obj_id' => $this->getForumId(),
730  'ref_id' => $this->getForumRefId(),
731  'user_ids' => $affected_user_ids
732  ]
733  );
734  }
735 
736  return $dead_thr;
737  }
738 
742  public function getAllThreads(int $a_topic_id, array $params = [], int $limit = 0, int $offset = 0): array
743  {
744  $frm_props = ilForumProperties::getInstance($this->getForumId());
745  $is_post_activation_enabled = $frm_props->isPostActivationEnabled();
746 
747  $user_id = $this->user->getId();
748 
749  $excluded_ids_condition = '';
750  if (isset($params['excluded_ids']) && is_array($params['excluded_ids']) && $params['excluded_ids']) {
751  $excluded_ids_condition = ' AND ' . $this->db->in('thr_pk', $params['excluded_ids'], true, 'integer') . ' ';
752  }
753 
754  if (!isset($params['order_column']) || !in_array(
755  strtolower($params['order_column']),
756  ['lp_date', 'rating', 'thr_subject', 'num_posts', 'num_visit']
757  )) {
758  $params['order_column'] = 'post_date';
759  }
760  if (!isset($params['order_direction']) || !in_array(strtolower($params['order_direction']), ['asc', 'desc'])) {
761  $params['order_direction'] = 'desc';
762  }
763 
764  $cnt_active_pos_query = '';
765  $cnt_join_type = 'LEFT';
766  if ($is_post_activation_enabled && !$params['is_moderator']) {
767  $cnt_active_pos_query = " AND (pos_status = {$this->db->quote(1, 'integer')} OR pos_author_id = {$this->db->quote($user_id, 'integer')}) ";
768  $cnt_join_type = "INNER";
769  }
770  $query =
771  "SELECT COUNT(DISTINCT(thr_pk)) cnt
772  FROM frm_threads
773  $cnt_join_type JOIN frm_posts
774  ON pos_thr_fk = thr_pk $cnt_active_pos_query
775  WHERE thr_top_fk = %s $excluded_ids_condition
776  ";
777  $res = $this->db->queryF($query, ['integer'], [$a_topic_id]);
778  $cntData = $this->db->fetchAssoc($res);
779  $cnt = (int) $cntData['cnt'];
780 
781  $active_query = '';
782  $having = '';
783  if ($is_post_activation_enabled && !$params['is_moderator']) {
784  $active_query = ' AND (pos_status = %s OR pos_author_id = %s) ';
785  $having = ' HAVING num_posts > 0';
786  }
787 
788  $threads = [];
789  $data = [];
790  $data_types = [];
791 
792  $optional_fields = '';
793  if ($frm_props->isIsThreadRatingEnabled()) {
794  $optional_fields = ', avg_rating';
795  }
796 
797  $additional_sort = '';
798 
799  if ($params['order_column'] === 'thr_subject') {
800  $dynamic_columns = [', thr_subject ' . $params['order_direction']];
801  } elseif ($params['order_column'] === 'num_posts') {
802  $dynamic_columns = [', num_posts ' . $params['order_direction']];
803  } elseif ($params['order_column'] === 'num_visit') {
804  $dynamic_columns = [', visits ' . $params['order_direction']];
805  } else {
806  $dynamic_columns = [', post_date ' . $params['order_direction']];
807  }
808 
809  if ($frm_props->isIsThreadRatingEnabled()) {
810  $dynamic_columns[] = ' ,avg_rating ' . $params['order_direction'];
811  }
812  if ('rating' === strtolower($params['order_column'])) {
813  $dynamic_columns = array_reverse($dynamic_columns);
814  }
815  $additional_sort .= implode(' ', $dynamic_columns);
816 
817 
818  if (!$this->user->isAnonymous()) {
819  $query = "SELECT
820  (CASE WHEN COUNT(DISTINCT(notification_id)) > 0 THEN 1 ELSE 0 END) usr_notification_is_enabled,
821  MAX(pos_date) post_date,
822  SUM(tree1.parent_pos != 0) num_posts,
823  SUM(tree1.parent_pos != 0) - SUM(tree1.parent_pos != 0 AND postread.post_id IS NOT NULL) num_unread_posts, ";
824 
825  $query .= " thr_pk, thr_top_fk, thr_subject, thr_author_id, thr_display_user_id, thr_usr_alias, thr_num_posts, thr_last_post, thr_date, thr_update, visits, frm_threads.import_name, is_sticky, is_closed
826  $optional_fields
827  FROM frm_threads
828 
829  LEFT JOIN frm_notification
830  ON frm_notification.thread_id = thr_pk
831  AND frm_notification.user_id = %s
832 
833  LEFT JOIN frm_posts
834  ON pos_thr_fk = thr_pk $active_query
835  LEFT JOIN frm_posts_tree tree1
836  ON tree1.pos_fk = frm_posts.pos_pk
837  LEFT JOIN frm_user_read postread
838  ON postread.post_id = pos_pk
839  AND postread.usr_id = %s";
840 
841  $query .= " WHERE thr_top_fk = %s
842  $excluded_ids_condition
843  GROUP BY thr_pk, thr_top_fk, thr_subject, thr_author_id, thr_display_user_id, thr_usr_alias, thr_num_posts, thr_last_post, thr_date, thr_update, visits, frm_threads.import_name, is_sticky, is_closed
844  $optional_fields
845  $having
846  ORDER BY is_sticky DESC $additional_sort, thr_date DESC";
847 
848 
849  $data_types[] = 'integer';
850  if ($is_post_activation_enabled && !$params['is_moderator']) {
851  $data_types[] = 'integer';
852  $data_types[] = 'integer';
853  }
854  $data_types[] = 'integer';
855  $data_types[] = 'integer';
856 
857 
858  $data[] = $user_id;
859  if ($is_post_activation_enabled && !$params['is_moderator']) {
860  $data[] = 1;
861  $data[] = $user_id;
862  }
863  $data[] = $user_id;
864  } else {
865  $query = "SELECT
866  0 usr_notification_is_enabled,
867  MAX(pos_date) post_date,
868  COUNT(DISTINCT(tree1.pos_fk)) num_posts,
869  COUNT(DISTINCT(tree1.pos_fk)) num_unread_posts,
870  thr_pk, thr_top_fk, thr_subject, thr_author_id, thr_display_user_id, thr_usr_alias, thr_num_posts, thr_last_post, thr_date, thr_update, visits, frm_threads.import_name, is_sticky, is_closed
871  $optional_fields
872  FROM frm_threads
873 
874  LEFT JOIN frm_posts
875  ON pos_thr_fk = thr_pk $active_query
876  LEFT JOIN frm_posts_tree tree1
877  ON tree1.pos_fk = frm_posts.pos_pk AND tree1.parent_pos != 0
878  ";
879 
880  $query .= " WHERE thr_top_fk = %s
881  $excluded_ids_condition
882  GROUP BY thr_pk, thr_top_fk, thr_subject, thr_author_id, thr_display_user_id, thr_usr_alias, thr_num_posts, thr_last_post, thr_date, thr_update, visits, frm_threads.import_name, is_sticky, is_closed
883  $optional_fields
884  $having
885  ORDER BY is_sticky DESC $additional_sort, thr_date DESC";
886 
887  if ($is_post_activation_enabled && !$params['is_moderator']) {
888  $data_types[] = 'integer';
889  $data_types[] = 'integer';
890  }
891  $data_types[] = 'integer';
892  if ($is_post_activation_enabled && !$params['is_moderator']) {
893  $data[] = 1;
894  $data[] = $user_id;
895  }
896  }
897  $data[] = $a_topic_id;
898 
899  if ($limit || $offset) {
900  $this->db->setLimit($limit, $offset);
901  }
902 
903  $threadIds = [];
904  $res = $this->db->queryF($query, $data_types, $data);
905  while ($row = $this->db->fetchAssoc($res)) {
906  $thread = new ilForumTopic((int) $row['thr_pk'], (bool) $params['is_moderator'], true);
907  $thread->assignData($row);
908  $threads[(int) $row['thr_pk']] = $thread;
909  $threadIds[] = (int) $row['thr_pk'];
910  }
911 
912  $inner_last_active_post_condition = '';
913  if ($is_post_activation_enabled && !$params['is_moderator']) {
914  $inner_last_active_post_condition = sprintf(
915  ' AND (iposts.pos_status = %s OR (iposts.pos_status = %s AND iposts.pos_author_id = %s)) ',
916  $this->db->quote(1, 'integer'),
917  $this->db->quote(0, 'integer'),
918  $this->db->quote($this->user->getId(), 'integer')
919  );
920  }
921 
922  $post_res = $this->db->query(
923  'SELECT frm_posts.*
924  FROM frm_posts
925  INNER JOIN (
926  SELECT pos_thr_fk, MAX(iposts.pos_date) i_pos_date
927  FROM frm_posts iposts
928  WHERE ' . $this->db->in('iposts.pos_thr_fk', $threadIds, false, 'integer') . '
929  ' . $inner_last_active_post_condition . '
930  GROUP BY pos_thr_fk
931  ) opost ON frm_posts.pos_thr_fk = opost.pos_thr_fk AND frm_posts.pos_date = opost.i_pos_date'
932  );
933  while ($post_row = $this->db->fetchAssoc($post_res)) {
934  $tmp_obj = new ilForumPost((int) $post_row['pos_pk'], (bool) $params['is_moderator'], true);
935  $tmp_obj->setPosAuthorId((int) $post_row['pos_author_id']);
936  $tmp_obj->setDisplayUserId((int) $post_row['pos_display_user_id']);
937  $tmp_obj->setUserAlias((string) $post_row['pos_usr_alias']);
938  $tmp_obj->setImportName((string) $post_row['import_name']);
939  $tmp_obj->setId((int) $post_row['pos_pk']);
940  $tmp_obj->setCreateDate((string) $post_row['pos_date']);
941 
942  $threads[(int) $post_row['pos_thr_fk']]->setLastPostForThreadOverview($tmp_obj);
943  }
944 
945  return [
946  'items' => $threads,
947  'cnt' => $cnt
948  ];
949  }
950 
951  public function getNumberOfPublishedUserPostings(int $usr_id, bool $post_activation_required): int
952  {
953  $query = '
954  SELECT
955  SUM(IF(f.pos_cens = %s, 1, 0)) cnt
956  FROM frm_posts f
957  INNER JOIN frm_posts_tree t ON f.pos_pk = t.pos_fk AND t.parent_pos != %s
958  INNER JOIN frm_threads th ON t.thr_fk = th.thr_pk
959  INNER JOIN frm_data d ON d.top_pk = f.pos_top_fk AND d.top_frm_fk = %s
960  WHERE f.pos_author_id = %s
961  ';
962 
963  if ($post_activation_required) {
964  $query .= ' AND f.pos_status = ' . $this->db->quote(1, 'integer');
965  }
966 
967  $res = $this->db->queryF(
968  $query,
969  ['integer', 'integer', 'integer', 'integer'],
970  [0, 0, $this->getForumId(), $usr_id]
971  );
972  $row = $this->db->fetchAssoc($res);
973  if (is_array($row)) {
974  return (int) $row['cnt'];
975  }
976 
977  return 0;
978  }
979 
983  public function getUserStatistics(bool $post_activation_required): array
984  {
985  $statistic = [];
986  $data_types = [];
987  $data = [];
988 
989  $query = '
990  SELECT
991  u.login, u.lastname, u.firstname, f.pos_author_id, u.usr_id,
992  p.value public_profile,
993  SUM(IF(f.pos_cens = %s, 1, 0)) num_postings
994  FROM frm_posts f
995  INNER JOIN frm_posts_tree t ON f.pos_pk = t.pos_fk
996  INNER JOIN frm_threads th ON t.thr_fk = th.thr_pk
997  INNER JOIN usr_data u ON u.usr_id = f.pos_author_id
998  INNER JOIN frm_data d ON d.top_pk = f.pos_top_fk
999  LEFT JOIN usr_pref p ON p.usr_id = u.usr_id AND p.keyword = %s
1000  WHERE t.parent_pos != %s
1001  ';
1002 
1003  $data_types[] = 'integer';
1004  $data_types[] = 'text';
1005  $data_types[] = 'integer';
1006  $data[] = 0;
1007  $data[] = 'public_profile';
1008  $data[] = 0;
1009 
1010  if ($post_activation_required) {
1011  $query .= ' AND pos_status = %s';
1012  $data_types[] = 'integer';
1013  $data[] = 1;
1014  }
1015 
1016  $query .= '
1017  AND d.top_frm_fk = %s
1018  GROUP BY u.login, p.value,u.lastname, u.firstname, f.pos_author_id
1019  ';
1020 
1021  $data_types[] = 'integer';
1022  $data[] = $this->getForumId();
1023 
1024  $res = $this->db->queryF($query, $data_types, $data);
1025  while ($row = $this->db->fetchAssoc($res)) {
1026  if (
1027  !in_array($row['public_profile'], [
1028  PersonalProfileMode::PROFILE_ENABLED_LOGGED_IN_USERS,
1029  PersonalProfileMode::PROFILE_ENABLED_GLOBAL], true)
1030  || ($this->user->isAnonymous() && $row['public_profile'] !== PersonalProfileMode::PROFILE_ENABLED_GLOBAL)
1031  ) {
1032  $row['lastname'] = '';
1033  $row['firstname'] = '';
1034  }
1035 
1036  $row['usr_id'] = (int) $row['usr_id'];
1037  $row['pos_author_id'] = (int) $row['pos_author_id'];
1038  $row['num_postings'] = (int) $row['num_postings'];
1039 
1040  $statistic[] = $row;
1041  }
1042 
1043  return $statistic;
1044  }
1045 
1046  public function getRootPostIdByThread(int $a_thread_id): int
1047  {
1048  $res = $this->db->queryF(
1049  'SELECT pos_fk FROM frm_posts_tree WHERE thr_fk = %s AND parent_pos = %s',
1050  ['integer', 'integer'],
1051  [$a_thread_id, 0]
1052  );
1053 
1054  $row = $this->db->fetchObject($res);
1055  if ($row instanceof stdClass) {
1056  return (int) $row->pos_fk;
1057  }
1058 
1059  return 0;
1060  }
1061 
1065  public function getModerators(): array
1066  {
1067  return self::_getModerators($this->getForumRefId());
1068  }
1069 
1073  public static function _getModerators(int $a_ref_id): array
1074  {
1075  global $DIC;
1076 
1077  $rbacreview = $DIC->rbac()->review();
1078 
1079  $role_arr = $rbacreview->getRolesOfRoleFolder($a_ref_id);
1080  foreach ($role_arr as $role_id) {
1081  if (ilObject::_lookupTitle($role_id) === 'il_frm_moderator_' . $a_ref_id) {
1082  return array_map('intval', $rbacreview->assignedUsers($role_id));
1083  }
1084  }
1085 
1086  return [];
1087  }
1088 
1089  public static function _isModerator(int $a_ref_id, int $a_usr_id): bool
1090  {
1091  if (!isset(self::$moderators_by_ref_id_map[$a_ref_id])) {
1092  self::$moderators_by_ref_id_map[$a_ref_id] = self::_getModerators($a_ref_id);
1093  }
1094 
1095  return in_array($a_usr_id, self::$moderators_by_ref_id_map[$a_ref_id], true);
1096  }
1097 
1098  public function countUserArticles(int $a_user_id): int
1099  {
1100  $res = $this->db->queryF(
1101  'SELECT * FROM frm_data
1102  INNER JOIN frm_posts ON pos_top_fk = top_pk
1103  INNER JOIN frm_posts_tree tree1 ON tree1.pos_fk = frm_posts.pos_pk AND tree1.parent_pos != 0
1104  WHERE top_frm_fk = %s
1105  AND pos_author_id = %s',
1106  ['integer', 'integer'],
1107  [$this->getForumId(), $a_user_id]
1108  );
1109 
1110  return $res->numRows();
1111  }
1112 
1113  public function countActiveUserArticles(int $a_user_id): int
1114  {
1115  $res = $this->db->queryF(
1116  'SELECT * FROM frm_data
1117  INNER JOIN frm_posts ON pos_top_fk = top_pk
1118  INNER JOIN frm_posts_tree tree1 ON tree1.pos_fk = frm_posts.pos_pk AND tree1.parent_pos != 0
1119  WHERE top_frm_fk = %s
1120  AND (pos_status = %s OR (pos_status = %s AND pos_author_id = %s))
1121  AND pos_author_id = %s',
1122  ['integer', 'integer', 'integer', 'integer', 'integer'],
1123  [$this->getForumId(), 1, 0, $this->user->getId(), $a_user_id]
1124  );
1125 
1126  return $res->numRows();
1127  }
1128 
1129  public function convertDate(string $date): string
1130  {
1132  }
1133 
1134  public function addPostTree(int $a_tree_id, int $a_node_id = -1, string $a_date = ''): bool
1135  {
1136  $a_date = $a_date ?: date('Y-m-d H:i:s');
1137 
1138  if ($a_node_id <= 0) {
1139  $a_node_id = $a_tree_id;
1140  }
1141 
1142  $nextId = $this->db->nextId('frm_posts_tree');
1143 
1144  $this->db->manipulateF(
1145  '
1146  INSERT INTO frm_posts_tree
1147  ( fpt_pk,
1148  thr_fk,
1149  pos_fk,
1150  parent_pos,
1151  lft,
1152  rgt,
1153  depth,
1154  fpt_date
1155  )
1156  VALUES(%s, %s, %s, %s, %s, %s, %s, %s )',
1157  ['integer', 'integer', 'integer', 'integer', 'integer', 'integer', 'integer', 'timestamp'],
1158  [$nextId, $a_tree_id, $a_node_id, 0, 1, 2, 1, $a_date]
1159  );
1160 
1161  return true;
1162  }
1163 
1167  public function insertPostNode(int $a_node_id, int $a_parent_id, int $tree_id, string $a_date = ''): void
1168  {
1169  $a_date = $a_date ?: date('Y-m-d H:i:s');
1170  $left = 1;
1171 
1172  $res = $this->db->queryF(
1173  'SELECT lft FROM frm_posts_tree WHERE pos_fk = %s AND thr_fk = %s',
1174  ['integer', 'integer'],
1175  [$a_parent_id, $tree_id]
1176  );
1177  $row = $this->db->fetchObject($res);
1178  if ($row instanceof stdClass) {
1179  $left = (int) $row->lft;
1180  }
1181 
1182  $lft = $left + 1;
1183  $rgt = $left + 2;
1184 
1185  $this->db->manipulateF(
1186  '
1187  UPDATE frm_posts_tree
1188  SET lft = CASE
1189  WHEN lft > %s
1190  THEN lft + 2
1191  ELSE lft
1192  END,
1193  rgt = CASE
1194  WHEN rgt > %s
1195  THEN rgt + 2
1196  ELSE rgt
1197  END
1198  WHERE thr_fk = %s',
1199  ['integer', 'integer', 'integer'],
1200  [$left, $left, $tree_id]
1201  );
1202 
1203  $depth = $this->getPostDepth($a_parent_id, $tree_id) + 1;
1204 
1205  $nextId = $this->db->nextId('frm_posts_tree');
1206  $this->db->manipulateF(
1207  '
1208  INSERT INTO frm_posts_tree
1209  ( fpt_pk,
1210  thr_fk,
1211  pos_fk,
1212  parent_pos,
1213  lft,
1214  rgt,
1215  depth,
1216  fpt_date
1217  )
1218  VALUES(%s,%s,%s, %s, %s, %s,%s, %s)',
1219  ['integer', 'integer', 'integer', 'integer', 'integer', 'integer', 'integer', 'timestamp'],
1220  [
1221  $nextId,
1222  $tree_id,
1223  $a_node_id,
1224  $a_parent_id,
1225  $lft,
1226  $rgt,
1227  $depth,
1228  $a_date
1229  ]
1230  );
1231  }
1232 
1233  public function getPostDepth(int $a_node_id, int $tree_id): int
1234  {
1235  if ($tree_id !== 0) {
1236  $res = $this->db->queryF(
1237  'SELECT depth FROM frm_posts_tree WHERE pos_fk = %s AND thr_fk = %s',
1238  ['integer', 'integer'],
1239  [$a_node_id, $tree_id]
1240  );
1241 
1242  $row = $this->db->fetchObject($res);
1243  if ($row instanceof stdClass) {
1244  return (int) $row->depth;
1245  }
1246  }
1247 
1248  return 0;
1249  }
1250 
1251  public function getFirstPostNode(int $tree_id): array
1252  {
1253  $res = $this->db->queryF(
1254  'SELECT * FROM frm_posts, frm_posts_tree WHERE pos_pk = pos_fk AND parent_pos = %s AND thr_fk = %s',
1255  ['integer', 'integer'],
1256  [0, $tree_id]
1257  );
1258 
1259  if (($row = $this->db->fetchObject($res)) !== null) {
1260  return $this->fetchPostNodeData($row);
1261  }
1262 
1263  return [];
1264  }
1265 
1266  public function getPostNode(int $post_id): array
1267  {
1268  $res = $this->db->queryF(
1269  'SELECT * FROM frm_posts, frm_posts_tree WHERE pos_pk = pos_fk AND pos_pk = %s',
1270  ['integer'],
1271  [$post_id]
1272  );
1273 
1274  if (($row = $this->db->fetchObject($res)) !== null) {
1275  return $this->fetchPostNodeData($row);
1276  }
1277 
1278  return [];
1279  }
1280 
1284  public function fetchPostNodeData(stdClass $a_row): array
1285  {
1286  $fullname = '';
1287  $loginname = '';
1288 
1289  if (ilObject::_exists((int) $a_row->pos_display_user_id)) {
1290  $tmp_user = new ilObjUser((int) $a_row->pos_display_user_id);
1291  $fullname = $tmp_user->getFullname();
1292  $loginname = $tmp_user->getLogin();
1293  }
1294 
1295  if ($fullname === '') {
1296  $fullname = $this->lng->txt('unknown');
1297  if ($a_row->import_name) {
1298  $fullname = $a_row->import_name;
1299  }
1300  }
1301 
1302  return [
1303  'type' => 'post',
1304  'pos_pk' => (int) $a_row->pos_pk,
1305  'pos_thr_fk' => (int) $a_row->pos_thr_fk,
1306  'child' => (int) $a_row->pos_pk,
1307  'author' => (int) $a_row->pos_display_user_id,
1308  'alias' => (string) $a_row->pos_usr_alias,
1309  'title' => $fullname,
1310  'loginname' => $loginname,
1311  'message' => (string) $a_row->pos_message,
1312  'subject' => (string) $a_row->pos_subject,
1313  'pos_cens_com' => (string) $a_row->pos_cens_com,
1314  'pos_cens' => (int) $a_row->pos_cens,
1315  'date' => $a_row->fpt_date,
1316  'create_date' => $a_row->pos_date,
1317  'update' => $a_row->pos_update,
1318  'update_user' => (int) $a_row->update_user,
1319  'tree' => (int) $a_row->thr_fk,
1320  'parent' => (int) $a_row->parent_pos,
1321  'lft' => (int) $a_row->lft,
1322  'rgt' => (int) $a_row->rgt,
1323  'depth' => (int) $a_row->depth,
1324  'id' => (int) $a_row->fpt_pk,
1325  'notify' => (int) $a_row->notify,
1326  'import_name' => $a_row->import_name,
1327  'pos_status' => (int) $a_row->pos_status
1328  ];
1329  }
1330 
1334  public function getSubPathIdsForNode(ilForumPost $post): array
1335  {
1336  $res = $this->db->queryF(
1337  'SELECT pos_fk FROM frm_posts_tree WHERE lft BETWEEN %s AND %s AND thr_fk = %s',
1339  [$post->getLft(), $post->getRgt(), $post->getTreeId()]
1340  );
1341 
1342  $post_ids = [];
1343  while ($post_tree_data = $this->db->fetchAssoc($res)) {
1344  $post_ids[] = (int) $post_tree_data['pos_fk'];
1345  }
1346 
1347  return $post_ids;
1348  }
1349 
1353  public function deletePostTree(array $a_node): array
1354  {
1355  $res = $this->db->queryF(
1356  'SELECT lft, rgt FROM frm_posts_tree WHERE thr_fk = %s AND pos_fk = %s AND parent_pos = %s',
1357  ['integer', 'integer', 'integer'],
1358  [$a_node['tree'], $a_node['pos_pk'], $a_node['parent']]
1359  );
1360 
1361  while ($row = $this->db->fetchObject($res)) {
1362  $a_node['lft'] = (int) $row->lft;
1363  $a_node['rgt'] = (int) $row->rgt;
1364  }
1365 
1366  $diff = $a_node['rgt'] - $a_node['lft'] + 1;
1367 
1368  $res = $this->db->queryF(
1369  'SELECT pos_fk FROM frm_posts_tree WHERE lft BETWEEN %s AND %s AND thr_fk = %s',
1370  ['integer', 'integer', 'integer'],
1371  [$a_node['lft'], $a_node['rgt'], $a_node['tree']]
1372  );
1373 
1374  $deleted_post_ids = [];
1375  while ($treeData = $this->db->fetchAssoc($res)) {
1376  $deleted_post_ids[] = (int) $treeData['pos_fk'];
1377  }
1378 
1379  $this->db->manipulateF(
1380  'DELETE FROM frm_posts_tree WHERE lft BETWEEN %s AND %s AND thr_fk = %s',
1381  ['integer', 'integer', 'integer'],
1382  [$a_node['lft'], $a_node['rgt'], $a_node['tree']]
1383  );
1384 
1385  $this->db->manipulateF(
1386  '
1387  UPDATE frm_posts_tree
1388  SET lft = CASE
1389  WHEN lft > %s
1390  THEN lft - %s
1391  ELSE lft
1392  END,
1393  rgt = CASE
1394  WHEN rgt > %s
1395  THEN rgt - %s
1396  ELSE rgt
1397  END
1398  WHERE thr_fk = %s',
1399  ['integer', 'integer', 'integer', 'integer', 'integer'],
1400  [$a_node['lft'], $diff, $a_node['lft'], $diff, $a_node['tree']]
1401  );
1402 
1403  return $deleted_post_ids;
1404  }
1405 
1406  public function updateVisits(int $ID): void
1407  {
1408  $checkTime = time() - (60 * 60);
1409  $session_key = 'frm_visit_' . $this->dbTable . '_' . $ID;
1410 
1411  if (ilSession::get($session_key) < $checkTime) {
1412  ilSession::set($session_key, time());
1413  $query = 'UPDATE ' . $this->dbTable . ' SET visits = visits + 1 WHERE ';
1414  $data_type = [];
1415  $data_value = [];
1416 
1417  if ($this->getMDB2Query() !== '' && $this->getMDB2DataType() !== [] && $this->getMDB2DataValue() !== []) {
1418  $query .= $this->getMDB2Query();
1419  $data_type += $this->getMDB2DataType();
1420  $data_value += $this->getMDB2DataValue();
1421 
1422  $this->db->manipulateF($query, $data_type, $data_value);
1423  }
1424  }
1425  }
1426 
1427  public function prepareText(string $text, int $edit = 0, string $quote_user = '', string $type = ''): string
1428  {
1429  if ($type === 'export') {
1430  $this->replQuote1 = "<blockquote class=\"quote\"><hr size=\"1\" color=\"#000000\">";
1431  $this->replQuote2 = "<hr size=\"1\" color=\"#000000\"/></blockquote>";
1432  }
1433 
1434  if ($edit === 1) {
1435  $lname = '';
1436  if ($quote_user !== '') {
1437  $lname = '="' . $quote_user . '"';
1438  }
1439 
1440  $text = "[quote$lname]" . $text . "[/quote]";
1441  } else {
1442  // check for quotation
1443  $startZ = substr_count($text, "[quote"); // also count [quote="..."]
1444  $endZ = substr_count($text, "[/quote]");
1445 
1446  if ($startZ > 0 || $endZ > 0) {
1447  // add missing opening and closing tags
1448  if ($startZ > $endZ) {
1449  $diff = $startZ - $endZ;
1450 
1451  for ($i = 0; $i < $diff; $i++) {
1452  if ($type === 'export') {
1453  $text .= $this->txtQuote2;
1454  } else {
1455  $text .= "[/quote]";
1456  }
1457  }
1458  } elseif ($startZ < $endZ) {
1459  $diff = $endZ - $startZ;
1460 
1461  for ($i = 0; $i < $diff; $i++) {
1462  if ($type === 'export') {
1463  $text = $this->txtQuote1 . $text;
1464  } else {
1465  $text = "[quote]" . $text;
1466  }
1467  }
1468  }
1469 
1470  if ($edit === 0) {
1471  $text = preg_replace(
1472  '@\[(quote\s*?=\s*?"([^"]*?)"\s*?)\]@i',
1473  $this->replQuote1 . '<div class="ilForumQuoteHead">' . $this->lng->txt('quote') . ' ($2)</div>',
1474  $text
1475  );
1476 
1477  $text = str_replace(
1478  ["[quote]", "[/quote]"],
1479  [
1480  $this->replQuote1 . '<div class="ilForumQuoteHead">' . $this->lng->txt('quote') . '</div>',
1482  ],
1483  $text
1484  );
1485  }
1486  }
1487  }
1488 
1489  if ($type !== 'export') {
1490  if ($edit === 0) {
1491  $text = ilRTE::replaceLatexSpan($text);
1492  $text = $this->ui_renderer->render($this->ui_factory->legacy()->latexContent($text));
1493  }
1494 
1495  // workaround for preventing template engine
1496  // from hiding text that is enclosed
1497  // in curly brackets (e.g. "{a}")
1498  $text = str_replace(["{", "}"], ["&#123;", "&#125;"], $text);
1499  }
1500 
1501  return $text;
1502  }
1503 
1507  private function deletePostFiles(array $a_ids): void
1508  {
1509  foreach ($a_ids as $pos_id) {
1510  $forumFiles = new ilFileDataForum($this->getForumId(), $pos_id);
1511  $files = $forumFiles->getFilesOfPost();
1512  foreach ($files as $file) {
1513  $forumFiles->unlinkFile($file['name']);
1514  }
1515  }
1516  }
1517 
1518  public function getImportName(): string
1519  {
1520  return $this->import_name;
1521  }
1522 
1523  public function setImportName(string $a_import_name): void
1524  {
1525  $this->import_name = $a_import_name;
1526  }
1527 
1528  public function enableForumNotification(int $user_id): bool
1529  {
1530  if (!$this->isForumNotificationEnabled($user_id)) {
1531  /* Remove all notifications of threads that belong to the forum */
1532  $res = $this->db->queryF(
1533  '
1534  SELECT frm_notification.thread_id FROM frm_data, frm_notification, frm_threads
1535  WHERE frm_notification.user_id = %s
1536  AND frm_notification.thread_id = frm_threads.thr_pk
1537  AND frm_threads.thr_top_fk = frm_data.top_pk
1538  AND frm_data.top_frm_fk = %s
1539  GROUP BY frm_notification.thread_id',
1540  ['integer', 'integer'],
1541  [$user_id, $this->id]
1542  );
1543 
1544  if ($res->numRows() > 0) {
1545  $thread_data = [];
1546  $thread_data_types = [];
1547 
1548  $query = ' DELETE FROM frm_notification WHERE user_id = %s AND thread_id IN (';
1549  $thread_data[] = $user_id;
1550  $thread_data_types[] = 'integer';
1551 
1552  $counter = 1;
1553  while ($row = $this->db->fetchAssoc($res)) {
1554  if ($counter < $res->numRows()) {
1555  $query .= '%s, ';
1556  $thread_data[] = $row['thread_id'];
1557  $thread_data_types[] = 'integer';
1558  }
1559 
1560  if ($counter === $res->numRows()) {
1561  $query .= '%s)';
1562  $thread_data[] = $row['thread_id'];
1563  $thread_data_types[] = 'integer';
1564  }
1565  $counter++;
1566  }
1567 
1568  $this->db->manipulateF($query, $thread_data_types, $thread_data);
1569  }
1570 
1571  /* Insert forum notification */
1572  $nextId = $this->db->nextId('frm_notification');
1573  $this->db->manipulateF(
1574  'INSERT INTO frm_notification (notification_id, user_id, frm_id) VALUES(%s, %s, %s)',
1575  ['integer', 'integer', 'integer'],
1576  [$nextId, $user_id, $this->id]
1577  );
1578  }
1579 
1580  return true;
1581  }
1582 
1583  public function disableForumNotification(int $user_id): bool
1584  {
1585  $this->db->manipulateF(
1586  'DELETE FROM frm_notification WHERE user_id = %s AND frm_id = %s',
1587  ['integer', 'integer'],
1588  [$user_id, $this->id]
1589  );
1590 
1591  return true;
1592  }
1593 
1594  public function isForumNotificationEnabled(int $user_id): bool
1595  {
1596  $res = $this->db->queryF(
1597  'SELECT COUNT(*) cnt FROM frm_notification WHERE user_id = %s AND frm_id = %s',
1598  ['integer', 'integer'],
1599  [$user_id, $this->id]
1600  );
1601 
1602  if ($row = $this->db->fetchAssoc($res)) {
1603  return (int) $row['cnt'] > 0;
1604  }
1605 
1606  return false;
1607  }
1608 
1609  public function isThreadNotificationEnabled(int $user_id, int $thread_id): bool
1610  {
1611  $res = $this->db->queryF(
1612  'SELECT COUNT(*) cnt FROM frm_notification WHERE user_id = %s AND thread_id = %s',
1613  ['integer', 'integer'],
1614  [$user_id, $thread_id]
1615  );
1616 
1617  if ($row = $this->db->fetchAssoc($res)) {
1618  return (int) $row['cnt'] > 0;
1619  }
1620 
1621  return false;
1622  }
1623 
1627  public static function getSortedThreadSubjects(int $a_obj_id, int $a_sort_mode = self::SORT_DATE): array
1628  {
1629  global $DIC;
1630 
1631  $sort = match ($a_sort_mode) {
1632  self::SORT_DATE => 'thr_date',
1633  default => 'thr_subject',
1634  };
1635 
1636  $res = $DIC->database()->queryF(
1637  'SELECT thr_pk, thr_subject FROM frm_threads INNER JOIN frm_data ON top_pk = thr_top_fk WHERE top_frm_fk = %s ORDER BY %s',
1638  ['integer', 'text'],
1639  [$a_obj_id, $sort]
1640  );
1641 
1642  $threads = [];
1643  while ($row = $DIC->database()->fetchObject($res)) {
1644  $threads[(int) $row->thr_pk] = $row->thr_subject;
1645  }
1646 
1647  return $threads;
1648  }
1649 
1650  public static function _lookupObjIdForForumId(int $a_for_id): int
1651  {
1652  global $DIC;
1653 
1654  $res = $DIC->database()->queryF('SELECT top_frm_fk FROM frm_data WHERE top_pk = %s', ['integer'], [$a_for_id]);
1655  if ($row = $DIC->database()->fetchAssoc($res)) {
1656  return (int) $row['top_frm_fk'];
1657  }
1658 
1659  return 0;
1660  }
1661 
1662  public function mergeThreads(int $source_id, int $target_id): void
1663  {
1664  // selected source and target objects
1665  $sourceThread = new ilForumTopic($source_id);
1666  $targetThread = new ilForumTopic($target_id);
1667 
1668  if ($sourceThread->getForumId() !== $targetThread->getForumId()) {
1669  throw new ilException('not_allowed_to_merge_into_another_forum');
1670  }
1671 
1672  // use the 'older' thread as target
1673  if ($sourceThread->getCreateDate() > $targetThread->getCreateDate()) {
1674  $sourceThreadForMerge = $sourceThread;
1675  $targetThreadForMerge = $targetThread;
1676  } else {
1677  $sourceThreadForMerge = $targetThread;
1678  $targetThreadForMerge = $sourceThread;
1679  }
1680 
1681  $threadSubject = $targetThreadForMerge->getSubject();
1682 
1683  $targetWasClosedBeforeMerge = $targetThreadForMerge->isClosed();
1684  $sourceThreadForMerge->close();
1685 
1686  if (!$targetWasClosedBeforeMerge) {
1687  $targetThreadForMerge->close();
1688  }
1689 
1690  $allSourcePostings = $sourceThreadForMerge->getAllPostIds();
1691  $sourceThreadRootNode = $sourceThreadForMerge->getPostRootNode();
1692  $targetThreadRootNode = $targetThreadForMerge->getPostRootNode();
1693 
1694  $sourceThreadRootArray = $this->getPostNode($sourceThreadRootNode->getId());
1695 
1696  $ilAtomQuery = $this->db->buildAtomQuery();
1697  $ilAtomQuery->addTableLock('frm_posts');
1698  $ilAtomQuery->addTableLock('frm_posts_tree');
1699  $ilAtomQuery->addTableLock('frm_threads');
1700  $ilAtomQuery->addTableLock('frm_data');
1701 
1702  $ilAtomQuery->addQueryCallable(static function (ilDBInterface $ilDB) use (
1703  $targetThreadForMerge,
1704  $sourceThreadForMerge,
1705  $targetThreadRootNode,
1706  $sourceThreadRootNode,
1707  $allSourcePostings
1708  ): void {
1709  $targetRootNodeRgt = $targetThreadRootNode->getRgt();
1710  $targetRootNodeId = $targetThreadRootNode->getId();
1711 
1712  // update target root node rgt: Ignore the root node itself from the source (= -2)
1714  $targetThreadRootNode->getId(),
1715  ($targetThreadRootNode->getRgt() + $sourceThreadRootNode->getRgt() - 2)
1716  );
1717 
1718  // get source post tree and update posts tree
1719  foreach ($allSourcePostings as $pos_pk) {
1720  $post_obj = new ilForumPost($pos_pk);
1721 
1722  if ($post_obj->getId() === $sourceThreadRootNode->getId()) {
1723  // Ignore the source root node (MUST be deleted later)
1724  continue;
1725  }
1726 
1727  $tree = new ilForumPostsTree();
1728  $tree->setPosFk($pos_pk);
1729 
1730  if ($post_obj->getParentId() === $sourceThreadRootNode->getId()) {
1731  $tree->setParentPos($targetRootNodeId);
1732  } else {
1733  $tree->setParentPos($post_obj->getParentId());
1734  }
1735 
1736  $tree->setLft(($post_obj->getLft() + $targetRootNodeRgt) - 2);
1737  $tree->setRgt(($post_obj->getRgt() + $targetRootNodeRgt) - 2);
1738 
1739  $tree->setDepth($post_obj->getDepth());
1740  $tree->setTargetThreadId($targetThreadForMerge->getId());
1741  $tree->setSourceThreadId($sourceThreadForMerge->getId());
1742 
1743  $tree->merge();
1744  }
1745 
1747  $sourceThreadForMerge->getId(),
1748  $targetThreadForMerge->getId(),
1749  [$sourceThreadRootNode->getId()]
1750  );
1751  });
1752  $ilAtomQuery->run();
1753 
1754  ilForumNotification::mergeThreadNotifications($sourceThreadForMerge->getId(), $targetThreadForMerge->getId());
1755  ilObjForum::mergeForumUserRead($sourceThreadForMerge->getId(), $targetThreadForMerge->getId());
1756 
1757  $lastPostString = $targetThreadForMerge->getLastPostString() ?? '';
1758  $exp = explode('#', $lastPostString);
1759  if (array_key_exists(2, $exp)) {
1760  try {
1761  $exp[2] = $targetThreadForMerge->getLastPost()->getId();
1762  $lastPostString = implode('#', $exp);
1763  } catch (OutOfBoundsException) {
1764  $lastPostString = null;
1765  }
1766  }
1767 
1768  $frm_topic_obj = new ilForumTopic(0, false, true);
1769  $frm_topic_obj->setNumPosts($sourceThreadForMerge->getNumPosts() + $targetThreadForMerge->getNumPosts());
1770  $frm_topic_obj->setVisits($sourceThreadForMerge->getVisits() + $targetThreadForMerge->getVisits());
1771  $frm_topic_obj->setLastPostString($lastPostString);
1772  $frm_topic_obj->setSubject($threadSubject);
1773  $frm_topic_obj->setId($targetThreadForMerge->getId());
1774  $frm_topic_obj->updateMergedThread();
1775 
1776  if (!$targetWasClosedBeforeMerge) {
1777  $targetThreadForMerge->reopen();
1778  }
1779 
1780  $this->event->raise(
1781  'components/ILIAS/Forum',
1782  'mergedThreads',
1783  [
1784  'obj_id' => $this->getForumId(),
1785  'source_thread_id' => $sourceThreadForMerge->getId(),
1786  'target_thread_id' => $targetThreadForMerge->getId()
1787  ]
1788  );
1789 
1790  $this->deletePost($sourceThreadRootArray, false);
1791  }
1792 }
static _replaceMediaObjectImageSrc(string $a_text, int $a_direction=0, string $nic='')
Replaces image source from mob image urls with the mob id or replaces mob id with the correct image s...
addPostTree(int $a_tree_id, int $a_node_id=-1, string $a_date='')
getFirstPostNode(int $tree_id)
static get(string $a_var)
isForumNotificationEnabled(int $user_id)
$res
Definition: ltiservices.php:66
Global event handler.
getPostNode(int $post_id)
static mergeForumUserRead(int $merge_source_thread_id, int $merge_target_thread_id)
static array $moderators_by_ref_id_map
const IL_CAL_DATETIME
ilObjUser $user
Personal profile publishing mode of a user.
Definition: Mode.php:29
ilErrorHandling $error
getRootPostIdByThread(int $a_thread_id)
setCreateDate(?string $a_createdate)
if(! $DIC->user() ->getId()||!ilLTIConsumerAccess::hasCustomProviderCreationAccess()) $params
Definition: ltiregstart.php:31
setMDB2WhereCondition(string $query_string, array $data_type, array $data_value)
getUserStatistics(bool $post_activation_required)
const int SORT_DATE
ilSetting $settings
disableForumNotification(int $user_id)
deletePostFiles(array $a_ids)
enableNotification(int $a_user_id)
getPostDepth(int $a_node_id, int $tree_id)
setDbTable(string $dbTable)
static mergePosts(int $sourceThreadId, int $targetThreadId, array $excludedPostIds=[])
ILIAS UI Renderer $ui_renderer
moveThreads(array $thread_ids, ilObjForum $src_forum, int $target_obj_id)
setForumRefId(int $a_ref_id)
convertDate(string $date)
string $mdb2Query
static _deleteReadEntries(int $a_post_id)
static replaceLatexSpan(string $text)
Replace the latex delimiters used by the rich text editor Unfortunately these can&#39;t be processed by M...
Class ilForumDraftHistory.
static getInstance(int $a_obj_id=0)
array $mdb2DataValue
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
setImportName(?string $a_import_name)
mergeThreads(int $source_id, int $target_id)
string $className
insertPostNode(int $a_node_id, int $a_parent_id, int $tree_id, string $a_date='')
insert node under parent node
getNumberOfPublishedUserPostings(int $usr_id, bool $post_activation_required)
string $replQuote1
fetchPostNodeData(stdClass $a_row)
const int DEFAULT_PAGE_HITS
static _exists(int $id, bool $reference=false, ?string $type=null)
checks if an object exists in object_data
ilLanguage $lng
string $txtQuote2
static getInstanceFromArray(array $record)
static updateTargetRootRgt(int $root_node_id, int $rgt)
static _lookupTitle(int $obj_id)
const int SORT_TITLE
updateVisits(int $ID)
readonly ilAppEventHandler $event
prepareText(string $text, int $edit=0, string $quote_user='', string $type='')
static _lookupObjIdForForumId(int $a_for_id)
const NEWS_NOTICE
static getInstanceByRefId(int $ref_id, bool $stop_on_error=true)
get an instance of an Ilias object by reference id
countUserArticles(int $a_user_id)
ILIAS UI Factory $ui_factory
global $DIC
Definition: shib_login.php:26
bool $notify
string $txtQuote1
array $mdb2DataType
static getEmptyInstance()
getSubPathIdsForNode(ilForumPost $post)
getAllThreads(int $a_topic_id, array $params=[], int $limit=0, int $offset=0)
static _exists(int $id, bool $reference=false, ?string $type=null)
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
static _removeUsage(int $a_mob_id, string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
Remove usage of mob in another container.
enableForumNotification(int $user_id)
generateThread(ilForumTopic $thread, string $message, bool $notify, bool $notify_posts, bool $status=true, bool $withFirstVisibleEntry=true)
ilDBInterface $db
setPageHits(int $pageHits)
static _isModerator(int $a_ref_id, int $a_usr_id)
A news item can be created by different sources.
int $pageHits
countActiveUserArticles(int $a_user_id)
static getFirstNewsIdForContext(int $a_context_obj_id, string $a_context_obj_type, int $a_context_sub_obj_id=0, string $a_context_sub_obj_type="")
Get first new id of news set related to a certain context.
deletePost($postIdOrRecord, bool $raiseEvents=true)
static formatDate(ilDateTime $date, bool $a_skip_day=false, bool $a_include_wd=false, bool $include_seconds=false, ?ilObjUser $user=null,)
postCensorship(ilObjForum $forum, string $message, int $pos_pk, int $cens=0)
static _getModerators(int $a_ref_id)
setImportName(string $a_import_name)
$message
Definition: xapiexit.php:31
const NEWS_USERS
static getSortedThreadSubjects(int $a_obj_id, int $a_sort_mode=self::SORT_DATE)
string $replQuote2
setForumId(int $a_obj_id)
getMDB2DataValue()
$post
Definition: ltitoken.php:46
deletePostTree(array $a_node)
static mergeThreadNotifications($merge_source_thread_id, $merge_target_thread_id)
static set(string $a_var, $a_val)
Set a value.
string $dbTable
isThreadNotificationEnabled(int $user_id, int $thread_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
string $import_name