# ================================================================== # Gossamer Forum - Advanced web community # # Website : http://gossamer-threads.com/ # Support : http://gossamer-threads.com/scripts/support/ # Revision : $Id: Post.pm,v 1.41 2002/03/23 21:20:33 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. # ================================================================== package GForum::Table::Post; # ================================================================== use strict; use GForum qw/$DB $USER $CFG/; use GT::SQL::Table; use vars qw/@ISA $VERSION $DEBUG $ERRORS $ERROR_MESSAGE $AUTH/; @ISA = qw/GT::SQL::Table/; $VERSION = sprintf "%d.%03d", q$Revision: 1.41 $ =~ /(\d+)\.(\d+)/; $DEBUG = 0; $ERROR_MESSAGE = 'GT::SQL'; sub insert { my $self = shift; my @args = @_; GT::Plugins->dispatch($CFG->{admin_root_path} . '/Plugins/GForum', 'insert_post', sub { $self->_plg_insert(@args) }); } sub _plg_insert { my $self = shift; my $post = ref $_[0] eq 'HASH' ? {%{$_[0]}} : {@_}; my $no_update_forum_last = delete $post->{no_update_forum_last}; $post->{post_latest_reply} = 2_000_000_000 - $post->{post_time}; # If this is a root post, the latest poster is the poster. $post->{post_latest_poster} = $post->{post_username} if not $post->{post_father_id}; my $post_views = delete $post->{post_views} || 0; my $post_thread_views = delete $post->{post_thread_views} || 0; my $root = $post->{post_father_id} ? $self->select('post_root_id' => { post_id => $post->{post_father_id} })->fetchrow : 0; my $already_hot = $self->count({ post_id => $root, post_thread_hot => 1 }) if $root; if ($already_hot) { $post->{post_thread_hot} = 1; } my ($guest, $anon); if ($guest = delete $post->{guest_username}) { $anon = $post->{post_username}; $post->{post_username} = $guest; } my $ret = $self->SUPER::insert($post) or return; if ($guest) { $post->{post_username} = $anon; # From here on, post_username should be 'Anonymous' instead of whatever they entered } my $post_id = $ret->insert_id; if ($post->{post_father_id}) { $DB->table('PostView')->insert({ post_id_fk => $post_id, post_views => $post_views, post_thread_views => undef }); } else { $DB->table('PostView')->insert({ post_id_fk => $post_id, post_views => $post_views, post_thread_views => $post_thread_views }); } if ($post->{post_father_id}) { my $tree = $self->tree; my @posts = ($post->{post_father_id}, @{$tree->parent_ids(id => $post->{post_father_id})}); # Update the number of replies, post times, and lastest poster $self->update({ post_replies => \'post_replies + 1', post_latest_reply => $post->{post_latest_reply}, post_latest_poster => $post->{post_username} } => { post_id => \@posts }); if ($CFG->{hot_replies} and not $self->count({ post_id => $root, post_thread_hot => 1 })) { # If hot_replies is enabled, and the thread isn't already hot, update the thread to "hot" if it should be hot. if ($CFG->{hot_minutes}) { my $cutoff = time - 60 * $CFG->{hot_minutes}; my $new = $self->count( GT::SQL::Condition->new( post_root_id => '=' => $root, post_time => '>=' => $cutoff ) ); if ($new >= $CFG->{hot_replies}) { # Done as two queries - one query with an OR would be much slower $self->update({ post_thread_hot => 1 }, { post_id => $root }); $self->update({ post_thread_hot => 1 }, { post_root_id => $root }); } } else { my $replies = $self->select(post_replies => { post_id => $root })->fetchrow; if ($replies >= $CFG->{hot_replies}) { $self->update({ post_thread_hot => 1 }, { post_id => $root }); $self->update({ post_thread_hot => 1 }, { post_root_id => $root }); } } } } my $forum_id = $post->{forum_id_fk}; $DB->table('Forum')->update({ forum_total => \'forum_total + 1', ($root ? () : (forum_total_threads => \'forum_total_threads + 1')), ($no_update_forum_last ? () : (forum_last => time, forum_last_poster => $post->{post_username}, forum_last_id => $post_id)) } => { forum_id => $forum_id }); $DB->table('User')->update({ user_posts => \'user_posts + 1' } => { user_id => $USER->{user_id} }) if $USER; return $ret; } sub update { my $self = shift; my @args = @_; GT::Plugins->dispatch($CFG->{admin_root_path} . '/Plugins/GForum', 'update_post', sub { $self->_plg_update(@args) }); } sub _plg_update { my $self = shift; my ($set, $where, $opts) = @_; my ($new_forum, @old_forum); if ($set and $set->{forum_id_fk}) { $new_forum = $set->{forum_id_fk}; if ($where->{forum_id_fk} and not ref $where->{forum_id_fk}) { @old_forum = $where->{forum_id_fk} unless $where->{forum_id_fk} == $new_forum; } else { @old_forum = grep $_ != $new_forum, $self->select('forum_id_fk' => $where)->fetchall_list; } } my $ret = $self->SUPER::update($set, $where, $opts); return $ret unless $ret and $new_forum and @old_forum; for my $fid ($new_forum, @old_forum) { my $forum_total = $self->count({ forum_id_fk => $fid }); my $forum_total_threads = $self->count({ forum_id_fk => $fid, post_root_id => 0 }); $self->select_options('ORDER BY post_time DESC', 'LIMIT 1'); my ($last_post, $last_poster, $last_time) = $self->select(qw(post_id post_username post_time) => { forum_id_fk => $fid })->fetchrow; $DB->table('Forum')->update({ forum_last => $last_time, forum_last_id => $last_post, forum_last_poster => $last_poster, forum_total => $forum_total, forum_total_threads => $forum_total_threads }, { forum_id => $fid }); } return $ret; } # $DB->table('Post')->attach(...) is meant to be used by plugins to provide an # easy method to attach files to posts. Usage: # $Post->attach($post_id, $file); # $post_id is the post the file should be attached to, and $file is the full # path and filename to the file. # For example: # $Post->attach(23, "/home/jagerman/test.txt") will make create an attachment named # "test.txt" and associate that attachment with the post with post_id = 23. # # NO CHECKING WILL BE DONE! This includes attachment filters and forums that # do not allow attachments, so you should do those checks yourself (unless you # want to attach regardless). # # Returns 1 on success, undef on failure. The failure message can be retrieved # via $self->{attachment_error} or, if that is not defined, $self->error. sub attach { my ($self, $post, $file) = @_; return $self->error(BADARGS => FATAL => 'Usage: $DB->table(\'Post\')->attach($post_id, $filename)') if not $post or not $file or not -f $file; delete $self->{attachment_error}; my $file_size = -s $file; my ($filename, $ext) = $file =~ m{(?:^|[\\/])([^\\/]+?(\.[^\\/.]+)?)$}; require GForum::ContentType; my $content_type = $GForum::ContentType::CONTENT_TYPE{$ext} || 'application/octet-stream'; local $CFG->{attachment_filters}; my $fh = \do { local *FH; *FH }; open $fh, $file; my $sth = $DB->table('TempAttachment')->insert(Post => { forum_id => -1, # -1 bypasses the forum checks for whether or not attachments are allowed tempatt_filename => $filename, tempatt_content => $content_type, tempatt_size => $file_size, tempatt_fh => $fh }) or return; close $fh; my $temp_id = $sth->insert_id or return; $DB->table('PostAttachment')->insert({ tempatt_id => $temp_id, post_id_fk => $post }) or return; return 1; } # Deleting a post in a pain; all ancestors of the post deleted have to have # their post_latest_reply and post_latest_poster updated. sub rebuild_latest { my $self = shift; my @pids = @_; my %posts; my $rel = $DB->table('Post' => 'User'); my $sth = $rel->select(left_join => { post_id => \@pids }); while (my $post = $sth->fetchrow_hashref) { $posts{$post->{post_id}} = $post; } for my $post_id (@pids) { my $post = $posts{$post_id}; my ($post_latest_reply, $post_latest_poster) = (2000000000 - $post->{post_time}, $post->{user_username} || $post->{post_username}); # Default them back to the values of this post my @children = @{$self->tree->child_ids(id => $post_id)}; if (@children) { $rel->select_options("ORDER BY post_time DESC", "LIMIT 1"); my $sth = $rel->select(left_join => 'user_username', 'post_username', 'post_time' => { post_id => \@children }); if (my $row = $sth->fetchrow_arrayref) { $post_latest_poster = $row->[0] || $row->[1]; $post_latest_reply = 2_000_000_000 - $row->[2]; } } $self->update({ post_latest_poster => $post_latest_poster, post_latest_reply => $post_latest_reply }, { post_id => $post_id }); } } sub delete { my $self = shift; my @args = @_; GT::Plugins->dispatch($CFG->{admin_root_path} . '/Plugins/GForum', 'delete_post', sub { return $self->_plg_delete(@args) }); } # Post deletes are very complex - the fathers have to be updated, the roots have to be updated, # and the forums have to be updated. There's only one word to describe this: Uhg. sub _plg_delete { my $self = shift; my $tree = $self->tree; my $Forum = $DB->table('Forum'); if (@_ == 1 and not ref $_[0]) { # It's just a single post ID my (@children, $father, @ancestors); my $post_id = shift; my $post = $self->get($post_id, 'HASH', ['post_root_id', 'post_father_id', 'forum_id_fk']); @ancestors = @{$tree->parent_ids(id => $post_id)}; @children = @{$tree->child_ids (id => $post_id)}; unshift @children, $post_id; my $num_posts = @children; my $return = $self->SUPER::delete({ post_id => \@children }) or return; my $forum_id = $post->{forum_id_fk}; $Forum->update({ forum_total => \"forum_total - $num_posts", ($post->{post_root_id} ? () : (forum_total_threads => \'forum_total_threads - 1')) }, { forum_id => $forum_id }); # Update the ancestors $self->update({ post_replies => \'post_replies - 1' } => { post_id => \@ancestors }); # Update the latest poster and post time for each parent affected by the delete $self->rebuild_latest(@ancestors); $Forum->rebuild_latest($forum_id); return $return; } else { my ($where) = GT::SQL::Table::_extract_where(grep ! /^(?:abort|cascade|ignore|cleanup)$/, @_); my $sth = $self->select(('post_id', 'forum_id_fk', 'post_root_id', 'post_father_id') => $where); my (@posts, %forums, %roots, %forum_roots, %fathers, %ancestors, @check); while (my $post = $sth->fetchrow_arrayref) { push @posts, $post->[0]; $forums{$post->[1]}++; $roots{$post->[2]}++ if $post->[2]; $forum_roots{$post->[1]}++ if not $post->[2]; push @check, $post->[0]; } return "0e0" if not @posts; my $check = $tree->parent_ids(id => \@check); for (map { @{$check->{$_}} } keys %$check) { $ancestors{$_}++; } my $children = $tree->children(id => \@posts, cols => ['post_id', 'forum_id_fk']); for my $id (keys %$children) { for (@{$children->{$id}}) { push @posts, $_->{post_id}; $forums{$_->{forum_id_fk}}++; } } # delete the posts my $return = $self->SUPER::delete({ post_id => \@posts }, grep /^(?:abort|cascade|ignore|cleanup)$/, @_) or return $self->error($GT::SQL::error); my %decrement = reverse %ancestors; for (keys %ancestors) { if (ref $decrement{$ancestors{$_}}) { push @{$decrement{$ancestors{$_}}}, $_; } else { $decrement{$ancestors{$_}} = [$_]; } } for (keys %decrement) { $self->update({ post_replies => \"post_replies - $_" }, { post_id => $decrement{$_} }); } for (keys %forums) { my $update = { forum_total => \"forum_total - $forums{$_}" }; $update->{forum_total_threads} = \"forum_total_threads - $forum_roots{$_}" if $forum_roots{$_}; $Forum->update($update, { forum_id => $_ }) or $self->error($GT::SQL::error, 'WARN'); } $self->rebuild_latest(@posts); $Forum->rebuild_latest(keys %forums); return $return; } } 1;