# ==================================================================
# Gossamer Forum - Advanced web community
#
# Website : http://gossamer-threads.com/
# Support : http://gossamer-threads.com/scripts/support/
# Revision : $Id: Post.pm,v 1.85 2002/01/04 19:50:53 jagerman Exp $
#
# Copyright (c) 2001 Gossamer Threads Inc. All Rights Reserved.
# Redistribution in part or in whole strictly prohibited. Please
# see LICENSE file for full details.
# ==================================================================
#
# Every post function other than viewing or writing (for example,
# deleting) goes in here.
#
package GForum::Post;
use strict;
use vars qw/@EXPORT_OK/;
use GForum qw/:user :forum $DB $IN $CFG $USER $GUEST $SESSION/;
use GForum::Convert; # exports escape_html(), unescape_html(), escape_string(), unescape_string(), convert_signature(), and convert_markup()
use Exporter;
use constants THREADED => 0,
FLAT => 1;
@EXPORT_OK = qw/THREADED FLAT/;
sub icons { +{ icons => [(shift() ? {} : ()), map +{ icon_name => $_, icon_filename => $CFG->{post_icons}->{$_} }, keys %{$CFG->{post_icons}}] } }
# Called from the admin templates
sub icon_add {
my ($icon_name, $icon_filename) = @_;
if (exists $CFG->{post_icons}->{$icon_name}) {
return { add_success => 0, reason => "An icon with that name already exists" }
}
$CFG->{post_icons}->{$icon_name} = $icon_filename;
$CFG->save();
return { add_success => 1 }
}
# Called from the admin templates
sub icon_delete {
my $icon_name = shift;
if (not exists $CFG->{post_icons}->{$icon_name}) {
return { delete_sucess => 0, reason => "No such icon" }
}
delete $CFG->{post_icons}->{$icon_name};
$CFG->save();
return { delete_success => 1 }
}
sub move {
shift;
my ($do, $func) = @_;
my ($Forum, $Post) = ($DB->table('Forum'), $DB->table('Post'));
my $page = $func->{page};
my $root_id = $IN->param('root_id');
my $old_forum_id = $Post->select(forum_id_fk => { post_id => $root_id })->fetchrow;
my $forum_id = $IN->param('forum_id');
my @post_ids = ($root_id, $Post->select(post_id => { post_root_id => $root_id })->fetchall_list);
$Post->update({ forum_id_fk => $forum_id }, { post_id => \@post_ids });
for ($old_forum_id, $forum_id) {
my $forum_total = $Post->count({ forum_id_fk => $_ });
my $forum_total_threads = $Post->count({ forum_id_fk => $_, post_root_id => 0 });
$Post->select_options('ORDER BY post_time DESC', 'LIMIT 1');
my ($last_poster, $last_time) = $Post->select('post_username', 'post_time', { forum_id_fk => $_ })->fetchrow;
$Forum->update({ forum_last => $last_time, forum_last_poster => $last_poster, forum_total => $forum_total, forum_total_threads => $forum_total_threads }, { forum_id => $_ });
}
my $post = $DB->table('Post', 'User')->select(left_join => { post_id => $root_id })->fetchrow_hashref;
normalize($post);
my $old_forum = $DB->table('Forum', 'Category')->select({ forum_id => $old_forum_id })->fetchrow_hashref;
my $forum = $DB->table('Forum', 'Category')->select({ forum_id => $forum_id })->fetchrow_hashref;
require GForum::Forum;
GForum::Forum::normalize($old_forum);
GForum::Forum::normalize($forum);
@$forum{map "old_$_", keys %$old_forum} = values %$old_forum;
return(
$page->{moved} => {
%$post,
%$forum
}
);
}
# Detaching is just like moving, except that it works on a reply.
sub detach {
shift;
my ($do, $func) = @_;
my ($Forum, $Post, $Ancestor, $PostView) = ($DB->table('Forum'), $DB->table('Post'), $DB->table('Ancestor'), $DB->table('PostView'));
my $page = $func->{page};
my $post_id = $IN->param('post_id');
my ($old_forum_id, $old_root_id, $old_depth) = $Post->select('forum_id_fk', 'post_root_id', 'post_depth' => { post_id => $post_id })->fetchrow;
my $new_forum_id = $IN->param('forum_id');
my @ancestors = $Ancestor->select(anc_id_fk => { post_id_fk => $post_id })->fetchall_list;
my @new_thread_ids = ($post_id, $Ancestor->select(post_id_fk => { anc_id_fk => $post_id })->fetchall_list);
my @old_thread_ids = ($old_root_id, $Post->select(post_id => { post_root_id => $old_root_id })->fetchall_list);
# @old_thread_ids contains everything in the root - we now have to take the new_thread_ids out of it.
OLD: for (my $o = 0; $o < @old_thread_ids; $o++) {
for (my $n = 0; $n < @new_thread_ids; $n++) {
if ($old_thread_ids[$o] == $new_thread_ids[$n]) {
splice @old_thread_ids, $o, 1;
$o < @old_thread_ids ? redo OLD : last OLD; # redo doesn't check the for loop condition
}
}
}
my $old_thread_views = $PostView->select(post_thread_views => { post_id_fk => $old_root_id })->fetchrow;
$PostView->update({ post_thread_views => $old_thread_views }, { post_id_fk => $post_id }); # The new root inherits the thread views of the old root
$Post->update({ post_root_id => 0, post_father_id => 0, post_depth => 0 }, { post_id => $post_id });
$Post->update({ post_depth => \"post_depth - $old_depth", post_root_id => $post_id }, { post_id => [@new_thread_ids[1 .. $#new_thread_ids]] }) if @new_thread_ids > 1;
my @update_forums = $new_forum_id;
if ($old_forum_id != $new_forum_id) { # A detachment doesn't necessarily go to a new forum
$Post->update({ forum_id_fk => $new_forum_id }, { post_id => \@new_thread_ids });
push @update_forums, $old_forum_id;
}
for (@update_forums) {
my $forum_total = $Post->count({ forum_id_fk => $_ });
my $forum_total_threads = $Post->count({ forum_id_fk => $_, post_root_id => 0 });
$Post->select_options('ORDER BY post_time DESC', 'LIMIT 1');
my ($last_poster, $last_time) = $Post->select('post_username', 'post_time', { forum_id_fk => $_ })->fetchrow;
$Forum->update({ forum_last => $last_time, forum_last_poster => $last_poster, forum_total => $forum_total, forum_total_threads => $forum_total_threads }, { forum_id => $_ });
}
# Now we have to delete all of the ancestor relations between the old thread and the new one:
$Ancestor->delete({ post_id_fk => \@new_thread_ids, anc_id_fk => \@old_thread_ids });
# Now all the ancestors of the new root post have to have their number of replies reduced:
my $fewer_replies = @new_thread_ids;
$Post->update({ post_replies => \"post_replies - $fewer_replies" }, { post_id => \@ancestors });
# And the old thread has to have all of its latest_reply and latest_poster times updated.
$Post->rebuild_latest(@old_thread_ids);
my $post = $DB->table('Post', 'User')->select(left_join => { post_id => $post_id })->fetchrow_hashref;
normalize($post);
my $forum = $DB->table('Forum', 'Category')->select({ forum_id => $new_forum_id })->fetchrow_hashref;
require GForum::Forum;
GForum::Forum::normalize($forum);
my $old_forum;
if ($old_forum_id == $new_forum_id) {
$old_forum = $forum;
}
else {
$old_forum = $DB->table('Forum', 'Category')->select({ forum_id => $old_forum_id })->fetchrow_hashref;
GForum::Forum::normalize($old_forum) if $old_forum_id != $new_forum_id;
}
@$forum{map "old_$_", keys %$old_forum} = values %$old_forum;
return(
$page->{detached} => {
%$post,
%$forum
}
);
}
sub lock {
shift;
my ($do, $func) = @_;
my $page = $func->{page};
my $post_id = $IN->param('root_id');
$DB->table('Post')->update({ post_locked => 1 }, { post_id => $post_id });
GForum::do_func($IN->param('redo'));
}
sub unlock {
shift;
my ($do, $func) = @_;
my $page = $func->{page};
my $post_id = $IN->param('root_id');
$DB->table('Post')->update({ post_locked => undef }, { post_id => $post_id });
GForum::do_func($IN->param('redo'));
}
sub keep {
shift;
my ($do, $func) = @_;
my $page = $func->{page};
my $post_id = $IN->param('root_id');
$DB->table('Post')->update({ post_keep => 1 }, { post_id => $post_id });
GForum::do_func($IN->param('redo'));
}
sub unkeep {
shift;
my ($do, $func) = @_;
my $page = $func->{page};
my $post_id = $IN->param('root_id');
$DB->table('Post')->update({ post_keep => 0 }, { post_id => $post_id });
GForum::do_func($IN->param('redo'));
}
# This marks all posts in all forums as read. GForum::Forum::mark_all_new marks
# all posts in a forum as read.
sub mark_all_read {
if ($USER) {
my $UserNew = $DB->table('UserNew');
my $now = time;
my @forums = $DB->table('Forum')->select('forum_id')->fetchall_list;
# Don't worry about permissions, even if we set a hidden forum as read it's no big deal.
for my $forum_id (@forums) {
if ($UserNew->count({ forum_id_fk => $forum_id, user_id_fk => $USER->{user_id} })) {
$UserNew->update({ usernew_last => $now }, { forum_id_fk => $forum_id, user_id_fk => $USER->{user_id} });
}
else {
$UserNew->insert({ forum_id_fk => $forum_id, user_id_fk => $USER->{user_id}, usernew_last => $now });
}
if ($SESSION) {
$SESSION->{info}->{session_data}->{usernew}->{$forum_id} = $now;
$SESSION->{info}->{session_data}->{userlast}->{$forum_id} = $now;
$SESSION->save();
}
}
# Delete any "new" posts from the PostNew table.
$DB->table('PostNew')->delete({ user_id_fk => $USER->{user_id} });
}
GForum::do_func($IN->param('redo'));
}
# Takes one argument and does whatever needs to be done prior to display of the post(s).
# To normalize a single post, you pass in a hash ref from $DB->table('Post', 'User')
# To normalize multiple posts, you pass in a single array reference containing the hash
# refs from $DB->table('Post', 'User');
# This subroutine returns the post (or posts), but you don't have to use it - the post
# hashref(s) are directly altered as required.
# If you pass in "post_literal" (via a post hashref) the post will be viewed as if
# it were plain text - that is, markup will not be converted and HTML will be escaped.
sub normalize {
my $p = shift;
GT::Plugins->dispatch($CFG->{admin_root_path} . '/Plugins/GForum', "post_normalize", sub { return _plg_normalize(@_) }, $p);
}
sub _plg_normalize {
my $posts = ref $_[0] eq 'ARRAY' ? shift : ref $_[0] eq 'HASH' ? [shift] : return;
return unless @$posts;
require GT::Date;
# Normalize any users first:
my @posts_with_users = grep $_->{user_id}, @$posts;
if (@posts_with_users) {
require GForum::User;
GForum::User::normalize(\@posts_with_users);
}
my $pv = $DB->table('PostView');
my @post_ids = map $_->{post_id}, @$posts;
my %views = map { ($_->[0] => [ $_->[1], $_->[2] ]) } @{$DB->table('PostView')->select('post_id_fk', 'post_views', 'post_thread_views' => { post_id_fk => \@post_ids })->fetchall_arrayref} if @post_ids;
my @forum_ids;
{
my %forum_ids;
for (@$posts) {
push @forum_ids, $_->{forum_id_fk} unless $forum_ids{$_->{forum_id_fk}}++;
}
}
my %moderators; # { forum_id => { user_id => 1, user_id => 1, ... }, ... }
if (@forum_ids) {
my $sth = $DB->table('ForumModerator')->select('forum_id_fk', 'user_id_fk', { forum_id_fk => \@forum_ids });
while (my ($fid, $uid) = $sth->fetchrow) {
$moderators{$fid}->{$uid} = 1;
}
}
my (%anc_info, $anc_loaded);
for my $post (@$posts) {
@$post{'post_views', 'post_thread_views'} = @{$views{$post->{post_id}}} if exists $views{$post->{post_id}};
$post->{post_user_is_moderator} = ($post->{forum_id_fk} and $post->{user_id_fk} and $moderators{$post->{forum_id_fk}}->{$post->{user_id_fk}});
$post->{post_latest_reply} = 2_000_000_000 - $post->{post_latest_reply};
$post->{post_date} = GForum::date($post->{post_time});
$post->{post_latest_reply_date} = GForum::date($post->{post_latest_reply});
if ($post->{post_deleted}) {
$post->{post_deleted_date} = GForum::date($post->{post_deleted_time});
}
if ($post->{post_last_edit_username}) {
$post->{post_last_edit_date} = GForum::date($post->{post_last_edit_time});
}
if (!$post->{user_id}) { # Handle any posts without users now
$post->{user_username} = $post->{post_username};
$post->{user_title} = \GForum::language('USER_DELETED');
$post->{user_signature} = $post->{post_signature_deleted};
}
my $converted = 0;
my $message = $post->{post_message};
# These variables have to be copied out here because of replies - the keys of $post
# are renamed "parent_post_*", which breaks this closure if using $post->{...} :(
my $style = $post->{post_style};
my $signature = $post->{$post->{user_id} ? "user_signature" : "post_signature_deleted"};
$post->{post_message} = sub {
return \$message if $converted++;
if ($style < 2) { # 2 and 3 allow HTML, 0 and 1 don't.
escape_html($message);
}
if ($style % 2) { # 1 and 3 allow Markup
convert_markup(\$message);
}
unless ($CFG->{signature_allow_html}) {
escape_html($signature);
}
if ($CFG->{signature_allow_markup} == 2) {
convert_markup(\$signature);
}
elsif ($CFG->{signature_allow_markup} == 1) {
my $save = $GForum::Convert::No_Image; # Implement local() without local()
$GForum::Convert::No_Image = 1;
convert_markup(\$signature);
$GForum::Convert::No_Image = $save;
}
unless ($CFG->{signature_allow_html} or $CFG->{signature_allow_markup}) {
$signature =~ s/ / /g;
}
convert_signature(\$message, \$signature);
$message =~ s/\r?\n/
/g; # That space keeps IE from condensing multiple
's into 1. It is only needed where you have
, but that regex would slow the converter down quite a bit.
$message =~ s/^( +)/' ' x length $1/gem;
\$message;
};
$post->{post_depth} ||= 0;
if ($SESSION) {
my $data = $SESSION->data();
if (not $data->{posts}->{$post->{forum_id_fk}}->{$post->{post_id}} # Not previously viewed
and $post->{post_time} > $data->{userlast}->{$post->{forum_id_fk}} # Posted since the last time we were in this forum BEFORE the current session
and $post->{user_id_fk} != $USER->{user_id}) { # Not posted by the current user
$post->{post_new} = 1;
}
else {
$post->{post_new} = undef;
}
if ($post->{post_replies} and # There are replies
$post->{post_latest_reply} > $data->{userlast}->{$post->{forum_id_fk}}) { # Posted since the last time we were in this forum BEFORE the current session
# There _might_ be new ones, but we need to do a check to be sure.
my %reply; # child_post_id => child_post_time
my %replier; # child_post_id => child_user_id
unless ($anc_loaded++) { # Do a little caching here to save on the number of selects
my $sth = $DB->table(Ancestor => 'Post')->select(qw/anc_id_fk post_id post_time user_id_fk/, { anc_id_fk => [map $_->{post_id}, @$posts] });
while (my $row = $sth->fetchrow_arrayref) {
push @{$anc_info{$row->[0]}}, [@$row[1,2,3]]
}
}
for (@{$anc_info{$post->{post_id}}}) {
$reply{$_->[0]} = $_->[1];
$replier{$_->[0]} = $_->[2];
}
my $new_replies;
for (keys %reply) {
if (!$data->{posts}->{$post->{forum_id_fk}}->{$_} and
$reply{$_} > $data->{userlast}->{$post->{forum_id_fk}} and
$replier{$_} != $USER->{user_id}) {
$new_replies = 1;
last;
}
}
$post->{new_replies} = $new_replies;
}
else {
$post->{new_replies} = undef;
}
}
if ($USER) {
if ($USER->{user_default_post_display} == THREADED) {
$post->{post_display_is_threaded} = 1;
}
else {
$post->{post_display_is_flat} = 1;
}
}
elsif ($CFG->{post_display_default} eq 'post_view_flat') {
$post->{post_display_is_flat} = 1;
}
else {
$post->{post_display_is_threaded} = 1;
}
}
attachments($posts);
$posts;
}
# Takes two arguments: A scalar reference to a non-normalized post_message
# value, and the normalized Post,User hash it came from. Returns nothing.
sub plain_text {
my ($str, $post) = @_;
$$str =~ s/
/\n/g if $post->{post_style} >= 2;
$$str =~ s/<.*?>//g if $post->{post_style} >= 2;
$$str =~ s/\[(\s*(.*?)\s*)\]/if (exists $CFG->{markup_tags}->{lc $2} or lc $2 eq lc $CFG->{signature_markup_tag}) { "" } elsif (substr($1, 0, 1) eq ".") { "[" . substr($1, 1) . "]" } else { "[$1]" }/eg if $post->{post_style} % 2 != 0;
convert_signature($str, \$post->{user_signature});
return;
}
# This doesn't do any selects; the posts are determined by expand_threads and expandable_threads,
# which call this with the proper arguments. This takes that data and sorts and flattens it
# into a plain array ref.
sub _expand_threads {
my ($posts, $root_pos) = @_;
my @sorted_posts;
# @sorted_posts will look like this:
# @sorted_posts = (
# [$root, $reply1, $reply2, ...],
# [$root, $reply1, $reply2, ...],
# ...
# );
# All replies will be correctly sorted. The only thing left it to flatten and normalize the array.
# The mess below properly sorts out a thread, paying attention to both the
# parent and the post_time.
for my $thread (@$posts) {
my $root = $thread->[0]->[0];
my $sort_i = $root_pos->{$root->{post_id}};
push @{$sorted_posts[$sort_i]}, $root;
for my $level (1 .. $#$thread) {
for my $parent (@{$thread->[$level-1]}) {
for my $current (sort { $b->{post_time} <=> $a->{post_time} } @{$thread->[$level]}) {
next unless $current->{post_father_id} == $parent->{post_id};
for my $i (0 .. $#{$sorted_posts[$sort_i]}) {
if ($sorted_posts[$sort_i]->[$i]->{post_id} == $current->{post_father_id}) {
splice(@{$sorted_posts[$sort_i]}, $i+1, 0, $current);
last;
}
}
}
}
}
}
my @ret = map { @$_ } @sorted_posts; # @sorted_posts is flattened and put into @ret.
normalize(\@ret);
return \@ret;
}
# Takes an array ref of post ID's of root posts as the first argument. Returned
# is an array reference containing a sorted list of all the roots and children.
# The sorting for threads is by time, and for roots is the order in which the
# ID's were passed in. The posts in the array _are_ normalized and _have_ had
# attachments added. It takes an optional second argument - another post ID.
# This post ID will have "this_post" set to 1 in its information as soon as
# encountered - to be used when viewing a post.
sub expand_threads {
my $roots = ref $_[0] eq 'ARRAY' ? shift : [shift];
my $this_post_id = shift;
my %root_pos;
my $i = 0;
for (@$roots) {
$root_pos{$_} = $i++; # $roots{$root_id} == the position of the root post in the posts to be returned (relative to the other roots)
}
my @post_ids = @$roots;
push @post_ids, $DB->table('Ancestor')->select(post_id_fk => { anc_id_fk => \@post_ids })->fetchall_list;
# @post_ids is now a list of all the roots, and all the children of all the roots.
my $PostUser = $DB->table('Post' => 'User');
my $sth = $PostUser->select('left_join', { post_id => \@post_ids });
my @posts;
# When we're done, @posts is going to look like this:
# @posts = (
# [[$root], [$reply_level_1_post_1, $reply_level_1_post_2 ], [$reply_level_2_post_1, $reply_level_2_post_2], ...],
# [[$root], [$reply_level_1_post_1, $reply_level_1_post_2 ], [$reply_level_2_post_1, $reply_level_2_post_2], ...],
# ...
# );
# All roots will be in the correct position by reading from %root_pos.
# The weird structure about is needed to properly sort out the structure; the
# code to sort it is below - look for _FIVE_ nested for loops (ACK!).
while (my $post = $sth->fetchrow_hashref) {
# All posts should know that both they and their threads are expanded
$post->{post_expanded} = $post->{post_thread_expanded} = 1 if $post->{post_replies};
# If this is the current post, mark it as such.
$post->{this_post} = 1 if $this_post_id and $post->{post_id} == $this_post_id;
if (not $post->{post_root_id}) { # In other words, a root post
$posts[$root_pos{$post->{post_id}}]->[0] = [$post];
}
else {
push @{$posts[$root_pos{$post->{post_root_id}}]->[$post->{post_depth}]}, $post;
}
}
# Now @posts should be exactly as described above :)
return _expand_threads(\@posts, \%root_pos);
}
# The return from this is the same as that of expand_thread.
# It takes one root ID. It looks at $USER or $GUEST and does counts from the Expanded table.
sub expandable_threads {
my $roots = ref $_[0] eq 'ARRAY' ? shift : [shift];
my %root_pos;
my $i;
for (@$roots) {
$root_pos{$_} = $i++;
}
my $sth = $DB->table('Expanded')->select('thread_id_fk', 'post_id_fk' => ($USER ? { user_id_fk => $USER->{user_id} } : { guest_id_fk => $GUEST->{guest_id} }));
my (%expanded_roots, %expanded_posts);
while (my $row = $sth->fetchrow_arrayref) {
if (my $pid = $row->[0]) { # This thread is expanded
$expanded_roots{$pid} = 1;
}
elsif ($pid = $row->[1]) { # This post is expanded
$expanded_posts{$pid} = 1;
}
}
# %expanded_roots and %expanded_posts now contain all expanded threads and posts, respectively.
my @post_ids; # This has to become a list of all the posts that are to be shown.
@post_ids = @$roots; # First, get ALL posts, as if we were in expand_threads. We'll strip them off soon enough root children
push @post_ids, $DB->table('Ancestor')->select(post_id_fk => { anc_id_fk => \@post_ids })->fetchall_list;
my $PostUser = $DB->table('Post' => 'User');
$PostUser->select_options("ORDER BY post_depth"); # Parents must come before their children
$sth = $PostUser->select('left_join', { post_id => \@post_ids });
my @posts;
# When we're done, @posts is going to look like this:
# @posts = (
# [[$root], [$reply_level_1_post_1, $reply_level_1_post_2 ], [$reply_level_2_post_1, $reply_level_2_post_2], ...],
# [[$root], [$reply_level_1_post_1, $reply_level_1_post_2 ], [$reply_level_2_post_1, $reply_level_2_post_2], ...],
# ...
# );
# All roots will be in the correct position by reading from %root_pos.
# The weird structure about is needed to properly sort out the structure; the
# code to sort it is below - look for _FIVE_ nested for loops (ACK!).
# ONLY posts that are expanded will be included in the data.
while (my $post = $sth->fetchrow_hashref) {
if ($post->{post_replies}) {
if ($expanded_roots{$post->{post_root_id} || $post->{post_id}}) {
$post->{post_expanded} = $post->{post_thread_expanded} = 1;
}
elsif ($expanded_posts{$post->{post_id}}) {
$post->{post_expanded} = 1;
}
}
if ($post->{post_father_id} and not $expanded_roots{$post->{post_root_id}} and not $expanded_posts{$post->{post_father_id}}) {
# If neither the post's thread nor the post's father has an expansion, skip it.
delete $expanded_posts{$post->{post_id}}; # This post should not show up in the list - otherwise a child would show up if THIS was expanded but its parent wasn't.
next;
}
if (not $post->{post_root_id}) { # In other words, a root post
$posts[$root_pos{$post->{post_id}}]->[0] = [$post];
}
else {
push @{$posts[$root_pos{$post->{post_root_id}}]->[$post->{post_depth}]}, $post;
}
}
# Now @posts should be exactly as described above :)
return _expand_threads(\@posts, \%root_pos);
}
# Takes a hash ref and sets $hash->{post_attachments} to an array ref of hash refs.
# The hash refs are the attachments of the post. If the post field "post_has_attachments"
# is not set, this subroutine does nothing.
sub attachments {
my $posts = ref $_[0] eq 'ARRAY' ? shift : [shift];
my %attachments;
for my $i (0 .. $#$posts) {
my $post = $posts->[$i];
$post->{post_has_attachments} or next;
$attachments{$post->{post_id}} = $i;
}
return unless keys %attachments;
my $sth = $DB->table('PostAttachment')->select({ post_id_fk => [keys %attachments] });
my $num_attachments = 0;
while (my $attachment = $sth->fetchrow_hashref) {
$attachment->{postatt_filename_escaped} = escape_string($IN->escape($attachment->{postatt_filename}));
my $i = $attachments{$attachment->{post_id_fk}};
push @{$posts->[$i]->{post_attachments}}, $attachment;
$posts->[$i]->{post_num_attachments}++;
}
return;
}
sub delete {
shift;
my ($do, $func) = @_;
my $page = $func->{page};
my $post_id = $IN->param('post');
$post_id and my $post = $DB->table('Post' => 'User')->select(left_join => { post_id => $post_id })->fetchrow_hashref
or return($page->{failed} => {
error => GForum::language('POST_DOES_NOT_EXIST')
}
);
my $forum = $DB->table('Forum', 'Category')->select({ forum_id => $post->{forum_id_fk} })->fetchrow_hashref;
unless ($USER->{user_forum_permission} >= FORUM_PERM_MODERATOR
or ($USER->{user_id} == $post->{user_id_fk}
and
$forum->{forum_allow_user_edit} >= 2 # 2 is delete, 3 is edit & delete, but 0 and 1 do not allow deleting
and
(!$forum->{forum_edit_timeout} or ($post->{post_time} + $forum->{forum_edit_timeout} * 60) > time)
)
) {
$GForum::Template::VARS{permission_denied_reason} = GForum::language('POST_EDIT_TIME_EXPIRED');
return GForum::do_func('permission_denied');
}
normalize($post);
require GForum::Forum;
GForum::Forum::normalize($forum);
if ($forum->{forum_hard_delete} == 1 or $forum->{forum_hard_delete} == 2 and not $post->{post_replies}) {
$DB->table('Post')->delete($post_id);
# Attachments are deleted by GT::SQL (PostAttachment has a foreign key to post_id)
}
else {
$DB->table('Post')->update({ post_deleted => 1, post_deleted_by => $USER->{user_username}, post_deleted_time => time }, { post_id => $post_id });
# Attachments have to be deleted
$DB->table('PostAttachment')->delete({ post_id_fk => $post_id });
}
return(
$page->{delete} => {
%$post,
%$forum
}
);
}
# Just like delete above, except that this is meant for moderators only. It
# deletes the post and all replies, regardless of the forum_hard_delete setting.
sub remove {
shift;
my ($do, $func) = @_;
my $page = $func->{page};
my $post_id = $IN->param('post');
$post_id and my $post = $DB->table('Post' => 'User')->select(left_join => { post_id => $post_id })->fetchrow_hashref
or return($page->{failed} => {
error => GForum::language('POST_DOES_NOT_EXIST')
}
);
my $forum = $DB->table('Forum', 'Category')->select({ forum_id => $post->{forum_id_fk} })->fetchrow_hashref;
unless ($USER->{user_forum_permission} >= FORUM_PERM_MODERATOR) {
$GForum::Template::VARS{permission_denied_reason} = GForum::language('POST_REMOVE_NOT_MODERATOR');
return GForum::do_func('permission_denied');
}
normalize($post);
require GForum::Forum;
GForum::Forum::normalize($forum);
$DB->table('Post')->delete($post_id);
return(
$page->{delete} => {
%$post,
%$forum
}
);
}
# This function is called from the admin templates
sub delete_old {
my $num_days = shift or return;
my $cutoff = time - 24 * 60 * 60 * $num_days;
my $cond = GT::SQL::Condition->new(
post_latest_reply => '>=' => (2_000_000_000 - $cutoff),
post_keep => '=' => 0,
post_root_id => '=' => 0
);
my $deleted = $DB->table('Post')->delete($cond);
$deleted or die "$deleted: $GT::SQL::error";
return { posts_deleted => 0 + $deleted }
}
sub count_old {
my $num_days = shift or return;
my $cutoff = time - 24 * 60 * 60 * $num_days;
my $cond = GT::SQL::Condition->new(
post_latest_reply => '>=' => (2_000_000_000 - $cutoff),
post_keep => '=' => 0
);
my $count = $DB->table('Post')->count($cond);
die $GT::SQL::error if not defined $count;
return $count;
}
# This must be called either from the admin
sub reindex {
my $hires = eval "require Time::HiRes";
my $s = $hires ? Time::HiRes::time() : time;
my $t = localtime;
my $old_autoflush = $|;
$| = 1 if not $old_autoflush;
my $html = 1 if $ENV{REQUEST_METHOD};
print "Reindexing Gossamer Forum database ...\n\n" if not $html;
print "Started at $t.\n\nIndexing Post database ...\n\n";
my $post = $DB->table('Post');
my $total = $post->count({ post_deleted => 0 });
my $weights = $post->weight || {};
my $found;
for (keys %$weights) {
$found = 1 if $weights->{$_} > 0;
}
unless ($found) {
print "" if $html;
print "No search weights have been set, aborting!\n\n";
print "" if $html;
return;
}
print "$total posts.\n";
$post->reindex({ tick => 250, max => 1000, cond => { post_deleted => 0 } });
printf "\nDone! (%.2f s)\n\n", ($hires ? Time::HiRes::time() : time) - $s;
$| = 0 if not $old_autoflush;
return;
}
sub status_icon {
my ($thread_hot, $post_new, $post_replies, $new_replies) = @_;
my $ret;
$ret->{Icon} = '';
if ($thread_hot) {
$ret->{Icon} .= "hot_";
GForum::Template::store_gvars(legend_hot => 1);
}
if ($post_new) {
$ret->{Icon} .= "new_";
GForum::Template::store_gvars(legend_new => 1);
}
if ($post_replies) {
GForum::Template::store_gvars(legend_replies => 1);
if ($new_replies) {
$ret->{Icon} .= "with_new_replies";
}
else {
$ret->{Icon} .= "with_replies";
}
}
else {
GForum::Template::store_gvars(legend_single => 1);
$ret->{Icon} .= "no_replies";
}
$ret->{IconAlt} = GForum::language("POSTICON_$ret->{Icon}");
$ret->{Icon} .= ".gif";
$ret;
}
1;