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 . "&threadId=$threadId&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&$bytes#threadId$threadId", '', '', '', '', $contributions); 3008 } else { 3009 $logslib->add_action('Updated', $comment['object'], 'comment', "type=" . $comment['objectType'] . "&$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 . '&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] . '&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&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'] . '&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