Files
tyforum/script/topic_show.pl
2015-01-03 11:43:36 +01:00

901 lines
31 KiB
Perl
Executable File

#!/usr/bin/perl
#------------------------------------------------------------------------------
# mwForum - Web-based discussion forum
# Copyright (c) 1999-2015 Markus Wichitill
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#------------------------------------------------------------------------------
use strict;
use warnings;
no warnings qw(uninitialized redefine);
# Imports
use MwfMain;
#------------------------------------------------------------------------------
# Init
my ($m, $cfg, $lng, $user, $userId) = MwfMain->new($_[0]);
# Get CGI parameters
my $topicId = $m->paramInt('tid');
my $targetPostId = $m->paramInt('pid');
my $page = $m->paramInt('pg');
my $showResults = $m->paramBool('results');
my $hilite = $m->paramStr('hl');
$topicId || $targetPostId or $m->error('errParamMiss');
# Get missing topicId from post
my $arcPfx = $m->{archive} ? 'arc_' : "";
if (!$topicId && $targetPostId) {
$topicId = $m->fetchArray("
SELECT topicId FROM ${arcPfx}posts WHERE id = ?", $targetPostId);
$topicId or $m->error('errPstNotFnd');
}
# Get topic
my $topic = $m->fetchHash("
SELECT topics.*,
topicReadTimes.lastReadTime
FROM ${arcPfx}topics AS topics
LEFT JOIN topicReadTimes AS topicReadTimes
ON topicReadTimes.userId = :userId
AND topicReadTimes.topicId = :topicId
WHERE topics.id = :topicId",
{ userId => $userId, topicId => $topicId });
$topic or $m->error('errTpcNotFnd');
$topic->{lastReadTime} ||= 0;
my $boardId = $topic->{boardId};
my $basePostId = $topic->{basePostId};
# Get board/category
my $board = $m->fetchHash("
SELECT boards.*,
categories.id AS categId, categories.title AS categTitle
FROM ${arcPfx}boards AS boards
INNER JOIN categories AS categories
ON categories.id = boards.categoryId
WHERE boards.id = ?", $boardId);
$board or $m->error('errBrdNotFnd');
my $flat = $board->{flat};
# Shortcuts
my $autoCollapsing = !$flat && $user->{collapse};
my $showAvatars = $cfg->{avatars} && $user->{showAvatars};
my $emptyPixel = "src='$cfg->{dataPath}/epx.png'";
# Check if user can see and write to topic
my $boardAdmin = $user->{admin} || $m->boardAdmin($userId, $boardId);
my $topicAdmin = $board->{topicAdmins} && $m->topicAdmin($userId, $topicId);
$boardAdmin || $topicAdmin || $m->boardVisible($board) or $m->error('errNoAccess');
my $boardWritable = $boardAdmin || $topicAdmin || $m->boardWritable($board, 1);
# Get minimal version of all topic posts
my $sameTopic = $topicId == $user->{lastTopicId};
my $topicReadTime = $sameTopic ? $user->{lastTopicTime} : $topic->{lastReadTime};
my $lowestUnreadTime = $m->max($topicReadTime, $user->{fakeReadTime},
$m->{now} - $cfg->{maxUnreadDays} * 86400);
my $posts = $m->fetchAllHash("
SELECT id, parentId,
postTime > :prevOnTime AS new,
postTime > :lowestUnreadTime AS unread
FROM ${arcPfx}posts
WHERE topicId = :topicId
ORDER BY postTime",
{ prevOnTime => $user->{prevOnTime}, lowestUnreadTime => $lowestUnreadTime,
topicId => $topicId });
# Build post lookup tables and check if there are any new or unread posts
my %postsById = map(($_->{id} => $_), @$posts); # Posts by id - hash of hashrefs
my %postsByParent = (); # Posts by parent id - hash of arrayrefs of hashrefs
my @rootPosts = ();
my $newPostsExist = 0;
my $unreadPostsExist = 0;
for my $post (@$posts) {
push @{$postsByParent{$post->{parentId}}}, $post;
push @rootPosts, $post if !$post->{parentId} && $post->{id} != $basePostId;
$newPostsExist = 1 if $post->{new};
$unreadPostsExist = 1 if $post->{unread};
}
unshift @rootPosts, $postsById{$basePostId};
# Determine page numbers and collect IDs of new or unread posts
my $postsPP = $m->min($user->{postsPP}, $cfg->{maxPostsPP}) || $cfg->{maxPostsPP};
my $postPos = 0;
my $firstUnrPostPage = undef;
my $firstNewPostPage = undef;
my $firstUnrPostId = undef;
my $firstNewPostId = undef;
my @newUnrPostIds = ();
my $preparePost = sub {
my $self = shift();
my $postId = shift();
# Shortcuts
my $post = $postsById{$postId};
# Assign page numbers to posts
$post->{page} = int($postPos / $postsPP) + 1;
# Set current page to a requested post's page
$page = $post->{page} if $postId == $targetPostId;
# Determine first new post and its page
if (!$page && !$firstNewPostPage && $post->{new}) {
$firstNewPostPage = $post->{page};
$firstNewPostId = $postId;
}
# Determine first unread post and its page
if (!$page && !$firstUnrPostPage && $post->{unread} && $userId) {
$firstUnrPostPage = $post->{page};
$firstUnrPostId = $postId;
}
# Add new/unread post ID to list
push @newUnrPostIds, $postId if $post->{new} || ($post->{unread} && $userId);
# Recurse through children
$postPos++;
for my $child (@{$postsByParent{$postId}}) {
$child->{id} != $postId or $m->error("Post is its own parent?!");
$self->($self, $child->{id});
}
};
for my $rootPost (@rootPosts) {
$preparePost->($preparePost, $rootPost->{id});
}
$page = $firstUnrPostPage || $firstNewPostPage if !$page;
my $scrollPostId = $targetPostId || $firstUnrPostId || $firstNewPostId || 0;
$scrollPostId = 0 if $scrollPostId == $basePostId || $showResults;
# Print header
$m->printHeader($topic->{subject}, {
lng_tpcBrnExpand => $lng->{tpcBrnExpand},
lng_tpcBrnCollap => $lng->{tpcBrnCollap},
scrollPostId => $scrollPostId,
boardAdmin => $boardAdmin,
});
# Get the full content of those posts that are on the current page
# Note: full posts are not copied to @$posts, @rootPosts and %postsByParent
$page ||= 1;
my @pagePostIds = map($_->{page} == $page ? $_->{id} : (), @$posts);
@pagePostIds or $m->error('errPstNotFnd');
my $ignoreStr = $userId ? ",
userIgnores.userId IS NOT NULL AS ignored" : "";
my $ignoreJoin = $userId ? "
LEFT JOIN userIgnores AS userIgnores
ON userIgnores.userId = :userId
AND userIgnores.ignoredId = posts.userId" : "";
my $pagePosts = $m->fetchAllHash("
SELECT posts.*,
posts.postTime > :prevOnTime AS new,
posts.postTime > :lowestUnreadTime AS unread,
users.userName, users.title AS userTitle,
users.postNum AS userPostNum, users.avatar, users.signature,
users.openId, users.privacy
$ignoreStr
FROM ${arcPfx}posts AS posts
LEFT JOIN users AS users
ON users.id = posts.userId
$ignoreJoin
WHERE posts.id IN (:pagePostIds)",
{ userId => $userId, prevOnTime => $user->{prevOnTime}, lowestUnreadTime => $lowestUnreadTime,
pagePostIds => \@pagePostIds });
my %pageUserIds = ();
for my $post (@$pagePosts) {
$post->{page} = $page;
$postsById{$post->{id}} = $post;
$pageUserIds{$post->{userId}} = 1;
}
my $topicUserId = $postsById{$basePostId}{userId};
# Merge post likes into page posts
if ($cfg->{postLikes}) {
my $postLikes = $m->fetchAllArray("
SELECT posts.id,
COUNT(postLikes.postId) AS likes,
COUNT(postLiked.userId) > 0 AS liked
FROM posts AS posts
LEFT JOIN postLikes AS postLikes
ON postLikes.postId = posts.id
LEFT JOIN postLikes AS postLiked
ON postLiked.postId = posts.id
AND postLiked.userId = :userId
WHERE posts.id IN (:pagePostIds)
GROUP BY posts.id
HAVING COUNT(postLikes.postId) > 0
OR COUNT(postLiked.userId) > 0",
{ userId => $userId, pagePostIds => \@pagePostIds });
for my $like (@$postLikes) {
$postsById{$like->[0]}{likes} = $like->[1];
$postsById{$like->[0]}{liked} = $like->[2];
}
}
# Remove ignored and base crosslink posts from @newUnrPostIds
@newUnrPostIds = grep(!$postsById{$_}{ignored}, @newUnrPostIds) if $userId;
shift @newUnrPostIds if $postsById{$newUnrPostIds[0]}{userId} == -2;
# Mark branches that shouldn't be auto-collapsed
if ($autoCollapsing) {
for my $id (@newUnrPostIds) {
my $post = $postsById{$id};
while ($post = $postsById{$post->{parentId}}) {
last if $post->{noCollapse};
$post->{noCollapse} = 1;
}
}
if ($targetPostId) {
my $post = $postsById{$targetPostId};
while ($post = $postsById{$post->{parentId}}) {
last if $post->{noCollapse};
$post->{noCollapse} = 1;
}
}
}
# Get poll
my $poll = undef;
my $polls = $cfg->{polls};
my $pollId = $topic->{pollId};
my $canPoll = ($polls == 1 || $polls == 2 && ($boardAdmin || $topicAdmin))
&& ($userId && $userId == $topicUserId || $boardAdmin);
$poll = $m->fetchHash("
SELECT * FROM polls WHERE id = ?", $pollId)
if $polls && $pollId;
# Get attachments
if ($cfg->{attachments} && $board->{attach}) {
my $attachments = $m->fetchAllHash("
SELECT *
FROM attachments
WHERE postId IN (:pagePostIds)
ORDER BY webImage, id",
{ pagePostIds => \@pagePostIds });
push @{$postsById{$_->{postId}}{attachments}}, $_ for @$attachments;
}
# Get user badges
my @badges = ();
my %userBadges = ();
if (@{$cfg->{badges}} && $user->{showDeco}) {
for my $line (@{$cfg->{badges}}) {
my ($id, $smallIcon, $title) = $line =~ /(\w+)\s+\w+\s+(\S+)\s+\S+\s+"([^"]+)"/;
push @badges, [ $id, $title, $smallIcon ] if $smallIcon ne '-';
}
my @pageUserIds = keys(%pageUserIds);
my $userBadges = $m->fetchAllArray("
SELECT userId, badge FROM userBadges WHERE userId IN (:pageUserIds)",
{ pageUserIds => \@pageUserIds });
push @{$userBadges{$_->[0]}}, $_->[1] for @$userBadges;
}
# Create or reuse GeoIP object
my $geoIp;
if (!$geoIp && $cfg->{geoIp} && $cfg->{userFlags} && $user->{showDeco}) {
if (eval { require Geo::IP }) {
$geoIp = Geo::IP->open($cfg->{geoIp},
defined($cfg->{geoIpCacheMode}) ? $cfg->{geoIpCacheMode} : 1);
}
elsif (eval { require Geo::IP::PurePerl }) {
$geoIp = Geo::IP::PurePerl->open($cfg->{geoIp});
}
else {
$m->error("Geo::IP or Geo::IP::PurePerl modules not available.");
}
}
# Highlighting
my @hiliteWords = ();
if ($hilite) {
# Split string and weed out stuff that could break entities
my $hiliteRxEsc = $hilite;
$hiliteRxEsc =~ s!([\\\$\[\](){}.*+?^|-])!\\$1!g;
@hiliteWords = split(' ', $hiliteRxEsc);
@hiliteWords = grep(length > 2, @hiliteWords);
@hiliteWords = grep(!/^(?:amp|quot|quo|uot|160)\z/, @hiliteWords);
}
# Page links
my $postNum = $topic->{postNum};
my $pageNum = int($postNum / $postsPP) + ($postNum % $postsPP != 0);
my @pageLinks = $pageNum < 2 ? ()
: $m->pageLinks('topic_show', [ tid => $topicId ], $page, $pageNum);
# User button links
my @userLinks = ();
if (!$m->{archive}) {
push @userLinks, { url => $m->url('post_add', tid => $topicId),
txt => 'tpcTpcRepl', ico => 'write' }
if $boardWritable && !$topic->{locked} || $boardAdmin || $topicAdmin;
push @userLinks, { url => $m->url('poll_add', tid => $topicId),
txt => 'tpcPolAdd', ico => 'poll' }
if !$poll && $canPoll && (!$topic->{locked} || $boardAdmin || $topicAdmin);
push @userLinks, { url => $m->url('topic_tag', tid => $topicId),
txt => 'tpcTag', ico => 'tag' }
if ($userId && $userId == $topicUserId || $boardAdmin || $topicAdmin)
&& ($cfg->{allowTopicTags} == 2 || $cfg->{allowTopicTags} == 1 && ($boardAdmin || $topicAdmin));
push @userLinks, { url => $m->url('topic_subscribe', tid => $topicId),
txt => 'tpcSubs', ico => 'subscribe' }
if $userId && ($cfg->{subsInstant} || $cfg->{subsDigest});
push @userLinks, { url => $m->url('forum_overview', act => 'new', tid => $topicId),
txt => 'comShowNew', ico => 'shownew' }
if $userId && $newPostsExist;
push @userLinks, { url => $m->url('forum_overview', act => 'unread', tid => $topicId,
time => $lowestUnreadTime), txt => 'comShowUnr', ico => 'showunread' }
if $userId && $unreadPostsExist;
for my $plugin (@{$cfg->{includePlg}{topicUserLink}}) {
$m->callPlugin($plugin, links => \@userLinks, board => $board, topic => $topic);
}
}
# Admin button links
my @adminLinks = ();
if (($boardAdmin || $topicAdmin) && !$m->{archive}) {
push @adminLinks, { url => $m->url('topic_stick', tid => $topicId,
act => $topic->{sticky} ? 'unstick' : 'stick', auth => 1),
txt => $topic->{sticky} ? 'tpcAdmUnstik' : 'tpcAdmStik', ico => 'stick' }
if $boardAdmin;
push @adminLinks, { url => $m->url('topic_lock', tid => $topicId,
act => $topic->{locked} ? 'unlock' : 'lock', auth => 1),
txt => $topic->{locked} ? 'tpcAdmUnlock' : 'tpcAdmLock', ico => 'lock' };
push @adminLinks, { url => $m->url('topic_move', tid => $topicId),
txt => 'tpcAdmMove', ico => 'move' }
if $boardAdmin;
push @adminLinks, { url => $m->url('topic_merge', tid => $topicId),
txt => 'tpcAdmMerge', ico => 'merge' }
if $boardAdmin;
push @adminLinks, { url => $m->url('user_confirm', script => 'topic_delete', tid => $topicId,
notify => ($topicUserId != $userId ? 1 : 0), name => $topic->{subject}),
txt => 'tpcAdmDelete', ico => 'delete' };
for my $plugin (@{$cfg->{includePlg}{topicAdminLink}}) {
$m->callPlugin($plugin, links => \@adminLinks, board => $board, topic => $topic);
}
}
# Print page bar
my $categUrl = $m->url('forum_show', tgt => "bid$boardId");
my $categStr = "<a href='$categUrl'>$board->{categTitle}</a> / ";
my $boardUrl = $m->url('board_show', tid => $topicId, tgt => "tid$topicId");
my $boardStr = "<a href='$boardUrl'>$board->{title}</a> / ";
my $lockStr = $topic->{locked} ? " $lng->{tpcLocked}" : "";
my @navLinks = ({ url => $m->url('board_show', tid => $topicId, tgt => "tid$topicId"),
txt => 'comUp', ico => 'up' });
$m->printPageBar(
mainTitle => $lng->{tpcTitle},
subTitle => $categStr . $boardStr . $topic->{subject} . $lockStr,
navLinks => \@navLinks, pageLinks => \@pageLinks, userLinks => \@userLinks,
adminLinks => \@adminLinks);
# Print poll
if ($poll && $polls && !$m->{archive}) {
# Check if user can vote
my $voted = $m->fetchArray("
SELECT 1 FROM pollVotes WHERE pollId = ? AND userId = ?", $pollId, $userId) ? 1 : 0;
my $canVote = (!$voted || $poll->{multi}) && (!$showResults && $userId && $boardWritable
&& !$topic->{locked} && !$poll->{locked});
# Print poll header
my $lockedStr = $poll->{locked} ? $lng->{tpcPolLocked} : "";
print
$canVote ? "<form action='poll_vote$m->{ext}' method='post'>\n" : "",
"<div class='frm pol'>\n",
"<div class='hcl'>\n",
"<span class='htt'>$lng->{tpcPolTtl}</span>\n",
"$poll->{title} $lockedStr\n",
"</div>\n";
# Print results
if ($voted || $poll->{multi} || $showResults || !$userId || !$boardWritable
|| $topic->{locked} || $poll->{locked}) {
my $options = undef;
my $voteSum = undef;
if ($poll->{locked}) {
# Get consolidated results
$options = $m->fetchAllHash("
SELECT id, title, votes FROM pollOptions WHERE pollId = ? ORDER BY id", $pollId);
# Get sum of votes
$voteSum = $m->fetchArray("
SELECT SUM(votes) FROM pollOptions WHERE pollId = ?", $pollId) || 1;
}
else {
# Get results from votes
$options = $m->fetchAllHash("
SELECT pollOptions.id, pollOptions.title,
COUNT(pollVotes.optionId) AS votes
FROM pollOptions AS pollOptions
LEFT JOIN pollVotes AS pollVotes
ON pollVotes.pollId = :pollId
AND pollVotes.optionId = pollOptions.id
WHERE pollOptions.pollId = :pollId
GROUP BY pollOptions.id, pollOptions.title
ORDER BY pollOptions.id",
{ pollId => $pollId });
# Get sum of votes
$voteSum = $m->fetchArray("
SELECT COUNT(*) FROM pollVotes WHERE pollId = ?", $pollId) || 1;
}
# Print results
print "<div class='ccl'>\n<table class='plr'>\n";
for my $option (@$options) {
my $votes = $option->{votes};
my $percent = int($votes / $voteSum * 100 + .5);
my $width = $percent * 4;
print
"<tr>\n",
"<td class='plo'>$option->{title}</td>\n",
"<td class='plv'>$votes</td>\n",
"<td class='plp'>$percent\%</td>\n",
"<td class='plg'><div class='plb' style='width: ${width}px'></div></td>\n",
"</tr>\n";
}
print "</table>\n</div>\n";
}
# Print poll form
if ($canVote) {
# Get poll options
my $options = $m->fetchAllHash("
SELECT id, title FROM pollOptions WHERE pollId = ? ORDER BY id", $pollId);
# Get user's votes to disable options in multi-vote polls
my $votes = $m->fetchAllArray("
SELECT optionId FROM pollVotes WHERE pollId = ? AND userId = ?", $pollId, $userId);
# Print poll options
print "<div class='ccl'>\n";
for my $option (@$options) {
my $votedAttr = "";
for my $vote (@$votes) {
$votedAttr = "disabled checked", last if $vote->[0] == $option->{id}
}
print $poll->{multi}
? "<div><label><input type='checkbox' name='option_$option->{id}' $votedAttr> "
. "$option->{title}</label></div>\n"
: "<div><label><input type='radio' name='option' value='$option->{id}'> "
. "$option->{title}</label></div>\n";
}
my $topicUrl = $m->url('topic_show', tid => $topicId, results => 1);
print
$m->submitButton('tpcPolVote', 'poll'),
$poll->{multi} ? "" : "<a href='$topicUrl'>$lng->{tpcPolShwRes}</a>\n",
"<input type='hidden' name='tid' value='$topicId'>\n",
$m->stdFormFields(),
"</div>\n",
}
# Print lock poll button
my @btlLines = ();
if ($canPoll && !$poll->{locked}) {
my $url = $m->url('poll_lock', tid => $topicId, auth => 1);
push @btlLines, "<a href='$url' title='$lng->{tpcPolLockTT}'>$lng->{tpcPolLock}</a>\n";
}
# Print delete poll button
if ($canPoll && (!$poll->{locked} || $boardAdmin || $topicAdmin)) {
my $url = $m->url('user_confirm', tid => $topicId, pollId => $pollId, script => 'poll_delete',
auth => 1, name => $poll->{title});
push @btlLines, "<a href='$url' title='$lng->{tpcPolDelTT}'>$lng->{tpcPolDel}</a>\n";
}
# Print button cell if not empty
print "<div class='bcl'>\n", @btlLines, "</div>\n" if @btlLines;
print "</div>\n";
print "</form>\n\n" if $canVote;
}
# Determine position number of first and last posts on current page
my $firstPostPos = $postsPP * ($page - 1);
my $lastPostPos = $postsPP ? $postsPP * $page - 1 : @$posts - 1;
# Call plugin that can process data for various purposes
for my $plugin (@{$cfg->{includePlg}{topicData}}) {
$m->callPlugin($plugin, board => $board, topic => $topic, pagePosts => $pagePosts,
postsById => \%postsById, boardAdmin => $boardAdmin, topicAdmin => $topicAdmin);
}
# Recursively print posts
$postPos = 0;
my $printPost = sub {
my $self = shift();
my $postId = shift();
my $depth = shift();
# Shortcuts
my $post = $postsById{$postId};
my $postUserId = $post->{userId};
my $ip = $post->{ip};
my $childNum = @{$postsByParent{$postId}};
# Branch collapsing flags
my $printBranchToggle = !$flat && $childNum && $post->{page} == $page;
my $collapsed = $autoCollapsing && @newUnrPostIds && !$post->{noCollapse} ? 1 : 0;
# Print if on current page
if ($post->{page} == $page) {
# Shortcuts
my $parentId = $post->{parentId};
my $indent = $flat ? 0 : $m->min(70, $user->{indent} * $depth);
# Print post
if ($post->{approved} || $boardAdmin || $topicAdmin || $userId && $userId == $postUserId) {
# Format times
my $postTimeStr = $m->formatTime($post->{postTime}, $user->{timezone});
my $editTimeStr = undef;
if ($post->{editTime}) {
$editTimeStr = $m->formatTime($post->{editTime}, $user->{timezone});
$editTimeStr = "<em>$editTimeStr</em>" if $post->{editTime} > $user->{prevOnTime};
$editTimeStr = "<span class='htt'>$lng->{tpcEdited}</span> $editTimeStr\n";
}
# Format username
my $userUrl = $m->url('user_info', uid => $postUserId);
my $userNameStr = $post->{userName} || $post->{userNameBak} || " - ";
my $openIdStr = $post->{openId} ? "title='OpenID: $post->{openId}'" : "";
$userNameStr = "<a href='$userUrl' $openIdStr>$userNameStr</a>" if $postUserId > 0;
$userNameStr .= " " . $m->formatUserTitle($post->{userTitle})
if $post->{userTitle} && $user->{showDeco};
$userNameStr .= " " . $m->formatUserRank($post->{userPostNum})
if @{$cfg->{userRanks}} && !$post->{userTitle} && $user->{showDeco};
# Format user badges
if (@badges && $userBadges{$postUserId} && $user->{showDeco}) {
for my $badge (@badges) {
for my $userBadge (@{$userBadges{$postUserId}}) {
if ($userBadge eq $badge->[0]) {
$userNameStr .= " <img class='ubs' src='$cfg->{dataPath}/$badge->[2]'"
. " title='$badge->[1]' alt=''>";
last;
}
}
}
}
# Format GeoIP country name and flag
if ($geoIp && $user->{showDeco} && (!$post->{privacy} || $user->{admin})) {
my ($code, $name);
if (index($cfg->{geoIp}, 'City') > -1) {
my $rec = $geoIp->record_by_addr($ip);
if ($rec) {
$code = lc($rec->country_code());
$name = $rec->country_name();
}
}
else {
$code = lc($geoIp->country_code_by_addr($ip));
$name = $geoIp->country_name_by_addr($ip);
}
if ($code && $code ne $cfg->{userFlagSkip}) {
$userNameStr .= " <img class='flg' src='$cfg->{dataPath}/flags/$code.png'"
. " alt='[$code]' title='$name'>";
}
}
# Format misc values
$m->dbToDisplay($board, $post);
my $pstClasses = "frm pst" . $post->{classes};
$pstClasses .= " new" if $post->{new};
$pstClasses .= " unr" if $post->{unread} && $userId;
$pstClasses .= " ign" if $post->{ignored};
# Format invisible and locked post icons
my $invisImg = !$post->{approved} ? " <img class='sic sic_post_i' $emptyPixel"
. " title='$lng->{tpcInvisTT}' alt='$lng->{tpcInvis}'> " : "";
my $lockImg = $post->{locked} ? " <img class='sic sic_topic_l' $emptyPixel"
. " title='$lng->{tpcLockdTT}' alt='$lng->{tpcLockd}'> " : "";
# Highlight search keywords
if (@hiliteWords) {
my $body = ">$post->{body}<";
$body =~ s|>(.*?)<|
my $text = $1;
eval { $text =~ s!($_)!<em>$1</em>!gi } for @hiliteWords;
">$text<";
|egs;
$post->{body} = substr($body, 1, -1);
}
# Determine variable post icon attributes
my ($imgName, $imgTitle, $imgAlt);
if ($userId) {
if ($post->{new} && $post->{unread}) {
$imgName = "post_nu"; $imgTitle = $lng->{comNewUnrdTT}; $imgAlt = $lng->{comNewUnrd};
}
elsif ($post->{new}) {
$imgName = "post_nr"; $imgTitle = $lng->{comNewReadTT}; $imgAlt = $lng->{comNewRead};
}
elsif ($post->{unread}) {
$imgName = "post_ou"; $imgTitle = $lng->{comOldUnrdTT}; $imgAlt = $lng->{comOldUnrd};
}
else {
$imgName = "post_or"; $imgTitle = $lng->{comOldReadTT}; $imgAlt = $lng->{comOldRead};
}
}
else {
if ($post->{new}) {
$imgName = "post_nu"; $imgTitle = $lng->{comNewTT}; $imgAlt = $lng->{comNew};
}
else {
$imgName = "post_ou"; $imgTitle = $lng->{comOldTT}; $imgAlt = $lng->{comOld};
}
}
my $imgAttr = "class='sic sic_$imgName' title='$imgTitle' alt='$imgAlt'";
# Print post header
print
"<div class='$pstClasses' id='pid$postId' style='margin-left: $indent%'>\n",
"<div class='hcl'>\n",
"<span class='nav'>\n";
# Print navigation buttons
if (!$flat) {
if (($post->{unread} || $post->{new} || $postPos == $firstPostPos)
&& @newUnrPostIds && @newUnrPostIds < $postNum && $postNum > 2
&& $postId != $newUnrPostIds[-1]) {
# Print goto next new/unread post button
my $nextPostId;
if ($postPos == 0) { $nextPostId = $newUnrPostIds[0] }
else {
for my $i (0 .. @newUnrPostIds) {
if ($newUnrPostIds[$i] == $postId) {
$nextPostId = $newUnrPostIds[$i+1];
last;
}
}
}
if ($nextPostId) {
my $url = $postsById{$nextPostId}{page} == $page
? "#pid$nextPostId" : $m->url('topic_show', pid => $nextPostId);
print
"<a class='nnl' href='$url'><img class='sic sic_post_nn' $emptyPixel",
" title='$lng->{tpcNxtPstTT}' alt='$lng->{tpcNxtPst}'></a>\n";
}
}
# Print jump to parent post button or alignment dummy
if (!$parentId) {
print "<img class='sic sic_nav_up' $emptyPixel style='visibility: hidden' alt=''>\n";
}
else {
my $url = $postsById{$parentId}{page} == $page
? "#pid$parentId" : $m->url('topic_show', pid => $parentId);
print
"<a class='prl' href='$url'><img class='sic sic_nav_up' $emptyPixel",
" title='$lng->{tpcParentTT}' alt='$lng->{tpcParent}'></a>\n";
}
}
elsif ($postPos == 0 && @newUnrPostIds && @newUnrPostIds < $postNum && $postNum > 2) {
# Print one goto new/unread post button in non-threaded boards
my ($nextPostId) = @newUnrPostIds;
my $url = $postsById{$nextPostId}{page} == $page
? "#pid$nextPostId" : $m->url('topic_show', pid => $nextPostId);
print
"<a href='$url'><img class='sic sic_post_nn' $emptyPixel",
" title='$lng->{tpcNxtPstTT}' alt='$lng->{tpcNxtPst}'></a>\n";
}
print "</span>\n";
# Print branch toggle icon
if ($printBranchToggle) {
my $img = $collapsed ? 'nav_plus' : 'nav_minus';
my $alt = $collapsed ? '+' : '-';
print
"<img class='tgl clk sic sic_$img' id='tgl$postId' $emptyPixel",
" title='$lng->{tpcBrnCollap}' alt='$alt'>\n";
}
# Print icon and main header items
my $postUrl = $m->url('topic_show', pid => $postId, tgt => "pid$postId");
print
"<a class='psl' href='$postUrl'><img $emptyPixel $imgAttr></a>\n",
$lockImg,
$invisImg,
$postUserId > -2 ? "<span class='htt'>$lng->{tpcBy}</span> $userNameStr\n" : "",
"<span class='htt'>$lng->{tpcOn}</span> $postTimeStr\n",
$editTimeStr,
$post->{likes} ? "<span class='htt'>$lng->{tpcLikes}</span> $post->{likes}\n" : "";
# Print IP
print "<span class='htt'>IP</span> $ip\n" if $boardAdmin && $cfg->{showPostIp};
# Print include plugin header items
for my $plugin (@{$cfg->{includePlg}{postHeader}}) {
$m->callPlugin($plugin, board => $board, topic => $topic, post => $post,
boardAdmin => $boardAdmin, topicAdmin => $topicAdmin);
}
print "</div>\n<div class='ccl'>\n";
# Print avatar
if ($showAvatars && index($post->{avatar}, "gravatar:") == 0) {
my $md5 = $m->md5(substr($post->{avatar}, 9));
my $url = "//gravatar.com/avatar/$md5?s=$cfg->{avatarWidth}";
print "<img class='ava' src='$url' alt=''>\n";
}
elsif ($showAvatars && $post->{avatar}) {
print "<img class='ava' src='$cfg->{attachUrlPath}/avatars/$post->{avatar}' alt=''>\n";
}
# Print body
print $post->{body}, "\n</div>\n";
# Print reply button
my @btlLines = ();
if (($boardWritable && !$topic->{locked} && !$post->{locked} || $boardAdmin || $topicAdmin)
&& $postUserId != -2) {
my $url = $m->url('post_add', pid => $postId);
push @btlLines, $m->buttonLink($url, 'tpcReply', 'write');
}
# Print reply with quote button
if (($boardWritable && !$topic->{locked} && !$post->{locked} || $boardAdmin || $topicAdmin)
&& $cfg->{quote} && ($flat || $cfg->{quote} == 2)
&& $postUserId != -2) {
my $url = $m->url('post_add', pid => $postId, quote => 1);
push @btlLines, $m->buttonLink($url, 'tpcQuote', 'write');
}
# Print edit button
if ($userId
&& ($userId == $postUserId && !$topic->{locked}
&& !$post->{locked} || $boardAdmin || $topicAdmin)
&& !($postUserId == -2 && $postId != $basePostId)) {
my $url = $m->url('post_edit', pid => $postId);
push @btlLines, $m->buttonLink($url, 'tpcEdit', 'edit');
}
# Print attach button
if ($cfg->{attachments} && $userId && $postUserId != -2
&& ($userId == $postUserId && !$topic->{locked}
&& !$post->{locked} || $boardAdmin || $topicAdmin)
&& ($board->{attach} == 1 || $board->{attach} == 2 && $boardAdmin)) {
my $url = $m->url('post_attach', pid => $postId);
push @btlLines, $m->buttonLink($url, 'tpcAttach', 'attach');
}
# Print notify button
if ($userId) {
my $url = $m->url('report_add', pid => $postId);
push @btlLines, $m->buttonLink($url, 'tpcReport', 'report');
}
# Print like buttons
if ($cfg->{postLikes} && $userId && $userId != $postUserId && $postUserId != -2) {
if ($post->{liked}) {
my $url = $m->url('post_like', pid => $postId, act => 'unlike', auth => 1);
push @btlLines, $m->buttonLink($url, 'tpcUnlike', 'rate');
}
else {
my $url = $m->url('post_like', pid => $postId, act => 'like', auth => 1);
push @btlLines, $m->buttonLink($url, 'tpcLike', 'rate');
}
}
# Print approve button
if (!$post->{approved} && ($boardAdmin || $topicAdmin)) {
my $url = $m->url('post_approve', pid => $postId, auth => 1);
push @btlLines, $m->buttonLink($url, 'tpcApprv', 'approve');
}
# Print lock/unlock button
if (($boardAdmin || $topicAdmin) && $postUserId != -2) {
if ($post->{locked}) {
my $url = $m->url('post_lock', pid => $postId, act => 'unlock', auth => 1);
push @btlLines, $m->buttonLink($url, 'tpcUnlock', 'lock');
}
else {
my $url = $m->url('post_lock', pid => $postId, act => 'lock', auth => 1);
push @btlLines, $m->buttonLink($url, 'tpcLock', 'lock');
}
}
# Print branch button
if ($postId != $basePostId && $postUserId != -2 && ($boardAdmin || $topicAdmin)) {
my $url = $m->url('branch_admin', pid => $postId);
push @btlLines, $m->buttonLink($url, 'tpcBranch', 'branch');
}
# Print delete button
if ($userId
&& ($userId == $postUserId && !$topic->{locked}
&& !$post->{locked} || $boardAdmin || $topicAdmin)
&& ($postId != $basePostId || @$posts == 1)
&& !@{$postsByParent{$postId}}) {
my $url = $m->url('user_confirm', script => 'post_delete', pid => $postId,
notify => ($postUserId != $userId ? 1 : 0), name => $postId);
push @btlLines, $m->buttonLink($url, 'tpcDelete', 'delete');
}
# Print include plugin buttons
for my $plugin (@{$cfg->{includePlg}{postLink}}) {
$m->callPlugin($plugin, lines => \@btlLines, board => $board, topic => $topic, post => $post,
boardAdmin => $boardAdmin, topicAdmin => $topicAdmin);
}
# Print button cell if there're button links
print "<div class='bcl'>\n", @btlLines, "</div>\n" if @btlLines && !$m->{archive};
print "</div>\n\n";
}
else {
# Print unapproved post bar
print
"<div class='frm hps' style='margin-left: $indent%'>\n",
"<div class='hcl'>\n",
"<a id='pid$postId'></a>\n",
"$lng->{tpcHidTtl} $lng->{tpcHidUnappr}\n",
"</div>\n",
"</div>\n\n";
}
}
# Print div for branch collapsing
if ($printBranchToggle) {
my $class = $collapsed ? "brn clp" : "brn";
print "<div class='$class' id='brn$postId'>\n";
}
# Print children recursively
$postPos++;
for my $child (@{$postsByParent{$postId}}) {
return if $postPos > $lastPostPos && !$printBranchToggle;
$child->{id} != $postId or $m->error("Post is its own parent?!");
$self->($self, $child->{id}, $depth + 1);
}
print "</div>\n" if $printBranchToggle;
};
for my $rootPost (@rootPosts) {
$printPost->($printPost, $rootPost->{id}, 0);
}
# Repeat page bar
$m->printPageBar(repeat => 1);
# Update topic read data
if ($userId && !$sameTopic && !$m->{archive}) {
if ($topic->{lastPostTime} > $lowestUnreadTime) {
# Replace topic's last read time
if ($m->{mysql}) {
$m->dbDo("
INSERT INTO topicReadTimes (userId, topicId, lastReadTime) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE lastReadTime = VALUES(lastReadTime)",
$userId, $topicId, $m->{now});
}
else {
$m->dbDo("
DELETE FROM topicReadTimes WHERE userId = ? AND topicId = ?", $userId, $topicId);
$m->dbDo("
INSERT INTO topicReadTimes (userId, topicId, lastReadTime) VALUES (?, ?, ?)",
$userId, $topicId, $m->{now});
}
}
# Update user stats
$m->{userUpdates}{lastTopicId} = $topicId;
$m->{userUpdates}{lastTopicTime} = $topic->{lastReadTime} || 0;
}
# Log action and finish
$m->logAction(2, 'topic', 'show', $userId, $boardId, $topicId);
$m->printFooter(undef, $boardId);
$m->finish();