Textpattern | PHP Cross Reference | Content Management Systems |
Description: Collection of comment tools.
1 <?php 2 3 /* 4 * Textpattern Content Management System 5 * http://textpattern.com 6 * 7 * Copyright (C) 2005 Dean Allen 8 * Copyright (C) 2016 The Textpattern Development Team 9 * 10 * This file is part of Textpattern. 11 * 12 * Textpattern is free software; you can redistribute it and/or 13 * modify it under the terms of the GNU General Public License 14 * as published by the Free Software Foundation, version 2. 15 * 16 * Textpattern is distributed in the hope that it will be useful, 17 * but WITHOUT ANY WARRANTY; without even the implied warranty of 18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 * GNU General Public License for more details. 20 * 21 * You should have received a copy of the GNU General Public License 22 * along with Textpattern. If not, see <http://www.gnu.org/licenses/>. 23 */ 24 25 /** 26 * Collection of comment tools. 27 * 28 * @package Comment 29 */ 30 31 /** 32 * Gets comments as an array from the given article. 33 * 34 * @param int $id The article ID 35 * @return array|null An array of comments, or NULL on error 36 * @example 37 * if ($comments = fetchComments(12)) 38 * { 39 * print_r($comments); 40 * } 41 */ 42 43 function fetchComments($id) 44 { 45 $rs = safe_rows( 46 "*, UNIX_TIMESTAMP(posted) AS time", 47 'txp_discuss', 48 "parentid = ".intval($id)." AND visible = ".VISIBLE." ORDER BY posted ASC" 49 ); 50 51 if ($rs) { 52 return $rs; 53 } 54 } 55 56 /** 57 * Gets next nonce. 58 * 59 * @param bool $check_only 60 * @return string A random MD5 hash 61 */ 62 63 function getNextNonce($check_only = false) 64 { 65 static $nonce = ''; 66 67 if (!$nonce && !$check_only) { 68 $nonce = md5(uniqid(rand(), true)); 69 } 70 71 return $nonce; 72 } 73 74 /** 75 * Gets next secret. 76 * 77 * @param bool $check_only 78 * @return string A random MD5 hash 79 */ 80 81 function getNextSecret($check_only = false) 82 { 83 static $secret = ''; 84 85 if (!$secret && !$check_only) { 86 $secret = md5(uniqid(rand(), true)); 87 } 88 89 return $secret; 90 } 91 92 /** 93 * Remembers comment form values. 94 * 95 * Creates a HTTP cookie for each value. 96 * 97 * @param string $name The name 98 * @param string $email The email address 99 * @param string $web The website 100 */ 101 102 function setCookies($name, $email, $web) 103 { 104 $cookietime = time() + (365 * 24 * 3600); 105 ob_start(); 106 setcookie("txp_name", $name, $cookietime, "/"); 107 setcookie("txp_email", $email, $cookietime, "/"); 108 setcookie("txp_web", $web, $cookietime, "/"); 109 setcookie("txp_last", date("H:i d/m/Y"), $cookietime, "/"); 110 setcookie("txp_remember", '1', $cookietime, "/"); 111 } 112 113 /** 114 * Deletes HTTP cookies created by the comment form. 115 */ 116 117 function destroyCookies() 118 { 119 $cookietime = time() - 3600; 120 ob_start(); 121 setcookie("txp_name", '', $cookietime, "/"); 122 setcookie("txp_email", '', $cookietime, "/"); 123 setcookie("txp_web", '', $cookietime, "/"); 124 setcookie("txp_last", '', $cookietime, "/"); 125 setcookie("txp_remember", '0', $cookietime + (365 * 25 * 3600), "/"); 126 } 127 128 /** 129 * Gets the received comment. 130 * 131 * Comment spam filter plugins should call this function to fetch 132 * comment contents. 133 * 134 * @return array 135 * @example 136 * print_r( 137 * getComment() 138 * ); 139 */ 140 141 function getComment($obfuscated = false) 142 { 143 $c = psa(array( 144 'parentid', 145 'name', 146 'email', 147 'web', 148 'message', 149 'backpage', 150 'remember', 151 )); 152 153 $n = array(); 154 155 foreach (stripPost() as $k => $v) { 156 if (preg_match('#^[A-Fa-f0-9]{32}$#', $k.$v)) { 157 $n[] = doSlash($k.$v); 158 } 159 } 160 161 $c['nonce'] = ''; 162 $c['secret'] = ''; 163 164 if (!empty($n)) { 165 $rs = safe_row("nonce, secret", 'txp_discuss_nonce', "nonce IN ('".join("','", $n)."')"); 166 $c['nonce'] = $rs['nonce']; 167 $c['secret'] = $rs['secret']; 168 } 169 170 if ($obfuscated || $c['message'] == '') { 171 $c['message'] = ps(md5('message'.$c['secret'])); 172 } 173 174 $c['name'] = trim(strip_tags(deEntBrackets($c['name']))); 175 $c['web'] = trim(clean_url(strip_tags(deEntBrackets($c['web'])))); 176 $c['email'] = trim(clean_url(strip_tags(deEntBrackets($c['email'])))); 177 $c['message'] = trim(substr(trim(doDeEnt($c['message'])), 0, 65535)); 178 179 return $c; 180 } 181 182 /** 183 * Saves a comment. 184 */ 185 186 function saveComment() 187 { 188 global $siteurl, $comments_moderate, $comments_sendmail, $comments_disallow_images, $prefs; 189 190 $ref = serverset('HTTP_REFERRER'); 191 $comment = getComment(true); 192 $evaluator = & get_comment_evaluator(); 193 194 extract($comment); 195 196 if (!checkCommentsAllowed($parentid)) { 197 txp_die(gTxt('comments_closed'), '403'); 198 } 199 200 $ip = serverset('REMOTE_ADDR'); 201 $blacklisted = is_blacklisted($ip); 202 203 if ($blacklisted) { 204 txp_die(gTxt('your_ip_is_blacklisted_by'.' '.$blacklisted), '403'); 205 } 206 207 if ($remember == 1 || ps('checkbox_type') == 'forget' && ps('forget') != 1) { 208 setCookies($name, $email, $web); 209 } else { 210 destroyCookies(); 211 } 212 213 $message2db = markup_comment($message); 214 215 $isdup = safe_row( 216 "message, name", 217 'txp_discuss', 218 "name = '".doSlash($name)."' AND message = '".doSlash($message2db)."' AND ip = '".doSlash($ip)."'" 219 ); 220 221 checkCommentRequired($comment); 222 223 if ($isdup) { 224 $evaluator->add_estimate(RELOAD, 1, gTxt('comment_duplicate')); 225 } 226 227 if (($evaluator->get_result() != RELOAD) && checkNonce($nonce)) { 228 callback_event('comment.save'); 229 $visible = $evaluator->get_result(); 230 231 if ($visible != RELOAD) { 232 $parentid = assert_int($parentid); 233 $commentid = safe_insert( 234 'txp_discuss', 235 "parentid = $parentid, 236 name = '".doSlash($name)."', 237 email = '".doSlash($email)."', 238 web = '".doSlash($web)."', 239 ip = '".doSlash($ip)."', 240 message = '".doSlash($message2db)."', 241 visible = ".intval($visible).", 242 posted = NOW()" 243 ); 244 245 if ($commentid) { 246 safe_update('txp_discuss_nonce', "used = 1", "nonce = '".doSlash($nonce)."'"); 247 248 if ($prefs['comment_means_site_updated']) { 249 update_lastmod('comment_saved', compact('commentid', 'parentid', 'name', 'email', 'web', 'message', 'visible', 'ip')); 250 } 251 252 callback_event('comment.saved', '', false, compact( 253 'message', 254 'name', 255 'email', 256 'web', 257 'parentid', 258 'commentid', 259 'ip', 260 'visible' 261 )); 262 263 mail_comment($message, $name, $email, $web, $parentid, $commentid); 264 265 $updated = update_comments_count($parentid); 266 $backpage = substr($backpage, 0, $prefs['max_url_len']); 267 $backpage = preg_replace("/[\x0a\x0d#].*$/s", '', $backpage); 268 $backpage = preg_replace("#(https?://[^/]+)/.*$#", "$1", hu).$backpage; 269 270 if (defined('PARTLY_MESSY') and (PARTLY_MESSY)) { 271 $backpage = permlinkurl_id($parentid); 272 } 273 274 $backpage .= ((strstr($backpage, '?')) ? '&' : '?').'commented='.(($visible == VISIBLE) ? '1' : '0'); 275 276 txp_status_header('302 Found'); 277 278 if ($comments_moderate) { 279 header('Location: '.$backpage.'#txpCommentInputForm'); 280 } else { 281 header('Location: '.$backpage.'#c'.sprintf("%06s", $commentid)); 282 } 283 284 log_hit('302'); 285 $evaluator->write_trace(); 286 exit; 287 } 288 } 289 } 290 291 // Force another Preview. 292 $_POST['preview'] = RELOAD; 293 //$evaluator->write_trace(); 294 } 295 296 /** 297 * Checks if all required comment fields are filled out. 298 * 299 * To be used only by TXP itself 300 * 301 * @param array comment fields (from getComment()) 302 */ 303 304 function checkCommentRequired($comment) 305 { 306 global $prefs; 307 308 $evaluator = & get_comment_evaluator(); 309 310 if ($prefs['comments_require_name'] && !$comment['name']) { 311 $evaluator->add_estimate(RELOAD, 1, gTxt('comment_name_required')); 312 } 313 if ($prefs['comments_require_email'] && !$comment['email']) { 314 $evaluator->add_estimate(RELOAD, 1, gTxt('comment_email_required')); 315 } 316 if (!$comment['message']) { 317 $evaluator->add_estimate(RELOAD, 1, gTxt('comment_required')); 318 } 319 } 320 321 /** 322 * Comment evaluator. 323 * 324 * Validates and filters comments. Keeps out spam. 325 * 326 * @package Comment 327 */ 328 329 class comment_evaluation 330 { 331 /** 332 * Stores estimated statuses. 333 * 334 * @var array 335 */ 336 337 public $status; 338 339 /** 340 * Stores estimated messages. 341 * 342 * @var array 343 */ 344 345 public $message; 346 347 /** 348 * Debug log. 349 * 350 * @var array 351 */ 352 353 public $txpspamtrace = array(); 354 355 /** 356 * List of available statuses. 357 * 358 * @var array 359 */ 360 361 public $status_text = array(); 362 363 /** 364 * Constructor. 365 */ 366 367 public function __construct() 368 { 369 global $prefs; 370 extract(getComment()); 371 372 $this->status = array( 373 SPAM => array(), 374 MODERATE => array(), 375 VISIBLE => array(), 376 RELOAD => array(), 377 ); 378 379 $this->status_text = array( 380 SPAM => gTxt('spam'), 381 MODERATE => gTxt('unmoderated'), 382 VISIBLE => gTxt('visible'), 383 RELOAD => gTxt('reload'), 384 ); 385 386 $this->message = $this->status; 387 $this->txpspamtrace[] = "Comment on $parentid by $name (".safe_strftime($prefs['archive_dateformat'], time()).")"; 388 389 if ($prefs['comments_moderate']) { 390 $this->status[MODERATE][] = 0.5; 391 } else { 392 $this->status[VISIBLE][] = 0.5; 393 } 394 } 395 396 /** 397 * Adds an estimate about the comment's status. 398 * 399 * @param int $type The status, either SPAM, MODERATE, VISIBLE or RELOAD 400 * @param float $probability Estimates probability - throughout 0 to 1, e.g. 0.75 401 * @param string $msg The error or success message shown to the user 402 * @example 403 * $evaluator =& get_comment_evaluator(); 404 * $evaluator->add_estimate(RELOAD, 1, 'Message'); 405 */ 406 407 public function add_estimate($type = SPAM, $probability = 0.75, $msg = '') 408 { 409 global $production_status; 410 411 if (!array_key_exists($type, $this->status)) { 412 trigger_error(gTxt('unknown_spam_estimate'), E_USER_WARNING); 413 } 414 415 $this->txpspamtrace[] = " $type; ".max(0, min(1, $probability))."; $msg"; 416 //FIXME trace is only viewable for RELOADS. Maybe add info to HTTP-Headers in debug-mode 417 418 $this->status[$type][] = max(0, min(1, $probability)); 419 420 if (trim($msg)) { 421 $this->message[$type][] = $msg; 422 } 423 } 424 425 /** 426 * Gets resulting estimated status. 427 * 428 * @param string $result_type If 'numeric' returns the ID of the status, a localised label otherwise 429 * @return int|string 430 * @example 431 * $evaluator =& get_comment_evaluator(); 432 * print_r( 433 * $evaluator->get_result() 434 * ); 435 */ 436 437 public function get_result($result_type = 'numeric') 438 { 439 $result = array(); 440 441 foreach ($this->status as $key => $value) { 442 $result[$key] = array_sum($value) / max(1, count($value)); 443 } 444 445 arsort($result, SORT_NUMERIC); 446 reset($result); 447 448 return (($result_type == 'numeric') ? key($result) : $this->status_text[key($result)]); 449 } 450 451 /** 452 * Gets resulting success or error message. 453 * 454 * @return array 455 * @example 456 * $evaluator =& get_comment_evaluator(); 457 * echo $evaluator->get_result_message(); 458 */ 459 460 public function get_result_message() 461 { 462 return $this->message[$this->get_result()]; 463 } 464 465 /** 466 * Writes a debug log. 467 */ 468 469 public function write_trace() 470 { 471 global $prefs; 472 $file = $prefs['tempdir'].DS.'evaluator_trace.php'; 473 474 if (!file_exists($file)) { 475 $fp = fopen($file, 'wb'); 476 477 if ($fp) { 478 fwrite($fp, "<?php return; ?>\n". 479 "This trace-file tracks saved comments. (created ".safe_strftime($prefs['archive_dateformat'], time()).")\n". 480 "Format is: Type; Probability; Message (Type can be -1 => spam, 0 => moderate, 1 => visible)\n\n" 481 ); 482 } 483 } else { 484 $fp = fopen($file, 'ab'); 485 } 486 487 if ($fp) { 488 fwrite($fp, implode("\n", $this->txpspamtrace)); 489 fwrite($fp, "\n RESULT: ".$this->get_result()."\n\n"); 490 fclose($fp); 491 } 492 } 493 } 494 495 /** 496 * Gets a comment evaluator instance. 497 * 498 * @return comment_evaluation 499 */ 500 501 function &get_comment_evaluator() 502 { 503 static $instance; 504 505 // If the instance is not there, create one 506 if (!isset($instance)) { 507 $instance = new comment_evaluation(); 508 } 509 510 return $instance; 511 } 512 513 /** 514 * Verifies a given nonce. 515 * 516 * This function will also do clean up and deletes expired nonces. 517 * 518 * @param string $nonce The nonce 519 * @return bool TRUE if the nonce is valid 520 * @see getNextNonce() 521 */ 522 523 function checkNonce($nonce) 524 { 525 if (!$nonce || !preg_match('#^[a-zA-Z0-9]*$#', $nonce)) { 526 return false; 527 } 528 529 // Delete expired nonces. 530 safe_delete('txp_discuss_nonce', "issue_time < DATE_SUB(NOW(), INTERVAL 10 MINUTE)"); 531 532 // Check for nonce. 533 return (safe_row("*", 'txp_discuss_nonce', "nonce = '".doSlash($nonce)."' AND used = 0")) ? true : false; 534 } 535 536 /** 537 * Checks if comments are open for the given article. 538 * 539 * @param int $id The article. 540 * @return bool FALSE if comments are closed 541 * @example 542 * if (checkCommentsAllowed(12)) 543 * { 544 * echo "Article accepts comments"; 545 * } 546 */ 547 548 function checkCommentsAllowed($id) 549 { 550 global $use_comments, $comments_disabled_after, $thisarticle; 551 552 $id = intval($id); 553 554 if (!$use_comments || !$id) { 555 return false; 556 } 557 558 if (isset($thisarticle['thisid']) && ($thisarticle['thisid'] == $id) && isset($thisarticle['annotate'])) { 559 $Annotate = $thisarticle['annotate']; 560 $uPosted = $thisarticle['posted']; 561 } else { 562 extract( 563 safe_row( 564 "Annotate, UNIX_TIMESTAMP(Posted) AS uPosted", 565 'textpattern', 566 "ID = $id" 567 ) 568 ); 569 } 570 571 if ($Annotate != 1) { 572 return false; 573 } 574 575 if ($comments_disabled_after) { 576 $lifespan = ($comments_disabled_after * 86400); 577 $timesince = (time() - $uPosted); 578 579 return ($lifespan > $timesince); 580 } 581 582 return true; 583 } 584 585 /** 586 * Renders a Textile help link. 587 * 588 * @return string HTML 589 */ 590 591 function comments_help() 592 { 593 return '<a id="txpCommentHelpLink" href="'.HELP_URL.'?item=textile_comments&language='.txpspecialchars(LANG).'" onclick="window.open(this.href, \'popupwindow\', \'width=300,height=400,scrollbars,resizable\'); return false;">'.gTxt('textile_help').'</a>'; 594 } 595 596 /** 597 * Emails a new comment to the article's author. 598 * 599 * This function can only be executed directly after a comment was sent, 600 * otherwise it will not run properly. 601 * 602 * Will not send comments flagged as spam, and follows site's 603 * comment preferences. 604 * 605 * @param string $message The comment message 606 * @param string $cname The comment name 607 * @param string $cemail The comment email 608 * @param string $cweb The comment website 609 * @param int $parentid The article ID 610 * @param int $discussid The comment ID 611 */ 612 613 function mail_comment($message, $cname, $cemail, $cweb, $parentid, $discussid) 614 { 615 global $sitename, $comments_sendmail; 616 617 if (!$comments_sendmail) { 618 return; 619 } 620 621 $evaluator = & get_comment_evaluator(); 622 623 if ($comments_sendmail == 2 && $evaluator->get_result() == SPAM) { 624 return; 625 } 626 627 $parentid = assert_int($parentid); 628 $discussid = assert_int($discussid); 629 $article = safe_row("Section, Posted, ID, url_title, AuthorID, Title", 'textpattern', "ID = $parentid"); 630 extract($article); 631 extract(safe_row("RealName, email", 'txp_users', "name = '".doSlash($AuthorID)."'")); 632 633 $out = gTxt('salutation', array('{name}' => $RealName)).n; 634 $out .= str_replace('{title}', $Title, gTxt('comment_recorded')).n; 635 $out .= permlinkurl_id($parentid).n; 636 637 if (has_privs('discuss', $AuthorID)) { 638 $out .= hu.'textpattern/index.php?event=discuss&step=discuss_edit&discussid='.$discussid.n; 639 } 640 641 $out .= gTxt('status').": ".$evaluator->get_result('text').'. '.implode(',', $evaluator->get_result_message()).n; 642 $out .= n; 643 $out .= gTxt('comment_name').": $cname".n; 644 $out .= gTxt('comment_email').": $cemail".n; 645 $out .= gTxt('comment_web').": $cweb".n; 646 $out .= gTxt('comment_comment').": $message"; 647 648 $subject = strtr(gTxt('comment_received'), array('{site}' => $sitename, '{title}' => $Title)); 649 650 if (!is_valid_email($cemail)) { 651 $cemail = null; 652 } 653 654 $success = txpMail($email, $subject, $out, $cemail); 655 } 656 657 /** 658 * Renders a HTML input. 659 * 660 * Deprecated, use fInput() instead. 661 * 662 * @param string $type 663 * @param string $name 664 * @param string $val 665 * @param int $size 666 * @param string $class 667 * @param int $tab 668 * @param bool $chkd 669 * @return string 670 * @deprecated in 4.0.4 671 * @see fInput() 672 */ 673 674 function input($type, $name, $val, $size = '', $class = '', $tab = '', $chkd = '') 675 { 676 trigger_error(gTxt('deprecated_function_with', array('{name}' => __FUNCTION__, '{with}' => 'fInput')), E_USER_NOTICE); 677 $o = array( 678 '<input type="'.$type.'" name="'.$name.'" id="'.$name.'" value="'.$val.'"', 679 ($size) ? ' size="'.$size.'"' : '', 680 ($class) ? ' class="'.$class.'"' : '', 681 ($tab) ? ' tabindex="'.$tab.'"' : '', 682 ($chkd) ? ' checked="checked"' : '', 683 ' />'.n, 684 ); 685 686 return join('', $o); 687 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title