Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/publish/comment.php - 702 lines - 18714 bytes - Summary - Text - Print

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

title

Description

title

Description

title

title

Body