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