#!/usr/bin/perl #------------------------------------------------------------------------------ # mwForum - Web-based discussion forum # Copyright (c) 1999-2009 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 my 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(@_); # Check if access should be denied $m->checkBan($userId) if $cfg->{banViewing}; # 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('errTPIdMiss'); # 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}; # 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 $stylePath = $m->{stylePath}; # Check if user can see and write to topic my $boardAdmin = $user->{admin} || $m->boardAdmin($userId, $boardId); my $topicAdmin = $board->{topicAdmins} && $m->fetchArray(" SELECT userId = ? FROM posts WHERE topicId = ? AND parentId = 0", $userId, $topicId); $boardAdmin || $topicAdmin || $m->boardVisible($board) or $m->error('errNoAccess'); my $boardWritable = $boardAdmin || $topicAdmin || $m->boardWritable($board, 1); # Post rating shortcuts my $postRating = $board->{rate} && @{$cfg->{postRatings}}; my $canRate = $postRating && $userId && ($board->{rate} == 1 || $board->{rate} == 2 && ($boardAdmin || $topicAdmin)); # Print header $m->printHeader($topic->{subject}); # 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 $newPostsExist = 0; my $unreadPostsExist = 0; for my $post (@$posts) { push @{$postsByParent{$post->{parentId}}}, $post; $newPostsExist = 1 if $post->{new}; $unreadPostsExist = 1 if $post->{unread}; } # 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 $basePost = $postsById{$topic->{basePostId}}; 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 unread post and its page if (!$page && !$firstUnrPostPage && $post->{unread}) { $firstUnrPostPage = $post->{page}; $firstUnrPostId = $postId; } # Determine first new post and its page if (!$page && !$firstNewPostPage && $post->{new}) { $firstNewPostPage = $post->{page}; $firstNewPostId = $postId; } # Add new/unread post ID to list push @newUnrPostIds, $postId if $userId && ($post->{new} || $post->{unread}); # Recurse through children $postPos++; for my $child (@{$postsByParent{$postId}}) { $child->{id} != $postId or $m->error("Post is its own parent?!"); $self->($self, $child->{id}); } }; $preparePost->($preparePost, $basePost->{id}); # Jump to first unread post if available, to first new post otherwise my $firstNewUnrPostId = undef; if ($userId && !$page) { $page = $firstUnrPostPage || $firstNewPostPage ; $firstNewUnrPostId = $firstUnrPostId || $firstNewPostId; } # Get the full content of those posts that are on the current page # Note: full posts are not copied to @$posts 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 $ratingsStr = $postRating ? ", postRatings.userId IS NOT NULL AS rated" : ""; my $ratingsJoin = $postRating ? " LEFT JOIN postRatings AS postRatings ON postRatings.postId = posts.id AND postRatings.userId = :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 $ratingsStr FROM ${arcPfx}posts AS posts LEFT JOIN users AS users ON users.id = posts.userId $ignoreJoin $ratingsJoin 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; } $basePost = $postsById{$topic->{basePostId}}; # Remove ignored and base crosslink posts from @newUnrPostIds if ($userId) { @newUnrPostIds = grep(!$postsById{$_}{ignored}, @newUnrPostIds); shift @newUnrPostIds if $postsById{$newUnrPostIds[0]}{userId} == -2; } # Mark branches that shouldn't be auto-collapsed if ($autoCollapsing) { for (@newUnrPostIds) { my $post = $postsById{$_}; 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 == $basePost->{userId} || $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; } # GeoIP my $geoIp = undef; my %geoCache; if ($cfg->{geoIp} && $user->{showDeco}) { if (eval { require Geo::IP }) { $geoIp = Geo::IP->open($cfg->{geoIp}); } elsif (eval { require Geo::IP::PurePerl }) { $geoIp = Geo::IP::PurePerl->open($cfg->{geoIp}); } } # Google highlighting my ($googleWords) = $m->{env}{referrer} =~ /www\.google.*?q=(.*?)(?:&|$)/; if ($googleWords && $googleWords !~ /^related:/) { $googleWords =~ s!\+! !g; $googleWords =~ s!%([0-9a-fA-F]{2})!chr hex $1!eg; $hilite = $googleWords; } # 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 @pageLinks = (); my $pageNum = int($postNum / $postsPP) + ($postNum % $postsPP != 0); if ($pageNum > 1) { my $maxPageNum = $m->min($pageNum, 8); push @pageLinks, { url => $m->url('topic_show', tid => $topicId, pg => $_), txt => $_, dsb => $_ == $page } for 1 .. $maxPageNum; push @pageLinks, { txt => "..." } if $maxPageNum < $pageNum - 1; push @pageLinks, { url => $m->url('topic_show', tid => $topicId, pg => $pageNum), txt => $pageNum, dsb => $pageNum == $page } if $maxPageNum < $pageNum; push @pageLinks, { url => $m->url('topic_show', tid => $topicId, pg => $page - 1), txt => 'comPgPrev', dsb => $page == 1 }; my $url = $cfg->{seoRewrite} && !$userId ? "topic_${topicId}_" . ($page + 1) . ".html" : $m->url('topic_show', tid => $topicId, pg => $page + 1); push @pageLinks, { url => $url, txt => 'comPgNext', dsb => $page == $pageNum }; } # Navigation button links my @navLinks = (); push @navLinks, { url => $m->url('prevnext', tid => $topicId, dir => 'prev'), txt => 'tpcPrev', ico => 'prev' }; push @navLinks, { url => $m->url('prevnext', tid => $topicId, dir => 'next'), txt => 'tpcNext', ico => 'next' }; push @navLinks, { url => $m->url('board_show', tid => $topicId, tgt => "tid$topicId"), txt => 'comUp', ico => 'up' }; # User button links my @userLinks = (); if (!$m->{archive}) { 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 == $basePost->{userId} || $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->{subscriptions}; push @userLinks, { url => $m->url('topic_ratings', tid => $topicId), txt => 'tpcRatings', ico => 'rate' } if $cfg->{linkAvgRatPgs} && $board->{rate}; 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; $m->callPlugin($cfg->{includePlg}{topicUserLink}, 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 => ($basePost->{userId} != $userId ? 1 : 0), name => $topic->{subject}), txt => 'tpcAdmDelete', ico => 'delete' }; $m->callPlugin($cfg->{includePlg}{topicAdminLink}, links => \@adminLinks, board => $board, topic => $topic); } # Print page bar my $url = $m->url('forum_show', tgt => "bid$boardId"); my $categStr = "$board->{categTitle} / "; $url = $m->url('board_show', tid => $topicId, tgt => "tid$topicId"); my $boardStr = "$board->{title} / "; my $lockStr = $topic->{locked} ? " $lng->{tpcLocked}" : ""; my $pageStr = $page > 1 ? " ($lng->{comPgTtl} $page)" : ""; my $hitStr = $cfg->{topicHits} ? " ($topic->{hitNum} $lng->{tpcHits})" : ""; $m->printPageBar( mainTitle => $lng->{tpcTitle}, subTitle => $categStr . $boardStr . $topic->{subject} . $lockStr . $pageStr . $hitStr, navLinks => \@navLinks, pageLinks => \@pageLinks, userLinks => \@userLinks, adminLinks => \@adminLinks); # Print spacer ($bvckup) print "
\n\n"; # Print poll if ($poll && $polls && !$m->{archive}) { # Print poll header my $lockedStr = $poll->{locked} ? $lng->{tpcPolLocked} : ""; print "$option->{title} | \n", "$votes | \n", "$percent\% | \n", "\n", " |