Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/publish/comment.php - 687 lines - 18020 bytes - Summary - Text - Print

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&amp;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

title

Description

title

Description

title

title

Body