forked from github/Quit.mwForum
1186 lines
40 KiB
Perl
Executable File
1186 lines
40 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 TyfMain;
|
|
|
|
#------------------------------------------------------------------------------
|
|
|
|
# Init
|
|
my ( $m, $cfg, $lng, $user, $userId ) = TyfMain->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();
|