# ================================================================== # 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;