DIR : /home/kozerus/public_html/go/dgs/forum/forum_functions.php

/home/kozerus/public_html/go/dgs/forum

<?php
/*
Dragon Go Server
Copyright (C) 2001-  Erik Ouchterlony, Rod Ival, Jens-Uwe Gaspar

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/*!
 * \file forum_functions.php
 *
 * \brief Functions for forum management.
 */

$TranslateGroups[] = "Forum"; //local use

require_once 'include/std_functions.php';
require_once 'include/gui_functions.php';
require_once 'include/std_classes.php';
require_once 'include/form_functions.php';
require_once 'include/classlib_user.php';
require_once 'include/classlib_goban.php';
require_once 'include/rating.php';
require_once 'include/dgs_cache.php';
require_once 'include/db_classes.php';
require_once 'forum/class_forum_options.php';
require_once 'forum/class_forum_read.php';


//must follow the "ORDER BY PosIndex" order and have at least 64 chars:
define('ORDER_STR', "*+-/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
define('FORUM_MAX_DEPTH', 40); //half the length of the Posts.PosIndex field
define('FORUM_MAX_INDENT', 25); //at the display time


define("LINK_FORUMS", 1 << 0);
define("LINK_THREADS", 1 << 1);
define("LINK_BACK_TO_THREAD", 1 << 2);
define("LINK_NEW_TOPIC", 1 << 3);
define("LINK_SEARCH", 1 << 4);
define("LINK_MARK_READ", 1 << 5);
define("LINK_PREV_PAGE", 1 << 6);
define("LINK_NEXT_PAGE", 1 << 7);
define("LINKPAGE_READ", 1 << 8);
define("LINKPAGE_LIST", 1 << 9);
define("LINKPAGE_INDEX", 1 << 10);
define("LINKPAGE_SEARCH", 1 << 11);
define("LINK_TOGGLE_MODERATOR", 1 << 12);
define("LINKPAGE_STATUS", 1 << 13); // used for status-page
define("LINK_REFRESH", 1 << 14);

define("LINK_MASKS", ~(LINKPAGE_READ | LINKPAGE_LIST | LINKPAGE_INDEX
          | LINKPAGE_SEARCH | LINKPAGE_STATUS) );


// Forumlog.Action
// NOTE: when adding new also adjust forum/admin_show_forumlog.php
// * for all users (incl. guest); new_..:(new_thread|reply)
define('FORUMLOGACT_NEW_POST',      'new_post');
define('FORUMLOGACT_NEW_PEND_POST', 'new_pend_post');
define('FORUMLOGACT_EDIT_POST',     'edit_post');
define('FORUMLOGACT_EDIT_PEND_POST', 'edit_pend_post');
// * for moderators
define('FORUMLOGACT_APPROVE_POST',  'approve_post');
define('FORUMLOGACT_REJECT_POST',   'reject_post');
define('FORUMLOGACT_SHOW_POST',     'show_post');
define('FORUMLOGACT_HIDE_POST',     'hide_post');
// * new_(pend_)post:(new_thread|reply)
define('FORUMLOGACT_SUFFIX_NEW_THREAD', ':new_thread');
define('FORUMLOGACT_SUFFIX_REPLY',      ':reply');

// Posts.Flags
define('FPOST_FLAG_READ_ONLY', 0x01);
define('FPOST_FLAG_KEEP_EMAIL', 0x02); // email-addresses in post are kept untouched

// Tags
define('TAG_LIKE', -1);


/* \brief Adds entry into Forumlog */
function add_forum_log( $tid, $pid, $action )
{
   global $player_row, $NOW;
   $uid = @$player_row['ID'];
   if ( $uid <= 0 )
      $uid = 1; // use guest-user as default-user
   $ip = (string) @$_SERVER['REMOTE_ADDR'];

   db_query( "forum.add_forum_log.insert($uid,$tid,$pid,$action)",
         "INSERT INTO Forumlog SET " .
               "User_ID='$uid', " .
               "Thread_ID='$tid', " .
               "Post_ID='$pid', " .
               "Time=FROM_UNIXTIME($NOW), " .
               "Action='$action', " .
               "IP='$ip' " );
}

/* \brief Returns Forum_ID for specified thread. */
function load_forum_id( $thread_id )
{
   $result = db_query( 'load_forum_id',
      "SELECT Forum_ID FROM Posts WHERE ID='$thread_id' LIMIT 1" );

   if ( @mysql_num_rows($result) != 1 )
      return 0;

   $row = mysql_fetch_array($result);
   return @$row['Forum_ID'];
}

// show list with posts on pending-approval (used on status-page)
// returns number of pending-approval posts
function display_posts_pending_approval()
{
   $result = // fields matching ForumPost::new_from_row
      db_query( 'display_posts_pending_approval.find',
         'SELECT Posts.ID, Forum_ID, Thread_ID, Flags, Subject, UNIX_TIMESTAMP(Time) as X_Time, '
            . 'User_ID, PAuthor.Name AS Author_Name, PAuthor.Handle AS Author_Handle, '
            . 'Forums.Name AS X_Forumname '
         . 'FROM Posts '
            . 'INNER JOIN Players AS PAuthor ON PAuthor.ID=Posts.User_ID '
            . 'INNER JOIN Forums ON Forums.ID=Posts.Forum_ID '
         . "WHERE Approved='P' ORDER BY Time" );

   $cnt = 0;
   if ( mysql_num_rows($result) > 0 )
   {
      $disp_forum = new DisplayForum( 0, false );
      $disp_forum->cols = $cols = 4;
      $disp_forum->headline = array(
         T_('Posts pending approval') => "colspan=".($cols-1),
         anchor( 'forum/admin_show_forumlog.php', T_('Show Forum Log') ) => '',
      );
      $disp_forum->links = LINKPAGE_STATUS;
      $disp_forum->forum_start_table('Pending');

      while ( $row = mysql_fetch_array( $result ) )
      {
         $post = ForumPost::new_from_row($row);
         $forum_name = $row['X_Forumname'];

         $Subject = make_html_safe( $post->subject, SUBJECT_HTML);
         $post_href = $post->build_url_post( null, 'moderator=y' );

         $color = ( ($cnt++ % 2) ? "" : " bgcolor=white" );
         echo "<tr$color>"
            . '<td>' . ( $cols > 3 ? $forum_name . '</td><td>' : '' )
            . "<a href=\"forum/$post_href\">$Subject</a></td><td>"
            . $post->author->user_reference()
            . '</td><td nowrap align=right>' . date(DATE_FMT, $post->created) . "</td></tr>\n";
      }
      $disp_forum->forum_end_table();
   }

   mysql_free_result($result);
   return $cnt;
} //display_posts_pending_approval


// mode-bitmask for get_new_string() and echo_links()
define('NEWMODE_TOP',      0x01);
define('NEWMODE_BOTTOM',   0x02);
define('NEWMODE_OVERVIEW', 0x04);
define('NEWMODE_NO_LINK',  0x08);

// draw-modes for draw_post()
define('MASK_DRAWPOST_MODES', 0x0f); // lower 4 bits reserved for draw-modes
define('DRAWPOST_NORMAL',  1); // post normal view
define('DRAWPOST_PREVIEW', 2); // post preview
define('DRAWPOST_EDIT',    3); // post edit
define('DRAWPOST_REPLY',   4); // post reply
define('DRAWPOST_SEARCH',  5); // post search-result view
define('MASK_DRAWPOST_HIDDEN', 0x10); // post-header hidden view
define('MASK_DRAWPOST_NO_BODY', 0x20); // no post-body shown
define('MASK_DRAWPOST_NO_NUM', 0x40); // no post-numbering
define('MASK_DRAWPOST_NO_TAGS', 0x80); // no tags

 /*!
  * \class DisplayForum
  *
  * \brief Class to help with display of forum-pages.
  */
class DisplayForum
{
   /*! \brief Current logged in user */
   private $user_id;
   /*! \brief true, if in moderating-mode */
   public $is_moderator;
   /*! \brief current forum-id (maybe 0) */
   private $forum_id;
   /*! \brief current thread-id (maybe 0) */
   private $thread_id;

   public $cols = 1;
   public $links = 0;
   public $headline = array();
   private $link_array_left = array();
   private $link_array_right = array();
   private $found_rows = -1; // not shown
   public $count_new_posts = 0; // for get_new_string() and echo_link() to support "first new"
   private $new_count = 0; // global new-counter for displaying
   public $back_post_id = 0;
   public $cur_depth = -1;
   public $show_score = false; // used for forum-search
   /*! \brief rx-terms (optionally array) that are to be highlighted in text. */
   private $rx_term = '';
   private $hide_post_users = array(); // uid-list with users to hide post
   private $ConfigBoard = null;
   private $forum_opts = null;
   private $ForumTags = array(); // map( tag-id => Tag, ...) in intended order
   public $flat_view = 0; // 0 = tree-view, -1 = flat-old-first (order Posts.Time ASC), 1 = flat-new-first (order Posts.Time DESC)

   // consts
   public $max_rows = MAXROWS_PER_PAGE_DEFAULT;
   public $offset = 0;
   private $navi_img = null;

   /*! \brief Constructs display handler for forum-pages. */
   public function __construct( $user_id, $is_moderator, $forum_id=0, $thread_id=0 )
   {
      $this->user_id = $user_id;
      $this->is_moderator = $is_moderator;
      $this->forum_id = $forum_id;
      $this->thread_id = $thread_id;
   }

   /*! \brief Setting rx-term (can be array or string). */
   public function set_rx_term( $rx_term='' )
   {
      // highlight terms (skipping XML-elements like tags & entities)
      if ( is_array($rx_term) && count($rx_term) > 0 )
         $this->rx_term = implode('|', $rx_term);
      elseif ( !is_string($rx_term) )
         $this->rx_term = '';
      else
         $this->rx_term = $rx_term;
   }

   public function set_hide_post_users( array $uids )
   {
      $this->hide_post_users = $uids;
   }

   public function setConfigBoard( ConfigBoard $cfg_board = null )
   {
      $this->ConfigBoard = $cfg_board;
   }

   public function setForumTags( array $arr_tags )
   {
      $this->ForumTags = array();
      foreach( $arr_tags as $tag )
         $this->ForumTags[$tag->ID] = $tag;
   }

   private function compareForumTags($a, $b)
   {
      if ( $a < 0 || $b < 0 )
         return cmp_int( $a, $b );
      else
         return strcmp( $this->ForumTags[$a]->Name, $this->ForumTags[$b]->Name );
   }

   public function sortForumUserTags( array $user_tags )
   {
      $comparableCallback = array( $this, 'compareForumTags');
      foreach( $user_tags as $post_id => $arr_tags)
         usort($user_tags[$post_id], $comparableCallback);
      return $user_tags;
   }


   /*! \param $forum_opts null | int Forum.Options */
   public function set_forum_options( /*Int*/ $forum_opts )
   {
      $this->forum_opts = $forum_opts;
   }

   public function set_threadpost_view( $forum_flags )
   {
      if ( ($forum_flags & FORUMFLAGS_POSTVIEW_FLAT) == FORUMFLAG_POSTVIEW_FLAT_NEW_FIRST )
         $this->flat_view = 1;
      elseif ( ($forum_flags & FORUMFLAGS_POSTVIEW_FLAT) == FORUMFLAG_POSTVIEW_FLAT_OLD_FIRST )
         $this->flat_view = -1;
      else
         $this->flat_view = 0;
   }

   public function show_found_rows( $row_count )
   {
      if ( $row_count >=0 )
         $this->found_rows = $row_count;
   }

   public function print_moderation_note( $width )
   {
      if ( $this->is_moderator)
         echo "<table width='$width'><tr><td align=right><font color=red>"
            . T_("Moderating") . "</font></td></tr></table>\n";
   }

   public function prepare_forum_tags()
   {
      echo "<div id=\"TagList\">\n",
         span( 'Edit', T_( 'Toggle tags' ), '%s: ' );
      foreach( $this->ForumTags as $tag_id => $tag )
         echo $tag->render_tag( 'tag'.$tag_id ), "\n";
      echo "</div>\n";
   }

   // param table_id: begining by an uppercase letter because used as sub-ID name (CSS);
   //                 values: 'Index', 'List', 'Read', 'Search', 'Revision', 'Pending'
   // param ReqParam: optional object RequestParameters containing URL-parts to be included for paging
   // note: sets cur_depth=-1
   public function forum_start_table( $table_id, RequestParameters $ReqParam = null )
   {
      echo name_anchor('ftop'), "<table id='forum$table_id' class=Forum>\n";
      $this->make_link_array( $ReqParam );

      if ( $this->links & LINK_MASKS )
         $this->echo_links(NEWMODE_TOP);

      $this->print_headline();

      $this->cur_depth = -1;
   }

   public function print_headline( $headline=NULL )
   {
      if ( is_null($headline) )
         $headline = $this->headline;

      echo "<tr class=Caption>";
      $first = true;
      foreach ( $headline as $name => $attbs )
      {
         if ( $first && $this->found_rows >= 0 )
         {
            $fmt_entries = ($this->found_rows == 1 ) ? T_('%s entry') : T_('%s entries');
            $name .= SMALL_SPACING . span('NaviInfo', sprintf( $fmt_entries, $this->found_rows ), '(%s)');
            $first = false;
         }
         echo "<td $attbs>$name</td>";
      }
      echo "</tr>\n";
   }//print_headline

   public function forum_end_table( $bottom_bar=true )
   {
      if ( $bottom_bar && $this->links & LINK_MASKS )
         $this->echo_links(NEWMODE_BOTTOM);
      echo "</table>\n", name_anchor('fbottom');
   }

   // return arr( headline1, headtitle2 )
   public function build_threadpostview_headlines( $base_url, $show_overview )
   {
      // build view-links "(tree | new/old first)"
      $arr = array();
      if ( $this->flat_view == 0 ) // tree-view
      {
         $arr[] = array( $base_url .URI_AMP.'view=fo', T_('Old First#thread_view') );
         $arr[] = array( $base_url .URI_AMP.'view=fn', T_('New First#thread_view') );
      }
      else
      {
         $arr[] = array( $base_url .URI_AMP.'view=t', T_('Tree View#thread_view') );
         if ( $this->flat_view > 0 ) // flat new-first
            $arr[] = array( $base_url .URI_AMP.'view=fo', T_('Old First#thread_view') );
         else //if ( $this->flat_view < 0 ) // flat old-first
            $arr[] = array( $base_url .URI_AMP.'view=fn', T_('New First#thread_view') );
      }

      $head_fmt = '%s%s<span class="HeaderToggle">( <a href="%s">%s</a> ) ( <a href="%s">%s</a> | <a href="%s">%s</a> )</span>';
      if ( $show_overview )
      {

         $headtitle1 = sprintf( $head_fmt,
            T_('Reading thread overview'),
            SMALL_SPACING,
            $base_url .URI_AMP.'toggleflag='.FORUMFLAG_POSTVIEW_OVERVIEW,
            T_('Hide overview#threadoverview'),
            $arr[0][0], $arr[0][1],
            $arr[1][0], $arr[1][1] );
         $headline1 = array(
            $headtitle1 => "colspan={$this->cols}"
         );

         $headtitle2 = T_('Reading thread posts');
      }
      else
      {
         $headline1 = null;

         $headtitle2 = sprintf( $head_fmt,
            T_('Reading thread posts'),
            SMALL_SPACING,
            $base_url .URI_AMP.'toggleflag='.FORUMFLAG_POSTVIEW_OVERVIEW,
            T_('Show overview#threadoverview'),
            $arr[0][0], $arr[0][1],
            $arr[1][0], $arr[1][1] );
      }

      return array( $headline1, $headtitle2 );
   }//build_threadpostview_headlines

   // param ReqParam: optional object RequestParameters containing URL-parts to be included for paging
   public function make_link_array( RequestParameters $ReqParam = null )
   {
      global $NOW;
      $links = $this->links;
      if ( !( $links & LINK_MASKS ) )
         return;
      $fid = $this->forum_id;
      $tid = $this->thread_id;

      if ( $links & LINK_FORUMS )
         $this->link_array_left[T_('Forums')] = "index.php";

      if ( $links & LINK_THREADS )
         $this->link_array_left[T_('Threads')] = "list.php?forum=$fid";

      if ( $links & LINK_BACK_TO_THREAD )
      {
         $this->link_array_left[T_('Back to thread')] = "read.php?forum=$fid"
               .URI_AMP."thread=$tid"
               .( ( $this->back_post_id ) ? '#'.$this->back_post_id : '' );
      }

      if ( $links & LINK_NEW_TOPIC )
         $this->link_array_left[T_('New Topic')] = "read.php?forum=$fid";

      if ( $links & LINK_REFRESH )
      {
         if ( $links & LINKPAGE_READ )
         {
            $url = make_url( 'read.php', array( 'forum' => $fid, 'thread' => $tid ));
            if ( isset($_REQUEST['raw']) )
               $url .= URI_AMP . 'raw='.$_REQUEST['raw'];
         }
         elseif ( $links & LINKPAGE_LIST )
            $url = make_url( 'list.php', array( 'forum' => $fid, 'maxrows' => $this->max_rows ));
         elseif ( $links & LINKPAGE_SEARCH )
            $url = '';
         else
            $url = "index.php";
         if ( $url )
            $this->link_array_left[T_('Refresh')] = $url;
      }

      if (ENABLE_FORUM_TAGS)
         $this->link_array_left[T_('Tags')] = "tags.php";

      if ( $links & LINK_SEARCH )
         $this->link_array_left[T_('Search')] = "search.php";

      if ( ($links & LINKPAGE_LIST) && ($links & LINK_MARK_READ) )
      {
         $this->link_array_left[T_('Mark All Read')] = make_url( 'list.php',
               array( 'forum' => $fid, 'maxrows' => $this->max_rows, 'markread' => $NOW ));
      }

      if ( $links & LINK_TOGGLE_MODERATOR )
      {
         // preserve all page-args on moderator switch
         $get = array_merge( $_GET, $_POST);
         unset($get['modact']);
         unset($get['modpid']);
         $get['moderator'] = ($this->is_moderator) ? 'n' : 'y';
         if ( $links & LINKPAGE_READ )
            $url = make_url( 'read.php', $get );
         elseif ( $links & LINKPAGE_LIST )
            $url = make_url( 'list.php', $get );
         elseif ( $links & LINKPAGE_SEARCH )
            $url = make_url( 'search.php', $get );
         else
            $url = make_url( 'index.php', $get );
         $this->link_array_right[T_("Toggle forum moderator")] = $url;
      }

      $navi = array( 'maxrows' => $this->max_rows );
      if ( !is_null($ReqParam) && ($links & (LINKPAGE_SEARCH|LINK_PREV_PAGE|LINK_NEXT_PAGE)) )
         $navi = array_merge( $navi, $ReqParam->get_entries() );

      if ( $links & LINK_PREV_PAGE )
      {
         $navi['offset'] = $this->offset - $this->max_rows;
         if ( $links & LINKPAGE_SEARCH )
            $href = 'search.php?';
         else
            $href = "list.php?forum=$fid";
         $this->link_array_right[T_("Prev Page")] =
            array( make_url( $href, $navi ), '', array( 'accesskey' => ACCKEY_ACT_PREV ) );
      }
      if ( $links & LINK_NEXT_PAGE )
      {
         $navi['offset'] = $this->offset + $this->max_rows;
         if ( $links & LINKPAGE_SEARCH )
            $href = 'search.php?';
         else
            $href = "list.php?forum=$fid";
         $this->link_array_right[T_("Next Page")] =
            array( make_url( $href, $navi ), '', array( 'accesskey' => ACCKEY_ACT_NEXT ) );
      }
   }//make_link_array

   public function echo_links( $new_mode )
   {
      if ( ($new_mode & (NEWMODE_TOP|NEWMODE_BOTTOM)) == NEWMODE_TOP )
         $id = 'T';
      elseif ( ($new_mode & (NEWMODE_TOP|NEWMODE_BOTTOM)) == NEWMODE_BOTTOM )
         $id = 'B';
      else
         error('invalid_args', "DisplayForum.echo_links.check.new_mode($new_mode)");

      $lcols = $this->cols; //1; $cols/2; $cols-1;
      $tmp = ( $lcols > 1 ? ' colspan='.$lcols : '' );
      echo "<tr class=Links$id><td$tmp><div class=TreeLinks>";

      $first = true;
      foreach ( $this->link_array_left as $name => $link )
      {
         if ( !$first )
            echo "&nbsp;|&nbsp;";
         else
            $first = false;
         if ( is_array($link) )
            echo anchor( $link[0], $name, $link[1], $link[2]);
         else
            echo anchor( $link, $name);
      }
      echo $this->get_new_string( $new_mode );

      $lcols = $this->cols - $lcols;
      $tmp = ( $lcols > 1 ? ' colspan='.$lcols : '' );
      if ( $lcols > 0 )
         echo "</div></td><td$tmp><div class=PageLinks>";
      else
         echo "</div><div class=PageLinks>";

      $first = true;
      foreach ( $this->link_array_right as $name => $link )
      {
         if ( !$first )
            echo "&nbsp;|&nbsp;";
         else
            $first = false;
         if ( is_array($link) )
            echo anchor( $link[0], $name, $link[1], $link[2]);
         else
            echo anchor( $link, $name);
      }

      echo "</div></td></tr>\n";
   }//echo_links

   /*!
    * \brief Increase global new counter, builds and returns current new-string.
    * param $mode bitmask of NEWMODE_TOP|BOTTOM ('new' for top/bottom-bar);
    *       NEWMODE_OVERVIEW, NEWMODE_NO_LINK
    */
   public function get_new_string( $mode=0 )
   {
      static $fmt_new = '<span class="NewFlag"><a name="%s%d"%s>%s</a></span>'; // anchor_prefix, new-idx, link-ref, new-text

      $new = '';
      $anchor_prefix = 'new';
      if ( $mode & (NEWMODE_TOP|NEWMODE_BOTTOM) ) // top/bottom-bar refer to 1st-NEW
      {
         $link = ($mode & NEWMODE_NO_LINK) ? '' : ' href="#new1"';
         if ( ($mode & NEWMODE_TOP) && $this->count_new_posts > 0 )
            $new = sprintf( $fmt_new, $anchor_prefix, 0, $link, T_('first new') );
         elseif ( ($mode & NEWMODE_BOTTOM) && $this->new_count > 0 )
            $new = sprintf( $fmt_new, $anchor_prefix, $this->new_count + 1, $link, T_('first new') );
      }
      else
      {
         if ( $mode & NEWMODE_OVERVIEW )
         {
            $anchor_prefix = 'treenew';
            $addnew = 0;
         }
         else
            $addnew = 1;

         $this->new_count++;
         $link = ($mode & NEWMODE_NO_LINK)
            ? '' : sprintf(' href="#new%d"', $this->new_count + $addnew );
         $new = sprintf( $fmt_new, $anchor_prefix, $this->new_count, $link, T_('new#forum') );
      }
      return $new;
   }//get_new_string

   // \param $drawmode one of DRAWPOST_NORMAL | PREVIEW | EDIT | REPLY
   // \note checking for thread-read-only flag must be done at caller-side
   public function forum_message_box( $drawmode, $post_id, $ErrorMsg='', $Subject='', $Text='', $Flags=0 )
   {
      global $player_row;
      if ( ($player_row['AdminOptions'] & ADMOPT_FORUM_NO_POST) ) // user not allowed to post
         return;

      if ( !Forum::allow_posting($player_row, $this->forum_opts) )
         return;

      // reply-prefix
      if ( ($drawmode == DRAWPOST_NORMAL || $drawmode == DRAWPOST_REPLY)
            && strlen($Subject) > 0 && strcasecmp(substr($Subject,0,3), "re:") != 0 )
         $Subject = "RE: " . $Subject;

      $msg_form = 'messageform';
      $form = new Form( $msg_form, "read.php#preview", FORM_POST );

      if ( @$player_row['ID'] <= GUESTS_ID_MAX )
      {
         $form->add_row( array(
               'DESCRIPTION', span('EmphasizeWarn', T_('NOTE#guest')),
               'TEXT', span('EmphasizeWarn',
                        T_("Forum posts by the guest-user can be approved or rejected by moderators.") . "<br>\n" .
                        T_("Do not post email-addresses or other private data, because other users can see it.") . "<br>\n" .
                        sprintf( T_("For private support please send an email to %s instead!#support"),
                           sprintf("<a href=\"mailto:".EMAIL_SUPPORT."?subject=%s&body=%s\">".EMAIL_SUPPORT."</a>",
                              T_('DGS: What\'s the issue?#support'),
                              T_('Describe the issue.#support')) )),
                           ));
      }
      if ( $ErrorMsg )
      {
         $form->add_row( array(
               'DESCRIPTION', span('ErrorMsg bold', T_('Error#forum')),
               'TEXT', span('ErrorMsg', $ErrorMsg), ));
      }

      $form->add_row( array(
            'DESCRIPTION', T_('Subject'),
            'TEXTINPUT', 'Subject', 50, 80, $Subject,
            'HIDDEN', ($drawmode == DRAWPOST_EDIT ? 'edit' : 'parent'), $post_id,
            'HIDDEN', 'thread', $this->thread_id,
            'HIDDEN', 'forum', $this->forum_id ));
      $form->add_row( array(
            'TAB', 'TEXTAREA', 'Text', 70, 25, $Text ));

      if ( $post_id == 0 && ForumPost::allow_post_read_only() ) // only for new thread
      {
         $form->add_row( array(
            'TAB', 'CHECKBOX', 'ReadOnly', 1, T_('Read-only thread (only admin executives may reply)'),
               ($Flags & FPOST_FLAG_READ_ONLY), ));
      }

      $form->add_row( array(
            'SUBMITBUTTONX', 'post', ' ' . T_('Post') . ' ', array( 'accesskey' => ACCKEY_ACT_EXECUTE ),
            'SUBMITBUTTONX', 'preview', ' ' . T_('Preview') . ' ', array( 'accesskey' => ACCKEY_ACT_PREVIEW ), ));

      $form->echo_string(1);
   }//forum_message_box


   // The table structure of the list controlled by depth:
   // level 1: the header, body and footer TABLE of the list
   // level 2: the body of the list: one row per post managing its indent
   // level 3: the post cell TABLE
   public function change_depth( $new_depth )
   {
      if ( $new_depth < 1 && $this->cur_depth < 1 )
         return;

      if ( $this->cur_depth >= 1 ) //this means that a cell table is already opened
         echo "</table></td></tr>\n";

      if ( $new_depth < 1 ) //this means close it
      {
         echo "</table></td></tr>\n";
         $this->cur_depth = -1;
         return;
      }

      if ( $this->cur_depth < 1 ) //this means opened it
         echo "<tr><td colspan={$this->cols}><table width=\"100%\" border=0 cellspacing=0 cellpadding=0>";

      // then build the indenting row
      $this->cur_depth = $new_depth;
      echo "<tr>";
      $indent = "<td class=Indent>&nbsp;</td>";
      $i = min( $this->cur_depth, FORUM_MAX_INDENT);
      $c = FORUM_MAX_INDENT+1 - $i;
      switch ( (int)$i )
      {
         case 1:
            break;

         case 2:
            echo "$indent";
            break;

         case 3:
            echo "<td class=Indent2></td>$indent";
            break;

         default:
            echo "<td class=Indent2 colspan=".($i-2)."></td>$indent";
            break;
      }

      // finally, open the cell table
      echo "<td colspan=$c><table width=\"100%\" border=0 cellspacing=0 cellpadding=3>";
   }//change_depth

   /*! \brief Inits and returns navigational images for draw_post if not initialized yet. */
   private function init_navi_images()
   {
      if ( !is_array($this->navi_img) )
      {
         global $base_path;
         $this->navi_img = array(
            'top'          => image( $base_path.'images/f_top.png',
                                     T_('Top'), null ),
            'prev_parent'  => image( $base_path.'images/f_prevparent.png',
                                     T_('Previous parent'), null ),
            'prev_answer'  => image( $base_path.'images/f_prevanswer.png',
                                     T_('Previous answer'), null ),
            'next_answer'  => image( $base_path.'images/f_nextanswer.png',
                                     T_('Next answer'), null ),
            'next_parent'  => image( $base_path.'images/f_nextparent.png',
                                     T_('Next parent'), null ),
            'bottom'       => image( $base_path.'images/f_bottom.png',
                                     T_('Bottom'), null ),
            'first_answer' => image( $base_path.'images/f_firstanswer.png',
                                     T_('First answer'), null ),
         );
      }
      return $this->navi_img;
   }//init_navi_images


   /*!
    * \brief Draws various views of post controlled by $drawmode.
    * \param $drawmode DRAWPOST_..., MASK_DRAWPOST_...
    * \param $post: ForumPost-object to draw
    */
   public function draw_post( $drawmode, ForumPost $post, $is_my_post )
   {
      global $NOW, $player_row, $base_path;

      // post-vars needed:
      //    id, forum_id, thread_id, parent_id, subject, text, author(id,name,handle,rating),
      //    pending_approval, last_edited, last_read
      // post-vars only needed for forum-search: forum_name, score; this->show_score

      $pid = $post->id;
      $thread_url = $post->build_url_post(''); //post_url ended by #$pid
      $term_url = ( $this->rx_term != '' ) ? URI_AMP."xterm=".urlencode($this->rx_term) : '';
      $user_may_post = !($player_row['AdminOptions'] & ADMOPT_FORUM_NO_POST); // user allowed to post?

      $cols = 2; //one for the subject header, one for the possible approved/hidden state

      // highlight terms in Subject/Text
      $sbj = make_html_safe( $post->subject, SUBJECT_HTML, $this->rx_term );
      $txt = make_html_safe( $post->text, true, $this->rx_term );
      $txt = MarkupHandlerGoban::replace_igoban_tags( $txt );
      if ( strlen($txt) == 0 ) $txt = '&nbsp;';

      $sbj_readonly = ( $post->is_thread_post() ) ? $post->format_flags() : '';

      // CSS-class for post header
      $drawmode_type = ($drawmode & MASK_DRAWPOST_MODES);
      if ( $drawmode == DRAWPOST_NORMAL ) // most frequent case, no extras
         $class = 'Normal';
      elseif ( $drawmode_type == DRAWPOST_SEARCH )
         $class = 'SearchResult';
      elseif ( $drawmode & MASK_DRAWPOST_HIDDEN )
         $class = 'Hidden';
      elseif ( $drawmode_type == DRAWPOST_PREVIEW )
         $class = 'Preview';
      elseif ( $drawmode_type == DRAWPOST_EDIT )
         $class = 'Edit';
      elseif ( $drawmode_type == DRAWPOST_REPLY )
         $class = 'Reply';
      else //if drawmode_type 0 or DRAWPOST_NORMAL
      {
         $drawmode_type = DRAWPOST_NORMAL;
         $class = 'Normal';
      }
      $hdrclass = 'PostHead'.$class;

      // post header
      if ( $drawmode_type == DRAWPOST_PREVIEW )
      {
         $hdrrows = 2;
         $hdrcols = $cols;

         echo "\n<tr class=\"$hdrclass Subject\"><td class=Subject colspan=$hdrcols>"
            , name_anchor('preview', 'PostSubject', $sbj), " $sbj_readonly</td></tr> "
            ,"\n<tr class=\"$hdrclass Author\"><td class=Author colspan=$hdrcols>"
            ,T_('by'),' ' ,user_reference( REF_LINK, 1, '', $player_row)
            , ', ', echo_rating($player_row['Rating2'], /*show%*/false, $player_row['ID'], /*engl*/false, /*short*/true)
            , ' ', SMALL_SPACING, date(DATE_FMT, $NOW)
            ,"</td></tr>";
      }
      else
      {
         if ( $drawmode & MASK_DRAWPOST_HIDDEN )
         {
            $hdrcols = $cols-1; //because of the rowspan=$hdrrows in the "hidden"-column
            $newstr = ''; // no NEW for hidden posts
         }
         else
         {
            $hdrcols = $cols;
            $newstr = ($post->is_read) ? '' : $this->get_new_string();
         }
         $subject_modstr = ( $drawmode & MASK_DRAWPOST_NO_BODY )
            ? '[* '.T_('subject moderated').' *]' : $sbj;

         if ( $drawmode_type == DRAWPOST_SEARCH )
         {
            $hdrrows = 3;

            // [header-row] search-found
            echo "\n<tr class=\"$hdrclass FoundForum\"><td class=FoundForum colspan=$hdrcols>";
            echo '<span class=FoundForum>' ,T_('found in forum')
               ,' <a href="list.php?forum=', $post->forum_id, '">', $post->forum_name, "</a></span>\n";
            if ( $this->show_score )
               echo ' <span class=FoundScore>' ,T_('with')
                  ,' <span>' ,T_('Score') ,' ' ,$post->score ,"</span></span>\n";
         }
         else
         {
            $hdrrows = 2;

            // [header-row] subject
            echo "\n<tr class=\"$hdrclass Subject\"><td class=Subject colspan=$hdrcols>";

            //from revision_history or because, when edited, the link will be obsolete
            if ( $drawmode_type == DRAWPOST_EDIT || $post->thread_no_link )
               echo name_anchor($pid, 'PostSubject', $subject_modstr), " $sbj_readonly";
            else
               echo "<a name=\"$pid\" class=\"PostSubject\" href=\"", $thread_url, $term_url,
                  "#$pid\">$subject_modstr</a>", $sbj_readonly, $newstr;
         }

         // first [header-row] with different content (adding hidden-state)
         if ( $hdrcols != $cols )
         {
            echo "</td>\n <td rowspan=$hdrrows class=PostStatus><span class=\"Hidden\">";
            echo ( $post->is_pending_approval() ? T_('Awaiting<br>approval') : T_('Hidden') );
            if ( $drawmode & MASK_DRAWPOST_NO_BODY )
               echo sprintf( ' (%s)', T_('Content moderated'));
            echo '</span>';
         }
         echo '</td></tr>';

         if ( $drawmode_type == DRAWPOST_SEARCH )
         {
            // [header-row] subject
            echo "\n<tr class=\"$hdrclass Subject\"><td class=Subject colspan=$hdrcols>";
            echo '<a class=PostSubject href="', $thread_url, $term_url, "#$pid\">$subject_modstr</a>";
            echo "</td></tr>";
         }

         // [header-row] post-info: "author, created (edited)  (No. X)"
         echo "\n<tr class=\"$hdrclass Author\"><td class=Author colspan=$hdrcols>";

         $author_rating_str = ( $post->author->hasRating(false) )
            ? ', ' . echo_rating($post->author->Rating, /*show%*/false, $post->author->ID, /*engl*/false, /*short*/true)
            : '';
         echo T_('by'), ' ',
              echo_image_admin( $post->author->AdminLevel ),
              ' ', $post->author->user_reference(), $author_rating_str, SMALL_SPACING,
              date(DATE_FMT, $post->created);

         if ( !($drawmode & MASK_DRAWPOST_NO_BODY) )
            echo $this->get_post_edited_string( $post );

         if ( $drawmode_type != DRAWPOST_SEARCH && !($drawmode & MASK_DRAWPOST_NO_NUM) )
            echo SMALL_SPACING, sprintf( '(%s %s)', T_('No.#num'), $post->creation_order );

         if ( ENABLE_FORUM_TAGS && !($drawmode & MASK_DRAWPOST_NO_TAGS) )
         {
            $js_enabled = is_javascript_enabled();
            $likedClass = $post->has_user_tag(TAG_LIKE) ? ' Liked' : '';
            $imgLikes = image( $base_path . 'images/likes.gif', '+', '', 'class="Action Likes'.$likedClass.'"' );
            if ($js_enabled)
               echo anchor( "#$pid", $imgLikes,
                  T_( 'Toggle like for post#tag' ), array( 'class' => 'LikePost', 'data-post-id' => $pid ) );
            else
               echo MED_SPACING, $imgLikes;
            echo span( "id=plike$pid class=Likes", $post->count_likes );

            if ( count( $this->ForumTags ) )
            {
               if ( $js_enabled )
                  echo anchor( "#$pid",
                        image( $base_path . 'images/edit.gif', 'E', '', 'class="Action"' ),
                        T_( 'Edit post tags' ), array( 'class' => 'EditTag', 'data-post-id' => $pid ) );
               echo sprintf( '<div id="%s" class="PostTags">%s</div>',
                     "ptags$pid", Tag::renderTagList($post->user_tags, $this->ForumTags));
            }
         }

         echo "</td></tr>";
      }

      // post body
      $reply_link = "<a href=\"".$base_path."forum/" . $thread_url.URI_AMP. "reply=$pid#$pid\">[ " . T_('reply#forum') . " ]</a>";
      if ( in_array($post->author->ID, $this->hide_post_users) && ($drawmode_type != DRAWPOST_REPLY) )
      {
         echo "\n<tr class=PostBodyHide><td colspan=$cols>",
            sprintf( T_('The author of this post is on your forum-post hide-list. Use %s to display it.'), $reply_link ),
            "</td></tr>";
      }
      elseif ( !($drawmode & MASK_DRAWPOST_NO_BODY) )
         echo "\n<tr class=PostBody><td colspan=$cols>$txt</td></tr>";


      // post bottom line (footer)
      if ( $drawmode_type == DRAWPOST_NORMAL || $drawmode_type == DRAWPOST_REPLY )
      {
         $flat = ( $this->flat_view != 0 );
         echo "\n<tr class=PostButtons><td colspan=$cols>";

         $imgarr = $this->init_navi_images();
         $prev_parent = ( is_null($post->prev_parent_post) )
            ? ''
            : anchor( '#'.$post->prev_parent_post->id, $imgarr['prev_parent'] ) . '&nbsp;';
         $next_parent = ( is_null($post->next_parent_post) )
            ? ''
            : anchor( '#'.$post->next_parent_post->id, $imgarr['next_parent'] ) . '&nbsp;';
         $prev_answer = ( is_null($post->prev_post) || $flat )
            ? ''
            : anchor( '#'.$post->prev_post->id, $imgarr['prev_answer'] ) . '&nbsp;';
         $next_answer = ( is_null($post->next_post) || $flat )
            ? ''
            : anchor( '#'.$post->next_post->id, $imgarr['next_answer'] ) . '&nbsp;';
         $first_answer = ( is_null($post->first_child_post) )
            ? ''
            : anchor( '#'.$post->first_child_post->id, $imgarr['first_answer'] ) . '&nbsp;';

         // BEGIN Navi (top/prev-parent/prev-answer)
         echo anchor( '#ftop', $imgarr['top'] ),
            '&nbsp;',
            $prev_parent,
            $prev_answer,
            '&nbsp;';

         if ( $user_may_post && Forum::allow_posting($player_row, $this->forum_opts) )
         {
            if ( !is_null($post->thread_post) && $post->thread_post->allow_post_reply() && $drawmode_type != DRAWPOST_REPLY )
            {
               // reply link
               echo $reply_link, "&nbsp;&nbsp;";
               if ( ALLOW_QUOTING )
                  echo '<a href="', $thread_url,URI_AMP,"quote=1",URI_AMP,"reply=$pid#$pid\">[ ",
                     T_('quote#forum'), " ]</a>&nbsp;&nbsp;";
            }

            // edit link
            if ( $is_my_post )
               echo '<a class=Highlight href="', $thread_url,URI_AMP,"edit=$pid#$pid\">[ ",
                  T_('edit'), " ]</a>&nbsp;&nbsp;";
         }

         // END Navi (next-answer/next-parent/bottom)
         echo $next_answer,
            $next_parent,
            anchor( '#fbottom', $imgarr['bottom'] ),
            '&nbsp;',
            $first_answer,
            '&nbsp;';

         // for moderator: hide/show/approve/reject-link
         if ( $this->is_moderator )
         {
            $modurl_fmt = '<a class=Highlight href="'.$thread_url
               . URI_AMP."modpid=$pid".URI_AMP."modact=%s#$pid\">[ %s ]</a>";
            if ( $post->is_pending_approval() )
            {
               echo sprintf( $modurl_fmt, 'approve',  T_('Approve') ),
                  MED_SPACING,
                  sprintf( $modurl_fmt, 'reject',  T_('Reject') );
            }
            else
            {
               if ( $post->is_approved() )
                  echo sprintf( $modurl_fmt, 'hide',  T_('hide#forum') );
               else
                  echo sprintf( $modurl_fmt, 'show',  T_('show#forum') );
            }
         }
         echo "</td></tr>\n";
      }//post-footer
   }//draw_post

   /*! \brief Draw tree-overview for this thread. */
   public function draw_overview( ForumThread $fthread )
   {
      global $base_path, $player_row;
      $this->new_count = 0;
      $this->change_depth( 1 );

      echo "\n<tr class=TreePostNormal><td><table class=ForumTreeOverview>",
         "\n<tr class=\"TreePostNormal Header\">",
         sprintf( '<th>%s</th><th>%s</th><th>%s</th><th>%s'.SMALL_SPACING.'(%s)</th></tr>',
            T_('Subject'), (ENABLE_FORUM_TAGS ? T_('Likes#tag') : ''), T_('Author'), T_('Last changed'), T_('No.#num') );

      // draw for post: subject, author, date
      $c=2;
      foreach ( $fthread->posts as $pid => $post )
      {
         $hidden = !$post->is_approved();
         $is_my_post = $post->is_author($this->user_id);
         $show_hidden_post = ( $is_my_post || $this->is_moderator );
         if ( $hidden && !$post->is_thread_post() && !$show_hidden_post )
            continue;

         $subj_part = cut_str( $post->subject, 40 );
         $sbj = make_html_safe( $subj_part, SUBJECT_HTML, $this->rx_term );
         $newstr = ( !$hidden && !$post->is_read ) ? $this->get_new_string(NEWMODE_OVERVIEW) : '';
         $modstr = ( $hidden && ( $post->is_thread_post() || $this->is_moderator || $is_my_post ) )
            ? MED_SPACING . span('Moderated', ForumPost::get_approved_text( $post->approved ), '(%s)')
            : '';

         $c = 3 - $c;
         $mypostclass = ($is_my_post) ? ' class=MyPost' : '';
         $mypost_fmt = ($is_my_post) ? '<span class="MyPost">%s</span>' : '%s';
         $depth = ( $this->flat_view ) ? 1 : $post->depth;
         if (ENABLE_FORUM_TAGS)
         {
            $likedHidden = ($post->count_likes == 0) ? ' hidden' : '';
            $likedClass = $post->has_user_tag(TAG_LIKE) ? ' Liked' : '';
            $likesInfo = '<span id="povlike'.$pid.'" class="LikePost'.$likedHidden.'">'
               . image( $base_path . 'images/likes.gif', '', '', 'class="Action Likes '.$likedClass.'"' )
               . span( 'Likes', $post->count_likes )
               . "</span>";
         }
         else
            $likesInfo = '';
         echo "\n<tr class=\"TreePostNormal Row{$c}". ($is_my_post ? ' MyPost' : '') ."\">",
            "<td$mypostclass>",
            str_repeat( '&nbsp;', 3*($depth - 1) ),
            sprintf( $mypost_fmt, anchor( '#'.$post->id, $sbj, '', 'class=PostSubject' ) ),
            $newstr,
            $modstr,
            '</td><td>',
            $likesInfo,
            "</td><td>",
            span('PostUser', $post->author->user_reference()),
            "</td><td>",
            sprintf( '<span class=PostDate>%s'.SMALL_SPACING.'(%s)</span>',
               date( DATE_FMT, max($post->created, $post->last_edited) ), $post->creation_order ),
            '</td></tr>';
      }

      echo "\n</table></td></tr>\n";

      $this->new_count = 0;
      $this->change_depth( -1 );
   }//draw_overview

   public function get_post_edited_string( ForumPost $post )
   {
      if ( $post->last_edited > 0 )
      {
         $result = SMALL_SPACING . sprintf( '(<a href="%s">%s</a> %s)',
               $post->build_url_post( '', 'revision_history='.$post->id ),
               T_('edited'),
               date( DATE_FMT, $post->last_edited ) );
      }
      else
         $result = '';
      return $result;
   }

} // end of 'DisplayForum'




 /*!
  * \class Forum
  *
  * \brief Class to handle forum
  */
class Forum
{
   /*! \brief Forums.ID : id */
   public $id;
   /*! \brief Forums.Name : str */
   public $name;
   /*! \brief Forums.Description : str */
   public $description;
   /*! \brief Forums.LastPost : id (=Posts.ID) */
   public $last_post_id;
   /*! \brief Forums.Updated (change-date for forum-thread-read) */
   private $updated;
   /*! \brief Forums.ThreadsInForum : int */
   public $count_threads;
   /*! \brief Forums.PostsInForum : int */
   public $count_posts;
   /*! \brief Forums.SortOrder : int */
   private $sort_order;
   /*! \brief Forums.Options : int, bit-values see FORUMOPT_MODERATED, etc. */
   public $options;

   // non-db vars

   /*! \brief partly filled ForumPost-object for last_post_id [default=null] */
   public $last_post = null;
   /*! \brief array of ForumPost-objects [default=null] */
   public $threads = null;
   /*! \brief true, if there are more threads to page-navigate. */
   private $navi_more_threads;
   /*! \brief true if forum has new posts. */
   public $has_new_posts = false;


   /*! \brief Constructs Forum with specified args. */
   private function __construct( $id=0, $name='', $description='', $last_post_id=0, $updated=0,
         $count_threads=0, $count_posts=0, $sort_order=0, $options=0 )
   {
      $this->id = $id;
      $this->name = $name;
      $this->description = $description;
      $this->last_post_id = $last_post_id;
      $this->updated = $updated;
      $this->count_threads = $count_threads;
      $this->count_posts = $count_posts;
      $this->sort_order = $sort_order;
      $this->options = $options;
   }

   /*! \brief Returns string-representation of this object (for debugging purposes). */
   public function to_string()
   {
      return "Forum(id={$this->id}): "
         . "name=[{$this->name}]"
         . ", description=[{$this->description}]"
         . ", last_post_id=[{$this->last_post_id}]"
         . ", updated=[{$this->updated}]"
         . ", #threads=[{$this->count_threads}]"
         . ", #posts=[{$this->count_posts}]"
         . ", sort_order=[{$this->sort_order}]"
         . ", options=[{$this->options}]"
         . ', last_post={' . ( is_null($this->last_post) ? '' : $this->last_post->to_string() ) . '}'
         . ", has_new_posts=[{$this->has_new_posts}]";
   }

   /*!
    * \brief Returns Forums.Options in text-form.
    * \param $forum_opts ForumOptions-object for specific user
    */
   public function build_options_text( ForumOptions $forum_opts, $formatted=true )
   {
      $opt_prefix = ' &nbsp;&nbsp;[';
      $str = '';

      if ( $this->options & FORUMOPT_MODERATED )
         $str .= $opt_prefix . T_('Moderated') . ']';
      if ( $this->options & FORUMOPT_READ_ONLY )
         $str .= $opt_prefix . T_('Read-Only') . ']';
      if ( $forum_opts->is_executive_admin() && ($this->options & FORUMOPTS_GROUPS_HIDDEN) )
         $str .= $opt_prefix . T_('Hidden#forum') . ']';

      if ( $formatted && $str )
         $str = SMALL_SPACING . span('ForumOpts', $str);
      return $str;
   }

   /*!
    * \brief Loads threads for current forum into this object (this->threads = ForumPost[]).
    * \return count of loaded rows
    */
   public function load_threads( $user_id, $is_moderator, $show_rows, $offset=0 )
   {
      if ( !is_numeric($user_id) )
         error('invalid_user', "Forum.load_threads($user_id)");
      if ( !is_numeric($show_rows) || !is_numeric($offset) )
         error('invalid_args', "Forum.load_threads($show_rows,$offset)");

      $forum_id = $this->id;
      if ( !is_numeric($forum_id) )
         error('unknown_forum', "Forum.load_threads($forum_id)");

      $min_date = ForumRead::get_min_date();
      $qsql = ForumPost::build_query_sql();
      $qsql->add_part( SQLP_FIELDS,
         'LPAuthor.ID AS LPAuthor_ID',
            'LPAuthor.Name AS LPAuthor_Name',
            'LPAuthor.Handle AS LPAuthor_Handle',
         'IF(ISNULL(FR.User_ID),0,UNIX_TIMESTAMP(FR.Time)) AS FR_X_Lastread' );
      $qsql->add_part( SQLP_FROM,
         'LEFT JOIN Posts AS LP ON LP.ID=P.LastPost',  // LastPost
         'LEFT JOIN Players AS LPAuthor ON LPAuthor.ID=LP.User_ID', // LastPost-Author
         "LEFT JOIN Forumreads AS FR ON FR.User_ID='$user_id' AND FR.Forum_ID=P.Forum_ID "
            . 'AND FR.Thread_ID=P.Thread_ID' );
      $qsql->add_part( SQLP_WHERE,
         "P.Forum_ID=$forum_id",
         'P.Parent_ID=0', 'P.Thread_ID=P.ID' ); //=thread (better query with thread=id for Forumreads)
      if ( !$is_moderator )
         $qsql->add_part( SQLP_WHERE, 'P.PostsInThread>0' );
      $qsql->add_part( SQLP_ORDER, 'P.LastChanged DESC' );
      $qsql->add_part( SQLP_LIMIT, sprintf( '%d,%d', $offset, $show_rows + 1) );

      $query = $qsql->get_select();
      $result = db_query( "Forum.load_threads($user_id,$is_moderator,$show_rows,$offset)", $query );
      $rows = mysql_num_rows($result);

      $this->navi_more_threads = false;
      $thlist = array();
      while ( $row = mysql_fetch_array( $result ) )
      {
         $thread = ForumPost::new_from_row( $row ); // Post
         $thread->last_post = new ForumPost( $thread->last_post_id, $this->id, $thread->thread_id,
               User::newForumUser( $row['LPAuthor_ID'], $row['LPAuthor_Name'], $row['LPAuthor_Handle'] ));
         $thread->has_new_posts =
            ( $thread->last_changed >= $min_date ) && ( $thread->last_changed > $row['FR_X_Lastread'] );

         $thlist[] = $thread;
      }
      mysql_free_result($result);

      if ( $rows > $show_rows )
      {
         array_pop( $thlist ); // remove last entry
         $this->navi_more_threads = true;
         $rows--;
      }

      $this->threads = $thlist;
      return $rows;
   }//load_threads

   /*! \brief Returns true, if there are new posts in loaded thread-list. */
   public function has_new_posts_in_threads()
   {
      if ( !is_null($this->threads) )
      {
         foreach ( $this->threads as $thread ) // $thread = ForumPost-obj
         {
            if ( $thread->has_new_posts )
               return true;
         }
      }

      return false;
   }

   /*! \brief Use after call of load_threads() to check, if there are more threads to load. */
   public function has_more_threads()
   {
      return $this->navi_more_threads;
   }

   /*!
    * \brief Fix forum consistency for fields (LastPost, PostsInForum, ThreadsInForum).
    * \param $debug if true only echoes sql-statements
    * \note see section 'Calculated database fields' in 'specs/forums.txt'
    * \return number of updates
    */
   public function fix_forum( $debug, $debug_format="%s\n" )
   {
      $upd_arr = array();
      $fid = $this->id;

      // read fix for Forums.LastPost
      $row = mysql_single_fetch( "Forum.fix_forum.read.LastPost($fid)",
            "SELECT ID AS X_LastPost FROM Posts " .
            "WHERE Forum_ID='$fid' AND Thread_ID>0 AND Approved='Y' AND PosIndex>'' " .
            "ORDER BY Time DESC LIMIT 1" );
      if ( $row && $row['X_LastPost'] != $this->last_post_id )
         $upd_arr[] = 'LastPost=' . $row['X_LastPost'];
      elseif ( !$row && $this->last_post_id != 0 )
         $upd_arr[] = 'LastPost=0';

      // read fix for Forums.PostsInForum
      $row = mysql_single_fetch( "Forum.fix_forum.read.PostsInForum($fid)",
            "SELECT COUNT(*) AS X_Count FROM Posts " .
            "WHERE Forum_ID='$fid' AND Thread_ID>0 AND Approved='Y' AND PosIndex>''" );
      if ( $row && $row['X_Count'] != $this->count_posts )
         $upd_arr[] = 'PostsInForum=' . $row['X_Count'];

      // read fix for Forums.ThreadsInForum
      // note: delivers correct value only if Posts.PostsInThread is correct, so fix Posts first
      $row = mysql_single_fetch( "Forum.fix_forum.read.ThreadsInForum($fid)",
            "SELECT COUNT(*) AS X_Count FROM Posts ".
            "WHERE Forum_ID='$fid' AND Parent_ID=0 AND PostsInThread>0" );
      if ( $row && $row['X_Count'] != $this->count_threads )
         $upd_arr[] = 'ThreadsInForum=' . $row['X_Count'];

      // fix Forums
      if ( count($upd_arr) > 0 )
      {
         $query = "UPDATE Forums SET " . implode(', ', $upd_arr) . " WHERE ID='$fid' LIMIT 1";
         echo sprintf( $debug_format, $query );
         if ( !$debug )
            db_query( "Forum.fix_forum.update($fid)", $query );

         self::delete_cache_forum( "Forum.fix_forum.update($fid)", $fid );
      }

      return (count($upd_arr) > 0) ? 1 : 0;
   }//fix_forum


   // ---------- Static Class functions ----------------------------

   public static function is_admin()
   {
      global $player_row;
      return ( @$player_row['admin_level'] & (ADMIN_FORUM|ADMIN_DEVELOPER) );
   }

   /*! \brief Returns true if "writing posts" is allowed for read-only forum for given user. */
   public static function allow_posting( array $user_row, /*Int*/$forum_opts )
   {
      if ( is_null($forum_opts) )
         $forum_opts = FORUMOPT_READ_ONLY; // assuming read-only to be safe
      return ( $forum_opts & FORUMOPT_READ_ONLY )
         ? ( (int)@$user_row['admin_level'] & (ADMIN_FORUM|ADMIN_DEVELOPER) )
         : true;
   }

   /*! \brief Returns db-fields to be used for query of Forum-object. */
   public static function build_query_sql()
   {
      // Forums: ID,Name,Description,LastPost,ThreadsInForum,PostsInForum,SortOrder,Options
      $qsql = new QuerySQL();
      $qsql->add_part( SQLP_FIELDS,
         'Forums.*',
         'UNIX_TIMESTAMP(Forums.Updated) AS X_Updated' );
      $qsql->add_part( SQLP_FROM, 'Forums' );
      return $qsql;
   }

   /*! \brief Returns Forum-object created from specified (db-)row. */
   public static function new_from_row( array $row )
   {
      $forum = new Forum(
            @$row['ID'],
            @$row['Name'],
            @$row['Description'],
            @$row['LastPost'],
            @$row['X_Updated'],
            @$row['ThreadsInForum'],
            @$row['PostsInForum'],
            @$row['SortOrder'],
            @$row['Options']
         );
      return $forum;
   }

   /*!
    * \brief Returns non-null Forum-object for specified forum-id.
    * Throws errors if forum cannot be found.
    */
   public static function load_forum( $id )
   {
      if ( !is_numeric($id) || $id <= 0 )
         error('unknown_forum', "Forum:load_forum($id)");

      $qsql = self::build_query_sql();
      $qsql->add_part( SQLP_WHERE, "ID='$id'" );
      $qsql->add_part( SQLP_LIMIT, '1' );

      $query = $qsql->get_select();
      $row = mysql_single_fetch( "Forum:load_forum2($id)", $query );
      if ( !$row )
         error('unknown_forum', "Forum:load_forum3($id)");

      return self::new_from_row( $row );
   }

   // cached version of self::load_forum()
   public static function load_cache_forum( $id )
   {
      $dbgmsg = "Forum:load_cache_forum($id)";
      $key = "Forum.$id";

      $forum = DgsCache::fetch( $dbgmsg, CACHE_GRP_FORUM, $key );
      if ( is_null($forum) )
      {
         $forum = self::load_forum( $id );
         DgsCache::store( $dbgmsg, CACHE_GRP_FORUM, $key, $forum, SECS_PER_DAY );
      }

      return $forum;
   }

   public static function delete_cache_forum( $dbgmsg, $fid )
   {
      DgsCache::delete( $dbgmsg, CACHE_GRP_FORUM, "Forum.$fid" );
   }

   /*!
    * \brief Returns array of Forum-objects for specified user-id (in ForumOptions);
    *        returns null if no feature found.
    * \param $forum_opts ForumOptions-object for given user-id
    * \note also sets Forum-field: has_new_posts
    * \note stores result for user in Forumreads-table
    */
   public static function load_forum_list( ForumOptions $forum_opts )
   {
      $user_id = $forum_opts->uid;

      $qsql = self::build_query_sql();
      $qsql->add_part( SQLP_FIELDS,
         'LP.Thread_ID AS LP_Thread',
         'UNIX_TIMESTAMP(LP.Time) AS LP_Time',
         'LP.User_ID AS LP_User_ID',
         'PLP.Name AS LP_Name',
         'PLP.Handle AS LP_Handle',
         'IF(ISNULL(FR.User_ID),0,UNIX_TIMESTAMP(FR.Time)) AS FR_X_Lastread',
         'COALESCE(FR.HasNew,0) AS FR_HasNew' );
      $qsql->add_part( SQLP_FROM,
         'LEFT JOIN Posts AS LP ON Forums.LastPost=LP.ID',
         'LEFT JOIN Players AS PLP ON PLP.ID=LP.User_ID',
         "LEFT JOIN Forumreads AS FR ON FR.User_ID='$user_id' AND FR.Forum_ID=Forums.ID "
            . 'AND FR.Thread_ID=0' );
      $qsql->add_part( SQLP_ORDER, 'SortOrder' );

      $query = $qsql->get_select();
      $result = db_query( "Forum:load_forum_list($user_id)", $query );

      $fread = new ForumRead( $user_id );
      $flist = array();
      while ( $row = mysql_fetch_array( $result ) )
      {
         $forum = self::new_from_row( $row );
         if ( !$forum_opts->is_visible_forum( $forum->options ) )
            continue;

         $fid = $forum->id;
         $post = new ForumPost( $forum->last_post_id, $fid, $row['LP_Thread'],
               User::newForumUser( $row['LP_User_ID'], $row['LP_Name'], $row['LP_Handle'] ));
         $post->created = $row['LP_Time'];
         $forum->last_post = $post;

         // update forum-read for NEW-handling
         if ( $forum->count_posts > 0 )
         {
            if ( $row['FR_X_Lastread'] <= 0 || $forum->updated > $row['FR_X_Lastread'] )
            {
               $forum->has_new_posts = ForumRead::has_new_posts_in_forums( $user_id, $fid );
               $fread->replace_row_forumread( "Forum:load_forum_list.forum_read.upd",
                  $fid, 0, $forum->updated, $forum->has_new_posts );
            }
            else
               $forum->has_new_posts = $row['FR_HasNew'];
         }

         $flist[] = $forum;
      }
      mysql_free_result($result);

      return $flist;
   }//load_forum_list

   /*!
    * \brief Returns array of partial Forum-objects (only those visible to a player)
    *        with [ id => name ] entries.
    * \param forum_opts is object ForumOptions($player_row), load all forum names if omitted
    */
   public static function load_cache_forum_names( ForumOptions $forum_opts )
   {
      $dbgmsg = "Forum:load_cache_forum_names";
      $key = "ForumNames";

      $arr_forum_names = DgsCache::fetch( $dbgmsg, CACHE_GRP_FORUM_NAMES, $key );
      if ( is_null($arr_forum_names) )
      {
         // build forum-array for filter: ( Name => Forum_ID )
         $db_result = db_query( 'Forum:load_cache_forum_names',
               'SELECT ID, Name, Options FROM Forums ORDER BY SortOrder' );

         $arr_forum_names = array();
         while ( $row = mysql_fetch_array($db_result) )
            $arr_forum_names[$row['ID']] = array( $row['Name'], $row['Options'] );
         mysql_free_result($db_result);

         DgsCache::store( $dbgmsg, CACHE_GRP_FORUM_NAMES, $key, $arr_forum_names, SECS_PER_DAY );
      }

      $forum_names = array();
      foreach ( $arr_forum_names as $fid => $arr )
      {
         list( $f_name, $f_opts ) = $arr;

         // can user view forum?
         if ( $forum_opts && !$forum_opts->is_visible_forum($f_opts) )
            continue;

         $forum_names[$fid] = $f_name;
      }

      return $forum_names;
   }//load_cache_forum_names

   public static function delete_cache_forum_names( $dbgmsg )
   {
      DgsCache::delete( $dbgmsg, CACHE_GRP_FORUM_NAMES, "ForumNames" );
   }

   /*! \brief Returns array of Forum-objects with raw Forums-fields (for forum-fixes). */
   public static function load_fix_forum_list()
   {
      $qsql = self::build_query_sql();
      $qsql->add_part( SQLP_ORDER, 'ID' );

      $result = db_query( "Forum:load_fix_forum_list", $qsql->get_select() );
      $flist = array();
      while ( $row = mysql_fetch_array( $result ) )
      {
         $forum = self::new_from_row( $row );
         $flist[] = $forum;
      }
      mysql_free_result($result);
      return $flist;
   }

} // end of 'Forum'



 /*!
  * \class ForumThread
  *
  * \brief Class to handle thread with list of thread-posts
  */
class ForumThread
{
   /*! \brief ForumRead-object to be used to mark posts as read. */
   private $forum_read;
   /*! \brief array of posts in this thread: [ post->id => ForumPost ]. */
   public $posts = array();

   /*! \brief Thread starter post [default=null]; null, if none found. */
   public $thread_post = null;
   /*! \brief Timestamp of last created post; 0=no post. */
   public $last_created = 0;
   /*! \brief Map with user-tags for posts: Map{ post-id => [ tag-id, ...], ...} loaded by Tagged::load_tagged_by_user(..). */
   private $user_tags = array();

   public function __construct( ForumRead $forum_read = null )
   {
      $this->forum_read = $forum_read;
   }

   /*! \brief Returns post with given post-id from this ForumThread object; or NULL if not found. */
   public function get_post( $pid )
   {
      return (isset($this->posts[$pid])) ? $this->posts[$pid] : NULL;
   }

   /*! \brief Returns thread-hits or 0 if thread has no posts. */
   public function get_thread_hits()
   {
      return (is_null($this->thread_post)) ? 0 : $this->thread_post->count_hits;
   }

   public function set_user_tags( array $user_tags )
   {
      $this->user_tags = $user_tags;
   }

   /*!
    * \brief Loads and adds posts (to posts-arr): query fields and FROM set,
    *        needs WHERE and ORDER in qsql2-arg QuerySQL; Needs fresh object-instance.
    * \return number of new posts
    * \note sets ForumPost.is_read
    * \note sets this.last_created
    * \note Needs attribute forum_read set in this object!
    */
   public function load_posts( QuerySQL $qsql2 = null )
   {
      global $NOW;
      $qsql = ForumPost::build_query_sql();
      $qsql->merge( $qsql2 );

      $query = $qsql->get_select();
      $result = db_query( "ForumThread.load_posts", $query );

      $this->thread_post = null;
      $this->last_created = 0;
      $new_posts = 0;
      while ( $row = mysql_fetch_array( $result ) )
      {
         $post = ForumPost::new_from_row( $row );
         if ( $post->parent_id == 0 )
            $this->thread_post = $post;
         $post->set_post_is_read( $this->forum_read );
         if ( !$post->is_read )
            ++$new_posts;
         if ( $post->created > $this->last_created )
            $this->last_created = $post->created;

         if (isset($this->user_tags[$post->id]))
            $post->add_user_tags( $this->user_tags[$post->id] );

         $this->posts[$post->id] = $post;
      }
      mysql_free_result($result);

      foreach ( $this->posts as $pid => $post )
         $post->thread_post = $this->thread_post;

      return $new_posts;
   }//load_posts

   /*!
    * \brief Loads and adds posts (to posts-arr), current active post stored
    *        in thread_post; Needs fresh object-instance.
    */
   public function load_revision_history( $post_id )
   {
      global $NOW, $player_row;

      // select current active post
      $qsql = ForumPost::build_query_sql();
      $qsql->add_part( SQLP_WHERE, "P.ID='$post_id'" );
      $qsql->add_part( SQLP_LIMIT, '1' );

      $row = mysql_single_fetch( "ForumThread.load_revision_history.find_post($post_id)", $qsql->get_select() )
         or error('unknown_post', "ForumThread.load_revision_history.find_post2($post_id)");

      $post = ForumPost::new_from_row($row);
      $this->thread_post = $post;

      // check if allowed to view
      if ( !$post->is_approved() )
      {
         $my_id = $player_row['ID'];
         if ( !$post->is_author($my_id) && !Forum::is_admin() )
            error('forbidden_post', "ForumThread.load_revision_history.check_viewer($my_id,$post_id)");
      }

      // select all inactive history posts
      $qsql = ForumPost::build_query_sql();
      $qsql->add_part( SQLP_FIELDS,
         'GREATEST(P.Time,P.Lastedited) AS X_SortTime' ); // only sorting, so need no UNIX-time
      $qsql->add_part( SQLP_WHERE,
         "P.Parent_ID='$post_id'",
         "PosIndex=''" ); // '' == inactivated (edited)
      $qsql->add_part( SQLP_ORDER, 'X_SortTime DESC' );
      $result = db_query( "ForumThread.load_revision_history.find_edits($post_id)", $qsql->get_select() );

      while ( $row = mysql_fetch_array( $result ) )
      {
         $post = ForumPost::new_from_row($row);
         $post->thread_no_link = true; // display-opt
         $post->thread_post = $this->thread_post;
         $this->posts[$post->id] = $post;
      }
      mysql_free_result($result);
   }//load_revision_history

   /*! \brief Returns true, if one of loaded posts contains a go-diagram (igoban-tag). */
   public function contains_goban()
   {
      foreach ( $this->posts as $post_id => $post )
      {
         if ( MarkupHandlerGoban::contains_goban($post->text) )
            return true;
      }
      return false;
   }

   /*! \brief Returns string-representation of this object (for debugging purposes). */
   public function to_string()
   {
      $cnt = 0;
      $size = count($this->posts);
      $result = "ForumThread:\n";
      foreach ( $this->posts as $post_id => $post )
         $result .= sprintf( "[%d/%d]. pid=[%s]: {%s}\n", ++$cnt, $size, $post_id, $post->to_string() );
      return $result;
   }

   /*!
    * \brief Builds data-structure for navigation within post-list.
    * \param $set_in_posts if true, set navigation-links in ForumPosts in this object
    *
    * navtree[post_id] = map with keys: value=post-id or 0 (=no according node)
    *   prevP, nextP - prev/next-parent post
    *   prevA, nextA - prev/next-answer post for "parent"-thread
    *   child        - first-answer post
    *
    * \note order of post_id's in navtree is same as in posts of this ForumThread,
    *       but is expecting tree-sort by PosIndex
    */
   public function create_navigation_tree( $set_in_posts=true )
   {
      // find out flat-order
      $arr_order = array();
      foreach ( $this->posts as $post_id => $post )
         $arr_order[$post_id] = $post->created;
      asort($arr_order, SORT_NUMERIC);
      $idx = 0;
      foreach ( array_keys($arr_order) as $post_id )
         $arr_order[$post_id] = ++$idx;

      // build tree
      $navtree = array();
      $last_parent_posts = array(); // [ parent_id => last_post_id in parent-thread ]
      $parent_children = array(); // [ parent_id => [ post_id1, post_id2, ... ] ]
      foreach ( $this->posts as $post_id => $post )
      {
         $parent_id = $post->parent_id;
         $navmap = array();
         $navmap['prevP'] = $parent_id;
         $navmap['nextP'] = 0;
         $navmap['prevA'] = 0;
         $navmap['nextA'] = 0;
         $navmap['child'] = 0;

         $last_post_id = @$last_parent_posts[$parent_id];
         if ( $last_post_id )
         {
            $navmap['prevA'] = $last_post_id;
            $navtree[$last_post_id]['nextA'] = $post_id;
         }

         if ( $parent_id )
         {
            $last_parent_posts[$parent_id] = $post_id;
            $parent_children[$parent_id][] = $post_id;
         }
         $navtree[$post_id] = $navmap;

         $post->creation_order = $arr_order[$post_id]; // set flat-order (sort by creation-date)
      }

      foreach ( $parent_children as $parent_id => $children )
      {
         if ( isset($navtree[$parent_id]) )
         {
            $navtree[$parent_id]['child'] = $children[0]; // children non-empty
            foreach ( $children as $post_id )
               $navtree[$post_id]['nextP'] = $navtree[$parent_id]['nextA'];
         }
      }

      if ( $set_in_posts )
      {
         foreach ( $navtree as $post_id => $navmap )
         {
            $this->posts[$post_id]->set_navigation(
               ( $navmap['prevP'] ) ? $this->get_post($navmap['prevP']) : NULL,
               ( $navmap['nextP'] ) ? $this->get_post($navmap['nextP']) : NULL,
               ( $navmap['prevA'] ) ? $this->get_post($navmap['prevA']) : NULL,
               ( $navmap['nextA'] ) ? $this->get_post($navmap['nextA']) : NULL,
               ( $navmap['child'] ) ? $this->get_post($navmap['child']) : NULL
            );
         }
      }

      $this->navtree = $navtree;
   }//create_navigation_tree

} // end of 'ForumThread'



 /*!
  * \class ForumPost
  *
  * \brief Class to handle a thread-post.
  */
class ForumPost
{
   // IDs

   /*! \brief Posts.ID */
   public $id;
   /*! \brief Posts.Forum_ID */
   public $forum_id;
   /*! \brief Posts.Thread_ID */
   public $thread_id;

   // Thread Meta

   /*! \brief Posts.PostsInThread */
   public $count_posts;
   /*! \brief Posts.Hits */
   public $count_hits;
   /*! \brief Posts.LastPost */
   public $last_post_id;

   // Post Meta & Content

   /*! \brief non-null User-object ( .ID = Posts.User_ID ) */
   public $author;
   /*! \brief Posts.Subject */
   public $subject;
   /*! \brief Posts.Text */
   public $text;
   /*! \brief Posts.Flags */
   public $flags;

   /*! \brief Posts.Parent_ID */
   public $parent_id;
   /*! \brief Posts.AnswerNr */
   private $answer_num;
   /*! \brief Posts.Depth */
   public $depth;
   /*! \brief Posts.PosIndex : string */
   private $posindex;

   /*! \brief Posts.Approved : string (DB=Y|N|P); use is_approved/is_pending_approval-funcs. */
   public $approved;

   /*! \brief Posts.Time */
   public $created;
   /*! \brief Posts.Lastchanged, SQL X_Lastchanged; date of last-post in thread */
   public $last_changed;
   /*! \brief Posts.Lastedited */
   public $last_edited;

   /*! \brief Posts.crc32 */
   private $crc32;

   /*! \brief Posts.CountLikes */
   public $count_likes;

   // non-db vars

   /*! \brief ref to thread ForumPost (set by ForumThread.load_posts). */
   public $thread_post = null;
   /*! \brief ref to last created visible ForumPost (set by Forum.load_threads). */
   public $last_post = null;

   /*! \brief true, if for thread no link should be drawn (used in draw_post-func) [default=false] */
   public $thread_no_link = false;
   /*! \brief [int] order in thread-view (1..n); 0 = unset. */
   public $creation_order = 0;

   /*! \brief true if thread has new posts. */
   public $has_new_posts = false;
   /*! \brief true if post marked as read. */
   public $is_read = true;

   /*! \brief for forum-search: forum-name */
   public $forum_name;
   /*! \brief for forum-search: score */
   public $score = 0;

   /*! \brief Active tags list [ tag_id, ...] of currently logged-in user; tag_id can be <0 for special tags, e.g. TAG_LIKE. */
   public $user_tags = array();

   // tree-navigation (set by ForumThread::create_navigation_tree-func), NULL=not-set

   public $prev_parent_post;
   public $next_parent_post;
   public $prev_post;
   public $next_post;
   public $first_child_post;


   /*! \brief Constructs ForumPost-object with specified arguments: dates are in UNIX-time. */
   public function __construct( $id=0, $forum_id=0, $thread_id=0, User $author=null, $last_post_id=0,
         $count_posts=0, $count_hits=0, $subject='', $text='', $flags=0, $parent_id=0, $answer_num=0,
         $depth=0, $posindex='', $approved='Y',
         $created=0, $last_changed=0, $last_edited=0, $crc32=0, $count_likes=0 )
   {
      $this->id = (int) $id;
      $this->forum_id = (int) $forum_id;
      $this->thread_id = (int) $thread_id;
      $this->count_posts = (int) $count_posts;
      $this->count_hits = (int) $count_hits;
      $this->last_post_id = (int) $last_post_id;
      $this->author = ( is_null($author) ? new User() : $author );
      $this->subject = $subject;
      $this->text = $text;
      $this->flags = (int) $flags;
      $this->parent_id = (int) $parent_id;
      $this->answer_num = (int) $answer_num;
      $this->depth = (int) $depth;
      $this->posindex = $posindex;
      $this->approved = $approved;
      $this->created = (int) $created;
      $this->last_changed = (int) $last_changed;
      $this->last_edited = (int) $last_edited;
      $this->crc32 = (int) $crc32;
      $this->count_likes = (int) $count_likes;
   }

   public function copy_post()
   {
      global $player_row;
      return new ForumPost( 0, $this->forum_id, $this->thread_id, User::new_from_row($player_row),
         0, 0, 0, $this->subject, $this->text, 0, $this->parent_id );
   }

   /*! \brief Returns true, if post is thread-post. */
   public function is_thread_post()
   {
      return ( $this->id == $this->thread_id );
   }

   /*! \brief Returns true, if post is approved (Approved=Y). */
   public function is_approved()
   {
      return ( $this->approved === 'Y' );
   }

   /*! \brief Returns true, if post is pending-approval (Approved=P). */
   public function is_pending_approval()
   {
      return ( $this->approved === 'P' );
   }

   /*! \brief Returns true, if authors post matches given user-id. */
   public function is_author( $uid )
   {
      return ( $this->author->ID == $uid );
   }

   public function allow_post_reply()
   {
      return ( !($this->flags & FPOST_FLAG_READ_ONLY) || self::allow_post_read_only() );
   }

   public function format_flags( /*Int*/$flags=null )
   {
      if ( is_null($flags) )
         $flags = $this->flags;

      $out = array();
      if ( $flags & FPOST_FLAG_READ_ONLY )
         $out[] = span('ForumOpts', '[' . T_('Read-Only') . ']' );

      return (count($out)) ? MED_SPACING . implode(' ', $out) : '';
   }

   public function add_user_tags( array $tags = null )
   {
      if ( is_array( $tags ) )
         $this->user_tags = array_unique( array_merge( $this->user_tags, $tags ), SORT_NUMERIC );
   }

   public function has_user_tag( $tag_id ) {
      return in_array( $tag_id, $this->user_tags );
   }

   /*! \brief Sets tree-navigation vars for this post (NULL=not-set). */
   public function set_navigation( ForumPost $prev_parent_post = null, ForumPost $next_parent_post = null,
      ForumPost $prev_post = null, ForumPost $next_post = null, ForumPost $first_child_post = null )
   {
      $this->prev_parent_post = $prev_parent_post;
      $this->next_parent_post = $next_parent_post;
      $this->prev_post = $prev_post;
      $this->next_post = $next_post;
      $this->first_child_post = $first_child_post;
   }

   /*!
    * \brief Builds URL for forum-thread-post (without subdir-prefix) with specified anchor.
    * \param $anchor anchorname to link to; if null use current post-id
    */
   public function build_url_post( $anchor=null, $url_suffix='' )
   {
      if ( is_null($anchor) )
         $anchor = '#' . ((int)$this->id);
      elseif ( (string)$anchor != '' )
         $anchor = '#' . ((string)$anchor);
      // else: anchor=''

      if ( $url_suffix != '' && $url_suffix[0] != URI_AMP )
         $url_suffix = URI_AMP . $url_suffix;

      $url = sprintf( 'read.php?forum=%d'.URI_AMP.'thread=%d%s%s',
         $this->forum_id, $this->thread_id, $url_suffix, $anchor );
      return $url;
   }//build_url_post

   /*! \brief Builds link to this post for specified date using given anchor-attribs. */
   public function build_link_postdate( $date, $attbs='' )
   {
     if ( empty($date) )
         return NO_VALUE;

     $datestr = date( DATE_FMT, $date );
     return anchor( $this->build_url_post(), $datestr, '', $attbs );
   }

   /*! \brief Returns string-representation of this object (for debugging purposes). */
   public function to_string()
   {
      return "ForumPost(id={$this->id}): "
         . "forum_id=[{$this->forum_id}], "
         . "thread_id=[{$this->thread_id}], "
         . "#posts=[{$this->count_posts}], "
         . "#hits=[{$this->count_hits}], "
         . "last_post_id=[{$this->last_post_id}], "
         . "subject=[{$this->subject}], "
         . 'text..=[' . cut_str($this->text, 30, false, '..], ')
         . "flags=[{$this->flags}], "
         . 'author={' . ( is_null($this->author) ? 'null' : $this->author->to_string() ) . '}, '
         . "parent_id=[{$this->parent_id}], "
         . "answer#=[{$this->answer_num}], "
         . "depth=[{$this->depth}], "
         . "posidx=[{$this->posindex}], "
         . "approved=[{$this->approved}], "
         . "created=[{$this->created}], "
         . "last_changed=[{$this->last_changed}], "
         . "last_edited=[{$this->last_edited}], "
         . "crc32=[{$this->crc32}], "
         . "count_likes=[{$this->count_likes}], "
         . "thread_no_link=[{$this->thread_no_link}], "
         . "creation_order=[{$this->creation_order}], "
         . "has_new_posts=[{$this->has_new_posts}], "
         . "is_read=[{$this->is_read}], "
         . "score=[{$this->score}]";
   }//to_string

   /*!
    * \brief Returns true, if user has read specified ForumPost,
    *        i.e. own post, newer than FORUM_WEEKS_NEW_END or has newer thread-read-date
    * \param $fread ForumRead-object pre-loaded for post
    */
   public function is_post_read( ForumRead $fread )
   {
      // own posts are always read
      if ( $this->is_author($fread->uid) )
         return true;

      $chkdate = $this->created; // check against post creation-date

      // mark read, if date passed global read-date
      if ( $fread->min_date >= $chkdate )
         return true;

      // check if mark as read, if date passed thread read-date
      if ( $fread->has_newer_read_date($chkdate, $fread->fid, $fread->tid) )
         return true;

      return false; // unread = new
   }

   /*! \brief Sets this->is_read using is_post_read(fread). */
   public function set_post_is_read( ForumRead $fread )
   {
      $this->is_read = $this->is_post_read( $fread );
   }


   // ---------- Static Class functions ----------------------------

   /*! \brief Builds basic QuerySQL to load post(s). */
   public static function build_query_sql( $with_user = true )
   {
      // Posts: ID,Forum_ID,Time,Lastchanged,Lastedited,Subject,Text,User_ID,Parent_ID,Thread_ID,Flags,
      //        AnswerNr,Depth,crc32,PosIndex,Approved,PostsInThread,LastPost,CountLikes
      $qsql = new QuerySQL();
      $qsql->add_part( SQLP_FIELDS,
         'P.*',
         'UNIX_TIMESTAMP(P.Time) AS X_Time',
         'UNIX_TIMESTAMP(P.Lastchanged) AS X_Lastchanged',
         'UNIX_TIMESTAMP(P.Lastedited) AS X_Lastedited',
         'IF(P.Time>P.Lastedited,P.Time,P.Lastedited) as X_MaxEdit' ); // for order-by modication-date
      $qsql->add_part( SQLP_FROM,
         'Posts AS P' );
      if ($with_user)
      {
         $qsql->add_part( SQLP_FIELDS,
             'PAuthor.Name AS Author_Name',
             'PAuthor.Handle AS Author_Handle',
             'PAuthor.Rating2 AS Author_Rating',
             'PAuthor.Adminlevel+0 AS Author_AdminLevel'
         );
         $qsql->add_part( SQLP_FROM,
             'INNER JOIN Players AS PAuthor ON PAuthor.ID=P.User_ID'  // Post-Author
         );
      }
      return $qsql;
   }

   /*! \brief Returns ForumPost-object created from specified (db-)row. */
   public static function new_from_row( array $row )
   {
      $post = new ForumPost(
            @$row['ID'],
            @$row['Forum_ID'],
            @$row['Thread_ID'],
            // Author_* not part of Posts-table, but are read if set in row
            User::newForumUser( @$row['User_ID'], @$row['Author_Name'], @$row['Author_Handle'],
               @$row['Author_AdminLevel'], @$row['Author_Rating'] ),
            @$row['LastPost'],
            @$row['PostsInThread'],
            @$row['Hits'],
            @$row['Subject'],
            @$row['Text'],
            @$row['Flags'],
            @$row['Parent_ID'],
            @$row['AnswerNr'],
            @$row['Depth'],
            @$row['PosIndex'],
            @$row['Approved'],
            @$row['X_Time'],
            @$row['X_Lastchanged'],
            @$row['X_Lastedited'],
            @$row['crc32'],
            @$row['CountLikes']
         );
      return $post;
   }

   /*! \brief Returns ForumPost for given $post_id or NULL if nothing found. */
   public static function load_post( $post_id )
   {
      $post_id = (int)$post_id;

      $qsql = self::build_query_sql( false );
      $qsql->add_part( SQLP_WHERE, "P.ID=$post_id" );
      $qsql->add_part( SQLP_LIMIT, '1' );

      $row = mysql_single_fetch( "ForumPost:load_post.find_post($post_id)",
          $qsql->get_select() );
      return ($row) ? self::new_from_row($row) : NULL;
   }

   /*!
    * \brief Updates Posts.CountLikes by $change +/-1.
    * \return new counter value; otherwise ends with error
    */
   public static function update_post_likes( $dbgmsg, $post_id, /*int*/$change )
   {
      $dbgmsg .= ".ForumPost:update_post_likes($post_id,$change)";
      $post_id = (int)$post_id;
      $op_change = ($change > 0 ? '+' : '') . (int)$change;
      $success = db_query( $dbgmsg . ".upd",
         "UPDATE Posts SET CountLikes=CountLikes $op_change WHERE ID=$post_id LIMIT 1" );
      if ( !$success )
         return error( 'mysql_query_failed', $dbgmsg . ".upd_db" );

      $row = mysql_single_fetch( $dbgmsg.".count",
         "SELECT CountLikes FROM Posts WHERE ID=$post_id LIMIT 1" );
      return ($row) ? (int)@$row['CountLikes'] : 0;
   }

   /*! \brief Returns M(=pending approval), H=hidden, S=shown for given approved value P|N|Y. */
   public static function get_approved_text( $approved )
   {
      if ( $approved == 'P' )
         return 'M';
      elseif ( $approved == 'N' )
         return 'H';
      else //if ( $approved == 'Y' )
         return 'S';
   }

   public static function allow_post_read_only()
   {
      global $player_row;
      return ( @$player_row['admin_level'] & ADMINGROUP_EXECUTIVE );
   }

} // end of 'ForumPost'



global $ENTITY_TAGS; //PHP5
$ENTITY_TAGS = new Entity( 'Tags',
      FTYPE_PKEY, 'ID',
      FTYPE_AUTO, 'ID',
      FTYPE_INT,  'ID', 'User_ID',
      FTYPE_TEXT, 'Name', 'ColorBG', 'ColorFG'
   );

 /*!
  * \class Tag
  *
  * \brief Class to handle forum tag.
  */
class Tag
{
   public $ID;
   public $User_ID;
   public $Name;
   public $ColorBG;
   public $ColorFG;

   /*! \brief Constructs Tag-object with specified arguments. */
   public function __construct( $id=0, $user_id=0, $name='', $col_bg='c0c0c0', $col_fg='000000' )
   {
      $this->ID = (int)$id;
      $this->User_ID = (int)$user_id;
      $this->Name = trim($name);
      $this->ColorBG = $col_bg;
      $this->ColorFG = $col_fg;
   }

   public function delete_tag()
   {
      return ( $this->ID > 0 )
         ? db_query( 'tag.delete_tag', "DELETE FROM Tags WHERE ID='{$this->ID}' LIMIT 1" )
         : false;
   }

   public function render_tag( $class='' )
   {
      return Tag::renderTag( $this->ID, $this->Name, $this->ColorBG, $this->ColorFG, $class );
   }

   /*! \brief Inserts or updates Tag-entry in database. */
   public function persist()
   {
      if ( $this->ID > 0 )
         $success = $this->update();
      else
         $success = $this->insert();
      return $success;
   }

   public function insert()
   {
      $entityData = $this->fillEntityData();
      $result = $entityData->insert( "Tag.insert(%s)" );
      if ( $result )
         $this->ID = mysql_insert_id();
      return $result;
   }

   public function update()
   {
      $entityData = $this->fillEntityData();
      return $entityData->update( "Tag.update(%s)" );
   }

   public function delete()
   {
      $entityData = $this->fillEntityData();
      $result = $entityData->delete( "Tag.delete(%s)" );
      return $result;
   }

   public function fillEntityData( EntityData $data = null )
   {
      if ( is_null($data) )
         $data = $GLOBALS['ENTITY_TAGS']->newEntityData();
      $data->set_value( 'ID', $this->ID );
      $data->set_value( 'User_ID', $this->User_ID );
      $data->set_value( 'Name', $this->Name );
      $data->set_value( 'ColorBG', $this->ColorBG );
      $data->set_value( 'ColorFG', $this->ColorFG );
      return $data;
   }//fillEntityData


   // ---------- Static Class functions ----------------------------

   /*! \brief Returns Tag-object for logged-in user with default fields. */
   public static function new_tag()
   {
      global $player_row;

      $uid = (int)@$player_row['ID'];
      if ( !is_numeric($uid) || $uid <= GUESTS_ID_MAX )
         error('invalid_args', "Tag:new_tag.check.uid($uid)");

      $tag = new Tag( 0, $uid );
      return $tag;
   }

   /*! \brief Returns db-fields to be used for query of tag-objects for given user-id. */
   public static function build_query_sql( $uid, $tag_id=0 )
   {
      $qsql = $GLOBALS['ENTITY_TAGS']->newQuerySQL('T');
      if ( $uid > 0 )
         $qsql->add_part( SQLP_WHERE, "T.User_ID=$uid" );
      if ( $tag_id > 0 )
         $qsql->add_part( SQLP_WHERE, "T.ID=$tag_id" );
      return $qsql;
   }//build_query_sql

   /*! \brief Returns Tag-object created from specified (db-)row. */
   public static function new_from_row( array $row )
   {
      $tag = new Tag(
            // from Tag
            @$row['ID'],
            @$row['User_ID'],
            @$row['Name'],
            @$row['ColorBG'],
            @$row['ColorFG']
         );
      return $tag;
   }//new_from_row

   /*!
    * \brief Loads and returns Tag-object for given tag-id limited to 1 result-entry.
    * \param $tag_id Tags.ID
    * \return NULL if nothing found; Tag-object otherwise
    */
   public static function load_tag( $tag_id )
   {
      global $player_row;
      $uid = (int)@$player_row['ID'];
      $tag_id = (int)$tag_id;

      $qsql = self::build_query_sql( $uid, $tag_id );
      $qsql->add_part( SQLP_LIMIT, '1' );

      $row = mysql_single_fetch( "Tag:load_tag.find_tag($tag_id)",
         $qsql->get_select() );
      return ($row) ? self::new_from_row($row) : NULL;
   }//load_tag

   /*! \brief Returns array with Tag-objects. */
   public static function load_tags( $uid )
   {
      if ( !ENABLE_FORUM_TAGS )
         return array();

      $qsql = self::build_query_sql( $uid );
      $qsql->add_part( SQLP_ORDER, "Name ASC" );
      $result = db_query( "Tag:load_tags", $qsql->get_select() );

      $arr = array();
      while ( $row = mysql_fetch_array( $result ) )
         $arr[] = self::new_from_row( $row );
      mysql_free_result($result);

      return $arr;
   }//load_all_tags

   // cached version of self::load_tags()
   public static function load_cache_tags( $uid )
   {
      $dbgmsg = "Tag:load_cache_tags($uid)";
      $key = "Tags.$uid";

      $tags = DgsCache::fetch( $dbgmsg, CACHE_GRP_TAGS, $key );
      if ( is_null($tags) )
      {
         $tags = self::load_tags( $uid );
         DgsCache::store( $dbgmsg, CACHE_GRP_TAGS, $key, $tags, SECS_PER_DAY );
      }

      return $tags;
   }

   public static function delete_cache_tags( $dbgmsg, $uid )
   {
      DgsCache::delete( $dbgmsg, CACHE_GRP_TAGS, "Tags.$uid" );
   }

   public static function renderTag( $tag_id, $tag_name, $colorBG, $colorFG, $class='' )
   {
      return sprintf('<span class="Tag %s" tag-id="%s" style="background-color: #%s; color: #%s;">%s</span>',
            $class, $tag_id, $colorBG, $colorFG, $tag_name );
   }

   public static function renderTagList( array $user_tags, array $forum_tags ) {
      $out = array();
      foreach( $user_tags as $tag_id )
      {
         if ( $tag_id > 0 && ($tag = @$forum_tags[$tag_id]) )
            $out[] = $tag->render_tag();
      }
      return join('', $out);
   }

} // end of 'Tag'


 /*!
  * \class Tagged
  *
  * \brief Class to handle forum tags pinned on forum posts.
  */
class Tagged {

   // ---------- Static Class functions ----------------------------

   /*!
    * \brief Returns map{ post-id => [ tag_id, ...] ...} for all user-tags of given user.
    * \param $post_ids optional array with Posts.ID to restrict query
    */
   public static function load_tagged_by_user( $uid, array $post_ids = null )
   {
      if ( !ENABLE_FORUM_TAGS )
         return array();

      $uid = (int)$uid;

      $qsql = new QuerySQL(
          SQLP_FIELDS, 'Post_ID', 'Tag_ID',
          SQLP_FROM, 'Tagged',
          SQLP_WHERE, "User_ID=$uid" );
      if (is_array($post_ids) && count($post_ids) > 0)
         $qsql->add_part( SQLP_WHERE, "Post_ID IN (" . join( ',', $post_ids ) . ")" );
      $result = db_query( "Tagged:load_tagged_by_user($uid)", $qsql->get_select() );

      $arr = array();
      while ( $row = mysql_fetch_array( $result ) )
      {
         $pid = $row['Post_ID'];
         if (isset($arr[$pid]))
            $arr[$pid][] = $row['Tag_ID'];
         else
            $arr[$pid] = array( $row['Tag_ID'] );
      }
      mysql_free_result($result);

      return $arr;
   }

   /*! \brief Returns false if entry for user/post-id/tag-id does NOT exist; true otherwise. */
   public static function exists_tagged( $uid, $post_id, $tag_id ) {
      $uid = (int)$uid;
      $post_id = (int)$post_id;
      $tag_id = (int)$tag_id;
      return (bool)mysql_single_fetch( "Tagged:exists_tagged($uid,$post_id,$tag_id)",
         "SELECT 1 FROM Tagged WHERE User_ID=$uid AND Post_ID=$post_id AND Tag_ID=$tag_id LIMIT 1" );
   }

   public static function insert( $uid, $post_id, $tag_id )
   {
      $uid = (int)$uid;
      $post_id = (int)$post_id;
      $tag_id = (int)$tag_id;
      return db_query( "Tagged:insert($uid,$post_id,$tag_id)",
          "INSERT IGNORE INTO Tagged (User_ID,Post_ID,Tag_ID) VALUES ($uid,$post_id,$tag_id)" );
   }

   public static function delete( $uid, $post_id, $tag_id )
   {
      $uid = (int)$uid;
      $post_id = (int)$post_id;
      $tag_id = (int)$tag_id;
      return db_query( "Tagged:delete($uid,$post_id,$tag_id)",
          "DELETE FROM Tagged WHERE User_ID=$uid AND Post_ID=$post_id AND Tag_ID=$tag_id LIMIT 1" );
   }

} // end of 'Tagged'

?>
//


koh5_pano



Your browser does not support the HTML5 canvas element.


Drag mouse to navigate.

Navigation





17.Aug.2010, Martin Wengenmayer