1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8//this script may only be included - so its better to die if called directly.
9if (strpos($_SERVER["SCRIPT_NAME"], basename(__FILE__)) !== false) {
10	header("location: index.php");
11	exit;
12}
13
14// A library to handle comments on object (notes, articles, etc)
15/**
16 *
17 */
18class Comments extends TikiLib
19{
20	public $time_control = 0;
21	private $extras = true;
22
23	/* Functions for the forums */
24	function report_post($forumId, $parentId, $threadId, $user, $reason = '')
25	{
26		$reported = $this->table('tiki_forums_reported');
27
28		$data = [
29			'forumId' => $forumId,
30			'parentId' => $parentId,
31			'threadId' => $threadId,
32			'user' => $user,
33		];
34		$reported->delete(['threadId' => $data['threadId']]);
35
36		$reported->insert(array_merge($data, ['timestamp' => $this->now, 'reason' => $reason]));
37	}
38
39	function list_reported($forumId, $offset, $maxRecords, $sort_mode, $find)
40	{
41
42		if ($find) {
43			$findesc = '%' . $find . '%';
44			$mid = " and (`reason` like ? or `user` like ? or tfr.`threadId` = ?)";
45			$bindvars = [$forumId, $findesc, $findesc, $find];
46		} else {
47			$mid = "";
48			$bindvars = [$forumId];
49		}
50
51		$query = "select `forumId`, tfr.`threadId`, tfr.`parentId`,
52			tfr.`reason`, tfr.`user`, `title`, SUBSTRING(`data` FROM 1 FOR 100) as `snippet` from `tiki_forums_reported`
53				tfr,  `tiki_comments` tc where tfr.`threadId` = tc.`threadId`
54				and `forumId`=? $mid order by " .
55				$this->convertSortMode($sort_mode);
56		$query_cant = "select count(*) from `tiki_forums_reported` tfr,
57			`tiki_comments` tc where tfr.`threadId` = tc.`threadId` and
58				`forumId`=? $mid";
59		$ret = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
60		$cant = $this->getOne($query_cant, $bindvars);
61
62		$retval = [];
63		$retval["data"] = $ret;
64		$retval["cant"] = $cant;
65		return $retval;
66	}
67
68	function is_reported($threadId)
69	{
70		return $this->table('tiki_forums_reported')->fetchCount(['threadId' => (int) $threadId]);
71	}
72
73	function remove_reported($threadId)
74	{
75		$this->table('tiki_forums_reported')->delete(['threadId' => (int) $threadId]);
76	}
77
78	function get_num_reported($forumId)
79	{
80		return $this->getOne("select count(*) from `tiki_forums_reported` tfr, `tiki_comments` tc where tfr.`threadId` = tc.`threadId` and `forumId`=?", [(int) $forumId]);
81	}
82
83	function mark_comment($user, $forumId, $threadId)
84	{
85		if (! $user) {
86			return false;
87		}
88
89		$reads = $this->table('tiki_forum_reads');
90
91		$reads->delete(['user' => $user, 'threadId' => $threadId]);
92		$reads->insert(
93			[
94				'user' => $user,
95				'threadId' => (int) $threadId,
96				'forumId' => (int) $forumId,
97				'timestamp' => $this->now,
98			]
99		);
100	}
101
102	function unmark_comment($user, $forumId, $threadId)
103	{
104		$this->table('tiki_forum_reads')->delete(['user' => $user, 'threadId' => (int) $threadId]);
105	}
106
107	function is_marked($threadId)
108	{
109		global $user;
110
111		if (! $user) {
112			return false;
113		}
114
115		return $this->table('tiki_forum_reads')->fetchCount(['user' => $user, 'threadId' => $threadId]);
116	}
117
118	/* Add an attachment to a post in a forum */
119	function add_thread_attachment($forum_info, $threadId, &$errors, $name, $type, $size, $inbound_mail = 0, $qId = 0, $fp = '', $data = '')
120	{
121		$perms = Perms::get(['type' => 'thread', 'object' => $threadId]);
122		if (! ($forum_info['att'] == 'att_all'
123				|| ($forum_info['att'] == 'att_admin' && $perms->admin_forum == 'y')
124				|| ($forum_info['att'] == 'att_perm' && $perms->forum_attach == 'y'))) {
125			$smarty = TikiLib::lib('smarty');
126			$smarty->assign('errortype', 401);
127			$smarty->assign('msg', tra('Permission denied'));
128			$smarty->display("error.tpl");
129			die;
130		}
131		if (! empty($prefs['forum_match_regex']) && ! preg_match($prefs['forum_match_regex'], $name)) {
132			$errors[] = tra('Invalid filename (using filters for filenames)');
133			return 0;
134		}
135		if ($size > $forum_info['att_max_size'] && ! $inbound_mail) {
136			$errors[] = tra('Cannot upload this file - maximum upload size exceeded');
137			return 0;
138		}
139		$fhash = '';
140		if ($forum_info['att_store'] == 'dir') {
141			$fhash = md5(uniqid('.'));
142			// Just in case the directory doesn't have the trailing slash
143			if (substr($forum_info['att_store_dir'], strlen($forum_info['att_store_dir']) - 1, 1) == '\\') {
144				$forum_info['att_store_dir'] = substr($forum_info['att_store_dir'], 0, strlen($forum_info['att_store_dir']) - 1) . '/';
145			} elseif (substr($forum_info['att_store_dir'], strlen($forum_info['att_store_dir']) - 1, 1) != '/') {
146				$forum_info['att_store_dir'] .= '/';
147			}
148
149			$filename = $forum_info['att_store_dir'] . $fhash;
150			@$fw = fopen($filename, "wb");
151			if (! $fw && ! $inbound_mail) {
152				$errors[] = tra('Cannot write to this file:') . ' ' . $forum_info['att_store_dir'] . $fhash;
153				return 0;
154			}
155		}
156		$filegallib = TikiLib::lib('filegal');
157		if ($fp) {
158			while (! feof($fp)) {
159				if ($forum_info['att_store'] == 'db') {
160					$data .= fread($fp, 8192 * 16);
161				} else {
162					$data = fread($fp, 8192 * 16);
163					fwrite($fw, $data);
164				}
165			}
166			fclose($fp);
167			if ($forum_info['att_store'] == 'db') {
168				try {
169					$filegallib->assertUploadedContentIsSafe($data, $name);
170				} catch (Exception $e) {
171					$errors[] = $e->getMessage();
172					return 0;
173				}
174			} else {
175				try {
176					$filegallib->assertUploadedFileIsSafe($filename, $name);
177				} catch (Exception $e) {
178					$errors[] = $e->getMessage();
179					fclose($fw);
180					unlink($filename);
181					return 0;
182				}
183			}
184		} else {
185			if ($forum_info['att_store'] == 'dir') {
186				try {
187					$filegallib->assertUploadedContentIsSafe($data, $name);
188				} catch (Exception $e) {
189					$errors[] = $e->getMessage();
190					return 0;
191				}
192				fwrite($fw, $data);
193			}
194		}
195
196		if ($forum_info['att_store'] == 'dir') {
197			fclose($fw);
198			$data = '';
199		}
200
201		return $this->forum_attach_file($threadId, $qId, $name, $type, $size, $data, $fhash, $forum_info['att_store_dir'], $forum_info['forumId']);
202	}
203
204	function forum_attach_file($threadId, $qId, $name, $type, $size, $data, $fhash, $dir, $forumId)
205	{
206		if ($fhash) {
207			// Do not store data if we have a file
208			$data = '';
209		}
210
211		$id = $this->table('tiki_forum_attachments')->insert(
212			[
213				'threadId' => $threadId,
214				'qId' => $qId,
215				'filename' => $name,
216				'filetype' => $type,
217				'filesize' => $size,
218				'data' => $data,
219				'path' => $fhash,
220				'created' => $this->now,
221				'dir' => $dir,
222				'forumId' => $forumId,
223			]
224		);
225		return $id;
226		// Now the file is attached and we can proceed.
227	}
228
229	function get_thread_attachments($threadId, $qId)
230	{
231		$conditions = [];
232
233		if ($threadId) {
234			$conditions['threadId'] = $threadId;
235		} else {
236			$conditions['qId'] = $qId;
237		}
238
239		$attachments = $this->table('tiki_forum_attachments');
240		return $attachments->fetchAll($attachments->all(), $conditions);
241	}
242
243	function get_thread_attachment($attId)
244	{
245		$attachments = $this->table('tiki_forum_attachments');
246		$res = $attachments->fetchAll($attachments->all(), ['attId' => $attId]);
247		if (empty($res[0])) {
248			return $res;
249		}
250
251		$res[0]['forum_info'] = $this->get_forum($res[0]['forumId']);
252		return $res[0];
253	}
254
255	public function list_all_attachements($offset = 0, $maxRecords = -1, $sort_mode = 'attId_asc', $find = '')
256	{
257		$attachments = $this->table('tiki_forum_attachments');
258
259		$order = $attachments->sortMode($sort_mode);
260		$fields = ['attId', 'threadId', 'qId', 'forumId', 'filename', 'filetype', 'filesize', 'data', 'dir', 'created', 'path'];
261		$conditions = [];
262		$data = [];
263
264		if ($find) {
265			$conditions['filename'] = $attachments->like("%$find%");
266		}
267
268		return [
269			'data' => $attachments->fetchAll($fields, $conditions, $maxRecords, $offset, $order),
270			'cant' => $attachments->fetchCount($conditions),
271		];
272	}
273
274	function remove_thread_attachment($attId)
275	{
276		$att = $this->get_thread_attachment($attId);
277		// Check if the attachment is stored in the filesystem and don't do anything by accident in root dir
278		if (empty($att['data']) && ! empty($att['path']) && ! empty($att['forum_info']['att_store_dir'])) {
279			unlink($att['forum_info']['att_store_dir'] . $att['path']);
280		}
281		$this->table('tiki_forum_attachments')->delete(['attId' => $attId]);
282	}
283
284	function parse_output(&$obj, &$parts, $i)
285	{
286		if (! empty($obj->parts)) {
287			$temp_max = count($obj->parts);
288			for ($i = 0; $i < $temp_max; $i++) {
289				$this->parse_output($obj->parts[$i], $parts, $i);
290			}
291		} else {
292			$ctype = $obj->ctype_primary . '/' . $obj->ctype_secondary;
293
294			switch ($ctype) {
295				case 'text/plain':
296				case 'TEXT/PLAIN':
297					if (! empty($obj->disposition)and $obj->disposition == 'attachment') {
298						$names = explode(';', $obj->headers["content-disposition"]);
299
300						$names = explode('=', $names[1]);
301						$aux['name'] = $names[1];
302						$aux['content-type'] = $obj->headers["content-type"];
303						$aux['part'] = $i;
304						$parts['attachments'][] = $aux;
305					} else {
306						if (isset($obj->ctype_parameters) && ($obj->ctype_parameters['charset'] == "iso-8859-1" || $obj->ctype_parameters['charset'] == "ISO-8859-1")) {
307							$parts['text'][] = utf8_encode($obj->body);
308						} else {
309							$parts['text'][] = $obj->body;
310						}
311					}
312
313					break;
314
315				case 'text/html':
316				case 'TEXT/HTML':
317					if (! empty($obj->disposition)and $obj->disposition == 'attachment') {
318						$names = explode(';', $obj->headers["content-disposition"]);
319
320						$names = explode('=', $names[1]);
321						$aux['name'] = $names[1];
322						$aux['content-type'] = $obj->headers["content-type"];
323						$aux['part'] = $i;
324						$parts['attachments'][] = $aux;
325					} else {
326						$parts['html'][] = $obj->body;
327					}
328
329					break;
330
331				default:
332					$names = explode(';', $obj->headers["content-disposition"]);
333
334					$names = explode('=', $names[1]);
335					$aux['name'] = $names[1];
336					$aux['content-type'] = $obj->headers["content-type"];
337					$aux['part'] = $i;
338					$parts['attachments'][] = $aux;
339			}
340		}
341	}
342
343	function process_inbound_mail($forumId)
344	{
345		global $prefs, $user;
346		require_once("lib/webmail/net_pop3.php");
347		require_once("lib/mail/mimelib.php");
348
349		$info = $this->get_forum($forumId);
350
351		// for any reason my sybase test machine adds a space to
352		// the inbound_pop_server field in the table.
353		$info["inbound_pop_server"] = trim($info["inbound_pop_server"]);
354
355		if (! $info["inbound_pop_server"] || empty($info["inbound_pop_server"])) {
356			return;
357		}
358
359		$pop3 = new Net_POP3();
360		$pop3->connect($info["inbound_pop_server"]);
361		$pop3->login($info["inbound_pop_user"], $info["inbound_pop_password"]);
362
363		if (! $pop3) {
364			return;
365		}
366
367		$mailSum = $pop3->numMsg();
368
369		//we don't want the operation to time out... this would result in the same messages being imported over and over...
370		//(messages are only removed from the pop server on a gracefull connection termination... ie .not php or webserver a timeout)
371		//$maximport should be in a admin config screen, but I don't know how to do that yet.
372		$maxImport = 10;
373		if ($mailSum > $maxImport) {
374			$mailSum = $maxImport;
375		}
376
377		for ($i = 1; $i <= $mailSum; $i++) {
378			//echo 'loop ' . $i;
379
380			$aux = $pop3->getParsedHeaders($i);
381
382			// If the mail came from Tiki, we don't need to add it again
383			if (isset($aux['X-Tiki']) && $aux['X-Tiki'] == 'yes') {
384				$pop3->deleteMsg($i);
385				continue;
386			}
387
388			// If the connection is done, or the mail has an error, or whatever,
389			// we try to delete the current mail (because something is wrong with it)
390			// and continue on. --rlpowell
391			if ($aux == false) {
392				$pop3->deleteMsg($i);
393				continue;
394			}
395
396			if (! isset($aux['From'])) {
397				if (isset($aux['Return-path'])) {
398					$aux['From'] = $aux['Return-path'];
399				} else {
400					$aux['From'] = "";
401					$aux['Return-path'] = "";
402				}
403			}
404
405			//try to get the date from the email:
406			$postDate = strtotime($aux['Date']);
407			if ($postDate == false) {
408				$postDate = $this->now;
409			}
410
411			//save the original email address, if we don't get a user match, then we
412			//can at least give some info about the poster.
413			$original_email = $aux["From"];
414
415			//fix mailman addresses, or there is no chance to get a match
416			$aux["From"] = str_replace(' at ', '@', $original_email);
417
418
419			preg_match('/<?([-!#$%&\'*+\.\/0-9=?A-Z^_`a-z{|}~]+@[-!#$%&\'*+\/0-9=?A-Z^_`a-z{|}~]+\.[-!#$%&\'*+\.\/0-9=?A-Z^_`a-z{|}~]+)>?/', $aux["From"], $mail);
420
421			// should we throw out emails w/ invalid (possibly obfusicated) email addressses?
422			//this should be an admin option, but I don't know how to put it there yet.
423			$throwOutInvalidEmails = false;
424			if (! array_key_exists(1, $mail)) {
425				if ($throwOutInvalidEmails) {
426					continue;
427				}
428			}
429
430			$email = $mail[1];
431			// Determine user from email
432			$userName = $this->table('users_users')->fetchOne('login', ['email' => $email]);
433
434			//use anonomus name feature if we don't have a real name
435			if (! $userName) {
436				$anonName = $original_email;
437			}
438			// Check permissions
439			if ($prefs['forum_inbound_mail_ignores_perms'] !== 'y') {
440				 // store currently logged-in user to restore later as setting the Perms_Context overwrites the global $user
441				$currentUser = $user;
442				// N.B. Perms_Context needs to be assigned to a variable or it gets destructed immediately and does nothing
443				/** @noinspection PhpUnusedLocalVariableInspection */
444				$permissionContext = new Perms_Context($userName ? $userName : '');
445				$forumperms = Perms::get(['type' => 'forum', 'object' => $forumId]);
446
447				if (! $forumperms->forum_post) {
448					// premission refused - TODO move this message to the moderated queue if there is one
449					continue;
450				}
451			}
452
453			$full = $pop3->getMsg($i);
454
455			$mimelib = new mime();
456			$output = $mimelib->decode($full);
457			$body = '';
458
459			if ($output['type'] == 'multipart/report') {			// mimelib doesn't seem to parse error reports properly
460				$pop3->deleteMsg($i);								// and we almost certainly don't want them in the forum
461				continue;											// TODO also move it to the moderated queue
462			}
463
464			require_once('lib/htmlpurifier_tiki/HTMLPurifier.tiki.php');
465
466			if ($prefs['feature_forum_parse'] === 'y' && $prefs['forum_inbound_mail_parse_html'] === 'y') {
467				$body = $mimelib->getPartBody($output, 'html');
468
469				if ($body) {
470					// on some systems HTMLPurifier fails with smart quotes in the html
471					$body = $mimelib->cleanQuotes($body);
472
473					// some emails have invalid font and span tags that create incorrect purifying of lists
474					$body = preg_replace_callback('/\<(ul|ol).*\>(.*)\<\/(ul|ol)\>/Umis', [$this, 'process_inbound_mail_cleanlists'], $body);
475
476					// Clean the string using HTML Purifier next
477					$body = HTMLPurifier($body);
478
479					// html emails require some speciaal handling
480					$body = preg_replace('/--(.*)--/', '~np~--$1--~/np~', $body);	// disable strikethough syntax
481					$body = preg_replace('/\{(.*)\}/', '~np~{$1}~/np~', $body);		// disable plugin type things
482
483					// special handling for MS links which contain underline tags in the label which wiki doesn't like
484					$body = preg_replace(
485						'/(\<a .*\>)\<font .*\>\<u\>(.*)\<\/u\>\<\/font\>\<\/a\>/Umis',
486						'$1$2</a>',
487						$body
488					);
489
490					$body = str_replace("<br /><br />", "<br /><br /><br />", $body);	// double linebreaks seem to work better as three?
491					$body = TikiLib::lib('edit')->parseToWiki($body);
492					$body = str_replace("\n\n", "\n", $body);							// for some reason emails seem to get line feeds quadrupled
493					$body = preg_replace('/\[\[(.*?)\]\]/', '[~np~~/np~[$1]]', $body);		// links surrounded by [square brackets] need help
494				}
495			}
496
497			if (! $body) {
498				$body = $mimelib->getPartBody($output, 'text');
499
500				if (empty($body)) {	// no text part so look for html
501					$body = $mimelib->getPartBody($output, 'html');
502					$body = HTMLPurifier($body);
503					$body = $this->htmldecode(strip_tags($body));
504					$body = str_replace("\n\n", "\n", $body);	// and again
505					$body = str_replace("\n\n", "\n", $body);
506				}
507
508				if ($prefs['feature_forum_parse'] === 'y') {
509					$body = preg_replace('/--(.*)--/', '~np~--$1--~/np~', $body);    // disable strikethough if...
510					$body = preg_replace('/\{(.*)\}/', '~np~\{$1\}~/np~', $body);	// disable plugin type things
511				}
512				$body = $mimelib->cleanQuotes($body);
513			}
514
515			if (! empty($info['outbound_mails_reply_link']) && $info['outbound_mails_reply_link'] === 'y') {
516				$body = preg_replace('/^.*?Reply Link\: \<[^\>]*\>.*\r?\n/m', '', $body);		// remove previous reply links to reduce clutter and confusion
517
518				// remove "empty" lines at the end
519				$lines = preg_split("/(\r\n|\n|\r)/", $body);
520				$body = '';
521				$len = count($lines) - 1;
522				$found = false;
523				for ($line = $len; $line >= 0; $line--) {
524					if ($found || ! preg_match('/^\s*\>*\s*[\-]*\s*$/', $lines[$line])) {
525						$body = "{$lines[$line]}\r\n$body";
526						$found = true;
527					}
528				}
529			}
530
531			// Remove 're:' and [forum]. -rlpowell
532			$title = trim(
533				preg_replace(
534					"/[rR][eE]:/",
535					"",
536					preg_replace(
537						"/\[[-A-Za-z _:]*\]/",
538						"",
539						$output['header']['subject']
540					)
541				)
542			);
543			$title = $mimelib->cleanQuotes($title);
544
545			// trim off < and > from message-id
546			$message_id = substr($output['header']["message-id"], 1, strlen($output['header']["message-id"]) - 2);
547
548			if (isset($output['header']["in-reply-to"])) {
549				$in_reply_to = substr($output['header']["in-reply-to"], 1, strlen($output['header']["in-reply-to"]) - 2);
550			} else {
551				$in_reply_to = '';
552			}
553
554			// Determine if the thread already exists first by looking for a mail this is a reply to.
555			if (! empty($in_reply_to)) {
556				$parentId = $this->table('tiki_comments')->fetchOne(
557					'threadId',
558					['object' => $forumId, 'objectType' => 'forum', 'message_id' => $in_reply_to]
559				);
560			} else {
561				$parentId = 0;
562			}
563
564			// if not, check if there's a topic with exactly this title
565			if (! $parentId) {
566				$parentId = $this->table('tiki_comments')->fetchOne(
567					'threadId',
568					['object' => $forumId, 'objectType' => 'forum', 'parentId' => 0, 'title' => $title]
569				);
570			}
571
572			if (! $parentId) {
573				// create a thread to discuss a wiki page if the feature is on AND the page exists
574				if ($prefs['feature_wiki_discuss'] === 'y' && TikiLib::lib('tiki')->page_exists($title)) {
575					// No thread already; create it.
576					$temp_msid = '';
577
578					$parentId = $this->post_new_comment(
579						'forum:' . $forumId,
580						0,
581						$userName,
582						$title,
583						sprintf(tra("Use this thread to discuss the %s page."), "(($title))"),
584						$temp_msid,
585						$in_reply_to
586					);
587
588					$this->register_forum_post($forumId, 0);
589
590					// First post is in reply to this one
591					$in_reply_to = $temp_msid;
592				} else {
593					$parentId = 0;
594				}
595			}
596
597			// post
598			$threadId = $this->post_new_comment(
599				'forum:' . $forumId,
600				$parentId,
601				$userName,
602				$title,
603				$body,
604				$message_id,
605				$in_reply_to,
606				'n',
607				'',
608				'',
609				'',
610				$anonName,
611				$postDate
612			);
613
614			$this->register_forum_post($forumId, $parentId);
615
616			// Process attachments
617			if (array_key_exists('parts', $output) && count($output['parts']) > 1) {
618				$forum_info = $this->get_forum($forumId);
619				if ($forum_info['att'] != 'att_no') {
620					$errors = [];
621					foreach ($output['parts'] as $part) {
622						if (array_key_exists('disposition', $part)) {
623							if ($part['disposition'] == 'attachment') {
624								if (! empty($part['d_parameters']['filename'])) {
625									$part_name = $part['d_parameters']['filename'];
626								} elseif (preg_match('/filename=([^;]*)/', $part['d_parameters']['atend'], $mm)) {		// not sure what this is but it seems to have the filename in it
627									$part_name = $mm[1];
628								} else {
629									$part_name = "Unnamed File";
630								}
631								$this->add_thread_attachment($forum_info, $threadId, $errors, $part_name, $part['type'], strlen($part['body']), 1, '', '', $part['body']);
632							} elseif ($part['disposition'] == 'inline') {
633								if (! empty($part['parts'])) {
634									foreach ($part['parts'] as $p) {
635										$this->add_thread_attachment($forum_info, $threadId, $errors, '-', $p['type'], strlen($p['body']), 1, '', '', $p['body']);
636									}
637								} elseif (! empty($part['body'])) {
638									$this->add_thread_attachment($forum_info, $threadId, $errors, '-', $part['type'], strlen($part['body']), 1, '', '', $part['body']);
639								}
640							}
641						}
642					}
643				}
644			}
645
646			// Deal with mail notifications.
647			if (array_key_exists('outbound_mails_reply_link', $info) && $info['outbound_mails_for_inbound_mails'] == 'y') {
648				include_once('lib/notifications/notificationemaillib.php');
649				sendForumEmailNotification(
650					'forum_post_thread',
651					$info['forumId'],
652					$info,
653					$title,
654					$body,
655					$userName,
656					$title,
657					$message_id,
658					$in_reply_to,
659					$threadId,
660					$parentId
661				);
662			}
663			$pop3->deleteMsg($i);
664		}
665		$pop3->disconnect();
666
667		if (! empty($currentUser)) {
668			new Perms_Context($currentUser);    // restore current user's perms
669		}
670	}
671
672	/** Removes font and span tags from lists - should be only ones outside <li> elements but this currently removes all TODO?
673	 * @param $matches array from preg_replace_callback
674	 * @return string html list definition
675	 */
676	private function process_inbound_mail_cleanlists($matches)
677	{
678		return '<' . $matches[1] . '>' .
679				preg_replace('/\<\/?(?:font|span)[^>]*\>/Umis', '', $matches[2]) .
680				'</' . $matches[3] . '>';
681	}
682
683	/* queue management */
684	function replace_queue(
685		$qId,
686		$forumId,
687		$object,
688		$parentId,
689		$user,
690		$title,
691		$data,
692		$type = 'n',
693		$topic_smiley = '',
694		$summary = '',
695		$topic_title = '',
696		$in_reply_to = '',
697		$anonymous_name = '',
698		$tags = '',
699		$email = '',
700		$threadId = 0
701	) {
702
703		// timestamp
704		if ($threadId) {
705			$timestamp = (int) $this->table('tiki_comments')->fetchOne('commentDate', ['threadId' => $threadId]);
706		} else {
707			$timestamp = (int) $this->now;
708		}
709
710		$hash2 = md5($title . $data);
711
712		$queue = $this->table('tiki_forums_queue');
713
714		if ($qId == 0 && $queue->fetchCount(['hash' => $hash2])) {
715			return false;
716		}
717		if (! $user && $anonymous_name) {
718			$user = $anonymous_name;
719		}
720
721		$data = [
722			'object' => $object,
723			'parentId' => $parentId,
724			'user' => $user,
725			'title' => $title,
726			'data' => $data,
727			'forumId' => $forumId,
728			'type' => $type,
729			'hash' => $hash2,
730			'topic_title' => $topic_title,
731			'topic_smiley' => $topic_smiley,
732			'summary' => $summary,
733			'timestamp' => $timestamp,
734			'in_reply_to' => $in_reply_to,
735			'tags' => $tags,
736			'email' => $email
737		];
738
739		if ($qId) {
740			unset($data['timestamp']);
741
742			$queue->update($data, ['qId' => $qId]);
743
744			return $qId;
745		} else {
746			if ($threadId) {
747				// Existing thread being updated so delete previous queue before adding new one
748				if ($toDelete = TikiLib::lib('attribute')->get_attribute('forum post', $threadId, 'tiki.forumpost.queueid')) {
749					$this->remove_queued($toDelete);
750				}
751			}
752			$qId = $queue->insert($data);
753		}
754
755		if ($qId && $threadId) {
756			TikiLib::lib('attribute')->set_attribute('forum post', $threadId, 'tiki.forumpost.queueid', $qId);
757		}
758
759		return $qId;
760	}
761
762	function get_num_queued($object)
763	{
764		return $this->table('tiki_forums_queue')->fetchCount(['object' => $object]);
765	}
766
767	function list_forum_queue($object, $offset, $maxRecords, $sort_mode, $find)
768	{
769		$queue = $this->table('tiki_forums_queue');
770
771		$conditions = [
772			'object' => $object,
773		];
774
775		if ($find) {
776			$conditions['search'] = $queue->findIn($find, ['title', 'data']);
777		}
778
779		$ret = $queue->fetchAll($queue->all(), $conditions, $maxRecords, $offset, $queue->sortMode($sort_mode));
780		$cant = $queue->fetchCount($conditions);
781
782		foreach ($ret as &$res) {
783			$res['parsed'] = $this->parse_comment_data($res['data']);
784
785			$res['attachments'] = $this->get_thread_attachments(0, $res['qId']);
786		}
787
788		return [
789			'data' => $ret,
790			'cant' => $cant,
791		];
792	}
793
794	function queue_get($qId)
795	{
796		$res = $this->table('tiki_forums_queue')->fetchFullRow(['qId' => $qId]);
797		$res['attchments'] = $this->get_thread_attachments(0, $qId);
798
799		return $res;
800	}
801
802	function remove_queued($qId)
803	{
804		$this->table('tiki_object_attributes')->delete(['attribute' => 'tiki.forumpost.queueid', 'value' => $qId]);
805		$this->table('tiki_forums_queue')->delete(['qId' => $qId]);
806		$this->table('tiki_forum_attachments')->delete(['qId' => $qId]);
807	}
808
809	//Approve queued message -> post as new comment
810	function approve_queued($qId)
811	{
812		global $prefs;
813		$userlib = TikiLib::lib('user');
814		$tikilib = TikiLib::lib('tiki');
815		$info = $this->queue_get($qId);
816
817		$message_id = '';
818		if ($userlib->user_exists($info['user'])) {
819			$u = $w = $info['user'];
820			$a = '';
821		} else {
822			$u = '';
823			$a = $info['user'];
824			$w = $a . ' ' . tra('(not registered)', $prefs['site_language']);
825		}
826
827		$postToEdit = TikiLib::lib('attribute')->find_objects_with('tiki.forumpost.queueid', $qId);
828		if (! empty($postToEdit[0]['itemId'])) {
829			$threadId = $postToEdit[0]['itemId'];
830			$this->update_comment(
831				$threadId,
832				$info['title'],
833				'',
834				$info['data'],
835				$info['type'],
836				$info['summary'],
837				$info['topic_smiley'],
838				'forum:' . $info['forumId']
839			);
840		} else {
841			$threadId = $this->post_new_comment(
842				'forum:' . $info['forumId'],
843				$info['parentId'],
844				$u,
845				$info['title'],
846				$info['data'],
847				$message_id,
848				$info['in_reply_to'],
849				$info['type'],
850				$info['summary'],
851				$info['topic_smiley'],
852				'',
853				$a
854			);
855		}
856		if (! $threadId) {
857			return null;
858		}
859		// Deal with mail notifications
860		include_once('lib/notifications/notificationemaillib.php');
861		$forum_info = $this->get_forum($info['forumId']);
862		sendForumEmailNotification(
863			empty($info['in_reply_to']) ? 'forum_post_topic' : 'forum_post_thread',
864			$info['forumId'],
865			$forum_info,
866			$info['title'],
867			$info['data'],
868			$info['user'],
869			$info['title'],
870			$message_id,
871			$info['in_reply_to'],
872			$threadId,
873			isset($info['parentId']) ? $info['parentId'] : 0
874		);
875
876		if ($info['email']) {
877			$tikilib->add_user_watch(
878				$w,
879				'forum_post_thread',
880				$threadId,
881				'forum topic',
882				'' . ':' . $info['title'],
883				'tiki-view_forum_thread.php?comments_parentId=' . $threadId,
884				$info['email']
885			);
886		}
887		if ($info['tags']) {
888			$cat_type = 'forum post';
889			$cat_objid = $threadId;
890			$cat_desc = substr($info['data'], 0, 200);
891			$cat_name = $info['title'];
892			$cat_href = 'tiki-view_forum_thread.php?comments_parentId=' . $threadId;
893			$_REQUEST['freetag_string'] = $info['tags'];
894			include('freetag_apply.php');
895		}
896
897		$this->table('tiki_forum_attachments')->update(['threadId' => $threadId, 'qId' => 0], ['qId' => $qId]);
898		$this->remove_queued($qId);
899
900		return $threadId;
901	}
902
903	function get_forum_topics(
904		$forumId,
905		$offset = 0,
906		$max = -1,
907		$sort_mode = 'commentDate_asc',
908		$include_archived = false,
909		$who = '',
910		$type = '',
911		$reply_state = '',
912		$forum_info = ''
913	) {
914
915		$info = $this->build_forum_query($forumId, $offset, $max, $sort_mode, $include_archived, $who, $type, $reply_state, $forum_info);
916
917		$query = "select a.`threadId`,a.`object`,a.`objectType`,a.`parentId`,
918			a.`userName`,a.`commentDate`,a.`hits`,a.`type`,a.`points`,
919			a.`votes`,a.`average`,a.`title`,a.`data`,a.`hash`,a.`user_ip`,
920			a.`summary`,a.`smiley`,a.`message_id`,a.`in_reply_to`,a.`comment_rating`,a.`locked`, ";
921		$query .= $info['query'];
922
923		$ret = $this->fetchAll($query, $info['bindvars'], $max, $offset);
924		$ret = $this->filter_topic_perms($ret);
925
926		foreach ($ret as &$res) {
927			$tid = $res['threadId'];
928			if ($res["lastPost"] != $res["commentDate"]) {
929				// last post data is for tiki-view_forum.php.
930				// you can see the title and author of last post
931				$query = "select * from `tiki_comments`
932					where `parentId` = ? and `commentDate` = ?
933					order by `threadId` desc";
934				$r2 = $this->query($query, [$tid, $res['lastPost']]);
935				$res['lastPostData'] = $r2->fetchRow();
936			}
937
938			// Has the user read it?
939			$res['is_marked'] = $this->is_marked($tid);
940		}
941
942		return $ret;
943	}
944
945	function count_forum_topics(
946		$forumId,
947		$offset = 0,
948		$max = -1,
949		$sort_mode = 'commentDate_asc',
950		$include_archived = false,
951		$who = '',
952		$type = '',
953		$reply_state = ''
954	) {
955
956		$info = $this->build_forum_query($forumId, $offset, $max, $sort_mode, $include_archived, $who, $type, $reply_state);
957
958		$query = "SELECT COUNT(*) FROM (SELECT `a`.`threadId`, {$info['query']}) a";
959
960		return $this->getOne($query, $info['bindvars']);
961	}
962
963	private function filter_topic_perms($topics) {
964		$topic_ids = array_map(function($row){
965			return $row['parentId'] > 0 ? $row['parentId'] : $row['threadId'];
966		}, $topics);
967		$topic_ids = array_unique($topic_ids);
968
969		Perms::bulk(['type' => 'thread'], 'object', $topic_ids);
970		$ret = [];
971		foreach ($topics as $row) {
972			$topic_id = $row['parentId'] > 0 ? $row['parentId'] : $row['threadId'];
973			$perms = Perms::get(['type' => 'thread', 'object' => $topic_id]);
974			if ($perms->forum_read) {
975				$ret[] = $row;
976			}
977		}
978
979		return $ret;
980	}
981
982	private function build_forum_query(
983		$forumId,
984		$offset,
985		$max,
986		$sort_mode,
987		$include_archived,
988		$who,
989		$type,
990		$reply_state,
991		$forum_info = ''
992	) {
993
994		if ($sort_mode == 'points_asc') {
995			$sort_mode = 'average_asc';
996		}
997		if ($this->time_control) {
998			$limit = time() - $this->time_control;
999			$time_cond = " and b.`commentDate` > ? ";
1000			$bind_time = [(int) $limit];
1001		} else {
1002			$time_cond = '';
1003			$bind_time = [];
1004		}
1005		if (! empty($who)) {
1006			//get a list of threads the user has posted in
1007			//this needs to be a separate query otherwise it'll run once for every row in the db!
1008			$user_thread_ids_query = "SELECT DISTINCT if (parentId=0, threadId, parentId) threadId FROM tiki_comments WHERE object = ? AND userName = ? ORDER BY threadId DESC";
1009			$user_thread_ids_params = [$forumId, $who];
1010			$user_thread_ids_result = $this->query($user_thread_ids_query, $user_thread_ids_params, 1000);
1011
1012			if ($user_thread_ids_result->numRows()) {
1013				$time_cond .= ' and a.`threadId` IN (';
1014				$user_thread_ids = [];
1015				while ($res = $user_thread_ids_result->fetchRow()) {
1016					$user_thread_ids[] = $res['threadId'];
1017				}
1018				$time_cond .= implode(",", $user_thread_ids);
1019				$time_cond .= ") ";
1020			}
1021		}
1022		if (! empty($type)) {
1023			$time_cond .= ' and a.`type` = ? ';
1024			$bind_time[] = $type;
1025		}
1026
1027		$categlib = TikiLib::lib('categ');
1028		if ($jail = $categlib->get_jail()) {
1029			$categlib->getSqlJoin($jail, 'forum', '`a`.`object`', $join, $where, $bind_vars);
1030		} else {
1031			$join = '';
1032			$where = '';
1033		}
1034		$select = '';
1035		if (! empty($forum_info['att_list_nb']) && $forum_info['att_list_nb'] == 'y') {
1036			$select = ', count(distinct(tfa.`attId`)) as nb_attachments ';
1037			$join .= 'left join `tiki_comments` tca on (tca.`parentId`=a.`threadId` or (tca.`parentId`=0 and tca.`threadId`=a.`threadId`))left join `tiki_forum_attachments` tfa on (tfa.`threadId`=tca.`threadId`)';
1038		}
1039
1040		$query =
1041			$this->ifNull("a.`archived`", "'n'") . " as `archived`," .
1042			$this->ifNull("max(b.`commentDate`)", "a.`commentDate`") . " as `lastPost`," .
1043			$this->ifNull("a.`type`='s'", 'false') . " as `sticky`, count(distinct b.`threadId`) as `replies` $select
1044				from `tiki_comments` a left join `tiki_comments` b
1045				on b.`parentId`=a.`threadId` $join
1046				where 1 = 1 $where" . ($forumId ? 'AND a.`object`=?' : '')
1047				. (($include_archived) ? '' : ' and (a.`archived` is null or a.`archived`=?)')
1048				. " and a.`objectType` = 'forum'
1049				and a.`parentId` = ? $time_cond
1050				group by a.`threadId`, a.`object`, a.`objectType`, a.`parentId`, a.`userName`, a.`commentDate`, a.`hits`, a.`type`, a.`points`, a.`votes`, a.`average`, a.`title`, a.`data`, a.`hash`, a.`user_ip`, a.`summary`, a.`smiley`, a.`message_id`, a.`in_reply_to`, a.`comment_rating`, a.`locked`, a.archived ";
1051
1052		if ($reply_state == 'none') {
1053			$query .= ' HAVING `replies` = 0 ';
1054		}
1055
1056		// Prevent ambiguous field database errors
1057		if (strpos($sort_mode, 'commentDate') !== false) {
1058			$sort_mode = str_replace('commentDate', 'a.commentDate', $sort_mode);
1059		}
1060		if (strpos($sort_mode, 'smiley') !== false) {
1061			$sort_mode = str_replace('smiley', 'a.smiley', $sort_mode);
1062		}
1063
1064		if (strpos($sort_mode, 'hits') !== false) {
1065			$sort_mode = str_replace('hits', 'a.hits', $sort_mode);
1066		}
1067
1068		if (strpos($sort_mode, 'title') !== false) {
1069			$sort_mode = str_replace('title', 'a.title', $sort_mode);
1070		}
1071
1072		if (strpos($sort_mode, 'type') !== false) {
1073			$sort_mode = str_replace('type', 'a.type', $sort_mode);
1074		}
1075
1076		if (strpos($sort_mode, 'userName') !== false) {
1077			$sort_mode = str_replace('userName', 'a.userName', $sort_mode);
1078		}
1079
1080		$query .= "order by `sticky` desc, " . $this->convertSortMode($sort_mode) . ", `threadId`";
1081
1082		if ($forumId) {
1083			$bind_vars[] = (string) $forumId;
1084		}
1085
1086		if (! $include_archived) {
1087			$bind_vars[] = 'n';
1088		}
1089		$bind_vars[] = 0;
1090
1091		return [
1092			'query' => $query,
1093			'bindvars' => array_merge($bind_vars, $bind_time),
1094		];
1095	}
1096
1097	function get_last_forum_posts($forumId, $maxRecords = -1)
1098	{
1099		$comments = $this->table('tiki_comments');
1100
1101		return $comments->fetchAll(
1102			$comments->all(),
1103			['objectType' => 'forum', 'object' => $forumId],
1104			$maxRecords,
1105			0,
1106			['commentDate' => 'DESC']
1107		);
1108	}
1109
1110	/**
1111	 * @param int $forumId
1112	 * @param int $parentId
1113	 * @param string $name
1114	 * @param string $description
1115	 * @param string $controlFlood
1116	 * @param int $floodInterval
1117	 * @param string $moderator
1118	 * @param string $mail
1119	 * @param string $useMail
1120	 * @param string $usePruneUnreplied
1121	 * @param int $pruneUnrepliedAge
1122	 * @param string $usePruneOld
1123	 * @param int $pruneMaxAge
1124	 * @param int $topicsPerPage
1125	 * @param string $topicOrdering
1126	 * @param string $threadOrdering
1127	 * @param string $section
1128	 * @param string $topics_list_reads
1129	 * @param string $topics_list_replies
1130	 * @param string $topics_list_pts
1131	 * @param string $topics_list_lastpost
1132	 * @param string $topics_list_author
1133	 * @param string $vote_threads
1134	 * @param string $show_description
1135	 * @param string $inbound_pop_server
1136	 * @param int $inbound_pop_port
1137	 * @param string $inbound_pop_user
1138	 * @param string $inbound_pop_password
1139	 * @param string $outbound_address
1140	 * @param string $outbound_mails_for_inbound_mails
1141	 * @param string $outbound_mails_reply_link
1142	 * @param string $outbound_from
1143	 * @param string $topic_smileys
1144	 * @param string $topic_summary
1145	 * @param string $ui_avatar
1146	 * @param string $ui_rating_choice_topic
1147	 * @param string $ui_flag
1148	 * @param string $ui_posts
1149	 * @param string $ui_level
1150	 * @param string $ui_email
1151	 * @param string $ui_online
1152	 * @param string $approval_type
1153	 * @param string $moderator_group
1154	 * @param string $forum_password
1155	 * @param string $forum_use_password
1156	 * @param string $att
1157	 * @param string $att_store
1158	 * @param string $att_store_dir
1159	 * @param int $att_max_size
1160	 * @param int $forum_last_n
1161	 * @param string $commentsPerPage
1162	 * @param string $threadStyle
1163	 * @param string $is_flat
1164	 * @param string $att_list_nb
1165	 * @param string $topics_list_lastpost_title
1166	 * @param string $topics_list_lastpost_avatar
1167	 * @param string $topics_list_author_avatar
1168	 * @param string $forumLanguage
1169	 * @return int
1170	 */
1171	function replace_forum(
1172		$forumId = 0,
1173		$name = '',
1174		$description = '',
1175		$controlFlood = 'n',
1176		$floodInterval = 120,
1177		$moderator = 'admin',
1178		$mail = '',
1179		$useMail = 'n',
1180		$usePruneUnreplied = 'n',
1181		$pruneUnrepliedAge = 2592000,
1182		$usePruneOld = 'n',
1183		$pruneMaxAge = 259200,
1184		$topicsPerPage = 10,
1185		$topicOrdering = 'lastPost_desc',
1186		$threadOrdering = '',
1187		$section = '',
1188		$topics_list_reads = 'y',
1189		$topics_list_replies = 'y',
1190		$topics_list_pts = 'n',
1191		$topics_list_lastpost = 'y',
1192		$topics_list_author = 'y',
1193		$vote_threads = 'n',
1194		$show_description = 'n',
1195		$inbound_pop_server = '',
1196		$inbound_pop_port = 110,
1197		$inbound_pop_user = '',
1198		$inbound_pop_password = '',
1199		$outbound_address = '',
1200		$outbound_mails_for_inbound_mails = 'n',
1201		$outbound_mails_reply_link = 'n',
1202		$outbound_from = '',
1203		$topic_smileys = 'n',
1204		$topic_summary = 'n',
1205		$ui_avatar = 'y',
1206		$ui_rating_choice_topic = 'y',
1207		$ui_flag = 'y',
1208		$ui_posts = 'n',
1209		$ui_level = 'n',
1210		$ui_email = 'n',
1211		$ui_online = 'n',
1212		$approval_type = 'all_posted',
1213		$moderator_group = '',
1214		$forum_password = '',
1215		$forum_use_password = 'n',
1216		$att = 'att_no',
1217		$att_store = 'db',
1218		$att_store_dir = '',
1219		$att_max_size = 1000000,
1220		$forum_last_n = 0,
1221		$commentsPerPage = '',
1222		$threadStyle = '',
1223		$is_flat = 'n',
1224		$att_list_nb = 'n',
1225		$topics_list_lastpost_title = 'y',
1226		$topics_list_lastpost_avatar = 'n',
1227		$topics_list_author_avatar = 'n',
1228		$forumLanguage = '',
1229		$parentId = 0
1230	) {
1231
1232		global $prefs;
1233
1234		if (! $forumId && empty($att_store_dir)) {
1235			// Set new default location for forum attachments (only affect new forums for backward compatibility))
1236			$att_store_dir = 'files/forums/';
1237		}
1238
1239		$data = [
1240			'name' => $name,
1241			'parentId' => $parentId,
1242			'description' => $description,
1243			'controlFlood' => $controlFlood,
1244			'floodInterval' => (int) $floodInterval,
1245			'moderator' => $moderator,
1246			'hits' => 0,
1247			'mail' => $mail,
1248			'useMail' => $useMail,
1249			'section' => $section,
1250			'usePruneUnreplied' => $usePruneUnreplied,
1251			'pruneUnrepliedAge' => (int) $pruneUnrepliedAge,
1252			'usePruneOld' => $usePruneOld,
1253			'vote_threads' => $vote_threads,
1254			'topics_list_reads' => $topics_list_reads,
1255			'topics_list_replies' => $topics_list_replies,
1256			'show_description' => $show_description,
1257			'inbound_pop_server' => $inbound_pop_server,
1258			'inbound_pop_port' => $inbound_pop_port,
1259			'inbound_pop_user' => $inbound_pop_user,
1260			'inbound_pop_password' => $inbound_pop_password,
1261			'outbound_address' => $outbound_address,
1262			'outbound_mails_for_inbound_mails' => $outbound_mails_for_inbound_mails,
1263			'outbound_mails_reply_link' => $outbound_mails_reply_link,
1264			'outbound_from' => $outbound_from,
1265			'topic_smileys' => $topic_smileys,
1266			'topic_summary' => $topic_summary,
1267			'ui_avatar' => $ui_avatar,
1268			'ui_rating_choice_topic' => $ui_rating_choice_topic,
1269			'ui_flag' => $ui_flag,
1270			'ui_posts' => $ui_posts,
1271			'ui_level' => $ui_level,
1272			'ui_email' => $ui_email,
1273			'ui_online' => $ui_online,
1274			'approval_type' => $approval_type,
1275			'moderator_group' => $moderator_group,
1276			'forum_password' => $forum_password,
1277			'forum_use_password' => $forum_use_password,
1278			'att' => $att,
1279			'att_store' => $att_store,
1280			'att_store_dir' => $att_store_dir,
1281			'att_max_size' => (int) $att_max_size,
1282			'topics_list_pts' => $topics_list_pts,
1283			'topics_list_lastpost' => $topics_list_lastpost,
1284			'topics_list_lastpost_title' => $topics_list_lastpost_title,
1285			'topics_list_lastpost_avatar' => $topics_list_lastpost_avatar,
1286			'topics_list_author' => $topics_list_author,
1287			'topics_list_author_avatar' => $topics_list_author_avatar,
1288			'topicsPerPage' => (int) $topicsPerPage,
1289			'topicOrdering' => $topicOrdering,
1290			'threadOrdering' => $threadOrdering,
1291			'pruneMaxAge' => (int) $pruneMaxAge,
1292			'forum_last_n' => (int) $forum_last_n,
1293			'commentsPerPage' => $commentsPerPage,
1294			'threadStyle' => $threadStyle,
1295			'is_flat' => $is_flat,
1296			'att_list_nb' => $att_list_nb,
1297			'forumLanguage' => $forumLanguage,
1298		];
1299
1300		$forums = $this->table('tiki_forums');
1301
1302		if ($forumId) {
1303			$oldData = $forums->fetchRow([], ['forumId' => (int) $forumId]);
1304			$forums->update($data, ['forumId' => (int) $forumId]);
1305			$event = 'tiki.forum.update';
1306		} else {
1307			$oldData = null;
1308			$data['created'] = $this->now;
1309			$forumId = $forums->insert($data);
1310			$event = 'tiki.forum.create';
1311		}
1312
1313		TikiLib::events()->trigger($event, [
1314			'type' => 'forum',
1315			'object' => $forumId,
1316			'user' => $GLOBALS['user'],
1317			'title' => $name,
1318			'description' => $description,
1319			'forum_section' => $section,
1320		]);
1321
1322		//if the section changes, re-index forum posts to change section there as well
1323		if ($prefs['feature_forum_post_index'] == 'y' && $oldData && $oldData['section'] != $section) {
1324			$this->index_posts_by_forum($forumId);
1325		}
1326
1327		return $forumId;
1328	}
1329
1330	/**
1331	 * @param $forumId
1332	 * @return mixed
1333	 */
1334	function get_forum($forumId)
1335	{
1336		$res = $this->table('tiki_forums')->fetchFullRow(['forumId' => $forumId]);
1337		if (! empty($res)) {
1338			$res['is_locked'] = $this->is_object_locked('forum:' . $forumId) ? 'y' : 'n';
1339		}
1340
1341		return $res;
1342	}
1343
1344	/**
1345	 * Get all parents of specific forum
1346	 * @param $forum
1347	 * @return mixed
1348	 */
1349	function get_forum_parents($forum)
1350	{
1351		$parents = [];
1352
1353		while (($parent = $this->get_forum($forum['parentId'])) != null) {
1354			$parents[] = $parent;
1355			$forum = $parent;
1356		}
1357
1358		return array_reverse($parents);
1359	}
1360
1361	/**
1362	 * @param $forumId
1363	 * @return bool
1364	 */
1365	function remove_forum($forumId)
1366	{
1367		$forum = $this->get_forum($forumId);
1368
1369		$this->table('tiki_forums')->delete(['forumId' => $forumId]);
1370		$this->remove_object("forum", $forumId);
1371		$this->table('tiki_forum_attachments')->delete(['forumId' => $forumId]);
1372
1373		TikiLib::events()->trigger('tiki.forum.delete', [
1374			'type' => 'forum',
1375			'object' => $forumId,
1376			'user' => $GLOBALS['user'],
1377			'title' => $forum['name'],
1378			'description' => $forum['description'],
1379			'forum_section' => $forum['section'],
1380		]);
1381
1382		return true;
1383	}
1384
1385	/**
1386	 * @param int $offset
1387	 * @param $maxRecords
1388	 * @param string $sort_mode
1389	 * @param string $find
1390	 * @param int $parentId (0 to get forums without parents, <0 to get all forums, >0 to get forums of specific parent)
1391	 * @return array
1392	 */
1393	function list_forums($offset = 0, $maxRecords = -1, $sort_mode = 'name_asc', $find = '', $parentId = 0)
1394	{
1395		$bindvars = [];
1396
1397		$categlib = TikiLib::lib('categ');
1398		if ($jail = $categlib->get_jail()) {
1399			$categlib->getSqlJoin($jail, 'forum', '`tiki_forums`.`forumId`', $join, $where, $bindvars);
1400		} else {
1401			$join = '';
1402			$where = '';
1403		}
1404
1405		if ($find) {
1406			$findesc = '%' . $find . '%';
1407
1408			$mid = " AND `tiki_forums`.`name` like ? or `tiki_forums`.`description` like ? ";
1409			$bindvars[] = $findesc;
1410			$bindvars[] = $findesc;
1411		} else {
1412			$mid = "";
1413		}
1414
1415		if (in_array($sort_mode, ['age_asc', 'age_desc', 'users_asc', 'users_desc', 'posts_per_day_asc',
1416			'posts_per_day_desc'])) {
1417			$query_sort_mode = 'name_asc';
1418		} else {
1419			$query_sort_mode = $sort_mode;
1420		}
1421		if ($parentId < 0) { // get all forums
1422			$where .= ' AND parentID > ? ';
1423		} else { //get forums of specific parents
1424			$where .= ' AND parentID = ? ';
1425		}
1426		$bindvars[] = $parentId;
1427
1428		$query = "select * from `tiki_forums` $join WHERE 1=1 $where $mid order by `section` asc," . $this->convertSortMode('`tiki_forums`.' . $query_sort_mode);
1429		$result = $this->fetchAll($query, $bindvars);
1430		$result = Perms::filter(['type' => 'forum'], 'object', $result, ['object' => 'forumId'], 'forum_read');
1431		$count = 0;
1432		$cant = 0;
1433		$off = 0;
1434		$comments = $this->table('tiki_comments');
1435
1436		foreach ($result as &$res) {
1437			$cant++; // Count the whole number of forums the user has access to
1438
1439			$forum_age = ceil(($this->now - $res["created"]) / (24 * 3600));
1440
1441			// Get number of topics on this forum
1442			$res['threads'] = (int) $this->count_comments_threads('forum:' . $res['forumId']);
1443
1444			//Get sub forums
1445			$res['sub_forums'] = $this->get_sub_forums($res['forumId']);
1446
1447			// Get number of posts on this forum
1448			$res['comments'] = (int) $this->count_comments('forum:' . $res['forumId']);
1449
1450			// Get number of users that posted at least one comment on this forum
1451			$res['users'] = (int) $comments->fetchOne(
1452				$comments->expr('count(distinct `userName`)'),
1453				['object' => $res['forumId'], 'objectType' => 'forum']
1454			);
1455
1456			// Get lock status
1457			$res['is_locked'] = $this->is_object_locked('forum:' . $res['forumId']) ? 'y' : 'n';
1458
1459			// Get data of the last post of this forum
1460			if ($res['comments'] > 0) {
1461				$res['lastPostData'] = $comments->fetchFullRow(
1462					['object' => $res['forumId'], 'objectType' => 'forum'],
1463					['commentDate' => 'DESC']
1464				);
1465				$res['lastPost'] = $res['lastPostData']['commentDate'];
1466			} else {
1467				unset($res['lastPost']);
1468			}
1469
1470			// Generate stats based on this forum's age
1471			if ($forum_age > 0) {
1472				$res['age'] = (int) $forum_age;
1473				$res['posts_per_day'] = (int) $res['comments'] / $forum_age;
1474				$res['users_per_day'] = (int) $res['users'] / $forum_age;
1475			} else {
1476				$res['age'] = 0;
1477				$res['posts_per_day'] = 0;
1478				$res['users_per_day'] = 0;
1479			}
1480
1481			++$count;
1482		}
1483		//handle sorts for displayed columns not in the database
1484		if (substr($sort_mode, -4) === '_asc') {
1485			$sortdir = 'asc';
1486			$sortcol = substr($sort_mode, 0, strlen($sort_mode) - 4);
1487		} else {
1488			$sortdir = 'desc';
1489			$sortcol = substr($sort_mode, 0, strlen($sort_mode) - 5);
1490		}
1491		if (in_array($sortcol, ['threads', 'comments', 'age', 'posts_per_day', 'users'])) {
1492			$sortarray = array_column($result, $sortcol);
1493			if ($sortdir === 'asc') {
1494				asort($sortarray, SORT_NUMERIC);
1495			} else {
1496				arsort($sortarray, SORT_NUMERIC);
1497			}
1498			//need to sort within sections if sections are used (also works if sections aren't used)
1499			$sections = array_unique(array_column($result, 'section'));
1500			foreach ($sections as $section) {
1501				foreach ($sortarray as $key => $data) {
1502					if ($result[$key]['section'] === $section) {
1503						$sorted[] = $result[$key];
1504					}
1505				}
1506			}
1507			$result = $sorted;
1508		}
1509		if ($maxRecords > -1) {
1510			$result = array_slice($result, $offset, $maxRecords);
1511		}
1512
1513		$retval = [];
1514		$retval["data"] = $result;
1515		$retval["cant"] = $cant;
1516		return $retval;
1517	}
1518
1519	/**
1520	 * @param $section
1521	 * @param $offset
1522	 * @param $maxRecords
1523	 * @param $sort_mode
1524	 * @param $find
1525	 * @return array
1526	 */
1527	function list_forums_by_section($section, $offset, $maxRecords, $sort_mode, $find)
1528	{
1529		$conditions = [
1530			'section' => $section,
1531		];
1532
1533		$forums = $this->table('tiki_forums');
1534		$comments = $this->table('tiki_comments');
1535
1536		if ($find) {
1537			$conditions['search'] = $forums->findIn($find, ['name', 'description']);
1538		}
1539
1540		$ret = $forums->fetchAll($forums->all(), $conditions, $maxRecords, $offset, $forums->sortMode($sort_mode));
1541		$cant = $forums->fetchCount($conditions);
1542
1543		foreach ($ret as &$res) {
1544			$forum_age = ceil(($this->now - $res["created"]) / (24 * 3600));
1545
1546			$res["age"] = (int) $forum_age;
1547
1548			if ($forum_age) {
1549				$res["posts_per_day"] = (int) $res["comments"] / $forum_age;
1550			} else {
1551				$res["posts_per_day"] = 0;
1552			}
1553
1554			// Now select users
1555			$res['users'] = (int) $comments->fetchOne(
1556				$comments->expr('count(distinct `userName`)'),
1557				['object' => $res['forumId'], 'objectType' => 'forum']
1558			);
1559
1560			if ($forum_age) {
1561				$res["users_per_day"] = (int) $res["users"] / $forum_age;
1562			} else {
1563				$res["users_per_day"] = 0;
1564			}
1565
1566			$res['lastPostData'] = $comments->fetchFullRow(
1567				['object' => $res['forumId'], 'objectType' => 'forum'],
1568				['commentDate' => 'DESC']
1569			);
1570		}
1571
1572		return [
1573			'data' => $ret,
1574			'cant' => $cant,
1575		];
1576	}
1577
1578	/**
1579	 * @param $user
1580	 * @param $threadId
1581	 * @return bool
1582	 */
1583	function user_can_edit_post($user, $threadId)
1584	{
1585		$result = $this->table('tiki_comments')->fetchOne('userName', ['threadId' => $threadId]);
1586
1587		return $result == $user;
1588	}
1589
1590	/**
1591	 * @param $user
1592	 * @param $forumId
1593	 * @return bool
1594	 */
1595	function user_can_post_to_forum($user, $forumId)
1596	{
1597		// Check flood interval for the forum
1598		$forum = $this->get_forum($forumId);
1599
1600		if ($forum["controlFlood"] != 'y') {
1601			return true;
1602		}
1603
1604		if ($user) {
1605			$comments = $this->table('tiki_comments');
1606			$maxDate = $comments->fetchOne(
1607				$comments->max('commentDate'),
1608				['object' => $forumId, 'objectType' => 'forum', 'userName' => $user]
1609			);
1610
1611			if (! $maxDate) {
1612				return true;
1613			}
1614
1615			return $maxDate + $forum["floodInterval"] <= $this->now;
1616		} else {
1617			// Anonymous users
1618			if (! isset($_SESSION["lastPost"])) {
1619				return true;
1620			} else {
1621				if ($_SESSION["lastPost"] + $forum["floodInterval"] > $this->now) {
1622					return false;
1623				} else {
1624					return true;
1625				}
1626			}
1627		}
1628	}
1629
1630	/**
1631	 * @param $forumId
1632	 * @param $parentId
1633	 * @return bool
1634	 */
1635	function register_forum_post($forumId, $parentId)
1636	{
1637		$forums = $this->table('tiki_forums');
1638
1639		$forums->update(['comments' => $forums->increment(1)], ['forumId' => (int) $forumId]);
1640
1641		$lastPost = $this->getOne(
1642			"select max(`commentDate`) from `tiki_comments`,`tiki_forums`
1643			where `object` = `forumId` and `objectType` = 'forum' and `forumId` = ?",
1644			[(int) $forumId]
1645		);
1646		$query = "update `tiki_forums` set `lastPost`=? where `forumId`=? ";
1647		$result = $this->query($query, [(int) $lastPost, (int) $forumId]);
1648
1649		$this->forum_prune($forumId);
1650		return true;
1651	}
1652
1653	/**
1654	 * @param $forumId
1655	 * @param $parentId
1656	 */
1657	function register_remove_post($forumId, $parentId)
1658	{
1659		$this->forum_prune($forumId);
1660	}
1661
1662	/**
1663	 * @param $forumId
1664	 * @return bool
1665	 */
1666	function forum_add_hit($forumId)
1667	{
1668		global $prefs, $user;
1669
1670		if (StatsLib::is_stats_hit()) {
1671			$forums = $this->table('tiki_forums');
1672			$forums->update(['hits' => $forums->increment(1)], ['forumId' => (int) $forumId]);
1673			$this->forum_prune($forumId);
1674		}
1675		return true;
1676	}
1677
1678	/**
1679	 * @param $threadId
1680	 * @return bool
1681	 */
1682	function comment_add_hit($threadId)
1683	{
1684		global $prefs, $user;
1685
1686		if (StatsLib::is_stats_hit()) {
1687			require_once('lib/search/refresh-functions.php');
1688
1689			$comments = $this->table('tiki_comments');
1690			$comments->update(['hits' => $comments->increment(1)], ['threadId' => (int) $threadId]);
1691
1692			refresh_index("forum post", $threadId);
1693		}
1694		return true;
1695	}
1696
1697	/**
1698	 * @param $threadId
1699	 * @param int $generations
1700	 * @return array
1701	 */
1702	function get_all_children($threadId, $generations = 99)
1703	{
1704		$comments = $this->table('tiki_comments');
1705
1706		$children = [];
1707		$threadId = (array) $threadId;
1708
1709		for ($current_generation = 0; $current_generation < $generations; $current_generation++) {
1710			$children_this_generation = $comments->fetchColumn('threadId', ['parentId' => $comments->in($threadId)]);
1711
1712			$children[] = $children_this_generation;
1713
1714			if (! $children_this_generation) {
1715				break;
1716			}
1717
1718			$threadId = $children_this_generation;
1719		}
1720
1721		return array_unique($children);
1722	}
1723
1724	/**
1725	 * @param $forumId
1726	 * @return bool
1727	 */
1728	function forum_prune($forumId)
1729	{
1730		$comments = $this->table('tiki_comments');
1731
1732		$forum = $this->get_forum($forumId);
1733
1734		if ($forum["usePruneUnreplied"] == 'y') {
1735			$age = $forum["pruneUnrepliedAge"];
1736
1737			// Get all unreplied threads
1738			// Get all the top_level threads
1739			$oldage = $this->now - $age;
1740
1741			$result = $comments->fetchColumn(
1742				'threadId',
1743				[
1744					'parentId' => 0,
1745					'commentDate' => $comments->lesserThan((int) $oldage),
1746					'object' => $forumId,
1747					'objectType' => 'forum'
1748				]
1749			);
1750
1751			$result = array_filter($result);
1752
1753			foreach ($result as $id) {
1754				// Check if this old top level thread has replies
1755				$cant = $comments->fetchCount(['parentId' => (int) $id]);
1756
1757				// Remove this old thread without replies
1758				if ($cant == 0) {
1759					$this->remove_comment($id);
1760				}
1761			}
1762		}
1763
1764		if ($forum["usePruneOld"] == 'y') { // this is very dangerous as you can delete some posts in the middle or root of a tree strucuture
1765			$maxAge = $forum["pruneMaxAge"];
1766
1767			$old = $this->now - $maxAge;
1768
1769			// this aims to make it safer, by pruning only those with no children that are younger than age threshold
1770			$results = $comments->fetchColumn(
1771				'threadId',
1772				['object' => $forumId, 'objectType' => 'forum', 'commentDate' => $comments->lesserThan($old)]
1773			);
1774			foreach ($results as $threadId) {
1775				$children = $this->get_all_children($threadId);
1776				if ($children) {
1777					$maxDate = $comments->fetchOne($comments->max('commentDate'), ['threadId' => $comments->in($children)]);
1778					if ($maxDate < $old) {
1779						$this->remove_comment($threadId);
1780					}
1781				} else {
1782					$this->remove_comment($threadId);
1783				}
1784			}
1785		}
1786
1787		if ($forum["usePruneUnreplied"] == 'y' || $forum["usePruneOld"] == 'y') {	// Recalculate comments and threads
1788			$count = $comments->fetchCount(['objectType' => 'forum', 'object' => (int) $forumId]);
1789			$this->table('tiki_forums')->update(['comments' => $count], ['forumId' => (int) $forumId]);
1790		}
1791		return true;
1792	}
1793
1794	/**
1795	 * @param $user
1796	 * @param $max
1797	 * @param string $type
1798	 * @return array
1799	 */
1800	function get_user_forum_comments($user, $max, $type = '')
1801	{
1802		// get parent title as well, especially useful in flat forum
1803		$parentinfo = '';
1804		$mid = '';
1805		if ($type == 'replies') {
1806			$parentinfo .= ", b.`title` as parentTitle";
1807			$mid .= " inner join `tiki_comments` b on b.`threadId` = a.`parentId`";
1808		}
1809		$mid .= " where a.`objectType`='forum' AND a.`userName`=?";
1810		if ($type == 'topics') {
1811			$mid .= " AND a.`parentId`=0";
1812		} elseif ($type == 'replies') {
1813			$mid .= " AND a.`parentId`>0";
1814		}
1815		$query = "select a.`threadId`, a.`object`, a.`title`, a.`parentId`, a.`commentDate` $parentinfo, a.`userName` from `tiki_comments` a $mid ORDER BY a.`commentDate` desc";
1816
1817		$result = $this->fetchAll($query, [$user], $max);
1818		if ($type == 'topics') {
1819			$ret = Perms::filter(['type' => 'thread'], 'object', $result, ['object' => 'threadId', 'creator' => 'userName'], 'forum_read');
1820		} elseif ($type == 'replies') {
1821			$ret = Perms::filter(['type' => 'thread'], 'object', $result, ['object' => 'parentId', 'creator' => 'userName'], 'forum_read');
1822		} else {
1823			$ret = Perms::filter(['type' => 'forum'], 'object', $result, ['object' => 'object', 'creator' => 'userName'], 'forum_read');
1824		}
1825
1826		return $ret;
1827	}
1828
1829	public function extras_enabled($enabled)
1830	{
1831		$this->extras = (bool) $enabled;
1832	}
1833
1834	// FORUMS END
1835	/**
1836	 * @param $id
1837	 * @param null $message_id
1838	 * @param null $forum_info
1839	 * @return mixed
1840	 */
1841	function get_comment($id, $message_id = null, $forum_info = null)
1842	{
1843		$comments = $this->table('tiki_comments');
1844		if ($message_id) {
1845			$res = $comments->fetchFullRow(['message_id' => $message_id]);
1846		} else {
1847			$res = $comments->fetchFullRow(['threadId' => $id]);
1848		}
1849
1850		if ($res) { //if there is a comment with that id
1851			$this->add_comments_extras($res, $forum_info);
1852		}
1853
1854		if (! empty($res['objectType']) && $res['objectType'] == 'forum') {
1855			$res['deliberations'] = $this->get_forum_deliberations($res['threadId']);
1856		}
1857
1858		if (! empty($res['objectType']) && $res['objectType'] == 'trackeritem') {
1859			$res['version'] = TikiLib::lib('attribute')->get_attribute('comment', $res['threadId'], 'tiki.comment.version');
1860		}
1861
1862		return $res;
1863	}
1864
1865	/**
1866	 * @param $parentId
1867	 * @return mixed
1868	 */
1869	function get_sub_forums($parentId = 0)
1870	{
1871		$bindvars = [];
1872		$query_sort_mode = 'name_asc';
1873		$where = ' AND parentId = ? ';
1874		$mid = '';
1875		$join = '';
1876		$bindvars[] = $parentId;
1877
1878		$query = "select * from `tiki_forums` $join WHERE 1=1 $where $mid order by `section` asc," . $this->convertSortMode('`tiki_forums`.' . $query_sort_mode);
1879		$result = $this->fetchAll($query, $bindvars);
1880		return $result;
1881	}
1882
1883	/**
1884	* Returns the forum-id for a comment
1885	*/
1886	function get_comment_forum_id($commentId)
1887	{
1888		return $this->table('tiki_comments')->fetchOne('object', ['threadId' => $commentId]);
1889	}
1890
1891	/**
1892	 * @param $res
1893	 * @param null $forum_info
1894	 */
1895	function add_comments_extras(&$res, $forum_info = null)
1896	{
1897		if (! $this->extras) {
1898			return;
1899		}
1900
1901		// this function adds some extras to the referenced array.
1902		// This array should already contain the contents of the tiki_comments table row
1903		// used in $this->get_comment and $this->get_comments
1904		global $prefs;
1905
1906		$res["parsed"] = $this->parse_comment_data($res["data"]);
1907
1908		// these could be cached or probably queried along with the original query of the tiki_comments table
1909		if ($forum_info == null || $forum_info['ui_posts'] == 'y' || $forum_info['ui_level'] == 'y') {
1910			$res2 = $this->table('tiki_user_postings')->fetchRow(['posts', 'level'], ['user' => $res['userName']]);
1911			$res['user_posts'] = $res2['posts'];
1912			$res['user_level'] = $res2['level'];
1913		}
1914		// 'email is public' never has 'y' value, because it is now used to choose the email scrambling method
1915		// ... so, we need to test if it's not equal to 'n'
1916		if (($forum_info == null || $forum_info['ui_email'] == 'y') && $this->get_user_preference($res['userName'], 'email is public', 'n') != 'n') {
1917			$res['user_email'] = TikiLib::lib('user')->get_user_email($res['userName']);
1918		} else {
1919			$res['user_email'] = '';
1920		}
1921
1922		$res['attachments'] = $this->get_thread_attachments($res['threadId'], 0);
1923		// is the 'is_reported' really used? can be queried with orig table i think
1924		$res['is_reported'] = $this->is_reported($res['threadId']);
1925		$res['user_online'] = 'n';
1926		if ($res['userName']) {
1927			$res['user_online'] = $this->is_user_online($res['userName']) ? 'y' : 'n';
1928		}
1929		$res['user_exists'] = TikiLib::lib('user')->user_exists($res['userName']);
1930		if ($prefs['feature_contribution'] == 'y') {
1931			$contributionlib = TikiLib::lib('contribution');
1932			$res['contributions'] = $contributionlib->get_assigned_contributions($res['threadId'], 'comment');
1933		}
1934	}
1935
1936	/**
1937	 * @param $id
1938	 * @return mixed
1939	 */
1940	function get_comment_father($id)
1941	{
1942		static $cache;
1943		if (isset($cache[$id])) {
1944			return $cache[$id];
1945		}
1946		return $cache[$id] = $this->table('tiki_comments')->fetchOne('parentId', ['threadId' => $id]);
1947	}
1948
1949	/**
1950	 * Return the number of comments for a specific object.
1951	 * No permission check is done to verify if the user has permission
1952	 * to see the object itself or its comments.
1953	 *
1954	 * @param string $objectId example: 'blog post:2'
1955	 * @param string $approved 'y' or 'n'
1956	 * @return int the number of comments
1957	 */
1958	function count_comments($objectId, $approved = 'y')
1959	{
1960		global $tiki_p_admin_comments, $prefs;
1961
1962		$comments = $this->table('tiki_comments');
1963
1964		$conditions = [
1965			'objectType' => 'forum',
1966		];
1967
1968		$object = explode(":", $objectId, 2);
1969		if ($object[0] == 'topic') {
1970			$conditions['parentId'] = $object[1];
1971		} else {
1972			$conditions['objectType'] = $object[0];
1973			$conditions['object'] = $object[1];
1974		}
1975
1976		if ($tiki_p_admin_comments != 'y') {
1977			$conditions['approved'] = $approved;
1978		}
1979
1980		if ($prefs['comments_archive'] == 'y' && $tiki_p_admin_comments != 'y') {
1981			$conditions['archived'] = $comments->expr(' ( `archived` = ? OR `archived` IS NULL ) ', ['n']);
1982		}
1983
1984		return $comments->fetchCount($conditions);
1985	}
1986
1987	/**
1988	 * @param string $type
1989	 * @param string $lang
1990	 * @param $maxRecords
1991	 * @return array|bool
1992	 */
1993	function order_comments_by_count($type = 'wiki', $lang = '', $maxRecords = -1)
1994	{
1995		global $prefs;
1996		$bind = [];
1997		if ($type == 'article') {
1998			if ($prefs['feature_articles'] != 'y') {
1999				return false;
2000			}
2001			$query = "SELECT count(*),`tiki_articles`.`articleId`,`tiki_articles`.`title` FROM `tiki_comments` INNER JOIN `tiki_articles` ON `tiki_comments`.`object`=`tiki_articles`.`articleId` WHERE `tiki_comments`.`objectType`='article' and `tiki_comments`.`approved`='y' and `tiki_articles`.`ispublished`='y'";
2002
2003			if ($lang != '') {
2004				$query = $query . " and `tiki_articles`.`lang`=?";
2005				$bind[] = $lang;
2006			}
2007
2008			$query = $query . " GROUP BY `tiki_comments`.`object`,`tiki_articles`.`articleId`,`tiki_articles`.`title` ORDER BY count(*) DESC";
2009		} elseif ($type == 'blog') {
2010			if ($prefs['feature_blogs'] != 'y') {
2011				return false;
2012			}
2013			$query = "SELECT count(*),`tiki_blog_posts`.`postId`,`tiki_blog_posts`.`title` FROM `tiki_comments` INNER JOIN `tiki_blog_posts` ON `tiki_comments`.`object`=`tiki_blog_posts`.`postId` WHERE `tiki_comments`.`objectType`='blog post' and `tiki_comments`.`approved`='y' GROUP BY `tiki_comments`.`object`, `tiki_blog_posts`.`postId`, `tiki_blog_posts`.`title` ORDER BY count(*) DESC";
2014		} else {
2015			//Default to Wiki
2016			if ($prefs['feature_wiki'] != 'y') {
2017				return false;
2018			}
2019			$query = "SELECT count(*),`tiki_pages`.`pageName` FROM `tiki_comments` INNER JOIN `tiki_pages` ON `tiki_comments`.`object`=`tiki_pages`.`pageName` WHERE `tiki_comments`.`objectType`='wiki page' and `tiki_comments`.`approved`='y'";
2020
2021			if ($lang != '') {
2022				$query = $query . " and `tiki_pages`.`lang`=?";
2023				$bind[] = $lang;
2024			}
2025
2026			$query = $query . " GROUP BY `tiki_comments`.`object`,`tiki_pages`.`pageName` ORDER BY count(*) DESC";
2027		}
2028
2029		$ret = $this->fetchAll($query, $bind, $maxRecords);
2030		return ['data' => $ret];
2031	}
2032
2033	/**
2034	 * @param $objectId
2035	 * @param int $parentId
2036	 * @return mixed
2037	 */
2038	function count_comments_threads($objectId, $parentId = 0)
2039	{
2040		$object = explode(":", $objectId, 2);
2041		return $this->table('tiki_comments')->fetchCount(
2042			[
2043				'objectType' => $object[0],
2044				'object' => $object[1],
2045				'parentId' => $parentId,
2046			]
2047		);
2048	}
2049
2050	/**
2051	 * @param $id
2052	 * @param $sort_mode
2053	 * @param $offset
2054	 * @param $orig_offset
2055	 * @param $maxRecords
2056	 * @param $orig_maxRecords
2057	 * @param int $threshold
2058	 * @param string $find
2059	 * @param string $message_id
2060	 * @param int $forum
2061	 * @param string $approved
2062	 * @return array
2063	 */
2064	function get_comment_replies(
2065		$id,
2066		$sort_mode,
2067		$offset,
2068		$orig_offset,
2069		$maxRecords,
2070		$orig_maxRecords,
2071		$threshold = 0,
2072		$find = '',
2073		$message_id = "",
2074		$forum = 0,
2075		$approved = 'y'
2076	) {
2077
2078		global $tiki_p_admin_comments, $prefs;
2079		$retval = [];
2080
2081		if ($maxRecords <= 0 && $orig_maxRecords != 0) {
2082			$retval['numReplies'] = 0;
2083			$retval['totalReplies'] = 0;
2084			return $retval;
2085		}
2086
2087		if ($forum) {
2088			$real_id = $message_id;
2089		} else {
2090			$real_id = (int) $id;
2091		}
2092
2093		$query = "select `threadId` from `tiki_comments`";
2094
2095		$initial_sort_mode = $sort_mode;
2096		if ($prefs['rating_advanced'] == 'y') {
2097			$ratinglib = TikiLib::lib('rating');
2098			$query .= $ratinglib->convert_rating_sort($sort_mode, 'comment', '`threadId`');
2099		}
2100
2101		if ($forum) {
2102			$query = $query . " where `in_reply_to`=? and `average`>=? ";
2103		} else {
2104			$query = $query . " where `parentId`=? and `average`>=? ";
2105		}
2106		$bind = [$real_id, (int)$threshold];
2107
2108		if ($tiki_p_admin_comments != 'y') {
2109			$query .= 'and `approved`=? ';
2110			$bind[] = $approved;
2111		}
2112		if ($find) {
2113			$findesc = '%' . $find . '%';
2114
2115			$query = $query . " and (`title` like ? or `data` like ?) ";
2116			$bind[] = $findesc;
2117			$bind[] = $findesc;
2118		}
2119
2120		$query = $query . " order by " . $this->convertSortMode($sort_mode);
2121
2122		if ($sort_mode != 'commentDate_desc') {
2123			$query .= ",`commentDate` desc";
2124		}
2125
2126		$result = $this->query($query, $bind);
2127
2128
2129		$ret = [];
2130
2131		while ($res = $result->fetchRow()) {
2132			$res = $this->get_comment($res['threadId']);
2133
2134			/* Trim to maxRecords, including replies! */
2135			if ($offset >= 0 && $orig_offset != 0) {
2136				$offset = $offset - 1;
2137			}
2138			$maxRecords = $maxRecords - 1;
2139
2140			if ($offset >= 0 && $orig_offset != 0) {
2141				$res['doNotShow'] = 1;
2142			}
2143
2144			if ($maxRecords <= 0 && $orig_maxRecords != 0) {
2145				$ret[] = $res;
2146				break;
2147			}
2148
2149			if ($forum) {
2150				$res['replies_info'] = $this->get_comment_replies(
2151					$res['parentId'],
2152					$initial_sort_mode,
2153					$offset,
2154					$orig_offset,
2155					$maxRecords,
2156					$orig_maxRecords,
2157					$threshold,
2158					$find,
2159					$res['message_id'],
2160					$forum
2161				);
2162			} else {
2163				$res['replies_info'] = $this->get_comment_replies(
2164					$res['threadId'],
2165					$initial_sort_mode,
2166					$offset,
2167					$orig_offset,
2168					$maxRecords,
2169					$orig_maxRecords,
2170					$threshold,
2171					$find
2172				);
2173			}
2174
2175			if ($offset >= 0 && $orig_offset != 0) {
2176				$offset = $offset - $res['replies_info']['totalReplies'];
2177			}
2178			$maxRecords = $maxRecords - $res['replies_info']['totalReplies'];
2179
2180			if ($offset >= 0 && $orig_offset != 0) {
2181				$res['doNotShow'] = 1;
2182			}
2183
2184			if ($maxRecords <= 0 && $orig_maxRecords != 0) {
2185				$ret[] = $res;
2186				break;
2187			}
2188
2189			$ret[] = $res;
2190		}
2191
2192		$retval['replies'] = $ret;
2193
2194		$retval['numReplies'] = count($ret);
2195		$retval['totalReplies'] = $this->total_replies($ret, count($ret));
2196
2197		return $retval;
2198	}
2199
2200	/**
2201	 * @param $reply_array
2202	 * @param int $seed
2203	 * @return int
2204	 */
2205	function total_replies($reply_array, $seed = 0)
2206	{
2207		$retval = $seed;
2208
2209		foreach ($reply_array as $key => $res) {
2210			if (is_array($res) && array_key_exists('replies_info', $res)) {
2211				if (array_key_exists('numReplies', $res['replies_info'])) {
2212					$retval = $retval + $res['replies_info']['numReplies'];
2213				}
2214				$retval = $retval + $this->total_replies($res['replies_info']['replies']);
2215			}
2216		}
2217
2218		return $retval;
2219	}
2220
2221	/**
2222	 * @param $replies
2223	 * @param $rep_flat
2224	 * @param int $level
2225	 */
2226	function flatten_comment_replies(&$replies, &$rep_flat, $level = 0)
2227	{
2228		$reps = $replies['numReplies'];
2229		for ($i = 0; $i < $reps; $i++) {
2230			$replies['replies'][$i]['level'] = $level;
2231			$rep_flat[] = &$replies['replies'][$i];
2232			if (isset($replies['replies'][$i]['replies_info'])) {
2233				$this->flatten_comment_replies($replies['replies'][$i]['replies_info'], $rep_flat, $level + 1);
2234			}
2235		}
2236	}
2237
2238	/**
2239	 * @return string
2240	 */
2241	function pick_cookie()
2242	{
2243		$cookies = $this->table('tiki_cookies');
2244		$cant = $cookies->fetchCount('tiki_cookies', []);
2245
2246		if (! $cant) {
2247			return '';
2248		}
2249
2250		$bid = rand(0, $cant - 1);
2251		$cookie = $cookies->fetchAll(['cookie'], [], 1, $bid);
2252		$cookie = reset($cookie);
2253		$cookie = reset($cookie);
2254		$cookie = str_replace("\n", "", $cookie);
2255		return 'Cookie: ' . $cookie;
2256	}
2257
2258	/**
2259	 * @param $data
2260	 * @return mixed|string
2261	 */
2262	function parse_comment_data($data)
2263	{
2264		global $prefs, $section;
2265		$parserlib = TikiLib::lib('parser');
2266
2267		if (($prefs['feature_forum_parse'] == 'y' && $section == 'forums') || $prefs['section_comments_parse'] == 'y') {
2268			return $parserlib->parse_data($data);
2269		}
2270
2271		// Cookies
2272		if (preg_match_all("/\{cookie\}/", $data, $rsss)) {
2273			$temp_max = count($rsss[0]);
2274			for ($i = 0; $i < $temp_max; $i++) {
2275				$cookie = $this->pick_cookie();
2276
2277				$data = str_replace($rsss[0][$i], $cookie, $data);
2278			}
2279		}
2280
2281		// Fix up special characters, so it can link to pages with ' in them. -rlpowell
2282		$data = htmlspecialchars($data, ENT_QUOTES);
2283		$data = preg_replace("/\[([^\|\]]+)\|([^\]]+)\]/", '<a class="commentslink" href="$1">$2</a>', $data);
2284		// Segundo intento reemplazar los [link] comunes
2285		$data = preg_replace("/\[([^\]\|]+)\]/", '<a class="commentslink" href="$1">$1</a>', $data);
2286
2287		// smileys
2288
2289		$data = $parserlib->parse_smileys($data);
2290
2291		$data = preg_replace("/---/", "<hr/>", $data);
2292		// replace --- with <hr/>
2293		return nl2br($data);
2294	}
2295
2296	/**
2297	 * Deal with titles if comments_notitle to avoid them all appearing as "Unitled"
2298	 *
2299	 * @param & $comment		array contianing comment title and data
2300	 * @param $commentlength	length to truncate to
2301	 */
2302	function process_comment_title($comment, $commentlength)
2303	{
2304		global $prefs;
2305		if ($prefs['comments_notitle'] === 'y') {
2306			TikiLib::lib('smarty')->loadPlugin('smarty_modifier_truncate');
2307			return '"' .
2308					smarty_modifier_truncate(
2309						strip_tags(TikiLib::lib('parser')->parse_data($comment['data'])),
2310						$commentlength
2311					) . '"';
2312		} else {
2313			return $comment['title'];
2314		}
2315	}
2316
2317	/*****************/
2318	/**
2319	 * @param $time
2320	 */
2321	function set_time_control($time)
2322	{
2323		$this->time_control = $time;
2324	}
2325
2326	/**
2327	 * Get comments for a particular object
2328	 *
2329	 * @param string $objectId objectType:objectId (example: 'wiki page:HomePage' or 'blog post:1')
2330	 * @param int $parentId only return child comments of $parentId
2331	 * @param int $offset
2332	 * @param int $maxRecords
2333	 * @param string $sort_mode
2334	 * @param string $find search comment title and data
2335	 * @param int $threshold
2336	 * @param string $style
2337	 * @param int $reply_threadId
2338	 * @param string $approved if user doesn't have tiki_p_admin_comments this param display or not only approved comments (default to 'y')
2339	 * @return array
2340	 */
2341	function get_comments(
2342		$objectId,
2343		$parentId,
2344		$offset = 0,
2345		$maxRecords = 0,
2346		$sort_mode = 'commentDate_asc',
2347		$find = '',
2348		$threshold = 0,
2349		$style = 'commentStyle_threaded',
2350		$reply_threadId = 0,
2351		$approved = 'y'
2352	) {
2353
2354		global $tiki_p_admin_comments, $prefs;
2355		$userlib = TikiLib::lib('user');
2356
2357		$orig_maxRecords = $maxRecords;
2358		$orig_offset = $offset;
2359
2360		// $start_time = microtime(true);
2361		// Turn maxRecords into maxRecords + offset, so we can increment it without worrying too much.
2362		$maxRecords = $offset + $maxRecords;
2363
2364		if ($sort_mode == 'points_asc') {
2365			$sort_mode = 'average_asc';
2366		}
2367
2368		if ($this->time_control) {
2369			$limit = $this->now - $this->time_control;
2370
2371			$time_cond = " and `commentDate` > ? ";
2372			$bind_time = [$limit];
2373		} else {
2374			$time_cond = '';
2375			$bind_time = [];
2376		}
2377
2378		$old_sort_mode = '';
2379
2380		if (in_array($sort_mode, ['replies_desc', 'replies_asc'])) {
2381			$old_offset = $offset;
2382
2383			$old_maxRecords = $maxRecords;
2384			$old_sort_mode = $sort_mode;
2385			$sort_mode = 'title_desc';
2386			$offset = 0;
2387			$maxRecords = -1;
2388		}
2389
2390		// Break out the type and object parameters.
2391		$object = explode(":", $objectId, 2);
2392		$bindvars = array_merge([$object[0], $object[1], (float) $threshold], $bind_time);
2393
2394		if ($tiki_p_admin_comments != 'y') {
2395			$queue_cond = 'and tc1.`approved`=?';
2396			$bindvars[] = $approved;
2397		} else {
2398			$queue_cond = '';
2399		}
2400
2401		if ($prefs['comments_archive'] == 'y' && $tiki_p_admin_comments != 'y') {
2402			$queue_cond .= ' AND (tc1.`archived`=? OR tc1.`archived` IS NULL)';
2403			$bindvars[] = 'n';
2404		}
2405
2406		$query = "select count(*) from `tiki_comments` as tc1 where
2407			`objectType`=? and `object`=? and `average` < ? $time_cond $queue_cond";
2408		$below = $this->getOne($query, $bindvars);
2409
2410		if ($find) {
2411			$findesc = '%' . $find . '%';
2412
2413			$mid = " where tc1.`objectType` = ? and tc1.`object`=? and
2414			tc1.`parentId`=? and tc1.`average`>=? and (tc1.`title`
2415				like ? or tc1.`data` like ?) ";
2416			$bind_mid = [$object[0], $object[1], (int) $parentId, (int) $threshold, $findesc, $findesc];
2417		} else {
2418			$mid = " where tc1.`objectType` = ? and tc1.`object`=? and tc1.`parentId`=? and tc1.`average`>=? ";
2419			$bind_mid = [$object[0], $object[1], (int) $parentId, (int) $threshold];
2420		}
2421		if ($tiki_p_admin_comments != 'y') {
2422			$mid .= ' ' . $queue_cond;
2423			$bind_mid[] = $approved;
2424
2425			if ($prefs['comments_archive'] == 'y') {
2426				$bind_mid[] = 'n';
2427			}
2428		}
2429
2430		$initial_sort_mode = $sort_mode;
2431		if ($prefs['rating_advanced'] == 'y') {
2432			$ratinglib = TikiLib::lib('rating');
2433			$join = $ratinglib->convert_rating_sort($sort_mode, 'comment', '`tc1`.`threadId`');
2434		} else {
2435			$join = '';
2436		}
2437
2438
2439		if ($object[0] == "forum" && $style != 'commentStyle_plain') {
2440			$query = "select `message_id` from `tiki_comments` where `threadId` = ?";
2441			$parent_message_id = $this->getOne($query, [$parentId]);
2442
2443			$adminFields = '';
2444			if ($tiki_p_admin_comments == 'y') {
2445				$adminFields = ', tc1.`user_ip`';
2446			}
2447			$query = "select tc1.`threadId`, tc1.`object`, tc1.`objectType`, tc1.`parentId`, tc1.`userName`, tc1.`commentDate`, tc1.`hits`, tc1.`type`, tc1.`points`, tc1.`votes`, tc1.`average`, tc1.`title`, tc1.`data`, tc1.`hash`, tc1.`summary`, tc1.`smiley`, tc1.`message_id`, tc1.`in_reply_to`, tc1.`comment_rating`, tc1.`approved`, tc1.`locked`$adminFields  from `tiki_comments` as tc1
2448				left outer join `tiki_comments` as tc2 on tc1.`in_reply_to` = tc2.`message_id`
2449				and tc1.`parentId` = ?
2450				and tc2.`parentId` = ?
2451				$join
2452				$mid
2453				and (tc1.`in_reply_to` = ?
2454						or (tc2.`in_reply_to` = '' or tc2.`in_reply_to` is null or tc2.`message_id` is null or tc2.`parentId` = 0))
2455				$time_cond order by " . $this->convertSortMode($sort_mode) . ", tc1.`threadId`";
2456			$bind_mid_cant = $bind_mid;
2457			$bind_mid = array_merge([$parentId, $parentId], $bind_mid, [$parent_message_id]);
2458
2459			$query_cant = "select count(*) from `tiki_comments` as tc1 $mid $time_cond";
2460		} else {
2461			$query_cant = "select count(*) from `tiki_comments` as tc1 $mid $time_cond";
2462			$query = "select * from `tiki_comments` as tc1 $join $mid $time_cond order by " . $this->convertSortMode($sort_mode) . ",`threadId`";
2463			$bind_mid_cant = $bind_mid;
2464		}
2465
2466		if ($parentId === null) {
2467			$query_cant = str_replace('tc1.`parentId`=? and ', '', $query_cant);
2468			unset($bind_mid_cant[2]);
2469		}
2470
2471		$ret = [];
2472
2473		if ($reply_threadId > 0 && $style == 'commentStyle_threaded') {
2474			$ret[] = $this->get_comments_fathers($reply_threadId, $ret);
2475			$cant = 1;
2476		} else {
2477			$ret = $this->fetchAll($query, array_merge($bind_mid, $bind_time));
2478			$cant = $this->getOne($query_cant, array_merge($bind_mid_cant, $bind_time));
2479		}
2480
2481		foreach ($ret as $key => $res) {
2482			if ($offset > 0  && $orig_offset != 0) {
2483				$ret[$key]['doNotShow'] = 1;
2484			}
2485
2486			if ($maxRecords <= 0  && $orig_maxRecords != 0) {
2487				array_splice($ret, $key);
2488				break;
2489			}
2490
2491			// Get the grandfather
2492			if ($res["parentId"] > 0) {
2493				$ret[$key]["grandFather"] = $this->get_comment_father($res["parentId"]);
2494			} else {
2495				$ret[$key]["grandFather"] = 0;
2496			}
2497
2498			/* Trim to maxRecords, including replies! */
2499			if ($offset >= 0 && $orig_offset != 0) {
2500				$offset = $offset - 1;
2501			}
2502			$maxRecords = $maxRecords - 1;
2503
2504			if (! ($maxRecords <= 0 && $orig_maxRecords != 0)) {
2505				// Get the replies
2506				if (empty($parentId) || $style != 'commentStyle_threaded' || $object[0] == "forum") {
2507					if ($object[0] == "forum") {
2508						// For plain style, don't handle replies at all.
2509						if ($style == 'commentStyle_plain') {
2510							$ret[$key]['replies_info']['numReplies'] = 0;
2511							$ret[$key]['replies_info']['totalReplies'] = 0;
2512						} else {
2513							$ret[$key]['replies_info'] = $this->get_comment_replies(
2514								$res["parentId"],
2515								$initial_sort_mode,
2516								$offset,
2517								$orig_offset,
2518								$maxRecords,
2519								$orig_maxRecords,
2520								$threshold,
2521								$find,
2522								$res["message_id"],
2523								1
2524							);
2525						}
2526					} else {
2527						$ret[$key]['replies_info'] = $this->get_comment_replies(
2528							$res["threadId"],
2529							$initial_sort_mode,
2530							$offset,
2531							$orig_offset,
2532							$maxRecords,
2533							$orig_maxRecords,
2534							$threshold,
2535							$find
2536						);
2537					}
2538
2539					/* Trim to maxRecords, including replies! */
2540					if ($offset >= 0 && $orig_offset != 0) {
2541						$offset = $offset - $ret[$key]['replies_info']['totalReplies'];
2542					}
2543					$maxRecords = $maxRecords - $ret[$key]['replies_info']['totalReplies'];
2544				}
2545			}
2546
2547			if (empty($res["data"])) {
2548				$ret[$key]["isEmpty"] = 'y';
2549			} else {
2550				$ret[$key]["isEmpty"] = 'n';
2551			}
2552
2553			// to be able to distinct between a tiki user and a anonymous name
2554			if (! $userlib->user_exists($ret[$key]['userName'])) {
2555				$ret[$key]['anonymous_name'] = $ret[$key]['userName'];
2556			}
2557
2558			$ret[$key]['version'] = 0;
2559			$ret[$key]['diffInfo'] = [];
2560			if (! empty($ret[$key]['objectType']) && $ret[$key]['objectType'] == 'trackeritem') {
2561				$ret[$key]['version'] = TikiLib::lib('attribute')->get_attribute('comment', $ret[$key]['threadId'], 'tiki.comment.version');
2562				if ($ret[$key]['version']) {
2563					$history = TikiLib::lib('trk')->get_item_history(
2564						['itemId' => $ret[$key]['object']],
2565						0,
2566						['version' => $ret[$key]['version']]
2567					);
2568
2569					foreach ($history['data'] as &$hist) {
2570						$field_info = TikiLib::lib('trk')->get_field_info($hist['fieldId']);
2571						$hist['fieldName'] = $field_info['name'];
2572					}
2573
2574					if (! empty($history['data'])) {
2575						$ret[$key]['diffInfo'] = $history['data'];
2576					}
2577				}
2578			}
2579		}
2580
2581		if ($old_sort_mode == 'replies_asc') {
2582			usort($ret, 'compare_replies');
2583		}
2584
2585		if ($old_sort_mode == 'replies_desc') {
2586			usort($ret, 'r_compare_replies');
2587		}
2588
2589		if (in_array($old_sort_mode, ['replies_desc', 'replies_asc'])) {
2590			$ret = array_slice($ret, $old_offset, $old_maxRecords);
2591		}
2592
2593		$retval = [];
2594		$retval["data"] = $ret;
2595		$retval["below"] = $below;
2596		$retval["cant"] = $cant;
2597
2598		$msgs = count($retval['data']);
2599		for ($i = 0; $i < $msgs; $i++) {
2600			$r = &$retval['data'][$i]['replies_info'];
2601			$retval['data'][$i]['replies_flat'] = [];
2602			$rf = &$retval['data'][$i]['replies_flat'];
2603			$this->flatten_comment_replies($r, $rf);
2604		}
2605
2606		if (count($retval['data']) > $orig_maxRecords) {
2607			$retval['data'] = array_slice($retval['data'], -$orig_maxRecords);
2608		}
2609
2610		foreach ($retval['data'] as & $row) {
2611			$this->add_comments_extras($row);
2612		}
2613
2614		return $retval;
2615	}
2616
2617	/**
2618	 * Return the number of arquived comments for an object
2619	 *
2620	 * @param int|string $objectId
2621	 * @param string $objectType
2622	 * @return int the number of archived comments for an object
2623	 */
2624	function count_object_archived_comments($objectId, $objectType)
2625	{
2626		return $this->table('tiki_comments')->fetchCount(
2627			[
2628				'object' => $objectId,
2629				'objectType' => $objectType,
2630				'archived' => 'y',
2631			]
2632		);
2633	}
2634
2635	/**
2636	 * Return all comments. Administrative functions to get all the comments
2637	 * of some types + enlarge find. No perms checked as it is only for admin
2638	 *
2639	 * @param string|array $type one type or array of types (if empty function will return comments for all types except forum)
2640	 * @param int $offset
2641	 * @param int $maxRecords
2642	 * @param string $sort_mode
2643	 * @param string $find search comment title, data, user name, ip and object
2644	 * @param string $parent
2645	 * @param string $approved set it to y or n to return only approved or rejected comments (leave empty to return all comments)
2646	 * @param bool $toponly
2647	 * @param array|int $objectId limit comments return to one object id or array of objects ids
2648	 */
2649	function get_all_comments(
2650		$type = '',
2651		$offset = 0,
2652		$maxRecords = -1,
2653		$sort_mode = 'commentDate_asc',
2654		$find = '',
2655		$parent = '',
2656		$approved = '',
2657		$toponly = false,
2658		$objectId = ''
2659	) {
2660
2661		$join = '';
2662		if (empty($type)) {
2663			// If no type has been specified, get all comments except those used for forums which must not be handled here
2664			$mid = 'tc.`objectType`!=?';
2665			$bindvars[] = 'forum';
2666		} else {
2667			if (is_array($type)) {
2668				$mid = 'tc.`objectType` in (' . implode(',', array_fill(0, count($type), '?')) . ')';
2669				$bindvars = $type;
2670			} else {
2671				$mid = 'tc.`objectType`=?';
2672				$bindvars[] = $type;
2673			}
2674		}
2675
2676		if ($find) {
2677			$find = "%$find%";
2678			$mid .= ' and (tc.`title` like ? or tc.`data` like ? or tc.`userName` like ? or tc.`user_ip` like ? or tc.`object` like ?)';
2679			$bindvars[] = $find;
2680			$bindvars[] = $find;
2681			$bindvars[] = $find;
2682			$bindvars[] = $find;
2683			$bindvars[] = $find;
2684		}
2685
2686		if (! empty($approved)) {
2687			$mid .= ' and tc.`approved`=?';
2688			$bindvars[] = $approved;
2689		}
2690		if (! empty($objectId)) {
2691			if (is_array($objectId)) {
2692				$mid .= ' and tc.`object` in (' . implode(',', array_fill(0, count($objectId), '?')) . ')';
2693				$bindvars = array_merge($bindvars, $objectId);
2694			} else {
2695				$mid .= ' and tc.`object`=?';
2696				$bindvars[] = $objectId;
2697			}
2698		}
2699
2700		if ($parent != '') {
2701			$join = ' left join `tiki_comments` tc2 on(tc2.`threadId`=tc.`parentId`)';
2702		}
2703
2704		if ($toponly) {
2705			$mid .= ' and tc.`parentId` = 0 ';
2706		}
2707		if ($type == 'forum') {
2708			$join .= ' left join `tiki_forums` tf on (tf.`forumId`=tc.`object`)';
2709			$left = ', tf.`name` as parentTitle';
2710		} else {
2711			$left = ', tc.`title` as parentTitle';
2712		}
2713
2714		$categlib = TikiLib::lib('categ');
2715		if ($jail = $categlib->get_jail()) {
2716			$categlib->getSqlJoin($jail, '`objectType`', 'tc.`object`', $jail_join, $jail_where, $jail_bind, 'tc.`objectType`');
2717		} else {
2718			$jail_join = '';
2719			$jail_where = '';
2720			$jail_bind = [];
2721		}
2722
2723		$query = "select tc.* $left from `tiki_comments` tc $join $jail_join where $mid $jail_where order by " . $this->convertSortMode($sort_mode);
2724		$ret = $this->fetchAll($query, array_merge($bindvars, $jail_bind), $maxRecords, $offset);
2725		$query = "select count(*) from `tiki_comments` tc $jail_join where $mid $jail_where";
2726		$cant = $this->getOne($query, array_merge($bindvars, $jail_bind));
2727		foreach ($ret as &$res) {
2728			$res['href'] = $this->getHref($res['objectType'], $res['object'], $res['threadId']);
2729			$res['parsed'] = $this->parse_comment_data($res['data']);
2730		}
2731		return ['cant' => $cant, 'data' => $ret];
2732	}
2733
2734	/**
2735	 * Return the relative URL for a particular comment
2736	 *
2737	 * @param string $type Object type (e.g. 'wiki page')
2738	 * @param int|string $object object id (can be string for wiki pages or int for objects of other types)
2739	 * @param int $threadId Id of a specific comment or forum thread
2740	 * @return void|string void if unrecognized type or URL string otherwise
2741	 */
2742	function getHref($type, $object, $threadId)
2743	{
2744		global $prefs;
2745		switch ($type) {
2746			case 'wiki page':
2747				$href = 'tiki-index.php?page=';
2748				$object = urlencode($object);
2749				break;
2750			case 'article':
2751				$href = 'tiki-read_article.php?articleId=';
2752				break;
2753			case 'faq':
2754				$href = 'tiki-view_faq.php?faqId=';
2755				break;
2756			case 'blog':
2757				$href = 'tiki-view_blog.php?blogId=';
2758				break;
2759			case 'blog post':
2760				$href = 'tiki-view_blog_post.php?postId=';
2761				break;
2762			case 'forum':
2763				$href = 'tiki-view_forum_thread.php?forumId=';
2764				break;
2765			case 'file gallery':
2766				$href = 'tiki-list_file_gallery.php?galleryId=';
2767				break;
2768			case 'image gallery':
2769				$href = 'tiki-browse_gallery.php?galleryId=';
2770				break;
2771			case 'poll':
2772				$href = 'tiki-poll_results.php?pollId=';
2773				break;
2774			case 'trackeritem':
2775				$href = 'tiki-view_tracker_item.php?itemId=';
2776				break;
2777			default:
2778				break;
2779		}
2780
2781		if (empty($href)) {
2782			return;
2783		}
2784
2785		if ($type == 'trackeritem') {
2786			if ($prefs['tracker_show_comments_below'] == 'y') {
2787				$href .= $object . "&threadId=$threadId&cookietab=1#threadId$threadId";
2788			} else {
2789				$href .= $object . "&threadId=$threadId&cookietab=2#threadId$threadId";
2790			}
2791		} else {
2792			$href .= $object . "&amp;threadId=$threadId&amp;comzone=show#threadId$threadId";
2793		}
2794
2795		return $href;
2796	}
2797
2798	/* @brief: gets the comments of the thread and of all its fathers (ex cept first one for forum)
2799	 */
2800	function get_comments_fathers($threadId, $ret = null, $message_id = null)
2801	{
2802		$com = $this->get_comment($threadId, $message_id);
2803
2804		if ($com['objectType'] == 'forum' && $com['parentId'] == 0) {// don't want the 1 level
2805			return $ret;
2806		}
2807		if ($ret) {
2808			$com['replies_info']['replies'][0] = $ret;
2809			$com['replies_info']['numReplies'] = 1;
2810			$com['replies_info']['totalReplies'] = 1;
2811		}
2812		if ($com['objectType'] == 'forum' && $com['in_reply_to']) {
2813			return $this->get_comments_fathers(null, $com, $com['in_reply_to']);
2814		} elseif ($com['parentId'] > 0) {
2815			return $this->get_comments_fathers($com['parentId'], $com);
2816		} else {
2817			return $com;
2818		}
2819	}
2820
2821	/**
2822	 * @param $threadId
2823	 */
2824	function lock_comment($threadId)
2825	{
2826		$this->table('tiki_comments')->update(['locked' => 'y'], ['threadId' => $threadId]);
2827	}
2828
2829	/**
2830	 * @param $threadId
2831	 * @param $objectId
2832	 */
2833	function set_comment_object($threadId, $objectId)
2834	{
2835		// Break out the type and object parameters.
2836		$object = explode(":", $objectId, 2);
2837
2838		$data = [
2839			'objectType' => $object[0],
2840			'object' => $object[1],
2841		];
2842		$this->table('tiki_comments')->update($data, ['threadId' => $threadId]);
2843		$this->table('tiki_comments')->updateMultiple($data, ['parentId' => $threadId]);
2844	}
2845
2846	/**
2847	 * @param $threadId
2848	 * @param $parentId
2849	 */
2850	function set_parent($threadId, $parentId)
2851	{
2852		$comments = $this->table('tiki_comments');
2853		$parent_message_id = $comments->fetchOne('message_id', ['threadId' => $parentId]);
2854		$comments->update(
2855			['parentId' => (int) $parentId, 'in_reply_to' => $parent_message_id],
2856			['threadId' => (int) $threadId]
2857		);
2858	}
2859
2860	/**
2861	 * @param $threadId
2862	 */
2863	function unlock_comment($threadId)
2864	{
2865		$this->table('tiki_comments')->update(
2866			['locked' => 'n'],
2867			['threadId' => (int) $threadId]
2868		);
2869	}
2870
2871	// Lock all comments of an object
2872	/**
2873	 * @param $objectId
2874	 * @param string $status
2875	 * @return bool
2876	 */
2877	function lock_object_thread($objectId, $status = 'y')
2878	{
2879		if (empty($objectId)) {
2880			return false;
2881		}
2882		$object = explode(":", $objectId, 2);
2883		if (count($object) < 2) {
2884			return false;
2885		}
2886
2887		// Add object if it does not already exist. We assume it already exists when unlocking.
2888		if ($status == 'y') {
2889			TikiLib::lib('object')->add_object($object[0], $object[1], false);
2890		}
2891
2892		$this->table('tiki_objects')->update(
2893			['comments_locked' => $status],
2894			['type' => $object[0], 'itemId' => $object[1]]
2895		);
2896	}
2897
2898	// Unlock all comments of an object
2899	/**
2900	 * @param $objectId
2901	 * @return bool
2902	 */
2903	function unlock_object_thread($objectId)
2904	{
2905		return $this->lock_object_thread($objectId, 'n');
2906	}
2907
2908	// Get the status of an object (Lock / Unlock)
2909	/**
2910	 * @param $objectId
2911	 * @return bool
2912	 */
2913	function is_object_locked($objectId)
2914	{
2915		if (empty($objectId)) {
2916			return false;
2917		}
2918		$object = explode(":", $objectId, 2);
2919		if (count($object) < 2) {
2920			return false;
2921		}
2922		return 'y' == $this->table('tiki_objects')->fetchOne('comments_locked', ['type' => $object[0], 'itemId' => $object[1]]);
2923	}
2924
2925	/**
2926	 * @param $data
2927	 * @param $objectType
2928	 * @param $threadId
2929	 */
2930	function update_comment_links($data, $objectType, $threadId)
2931	{
2932		if ($objectType == 'forum') {
2933			$type = 'forum post'; // this must correspond to that used in tiki_objects
2934		} else {
2935			$type = $objectType . ' comment'; // comment types are not used in tiki_objects yet but maybe in future
2936		}
2937
2938		$wikilib = TikiLib::lib('wiki');
2939		$wikilib->update_wikicontent_relations($data, $type, (int)$threadId);
2940		$wikilib->update_wikicontent_links($data, $type, (int)$threadId);
2941	}
2942
2943	/**
2944	 * Call wikiplugin_*_rewrite function on wiki plugins used in a post
2945	 *
2946	 * @param $data
2947	 * @param $objectType
2948	 * @param $threadId
2949	 */
2950	function process_save_plugins($data, $objectType, $threadId = null)
2951	{
2952		global $prefs;
2953		if ($objectType == 'forum') {
2954			$type = 'forum post'; // this must correspond to that used in tiki_objects
2955			$wiki_parsed = $prefs['feature_forum_parse'] == 'y' || $prefs['section_comments_parse'] == 'y';
2956		} else {
2957			$type = $objectType . ' comment'; // comment types are not used in tiki_objects yet but maybe in future
2958			$wiki_parsed = $prefs['section_comments_parse'] == 'y';
2959		}
2960
2961		$context = ['type' => $type];
2962		if ($threadId !== null) {
2963			$context['itemId'] = $threadId;
2964		}
2965		$parserlib = TikiLib::lib('parser');
2966		return $parserlib->process_save_plugins($data, $context);
2967	}
2968
2969	/**
2970	 * @param $threadId
2971	 * @param $title
2972	 * @param $comment_rating
2973	 * @param $data
2974	 * @param string $type
2975	 * @param string $summary
2976	 * @param string $smiley
2977	 * @param string $objectId
2978	 * @param string $contributions
2979	 */
2980	function update_comment(
2981		$threadId,
2982		$title,
2983		$comment_rating,
2984		$data,
2985		$type = 'n',
2986		$summary = '',
2987		$smiley = '',
2988		$objectId = '',
2989		$contributions = ''
2990	) {
2991
2992		global $prefs;
2993
2994		$comments = $this->table('tiki_comments');
2995		$comment = $this->get_comment($threadId);
2996		$data = $this->process_save_plugins($data, $comment['objectType'], $threadId);
2997		$hash = md5($title . $data);
2998		$existingThread = $comments->fetchColumn('threadId', ['hash' => $hash]);
2999
3000		// if exactly same title and data comment does not already exist, and is not the current thread
3001		if (empty($existingThread) || in_array($threadId, $existingThread)) {
3002			if ($prefs['feature_actionlog'] == 'y') {
3003				include_once('lib/diff/difflib.php');
3004				$bytes = diff2($comment['data'], $data, 'bytes');
3005				$logslib = TikiLib::lib('logs');
3006				if ($comment['objectType'] == 'forum') {
3007					$logslib->add_action('Updated', $comment['object'], $comment['objectType'], "comments_parentId=$threadId&amp;$bytes#threadId$threadId", '', '', '', '', $contributions);
3008				} else {
3009					$logslib->add_action('Updated', $comment['object'], 'comment', "type=" . $comment['objectType'] . "&amp;$bytes#threadId$threadId", '', '', '', '', $contributions);
3010				}
3011			}
3012			$comments->update(
3013				[
3014					'title' => $title,
3015					'comment_rating' => (int) $comment_rating,
3016					'data' => $data,
3017					'type' => $type,
3018					'summary' => $summary,
3019					'smiley' => $smiley,
3020					'hash' => $hash,
3021				],
3022				['threadId' => (int) $threadId]
3023			);
3024			if ($prefs['feature_contribution'] == 'y') {
3025				$contributionlib = TikiLib::lib('contribution');
3026				$contributionlib->assign_contributions($contributions, $threadId, 'comment', $title, '', '');
3027			}
3028
3029			$this->update_comment_links($data, $comment['objectType'], $threadId);
3030			$type = $this->update_index($comment['objectType'], $threadId);
3031			if ($type == 'forum post') {
3032				TikiLib::events()->trigger(
3033					'tiki.forumpost.update',
3034					[
3035						'type' => $type,
3036						'object' => $threadId,
3037						'parent_id' => $comment['parentId'],
3038						'forum_id' => $comment['object'],
3039						'user' => $GLOBALS['user'],
3040						'title' => $title,
3041						'content' => $data,
3042						'index_handled' => true,
3043					]
3044				);
3045			} else {
3046				if ($comment['objectType'] == 'trackeritem') {
3047					$parentobject = TikiLib::lib('trk')->get_tracker_for_item($comment['object']);
3048				} else {
3049					$parentobject = 'not implemented';
3050				}
3051				TikiLib::events()->trigger(
3052					'tiki.comment.update',
3053					[
3054						'type' => $comment['objectType'],
3055						'object' => $comment['object'],
3056						'parentobject' => $parentobject,
3057						'title' => $title,
3058						'comment' => $threadId,
3059						'user' => $GLOBALS['user'],
3060						'content' => $data,
3061					]
3062				);
3063			}
3064		} // end hash check
3065	}
3066
3067	/**
3068	 * Post a new comment (forum post or comment on some Tiki object)
3069	 *
3070	 * @param string $objectId           object type and id separated by two colon ('wiki page:HomePage' or 'blog post:2')
3071	 * @param int $parentId              id of parent comment of this comment
3072	 * @param string $userName           if empty $anonumous_name is used
3073	 * @param string $title
3074	 * @param string $data
3075	 * @param string $message_id
3076	 * @param string $in_reply_to
3077	 * @param string $type
3078	 * @param string $summary
3079	 * @param string $smiley
3080	 * @param string $contributions
3081	 * @param string $anonymous_name      name when anonymous user post a comment (optional)
3082	 * @param string $postDate            when the post was created (defaults to now)
3083	 * @param string $anonymous_email     optional
3084	 * @param string $anonymous_website   optional
3085	 * @param array $parent_comment_info
3086	 * @param int    $version             version number being commented about (trackers only as yet)
3087	 * @return int $threadId id of the new comment
3088	 * @throws Exception
3089	 */
3090	function post_new_comment(
3091		$objectId,
3092		$parentId,
3093		$userName,
3094		$title,
3095		$data,
3096		&$message_id,
3097		$in_reply_to = '',
3098		$type = 'n',
3099		$summary = '',
3100		$smiley = '',
3101		$contributions = '',
3102		$anonymous_name = '',
3103		$postDate = '',
3104		$anonymous_email = '',
3105		$anonymous_website = '',
3106		$parent_comment_info = [],
3107		$version = 0
3108	) {
3109
3110		global $user;
3111
3112		if ($postDate == '') {
3113			$postDate = $this->now;
3114		}
3115
3116		if (! $userName) {
3117			$_SESSION["lastPost"] = $postDate;
3118		}
3119
3120		// Check for banned userName or banned IP or IP in banned range
3121
3122		// Check for duplicates.
3123		$title = strip_tags($title);
3124
3125		if ($anonymous_name) {
3126			// The leading tab is for recognizing anonymous entries. Normal usernames don't start with a tab
3127			$userName = "\t" . trim($anonymous_name);
3128		} elseif (! $userName) {
3129			$userName = tra('Anonymous');
3130		} elseif ($userName) {
3131			$postings = $this->table('tiki_user_postings');
3132			$count = $postings->fetchCount(['user' => $userName]);
3133
3134			if ($count) {
3135				$postings->update(['last' => (int) $postDate, 'posts' => $postings->increment(1)], ['user' => $userName]);
3136			} else {
3137				$posts = $this->table('tiki_comments')->fetchCount(['userName' => $userName]);
3138
3139				if (! $posts) {
3140					$posts = 1;
3141				}
3142				$postings->insert(['user' => $userName, 'first' => (int) $postDate, 'last' => (int) $postDate, 'posts' => (int) $posts]);
3143			}
3144
3145			// Calculate max
3146			$max = $postings->fetchOne($postings->max('posts'), []);
3147			$min = $postings->fetchOne($postings->min('posts'), []);
3148
3149			$min = max($min, 1);
3150
3151			$ids = $postings->fetchCount([]);
3152			$tot = $postings->fetchOne($postings->sum('posts'), []);
3153			$average = $tot / $ids;
3154			$range1 = ($min + $average) / 2;
3155			$range2 = ($max + $average) / 2;
3156
3157			$posts = $postings->fetchOne('posts', ['user' => $userName]);
3158
3159			if ($posts == $max) {
3160				$level = 5;
3161			} elseif ($posts > $range2) {
3162				$level = 4;
3163			} elseif ($posts > $average) {
3164				$level = 3;
3165			} elseif ($posts > $range1) {
3166				$level = 2;
3167			} else {
3168				$level = 1;
3169			}
3170
3171			$postings->update(['level' => $level], ['user' => $userName]);
3172		}
3173
3174		// Break out the type and object parameters.
3175		$object = explode(":", $objectId, 2);
3176
3177		$data = $this->process_save_plugins($data, $object[0]);
3178
3179		$hash = md5($title . $data);
3180
3181		// Check if we were passed a message-id.
3182		if (! $message_id) {
3183			// Construct a message id via proctological
3184			// extraction. -rlpowell
3185			$message_id = $userName . "-" .
3186				$parentId . "-" .
3187				substr($hash, 0, 10) .
3188				"@" . $_SERVER["SERVER_NAME"];
3189		}
3190
3191		// Handle comments moderation (this should not affect forums and user with admin rights on comments)
3192		$approved = $this->determine_initial_approval(
3193			[
3194				'type' => $object[0],
3195				'author' => $userName,
3196				'email' => $user ? TikiLib::lib('user')->get_user_email($user) : $anonymous_email,
3197				'website' => $anonymous_website,
3198				'content' => $data,
3199			]
3200		);
3201
3202		if ($approved === false) {
3203			Feedback::error(tr('Your comment was rejected.'));
3204			return false;
3205		}
3206
3207		$comments = $this->table('tiki_comments');
3208		$threadId = $comments->fetchOne('threadId', ['hash' => $hash]);
3209
3210		// If this post was not already found.
3211		if (! $threadId) {
3212			$threadId = $comments->insert(
3213				[
3214					'objectType' => $object[0],
3215					'object' => $object[1],
3216					'commentDate' => (int) $postDate,
3217					'userName' => $userName,
3218					'title' => $title,
3219					'data' => $data,
3220					'votes' => 0,
3221					'points' => 0,
3222					'hash' => $hash,
3223					'email' => $anonymous_email,
3224					'website' => $anonymous_website,
3225					'parentId' => (int) $parentId,
3226					'average' => 0,
3227					'hits' => 0,
3228					'type' => $type,
3229					'summary' => $summary,
3230					'user_ip' => $this->get_ip_address(),
3231					'message_id' => $message_id,
3232					'in_reply_to' => $in_reply_to,
3233					'approved' => $approved,
3234					'locked' => 'n',
3235				]
3236			);
3237		}
3238
3239		global $prefs;
3240		if ($prefs['feature_actionlog'] == 'y') {
3241			$logslib = TikiLib::lib('logs');
3242			$tikilib = TikiLib::lib('tiki');
3243			if ($parentId == 0) {
3244				$l = strlen($data);
3245			} else {
3246				$l = $tikilib->strlen_quoted($data);
3247			}
3248			if ($object[0] == 'forum') {
3249				$logslib->add_action(
3250					($parentId == 0) ? 'Posted' : 'Replied',
3251					$object[1],
3252					$object[0],
3253					'comments_parentId=' . $threadId . '&amp;add=' . $l,
3254					'',
3255					'',
3256					'',
3257					'',
3258					$contributions
3259				);
3260			} else {
3261				$logslib->add_action(
3262					($parentId == 0) ? 'Posted' : 'Replied',
3263					$object[1],
3264					'comment',
3265					'type=' . $object[0] . '&amp;add=' . $l . '#threadId=' . $threadId,
3266					'',
3267					'',
3268					'',
3269					'',
3270					$contributions
3271				);
3272			}
3273		}
3274
3275		if ($prefs['feature_contribution'] == 'y') {
3276			$contributionlib = TikiLib::lib('contribution');
3277			$contributionlib->assign_contributions($contributions, $threadId, 'comment', $title, '', '');
3278		}
3279
3280		$this->update_comment_links($data, $object[0], $threadId);
3281		$tx = $this->begin();
3282		$type = $this->update_index($object[0], $threadId, $parentId);
3283		$finalEvent = 'tiki.comment.post';
3284
3285		if ($type == 'forum post') {
3286			$finalEvent = $parentId ? 'tiki.forumpost.reply' : 'tiki.forumpost.create';
3287			if ($parent_comment_info) {
3288				$parent_title = $parent_comment_info['title'];
3289			} else {
3290				$parent_title = '';
3291			}
3292
3293			$forum_info = $this->get_forum($object[1]);
3294
3295			TikiLib::events()->trigger(
3296				$finalEvent,
3297				[
3298					'type' => $type,
3299					'object' => $threadId,
3300					'parent_id' => $parentId,
3301					'forum_id' => $object[1],
3302					'forum_section' => $forum_info['section'],
3303					'user' => $GLOBALS['user'],
3304					'title' => $title,
3305					'name' => $forum_info['name'],
3306					'parent_title' => $parent_title,
3307					'content' => $data,
3308					'index_handled' => true,
3309				]
3310			);
3311		} else {
3312			$finalEvent = $parentId ? 'tiki.comment.reply' : 'tiki.comment.post';
3313
3314			if ($object[0] == 'trackeritem') {
3315				$parentobject = TikiLib::lib('trk')->get_tracker_for_item($object[1]);
3316			} else {
3317				$parentobject = 'not implemented';
3318			}
3319			TikiLib::events()->trigger(
3320				$finalEvent,
3321				[
3322					'type' => $object[0],
3323					'object' => $object[1],
3324					'parentobject' => $parentobject,
3325					'user' => $GLOBALS['user'],
3326					'title' => $title,
3327					'content' => $data,
3328				]
3329			);
3330		}
3331
3332		// store the related version being commented about as an attribute of this comment
3333		if ($version) {
3334			TikiLib::lib('attribute')->set_attribute('comment', $threadId, 'tiki.comment.version', $version);
3335		}
3336
3337		$tx->commit();
3338
3339		return $threadId;
3340		//return $return_result;
3341	}
3342
3343	/**
3344	 * @param array $info
3345	 * @return bool|string
3346	 */
3347	private function determine_initial_approval(array $info)
3348	{
3349		global $prefs, $tiki_p_admin_comments;
3350
3351		if ($tiki_p_admin_comments == 'y' || $info['type'] == 'forum') {
3352			return 'y';
3353		}
3354
3355		if ($prefs['comments_akismet_filter'] == 'y') {
3356			$isSpam = $this->check_is_spam($info);
3357
3358			if ($prefs['feature_comments_moderation'] == 'y') {
3359				return $isSpam ? 'n' : 'y';
3360			} else {
3361				return $isSpam ? false : 'y';
3362			}
3363		} else {
3364			return ($prefs['feature_comments_moderation'] == 'y') ? 'n' : 'y';
3365		}
3366	}
3367
3368	/**
3369	 * @param array $info
3370	 * @return bool
3371	 */
3372	private function check_is_spam(array $info)
3373	{
3374		global $prefs, $user;
3375
3376		if ($prefs['comments_akismet_filter'] != 'y') {
3377			return false;
3378		}
3379
3380		if ($user && $prefs['comments_akismet_check_users'] != 'y') {
3381			return false;
3382		}
3383
3384		try {
3385			$tikilib = TikiLib::lib('tiki');
3386
3387			$url = $tikilib->tikiUrl();
3388			$httpClient = $tikilib->get_http_client();
3389			$akismet = new ZendService\Akismet\Akismet($prefs['comments_akismet_apikey'], $url, $httpClient);
3390
3391			return $akismet->isSpam(
3392				[
3393					'user_ip' => $tikilib->get_ip_address(),
3394					'user_agent' => $_SERVER['HTTP_USER_AGENT'],
3395					'referrer' => $_SERVER['HTTP_REFERER'],
3396					'comment_type' => 'comment',
3397					'comment_author' => $info['author'],
3398					'comment_author_email' => $info['email'],
3399					'comment_author_url' => $info['website'],
3400					'comment_content' => $info['content'],
3401				]
3402			);
3403		} catch (Exception $e) {
3404			Feedback::error(tr('Cannot perform spam check: %0', $e->getMessage()));
3405			return false;
3406		}
3407	}
3408
3409	// Check if a particular topic exists.
3410	/**
3411	 * @param $title
3412	 * @param $data
3413	 * @return mixed
3414	 */
3415	function check_for_topic($title, $data)
3416	{
3417		$hash = md5($title . $data);
3418		$comments = $this->table('tiki_comments');
3419		return $comments->fetchOne($comments->min('threadId'), ['hash' => $hash]);
3420	}
3421
3422	/**
3423	 * @param $threadId
3424	 * @param string $status
3425	 * @return bool
3426	 */
3427	function approve_comment($threadId, $status = 'y')
3428	{
3429		if ($threadId == 0) {
3430			return false;
3431		}
3432
3433		return (bool) $this->table('tiki_comments')->update(['approved' => $status], ['threadId' => $threadId]);
3434	}
3435
3436	/**
3437	 * @param $threadId
3438	 * @return bool
3439	 */
3440	function reject_comment($threadId)
3441	{
3442		return $this->approve_comment($threadId, 'r');
3443	}
3444
3445	/**
3446	 * @param $threadId
3447	 * @return bool
3448	 */
3449	function remove_comment($threadId)
3450	{
3451		if ($threadId == 0) {
3452			return false;
3453		}
3454		global $prefs;
3455
3456		$this->delete_forum_deliberations($threadId);
3457
3458		$comments = $this->table('tiki_comments');
3459		$threadOrParent = $comments->expr('`threadId` = ? OR `parentId` = ?', [(int) $threadId, (int) $threadId]);
3460		$result = $comments->fetchAll($comments->all(), ['threadId' => $threadOrParent]);
3461		foreach ($result as $res) {
3462			if ($res['objectType'] == 'forum') {
3463				$this->remove_object('forum post', $res['threadId']);
3464				if ($prefs['feature_actionlog'] == 'y') {
3465					$logslib = TikiLib::lib('logs');
3466					$logslib->add_action('Removed', $res['object'], 'forum', "comments_parentId=$threadId&amp;del=" . strlen($res['data']));
3467				}
3468			} else {
3469				$this->remove_object($res['objectType'] . ' comment', $res['threadId']);
3470				if ($prefs['feature_actionlog'] == 'y') {
3471					$logslib = TikiLib::lib('logs');
3472					$logslib->add_action(
3473						'Removed',
3474						$res['object'],
3475						'comment',
3476						'type=' . $res['objectType'] . '&amp;del=' . strlen($res['data']) . "threadId#$threadId"
3477					);
3478				}
3479			}
3480			if ($prefs['feature_contribution'] == 'y') {
3481				$contributionlib = TikiLib::lib('contribution');
3482				$contributionlib->remove_comment($res['threadId']);
3483			}
3484
3485			$this->table('tiki_user_watches')->deleteMultiple(['object' => (int) $threadId, 'type' => 'forum topic']);
3486			$this->table('tiki_group_watches')->deleteMultiple(['object' => (int) $threadId, 'type' => 'forum topic']);
3487		}
3488
3489		$comments->deleteMultiple(['threadId' => $threadOrParent]);
3490		//TODO in a forum, when the reply to a post (not a topic) id deletd, the replies to this post are not deleted
3491
3492		$this->remove_stale_comment_watches();
3493
3494		$this->remove_reported($threadId);
3495
3496		$atts = $this->table('tiki_forum_attachments')->fetchAll(['attId'], ['threadId' => $threadId]);
3497		foreach ($atts as $att) {
3498			$this->remove_thread_attachment($att['attId']);
3499		}
3500
3501		// remove range attribute for inline "annotation" comments
3502		TikiLib::lib('attribute')->set_attribute(
3503			'comment',
3504			$threadId,
3505			'tiki.comment.ranges',
3506			''
3507		);
3508
3509		$tx = $this->begin();
3510		// Update search index after deletion is done
3511		foreach ($result as $res) {
3512			$this->update_index($res['objectType'], $res['threadId']);
3513			refresh_index($res['objectType'], $res['object']);
3514		}
3515		$tx->commit();
3516
3517		return true;
3518	}
3519
3520	/**
3521	 * @param $threadId
3522	 * @param $user
3523	 * @param $vote
3524	 * @return bool
3525	 */
3526	function vote_comment($threadId, $user, $vote)
3527	{
3528		$userpoints = $this->table('tiki_userpoints');
3529		$comments = $this->table('tiki_comments');
3530
3531		// Select user points for the user who is voting (it may be anonymous!)
3532		$res = $userpoints->fetchRow(['points', 'voted'], ['user' => $user]);
3533
3534		if ($res) {
3535			$user_points = $res["points"];
3536			$user_voted = $res["voted"];
3537		} else {
3538			$user_points = 0;
3539			$user_voted = 0;
3540		}
3541
3542		// Calculate vote weight (the Karma System)
3543		if ($user_voted == 0) {
3544			$user_weight = 1;
3545		} else {
3546			$user_weight = $user_points / $user_voted;
3547		}
3548
3549		$vote_weight = ($vote * $user_weight) / 5;
3550
3551		// Get the user that posted the comment being voted
3552		$comment_user = $comments->fetchOne('userName', ['threadId' => (int) $threadId]);
3553
3554		if ($comment_user && ($comment_user == $user)) {
3555			// The user is voting a comment posted by himself then bail out
3556			return false;
3557		}
3558
3559		//print("Comment user: $comment_user<br />");
3560		if ($comment_user) {
3561			// Update the user points adding this new vote
3562			$count = $userpoints->fetchCount(['user' => $comment_user]);
3563
3564			if ($count) {
3565				$userpoints->update(
3566					['points' => $userpoints->increment($vote), 'voted' => $userpoints->increment(1)],
3567					['user' => $user]
3568				);
3569			} else {
3570				$userpoints->insert(['user' => $comment_user,	'points' => $vote, 'voted' => 1]);
3571			}
3572		}
3573
3574		$comments->update(
3575			['points' => $comments->increment($vote_weight), 'votes' => $comments->increment(1)],
3576			['threadId' => $threadId,]
3577		);
3578		$query = "update `tiki_comments` set `average` = `points`/`votes` where `threadId`=?";
3579		$result = $this->query($query, [$threadId]);
3580		return true;
3581	}
3582
3583	/**
3584	 * @param $forumId
3585	 * @param $name
3586	 * @param string $description
3587	 * @return int
3588	 */
3589	function duplicate_forum($forumId, $name, $description = '')
3590	{
3591		$forum_info = $this->get_forum($forumId);
3592		$newForumId = $this->replace_forum(
3593			0,
3594			$name,
3595			$description,
3596			$forum_info['controlFlood'],
3597			$forum_info['floodInterval'],
3598			$forum_info['moderator'],
3599			$forum_info['mail'],
3600			$forum_info['useMail'],
3601			$forum_info['usePruneUnreplied'],
3602			$forum_info['pruneUnrepliedAge'],
3603			$forum_info['usePruneOld'],
3604			$forum_info['pruneMaxAge'],
3605			$forum_info['topicsPerPage'],
3606			$forum_info['topicOrdering'],
3607			$forum_info['threadOrdering'],
3608			$forum_info['section'],
3609			$forum_info['topics_list_reads'],
3610			$forum_info['topics_list_replies'],
3611			$forum_info['topics_list_pts'],
3612			$forum_info['topics_list_lastpost'],
3613			$forum_info['topics_list_author'],
3614			$forum_info['vote_threads'],
3615			$forum_info['show_description'],
3616			$forum_info['inbound_pop_server'],
3617			$forum_info['inbound_pop_port'],
3618			$forum_info['inbound_pop_user'],
3619			$forum_info['inbound_pop_password'],
3620			$forum_info['outbound_address'],
3621			$forum_info['outbound_mails_for_inbound_mails'],
3622			$forum_info['outbound_mails_reply_link'],
3623			$forum_info['outbound_from'],
3624			$forum_info['topic_smileys'],
3625			$forum_info['topic_summary'],
3626			$forum_info['ui_avatar'],
3627			$forum_info['ui_rating_choice_topic'],
3628			$forum_info['ui_flag'],
3629			$forum_info['ui_posts'],
3630			$forum_info['ui_level'],
3631			$forum_info['ui_email'],
3632			$forum_info['ui_online'],
3633			$forum_info['approval_type'],
3634			$forum_info['moderator_group'],
3635			$forum_info['forum_password'],
3636			$forum_info['forum_use_password'],
3637			$forum_info['att'],
3638			$forum_info['att_store'],
3639			$forum_info['att_store_dir'],
3640			$forum_info['att_max_size'],
3641			$forum_info['forum_last_n'],
3642			$forum_info['commentsPerPage'],
3643			$forum_info['threadStyle'],
3644			$forum_info['is_flat'],
3645			$forum_info['att_list_nb'],
3646			$forum_info['topics_list_lastpost_title'],
3647			$forum_info['topics_list_lastpost_avatar'],
3648			$forum_info['topics_list_author_avatar']
3649		);
3650
3651		return $newForumId;
3652	}
3653
3654	/**
3655	 * Archive thread or comment (only admins can archive
3656	 * comments or see them). This is used both for forums
3657	 * and comments.
3658	 *
3659	 * @param int $threadId the comment or thread id
3660	 * @param int $parentId
3661	 * @return bool|TikiDb_Adodb_Result|TikiDb_Pdo_Result
3662	 */
3663	function archive_thread($threadId, $parentId = 0)
3664	{
3665		if ($threadId > 0 && $parentId >= 0) {
3666			return $this->table('tiki_comments')->update(
3667				['archived' => 'y'],
3668				['threadId' => (int) $threadId, 'parentId' => (int) $parentId]
3669			);
3670		}
3671		return false;
3672	}
3673
3674	/**
3675	 * Unarchive thread or comment (only admins can archive
3676	 * comments or see them).
3677	 *
3678	 * @param int $threadId the comment or thread id
3679	 * @param int $parentId
3680	 * @return bool|TikiDb_Adodb_Result|TikiDb_Pdo_Result
3681	 */
3682	function unarchive_thread($threadId, $parentId = 0)
3683	{
3684		if ($threadId > 0 && $parentId >= 0) {
3685			return $this->table('tiki_comments')->update(
3686				['archived' => 'n'],
3687				['threadId' => (int) $threadId, 'parentId' => (int) $parentId]
3688			);
3689		}
3690		return false;
3691	}
3692
3693	/**
3694	 * @return array
3695	 */
3696	function list_directories_to_save()
3697	{
3698		$dirs = [];
3699		$forums = $this->list_forums();
3700		foreach ($forums['data'] as $forum) {
3701			if (! empty($forum['att_store_dir'])) {
3702				$dirs[] = $forum['att_store_dir'];
3703			}
3704		}
3705		return $dirs;
3706	}
3707
3708	/**
3709	 * @return array
3710	 */
3711	function get_outbound_emails()
3712	{
3713		$forums = $this->table('tiki_forums');
3714		$ret = $forums->fetchAll(
3715			['forumId', 'outbound_address' => 'mail'],
3716			['useMail' => 'y',	'mail' => $forums->not('')]
3717		);
3718		$result = $forums->fetchAll(
3719			['forumId', 'outbound_address'],
3720			['outbound_address' => $forums->not('')]
3721		);
3722		return array_merge($ret, $result);
3723	}
3724
3725	/* post a topic or a reply in forum
3726	 * @param array forum_info
3727	 * @param array $params: list of options($_REQUEST)
3728	  * @return the threadId
3729	 * @return $feedbacks, $errors */
3730	/**
3731	 * @param $forum_info
3732	 * @param $params
3733	 * @param $feedbacks
3734	 * @param $errors
3735	 * @return bool|int
3736	 */
3737	function post_in_forum($forum_info, &$params, &$feedbacks, &$errors)
3738	{
3739		global $tiki_p_admin_forum, $tiki_p_forum_post_topic;
3740		global $tiki_p_forum_post, $prefs, $user, $tiki_p_forum_autoapp;
3741		$captchalib = TikiLib::lib('captcha');
3742		$smarty = TikiLib::lib('smarty');
3743		$tikilib = TikiLib::lib('tiki');
3744
3745		if (! empty($params['comments_grandParentId'])) {
3746			$parent_id = $params['comments_grandParentId'];
3747		} elseif (! empty($params['comments_parentId'])) {
3748			$parent_id = $params['comments_parentId'];
3749		} else {
3750			$parent_id = 0;
3751		}
3752		if (! ($tiki_p_admin_forum == 'y' || ($parent_id == 0 && $tiki_p_forum_post_topic == 'y') || ($parent_id > 0 && $tiki_p_forum_post == 'y'))) {
3753			$errors[] = tra('Permission denied');
3754			return 0;
3755		}
3756		if ($forum_info['is_locked'] == 'y') {
3757			$smarty->assign('msg', tra("This forum is locked"));
3758			$smarty->display("error.tpl");
3759			die;
3760		}
3761		$parent_comment_info = $this->get_comment($parent_id);
3762		if ($parent_comment_info['locked'] == 'y') {
3763			$smarty->assign('msg', tra("This thread is locked"));
3764			$smarty->display("error.tpl");
3765			die;
3766		}
3767
3768		if (empty($user) && $prefs['feature_antibot'] == 'y' && ! $captchalib->validate()) {
3769			$errors[] = $captchalib->getErrors();
3770		}
3771		if ($forum_info['controlFlood'] == 'y' && ! $this->user_can_post_to_forum($user, $forum_info['forumId'])) {
3772			$errors[] = tr('Please wait %0 seconds between posts', $forum_info['floodInterval']);
3773		}
3774		if ($tiki_p_admin_forum != 'y' && $forum_info['forum_use_password'] != 'n' && $params['password'] != $forum_info['forum_password']) {
3775			$errors[] = tra('Wrong password. Cannot post comment');
3776		}
3777		if ($parent_id > 0 && $forum_info['is_flat'] == 'y' && $params['comments_grandParentId'] > 0) {
3778			$errors[] = tra("This forum is flat and doesn't allow replies to other replies");
3779		}
3780		if ($prefs['feature_contribution'] == 'y' && $prefs['feature_contribution_mandatory_forum'] == 'y' && empty($params['contributions'])) {
3781			$errors[] = tra('A contribution is mandatory');
3782		}
3783		//if original post, comment title is necessary. Message is also necessary unless, pref says message is not.
3784		if (empty($params['comments_reply_threadId'])) {
3785			if (empty($params['comments_title']) || (empty($params['comments_data']) && $prefs['feature_forums_allow_thread_titles'] != 'y')) {
3786				$errors[] = tra('Please enter a Title and Message for your new forum topic.');
3787			}
3788		} else {
3789			//if comments require title and no title is given, or if message is empty
3790			if ($prefs['comments_notitle'] != 'y' && (empty($params['comments_title']) || empty($params['comments_data']))) {
3791				$errors[] = tra('Please enter a Title and Message for your forum reply.');
3792			} elseif (empty($params['comments_data'])) { //if comments do not require title but message is empty
3793				$errors[] = tra('Please enter a Message for your forum reply.');
3794			}
3795		}
3796		if (! empty($params['anonymous_email']) && ! validate_email($params['anonymous_email'], $prefs['validateEmail'])) {
3797			$errors[] = tra('Invalid Email');
3798		}
3799		// what do we do???
3800
3801		if (! empty($errors)) {
3802			return 0;
3803		}
3804
3805		$data = $params['comments_data'];
3806
3807		// Strip (HTML) tags. Tags in CODE plugin calls are spared using plugins_remove().
3808		//TODO: Use a standardized sanitization (if any)
3809		$noparsed = ['key' => [], 'data' => []];
3810		$parserlib = TikiLib::lib('parser');
3811		$parserlib->plugins_remove($data, $noparsed, function ($match) {
3812			return $match->getName() == 'code';
3813		});
3814		$data = strip_tags($data);
3815		$data = str_replace($noparsed['key'], $noparsed['data'], $data);
3816
3817		$params['comments_data'] = rtrim($data);
3818
3819		if ($tiki_p_admin_forum != 'y') {// non admin can only post normal
3820			$params['comment_topictype'] = 'n';
3821			if ($forum_info['topic_summary'] != 'y') {
3822				$params['comment_topicsummary'] = '';
3823			}
3824			if ($forum_info['topic_smileys'] != 'y') {
3825				$params['comment_topicsmiley'] = '';
3826			}
3827		}
3828		if (isset($params['comments_postComment_anonymous']) && ! empty($user) && $prefs['feature_comments_post_as_anonymous'] == 'y') {
3829			$params['comments_postComment'] = $params['comments_postComment_anonymous'];
3830			$user = '';
3831		}
3832		if (! isset($params['comment_topicsummary'])) {
3833			$params['comment_topicsummary'] = '';
3834		}
3835		if (! isset($params['comment_topicsmiley'])) {
3836			$params['comment_topicsmiley'] = '';
3837		}
3838		if (isset($params['anonymous_name'])) {
3839			$params['anonymous_name'] = trim(strip_tags($params['anonymous_name']));
3840		} else {
3841			$params['anonymous_name'] = '';
3842		}
3843		if (! isset($params['freetag_string'])) {
3844			$params['freetag_string'] = '';
3845		}
3846		if (! isset($params['anonymous_email'])) {
3847			$params['anonymous_email'] = '';
3848		}
3849		if (isset($params['comments_reply_threadId']) && ! empty($params['comments_reply_threadId'])) {
3850			$reply_info = $this->get_comment($params['comments_reply_threadId']);
3851			$in_reply_to = $reply_info['message_id'];
3852		} else {
3853			$in_reply_to = '';
3854		}
3855		$comments_objectId = 'forum:' . $params['forumId'];
3856
3857		if (($tiki_p_forum_autoapp != 'y')
3858				&& ($forum_info['approval_type'] == 'queue_all' || (! $user && $forum_info['approval_type'] == 'queue_anon'))) {
3859			$threadId = 0;
3860			$feedbacks[] = tra('Your message has been queued for approval and will be posted after a moderator approves it.');
3861			$qId = $this->replace_queue(
3862				0,
3863				$forum_info['forumId'],
3864				$comments_objectId,
3865				$parent_id,
3866				$user,
3867				$params['comments_title'],
3868				$params['comments_data'],
3869				$params['comment_topictype'],
3870				$params['comment_topicsmiley'],
3871				$params['comment_topicsummary'],
3872				isset($parent_comment_info['title']) ? $parent_comment_info['title'] : $params['comments_title'],
3873				$in_reply_to,
3874				$params['anonymous_name'],
3875				$params['freetag_string'],
3876				$params['anonymous_email'],
3877				isset($params['comments_threadId']) ? $params['comments_threadId'] : 0
3878			);
3879
3880			if ($prefs['forum_moderator_notification'] == 'y') {
3881				// Deal with mail notifications.
3882				include_once('lib/notifications/notificationemaillib.php');
3883				sendForumEmailNotification(
3884					'forum_post_queued',
3885					$forum_info['forumId'],
3886					$forum_info,
3887					$params['comments_title'],
3888					$params['comments_data'],
3889					$user,
3890					isset($parent_comment_info['title']) ? $parent_comment_info['title'] : $params['comments_title'],
3891					$message_id,
3892					$in_reply_to,
3893					! empty($params['comments_threadId']) ? $params['comments_threadId'] : 0,
3894					isset($params['comments_parentId']) ? $params['comments_parentId'] : 0,
3895					isset($params['contributions']) ? $params['contributions'] : '',
3896					$qId
3897				);
3898			}
3899		} else { // not in queue mode
3900			$qId = 0;
3901
3902			if ($params['comments_threadId'] == 0) { // new post
3903				$message_id = '';
3904
3905
3906				// The thread/topic does not already exist
3907				if (! $params['comments_threadId']) {
3908					$threadId =	$this->post_new_comment(
3909						$comments_objectId,
3910						$parent_id,
3911						$user,
3912						$params['comments_title'],
3913						$params['comments_data'],
3914						$message_id,
3915						$in_reply_to,
3916						$params['comment_topictype'],
3917						$params['comment_topicsummary'],
3918						$params['comment_topicsmiley'],
3919						isset($params['contributions']) ? $params['contributions'] : '',
3920						$params['anonymous_name'],
3921						'',
3922						$params['anonymous_email'],
3923						'',
3924						$parent_comment_info
3925					);
3926					// The thread *WAS* successfully created.
3927
3928					if ($threadId) {
3929						// Deal with mail notifications.
3930						include_once('lib/notifications/notificationemaillib.php');
3931						sendForumEmailNotification(
3932							empty($params['comments_reply_threadId']) ? 'forum_post_topic' : 'forum_post_thread',
3933							$params['forumId'],
3934							$forum_info,
3935							$params['comments_title'],
3936							$params['comments_data'],
3937							$user,
3938							$params['comments_title'],
3939							$message_id,
3940							$in_reply_to,
3941							$threadId,
3942							isset($params['comments_parentId']) ? $params['comments_parentId'] : 0,
3943							isset($params['contributions']) ? $params['contributions'] : ''
3944						);
3945						// Set watch if requested
3946						if ($prefs['feature_user_watches'] == 'y') {
3947							if ($user && isset($params['set_thread_watch']) && $params['set_thread_watch'] == 'y') {
3948								$this->add_user_watch(
3949									$user,
3950									'forum_post_thread',
3951									$threadId,
3952									'forum topic',
3953									$forum_info['name'] . ':' . $params['comments_title'],
3954									'tiki-view_forum_thread.php?comments_parentId=' . $threadId
3955								);
3956							} elseif (! empty($params['anonymous_email'])) { // Add an anonymous watch, if email address supplied.
3957								$this->add_user_watch(
3958									$params['anonymous_name'] . ' ' . tra('(not registered)'),
3959									$prefs['site_language'],
3960									'forum_post_thread',
3961									$threadId,
3962									'forum topic',
3963									$forum_info['name'] . ':' . $params['comments_title'],
3964									'tiki-view_forum_thread.php?comments_parentId=' . $threadId,
3965									$params['anonymous_email'],
3966									isset($prefs['language']) ? $prefs['language'] : ''
3967								);
3968							}
3969						}
3970
3971						// TAG Stuff
3972						$cat_type = 'forum post';
3973						$cat_objid = $threadId;
3974						$cat_desc = substr($params['comments_data'], 0, 200);
3975						$cat_name = $params['comments_title'];
3976						$cat_href = 'tiki-view_forum_thread.php?comments_parentId=' . $threadId;
3977						include('freetag_apply.php');
3978					}
3979				}
3980
3981				$this->register_forum_post($forum_info['forumId'], 0);
3982			} elseif ($tiki_p_admin_forum == 'y' || $this->user_can_edit_post($user, $params['comments_threadId'])) {
3983				$threadId = $params['comments_threadId'];
3984				$this->update_comment(
3985					$threadId,
3986					$params['comments_title'],
3987					'',
3988					($params['comments_data']),
3989					$params['comment_topictype'],
3990					$params['comment_topicsummary'],
3991					$params['comment_topicsmiley'],
3992					$comments_objectId,
3993					isset($params['contributions']) ? $params['contributions'] : ''
3994				);
3995			}
3996		}
3997		if (! empty($threadId) || ! empty($qId)) {
3998			// PROCESS ATTACHMENT HERE
3999			if (isset($_FILES['userfile1']) && ! empty($_FILES['userfile1']['name'])) {
4000				if (is_uploaded_file($_FILES['userfile1']['tmp_name'])) {
4001					$fp = fopen($_FILES['userfile1']['tmp_name'], 'rb');
4002					$ret = $this->add_thread_attachment(
4003						$forum_info,
4004						$threadId,
4005						$errors,
4006						$_FILES['userfile1']['name'],
4007						$_FILES['userfile1']['type'],
4008						$_FILES['userfile1']['size'],
4009						0,
4010						$qId,
4011						$fp,
4012						''
4013					);
4014					fclose($fp);
4015				} else {
4016					$errors[] = $this->uploaded_file_error($_FILES['userfile1']['error']);
4017				}
4018			} //END ATTACHMENT PROCESSING
4019
4020			//PROCESS FORUM DELIBERATIONS HERE
4021			if (! empty($params['forum_deliberation_description'])) {
4022				$this->add_forum_deliberations($threadId, $params['forum_deliberation_description'], $params['forum_deliberation_options'], $params['rating_override']);
4023			}
4024			//END FORUM DELIBERATIONS HERE
4025		}
4026		if (! empty($errors)) {
4027			return 0;
4028		} elseif ($qId) {
4029			return $qId;
4030		} else {
4031			return $threadId;
4032		}
4033	}
4034
4035	/**
4036	 * @param $threadId
4037	 * @param array $items
4038	 * @param array $options
4039	 * @param array $rating_override
4040	 */
4041	function add_forum_deliberations($threadId, $items = [], $options = [], $rating_override = [])
4042	{
4043		global $user;
4044
4045		foreach ($items as $i => $item) {
4046			$message_id = (isset($message_id) ? $message_id . $i : null);
4047			$deliberation_id = $this->post_new_comment(
4048				"forum_deliberation:$threadId",
4049				0,
4050				$user,
4051				json_encode(['item' => $i,'thread' => $threadId]),
4052				$item,
4053				$message_id
4054			);
4055
4056			if (isset($rating_override[$i])) {
4057				$ratinglib = TikiLib::lib('rating');
4058				$ratinglib->set_override('comment', $deliberation_id, $rating_override[$i]);
4059			}
4060		}
4061	}
4062
4063	/**
4064	 * @param $threadId
4065	 * @return mixed
4066	 */
4067	function get_forum_deliberations($threadId)
4068	{
4069		$ratinglib = TikiLib::lib('rating');
4070
4071		$deliberations = $this->fetchAll('SELECT * from tiki_comments WHERE object = ? AND objectType = "forum_deliberation"', [$threadId]);
4072
4073		$votings = [];
4074		$deliberationsUnsorted = [];
4075		foreach ($deliberations as &$deliberation) {
4076			$votings[$deliberation['threadId']] = $ratinglib->votings($deliberation['threadId'], 'comment', true);
4077			$deliberationsUnsorted[$deliberation['threadId']] = $deliberation;
4078		}
4079		unset($deliberations);
4080
4081		arsort($votings);
4082
4083		$deliberationsSorted = [];
4084		foreach ($votings as $threadId => $vote) {
4085			$deliberationsSorted[] = $deliberationsUnsorted[$threadId];
4086		}
4087
4088		unset($deliberationsUnsorted);
4089
4090		return $deliberationsSorted;
4091	}
4092
4093	/**
4094	 * @param $threadId
4095	 */
4096	function delete_forum_deliberations($threadId)
4097	{
4098		$this->table('tiki_comments')->deleteMultiple(
4099			[
4100				'object' => (int)$threadId,
4101				'objectType' => 'forum_deliberation'
4102			]
4103		);
4104	}
4105
4106	/**
4107	 * @param $threadId
4108	 * @param int $offset
4109	 * @param $maxRecords
4110	 * @param string $sort_mode
4111	 * @return array
4112	 */
4113	function get_all_thread_attachments($threadId, $offset = 0, $maxRecords = -1, $sort_mode = 'created_desc')
4114	{
4115		$query = 'select tfa.* from `tiki_forum_attachments` tfa, `tiki_comments` tc where tc.`threadId`=tfa.`threadId` and ((tc.`threadId`=? and tc.`parentId`=?) or tc.`parentId`=?) order by ' . $this->convertSortMode($sort_mode);
4116		$bindvars = [$threadId, 0, $threadId];
4117		$ret = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
4118		$query = 'select count(*) from `tiki_forum_attachments` tfa, `tiki_comments` tc where tc.`threadId`=tfa.`threadId` and ((tc.`threadId`=? and tc.`parentId`=?) or tc.`parentId`=?)';
4119		$cant = $this->getOne($query, $bindvars);
4120		return ['cant' => $cant, 'data' => $ret];
4121	}
4122
4123	/**
4124	 * Particularly useful for flat forums, you get the position and page of a comment.
4125	 *
4126	 * @param $comment_id
4127	 * @param $parent_id
4128	 * @param $sort_mode
4129	 * @param $max_per_page
4130	 */
4131	function get_comment_position($comment_id, $parent_id, $sort_mode, $max_per_page, $show_approved = 'y')
4132	{
4133
4134		$bindvars = [$parent_id];
4135		$query = "SELECT `threadId` FROM `tiki_comments` tc WHERE (tc.`parentId`=?)";
4136		if ($show_approved == "y") {
4137			$query .= " AND tc.`approved` = 'y'";
4138		}
4139		$query .= " ORDER BY " . $this->convertSortMode($sort_mode);
4140		$results = $this->fetchAll($query, $bindvars);
4141
4142		$position = 0;
4143		foreach ($results as $result) {
4144			if ($result['threadId'] == $comment_id) {
4145				break;
4146			}
4147			$position++;
4148		}
4149		$page_offset = floor($position / $max_per_page);
4150
4151		return [
4152			'position' => $position,
4153			'page_offset' => $page_offset,
4154		];
4155	}
4156
4157	/**
4158	 * This function is used to collectively index all of the forum threads that are parents
4159	 * of the forum thread being updated.
4160	 *
4161	 * @param $type
4162	 * @param $threadId
4163	 * @param null $parentId
4164	 * @return string
4165	 */
4166	private function update_index($type, $threadId, $parentId = null)
4167	{
4168		require_once(__DIR__ . '/../search/refresh-functions.php');
4169		global $prefs;
4170
4171		if ($type == 'forum') {
4172			$type = 'forum post';
4173
4174			$root = $this->find_root($parentId ? $parentId : $threadId);
4175			refresh_index($type, $root);
4176
4177			if ($prefs['unified_forum_deepindexing'] != 'y') {
4178				if ($threadId != $root) {
4179					refresh_index($type, $threadId);
4180				}
4181				if ($parentId && $parentId != $root && $parentId != $threadId) {
4182					refresh_index($type, $parentId);
4183				}
4184			}
4185
4186			return $type;
4187		} else {
4188			refresh_index('comments', $threadId);
4189			return $type . ' comment';
4190		}
4191	}
4192
4193	/**
4194	 * Re-indexes the forum posts within a specified forum
4195	 * @param $forumId
4196	 */
4197	private function index_posts_by_forum($forumId)
4198	{
4199		$topics = $this->get_forum_topics($forumId);
4200
4201		foreach ($topics as $topic) {
4202			if ($element === end($array)) { //if element is the last in the array, then run the process.
4203				refresh_index('forum post', $topic['threadId'], true);
4204			} else {
4205				refresh_index('forum post', $topic['threadId'], false); //don't run the process right away (re: false), wait until last element
4206			}
4207		}
4208	}
4209
4210	/**
4211	 * @param $threadId
4212	 * @return mixed
4213	 */
4214	function find_root($threadId)
4215	{
4216		$parent = $this->table('tiki_comments')->fetchOne('parentId', ['threadId' => $threadId]);
4217
4218		if ($parent) {
4219			return $this->find_root($parent);
4220		} else {
4221			return $threadId;
4222		}
4223	}
4224
4225	/**
4226	 * Get all comment IDs in the tree up to the root threadId
4227	 * @param $threadId
4228	 * @return array
4229	 */
4230	function get_root_path($threadId)
4231	{
4232		$parent = $this->table('tiki_comments')->fetchOne('parentId', ['threadId' => $threadId]);
4233
4234		if ($parent) {
4235			return array_merge($this->get_root_path($parent), [$parent]);
4236		} else {
4237			return [];
4238		}
4239	}
4240
4241	/**
4242	 * Utlity to check whether a user can admin a form, either through permissions or as moderator
4243	 *
4244	 * @param $forumId
4245	 * @return bool
4246	 * @throws Exception
4247	 */
4248	function admin_forum($forumId)
4249	{
4250		$perms = Perms::get('forum', $forumId);
4251		if (! $perms->admin_forum) {
4252			$info = $this->get_forum($forumId);
4253			global $user;
4254			if ($info['moderator'] !== $user) {
4255				$userlib = TikiLib::lib('user');
4256				if (! in_array($info['moderator_group'], $userlib->get_user_groups($user))) {
4257					return false;
4258				} else {
4259					return true;
4260				}
4261			} else {
4262				return true;
4263			}
4264		} else {
4265			return true;
4266		}
4267	}
4268
4269	/**
4270	 * @param $threadId
4271	 *
4272	 * @return array
4273	 */
4274	function get_lastPost($threadId)
4275	{
4276		$query = "select * from tiki_comments where parentId=? order by commentDate desc limit 1";
4277		$ret = $this->fetchAll($query, [$threadId]);
4278
4279		if (is_array($ret) && isset($ret[0])) {
4280			return $ret[0];
4281		} else {
4282			return [];
4283		}
4284	}
4285}
4286
4287/**
4288 * @param $ar1
4289 * @param $ar2
4290 * @return int
4291 */
4292function compare_replies($ar1, $ar2)
4293{
4294	if (($ar1['type'] == 's' && $ar2['type'] == 's') ||
4295			($ar1['type'] != 's' && $ar2['type'] != 's')) {
4296		return $ar1["replies_info"]["numReplies"] - $ar2["replies_info"]["numReplies"];
4297	} else {
4298		return $ar1['type'] == 's' ? -1 : 1;
4299	}
4300}
4301
4302/**
4303 * @param $ar1
4304 * @param $ar2
4305 * @return int
4306 */
4307function compare_lastPost($ar1, $ar2)
4308{
4309	if (($ar1['type'] == 's' && $ar2['type'] == 's') ||
4310			($ar1['type'] != 's' && $ar2['type'] != 's')) {
4311		return $ar1["lastPost"] - $ar2["lastPost"];
4312	} else {
4313		return $ar1['type'] == 's' ? -1 : 1;
4314	}
4315}
4316
4317/**
4318 * @param $ar1
4319 * @param $ar2
4320 * @return int
4321 */
4322function r_compare_replies($ar1, $ar2)
4323{
4324	if (($ar1['type'] == 's' && $ar2['type'] == 's') ||
4325			($ar1['type'] != 's' && $ar2['type'] != 's')) {
4326		return $ar2["replies_info"]["numReplies"] - $ar1["replies_info"]["numReplies"];
4327	} else {
4328		return $ar1['type'] == 's' ? -1 : 1;
4329	}
4330}
4331
4332/**
4333 * @param $ar1
4334 * @param $ar2
4335 * @return int
4336 */
4337function r_compare_lastPost($ar1, $ar2)
4338{
4339	if (($ar1['type'] == 's' && $ar2['type'] == 's') ||
4340			($ar1['type'] != 's' && $ar2['type'] != 's')) {
4341		return $ar2["lastPost"] - $ar1["lastPost"];
4342	} else {
4343		return $ar1['type'] == 's' ? -1 : 1;
4344	}
4345}
4346