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 " | ";
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 " | ";
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> </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 = ' ';
$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'] ) . ' ';
$next_parent = ( is_null($post->next_parent_post) )
? ''
: anchor( '#'.$post->next_parent_post->id, $imgarr['next_parent'] ) . ' ';
$prev_answer = ( is_null($post->prev_post) || $flat )
? ''
: anchor( '#'.$post->prev_post->id, $imgarr['prev_answer'] ) . ' ';
$next_answer = ( is_null($post->next_post) || $flat )
? ''
: anchor( '#'.$post->next_post->id, $imgarr['next_answer'] ) . ' ';
$first_answer = ( is_null($post->first_child_post) )
? ''
: anchor( '#'.$post->first_child_post->id, $imgarr['first_answer'] ) . ' ';
// BEGIN Navi (top/prev-parent/prev-answer)
echo anchor( '#ftop', $imgarr['top'] ),
' ',
$prev_parent,
$prev_answer,
' ';
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, " ";
if ( ALLOW_QUOTING )
echo '<a href="', $thread_url,URI_AMP,"quote=1",URI_AMP,"reply=$pid#$pid\">[ ",
T_('quote#forum'), " ]</a> ";
}
// edit link
if ( $is_my_post )
echo '<a class=Highlight href="', $thread_url,URI_AMP,"edit=$pid#$pid\">[ ",
T_('edit'), " ]</a> ";
}
// END Navi (next-answer/next-parent/bottom)
echo $next_answer,
$next_parent,
anchor( '#fbottom', $imgarr['bottom'] ),
' ',
$first_answer,
' ';
// 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( ' ', 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 = ' [';
$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
Drag mouse to navigate.
Navigation
- Left/Right Mouse drag: Changes camera heading.
- Up/Sown Mouse drag: Changes camera pitch.
- Scroll wheel: Changes camera field of view.
- I-Key: Displays Info panel with canvas size, image size and FPS.
17.Aug.2010, Martin Wengenmayer