#!/usr/bin/perl
#
#
# link: htdocs/userinfo.bml, htdocs/go.bml, htdocs/tools/memadd.bml, htdocs/editjournal.bml
# link: htdocs/tools/tellafriend.bml
# img: htdocs/img/btn_prev.gif, htdocs/img/memadd.gif, htdocs/img/btn_edit.gif
# img: htdocs/img/btn_next.gif, htdocs/img/btn_tellafriend.gif
#
use strict;
package LJ::Talk;
sub get_subjecticons
{
my %subjecticon;
$subjecticon{'types'} = [ 'sm', 'md' ];
$subjecticon{'lists'}->{'md'} = [
{ img => "md01_alien.gif", w => 32, h => 32 },
{ img => "md02_skull.gif", w => 32, h => 32 },
{ img => "md05_sick.gif", w => 25, h => 25 },
{ img => "md06_radioactive.gif", w => 20, h => 20 },
{ img => "md07_cool.gif", w => 20, h => 20 },
{ img => "md08_bulb.gif", w => 17, h => 23 },
{ img => "md09_thumbdown.gif", w => 25, h => 19 },
{ img => "md10_thumbup.gif", w => 25, h => 19 }
];
$subjecticon{'lists'}->{'sm'} = [
{ img => "sm01_smiley.gif", w => 15, h => 15 },
{ img => "sm02_wink.gif", w => 15, h => 15 },
{ img => "sm03_blush.gif", w => 15, h => 15 },
{ img => "sm04_shock.gif", w => 15, h => 15 },
{ img => "sm05_sad.gif", w => 15, h => 15 },
{ img => "sm06_angry.gif", w => 15, h => 15 },
{ img => "sm07_check.gif", w => 15, h => 15 },
{ img => "sm08_star.gif", w => 20, h => 18 },
{ img => "sm09_mail.gif", w => 14, h => 10 },
{ img => "sm10_eyes.gif", w => 24, h => 12 }
];
# assemble ->{'id'} portion of hash. the part of the imagename before the _
foreach (keys %{$subjecticon{'lists'}}) {
foreach my $pic (@{$subjecticon{'lists'}->{$_}}) {
next unless ($pic->{'img'} =~ /^(\D{2}\d{2})\_.+$/);
$subjecticon{'pic'}->{$1} = $pic;
$pic->{'id'} = $1;
}
}
return \%subjecticon;
}
# entryid-commentid-emailrecipientpassword hash
sub ecphash {
my ($itemid, $talkid, $password) = @_;
return "ecph-" . Digest::MD5::md5_hex($itemid . $talkid . $password);
}
# Returns talkurl with GET args added (don't pass #anchors to this :-)
sub talkargs {
my $talkurl = shift;
my $args = join("&", grep {$_} @_);
my $sep;
$sep = ($talkurl =~ /\?/ ? "&" : "?") if $args;
return "$talkurl$sep$args";
}
# Returns HTML to display an image, given the image id as an argument.
sub show_image
{
my $pics = shift;
my $id = shift;
my $extra = shift;
return unless defined $pics->{'pic'}->{$id};
my $p = $pics->{'pic'}->{$id};
my $pfx = "$LJ::IMGPREFIX/talk";
return "{'w'}' height='$p->{'h'}' valign='middle' $extra />";
}
# Returns 'none' icon.
sub show_none_image
{
my $extra = shift;
my $img = 'none.gif';
my $w = 15;
my $h = 15;
my $pfx = "$LJ::IMGPREFIX/talk";
return "";
}
sub link_bar
{
my $opts = shift;
my ($u, $up, $remote, $headref, $itemid) =
map { $opts->{$_} } qw(u up remote headref itemid);
my $ret;
my @linkele;
my $mlink = sub {
my ($url, $piccode) = @_;
return ("" .
LJ::img($piccode, "", { 'align' => 'absmiddle' }) .
"");
};
my $jarg = "journal=$u->{'user'}&";
my $jargent = "journal=$u->{'user'}&";
# << Previous
push @linkele, $mlink->("/go.bml?${jargent}itemid=$itemid&dir=prev", "prev_entry");
$$headref .= "\n";
# memories
unless ($LJ::DISABLED{'memories'}) {
push @linkele, $mlink->("/tools/memadd.bml?${jargent}itemid=$itemid", "memadd");
}
if (defined $remote && ($remote->{'user'} eq $u->{'user'} ||
$remote->{'user'} eq $up->{'user'} ||
LJ::can_manage($remote, $u)))
{
push @linkele, $mlink->("/editjournal.bml?${jargent}itemid=$itemid", "editentry");
}
unless ($LJ::DISABLED{tags}) {
if (defined $remote && LJ::Tags::can_add_tags($u, $remote)) {
push @linkele, $mlink->("/edittags.bml?${jargent}itemid=$itemid", "edittags");
}
}
unless ($LJ::DISABLED{'tellafriend'}) {
push @linkele, $mlink->("/tools/tellafriend.bml?${jargent}itemid=$itemid", "tellfriend");
}
## >>> Next
push @linkele, $mlink->("/go.bml?${jargent}itemid=$itemid&dir=next", "next_entry");
$$headref .= "\n";
if (@linkele) {
$ret .= BML::fill_template("standout", {
'DATA' => "
" .
join(" ", @linkele) .
"
",
});
}
return $ret;
}
sub init
{
my ($form) = @_;
my $init = {}; # structure to return
my $journal = $form->{'journal'};
my $ju = undef;
my $item = undef; # hashref; journal item conversation is in
# defaults, to be changed later:
$init->{'itemid'} = $form->{'itemid'}+0;
$init->{'ditemid'} = $init->{'itemid'};
$init->{'thread'} = $form->{'thread'}+0;
$init->{'dthread'} = $init->{'thread'};
$init->{'clustered'} = 0;
$init->{'replyto'} = $form->{'replyto'}+0;
$init->{'style'} = $form->{'style'} ? "mine" : undef;
if ($journal) {
# they specified a journal argument, which indicates new style.
$ju = LJ::load_user($journal);
return { 'error' => BML::ml('talk.error.nosuchjournal')} unless $ju;
return { 'error' => BML::ml('talk.error.bogusargs')} unless $ju->{'clusterid'};
$init->{'clustered'} = 1;
foreach (qw(itemid replyto)) {
next unless $init->{$_};
$init->{'anum'} = $init->{$_} % 256;
$init->{$_} = int($init->{$_} / 256);
last;
}
$init->{'thread'} = int($init->{'thread'} / 256)
if $init->{'thread'};
} else {
# perhaps it's an old URL for a user that's since been clustered.
# look up the itemid and see what user it belongs to.
if ($form->{'itemid'}) {
my $itemid = $form->{'itemid'}+0;
my $newinfo = LJ::get_newids('L', $itemid);
if ($newinfo) {
$ju = LJ::load_userid($newinfo->[0]);
return { 'error' => BML::ml('talk.error.nosuchjournal')} unless $ju;
$init->{'clustered'} = 1;
$init->{'itemid'} = $newinfo->[1];
$init->{'oldurl'} = 1;
if ($form->{'thread'}) {
my $tinfo = LJ::get_newids('T', $init->{'thread'});
$init->{'thread'} = $tinfo->[1] if $tinfo;
}
} else {
return { 'error' => BML::ml('talk.error.noentry') };
}
} elsif ($form->{'replyto'}) {
my $replyto = $form->{'replyto'}+0;
my $newinfo = LJ::get_newids('T', $replyto);
if ($newinfo) {
$ju = LJ::load_userid($newinfo->[0]);
return { 'error' => BML::ml('talk.error.nosuchjournal')} unless $ju;
$init->{'replyto'} = $newinfo->[1];
$init->{'oldurl'} = 1;
} else {
return { 'error' => BML::ml('talk.error.noentry') };
}
}
}
$init->{'journalu'} = $ju;
return $init;
}
# $u, $itemid
sub get_journal_item
{
my ($u, $itemid) = @_;
return unless $u && $itemid;
my $uid = $u->{'userid'}+0;
$itemid += 0;
my $item = LJ::get_log2_row($u, $itemid);
return undef unless $item;
$item->{'alldatepart'} = LJ::alldatepart_s2($item->{'eventtime'});
$item->{'itemid'} = $item->{'jitemid'}; # support old & new keys
$item->{'ownerid'} = $item->{'journalid'}; # support old & news keys
my $lt = LJ::get_logtext2($u, $itemid);
my $v = $lt->{$itemid};
$item->{'subject'} = $v->[0];
$item->{'event'} = $v->[1];
### load the log properties
my %logprops = ();
LJ::load_log_props2($u->{'userid'}, [ $itemid ], \%logprops);
$item->{'props'} = $logprops{$itemid} || {};
if ($LJ::UNICODE && $logprops{$itemid}->{'unknown8bit'}) {
LJ::item_toutf8($u, \$item->{'subject'}, \$item->{'event'},
$item->{'logprops'}->{$itemid});
}
return $item;
}
sub check_viewable
{
my ($remote, $item, $form, $errref) = @_;
# note $form no longer used
my $err = sub {
$$errref = " h1?>";
return 0;
};
unless (LJ::can_view($remote, $item)) {
return $err->(BML::ml('talk.error.mustlogin'))
unless defined $remote;
return $err->(BML::ml('talk.error.notauthorised'));
}
return 1;
}
#
# name: LJ::Talk::can_delete
# des: Determines if a user can delete a comment or entry. Basically, you can
# delete anything you've posted. You can delete anything posted in something
# you own (i.e. a comment in your journal, a comment to an entry you made in
# a community). You can also delete any item in an account you have the
# "A"dministration edge for.
# args: remote, u, up, userpost
# des-remote: User object we're checking access of. From LJ::get_remote.
# des-u: Username or object of the account the thing is located in.
# des-up: Username or object of person who owns the parent of the thing. (I.e. the poster
# of the entry a comment is in.)
# des-userpost: Username (NOT object) of person who posted the item.
# returns: Boolean indicating whether remote is allowed to delete the thing
# specified by the other options.
#
sub can_delete {
my ($remote, $u, $up, $userpost) = @_; # remote, journal, posting user, commenting user
return 0 unless $remote;
return 1 if $remote->{'user'} eq $userpost ||
$remote->{'user'} eq (ref $u ? $u->{'user'} : $u) ||
$remote->{'user'} eq (ref $up ? $up->{'user'} : $up) ||
LJ::can_manage($remote, $u);
return 0;
}
sub can_screen {
my ($remote, $u, $up, $userpost) = @_;
return 0 unless $remote;
return 1 if $remote->{'user'} eq $u->{'user'} ||
$remote->{'user'} eq (ref $up ? $up->{'user'} : $up) ||
LJ::can_manage($remote, $u);
return 0;
}
sub can_unscreen {
return LJ::Talk::can_screen(@_);
}
sub can_view_screened {
return LJ::Talk::can_delete(@_);
}
sub can_freeze {
return LJ::Talk::can_screen(@_);
}
sub can_unfreeze {
return LJ::Talk::can_unscreen(@_);
}
#
# name: LJ::Talk::screening_level
# des: Determines the screening level of a particular post given the relevent information.
# args: journalu, jitemid
# des-journalu: User object of the journal the post is in.
# des-jitemid: Itemid of the post.
# returns: Single character that indicates the screening level. Undef means don't screen
# anything, 'A' means screen All, 'R' means screen Anonymous (no-remotes), 'F' means
# screen non-friends.
#
sub screening_level {
my ($journalu, $jitemid) = @_;
die 'LJ::screening_level needs a user object.' unless ref $journalu;
$jitemid += 0;
die 'LJ::screening_level passed invalid jitemid.' unless $jitemid;
# load the logprops for this entry
my %props;
LJ::load_log_props2($journalu->{userid}, [ $jitemid ], \%props);
# determine if userprop was overriden
my $val = $props{$jitemid}{opt_screening};
return if $val eq 'N'; # N means None, so return undef
return $val if $val;
# now return userprop, as it's our last chance
LJ::load_user_props($journalu, 'opt_whoscreened');
return if $journalu->{opt_whoscreened} eq 'N';
return $journalu->{opt_whoscreened};
}
sub update_commentalter {
my ($u, $itemid) = @_;
LJ::set_logprop($u, $itemid, { 'commentalter' => time() });
}
#
# name: LJ::Talk::get_comments_in_thread
# class: web
# des: Gets a list of comment ids that are contained within a thread, including the
# comment at the top of the thread. You can also limit this to only return comments
# of a certain state.
# args: u, jitemid, jtalkid, onlystate, screenedref
# des-u: user object of user to get comments from
# des-jitemid: journal itemid to get comments from
# des-jtalkid: journal talkid of comment to use as top of tree
# des-onlystate: if specified, return only comments of this state (e.g. A, F, S...)
# des-screenedref: if provided and an array reference, will push on a list of comment
# ids that are being returned and are screened (mostly for use in deletion so you can
# unscreen the comments)
# returns: undef on error, array reference of jtalkids on success
#
sub get_comments_in_thread {
my ($u, $jitemid, $jtalkid, $onlystate, $screened_ref) = @_;
$u = LJ::want_user($u);
$jitemid += 0;
$jtalkid += 0;
$onlystate = uc $onlystate;
return undef unless $u && $jitemid && $jtalkid &&
(!$onlystate || $onlystate =~ /^\w$/);
# get all comments to post
my $comments = LJ::Talk::get_talk_data($u, 'L', $jitemid) || {};
# see if our comment exists
return undef unless $comments->{$jtalkid};
# create relationship hashref and count screened comments in post
my %parentids;
$parentids{$_} = $comments->{$_}{parenttalkid} foreach keys %$comments;
# now walk and find what to update
my %to_act;
foreach my $id (keys %$comments) {
my $act = ($id == $jtalkid);
my $walk = $id;
while ($parentids{$walk}) {
if ($parentids{$walk} == $jtalkid) {
# we hit the one we want to act on
$act = 1;
last;
}
last if $parentids{$walk} == $walk;
# no match, so move up a level
$walk = $parentids{$walk};
}
# set it as being acted on
$to_act{$id} = 1 if $act && (!$onlystate || $comments->{$id}{state} eq $onlystate);
# push it onto the list of screened comments? (if the caller is doing a delete, they need
# a list of screened comments in order to unscreen them)
push @$screened_ref, $id if ref $screened_ref && # if they gave us a ref
$to_act{$id} && # and we're acting on this comment
$comments->{$id}{state} eq 'S'; # and this is a screened comment
}
# return list from %to_act
return [ keys %to_act ];
}
#
# name: LJ::Talk::delete_thread
# class: web
# des: Deletes an entire thread of comments.
# args: u, jitemid, jtalkid
# des-u: Userid or user object to delete thread from.
# des-jitemid: Journal itemid of item to delete comments from.
# des-jtalkid: Journal talkid of comment at top of thread to delete.
# returns: 1 on success; undef on error
#
sub delete_thread {
my ($u, $jitemid, $jtalkid) = @_;
# get comments and delete 'em
my @screened;
my $ids = LJ::Talk::get_comments_in_thread($u, $jitemid, $jtalkid, undef, \@screened);
LJ::Talk::unscreen_comment($u, $jitemid, @screened) if @screened; # if needed only!
my $num = LJ::delete_comments($u, "L", $jitemid, @$ids);
LJ::replycount_do($u, $jitemid, "decr", $num);
LJ::Talk::update_commentalter($u, $jitemid);
return 1;
}
#
# name: LJ::Talk::freeze_thread
# class: web
# des: Freezes an entire thread of comments.
# args: u, jitemid, jtalkid
# des-u: Userid or user object to freeze thread from.
# des-jitemid: Journal itemid of item to freeze comments from.
# des-jtalkid: Journal talkid of comment at top of thread to freeze.
# returns: 1 on success; undef on error
#
sub freeze_thread {
my ($u, $jitemid, $jtalkid) = @_;
# now we need to update the states
my $ids = LJ::Talk::get_comments_in_thread($u, $jitemid, $jtalkid, 'A');
LJ::Talk::freeze_comments($u, "L", $jitemid, 0, $ids);
return 1;
}
#
# name: LJ::Talk::unfreeze_thread
# class: web
# des: unfreezes an entire thread of comments.
# args: u, jitemid, jtalkid
# des-u: Userid or user object to unfreeze thread from.
# des-jitemid: Journal itemid of item to unfreeze comments from.
# des-jtalkid: Journal talkid of comment at top of thread to unfreeze.
# returns: 1 on success; undef on error
#
sub unfreeze_thread {
my ($u, $jitemid, $jtalkid) = @_;
# now we need to update the states
my $ids = LJ::Talk::get_comments_in_thread($u, $jitemid, $jtalkid, 'F');
LJ::Talk::freeze_comments($u, "L", $jitemid, 1, $ids);
return 1;
}
#
# name: LJ::Talk::freeze_comments
# class: web
# des: Freezes comments. This is the internal helper function called by
# freeze_thread/unfreeze_thread. Use those if you wish to freeze or
# unfreeze a thread. This function just freezes specific comments.
# args: u, nodetype, nodeid, unfreeze, ids
# des-u: Userid or object of user to manipulate comments in.
# des-nodetype: Nodetype of the thing containing the specified ids. Typically "L".
# des-nodeid: Id of the node to manipulate comments from.
# des-unfreeze: If 1, unfreeze instead of freeze.
# des-ids: Array reference containing jtalkids to manipulate.
# returns: 1 on success; undef on error
#
sub freeze_comments {
my ($u, $nodetype, $nodeid, $unfreeze, $ids) = @_;
$u = LJ::want_user($u);
$nodeid += 0;
$unfreeze = $unfreeze ? 1 : 0;
return undef unless LJ::isu($u) && $nodetype =~ /^\w$/ && $nodeid && @$ids;
# get database and quote things
return undef unless $u->writer;
my $quserid = $u->{userid}+0;
my $qnodetype = $u->quote($nodetype);
my $qnodeid = $nodeid+0;
# now perform action
my $in = join(',', map { $_+0 } @$ids);
my $newstate = $unfreeze ? 'A' : 'F';
my $res = $u->talk2_do($nodetype, $nodeid, undef,
"UPDATE talk2 SET state = '$newstate' " .
"WHERE journalid = $quserid AND nodetype = $qnodetype " .
"AND nodeid = $qnodeid AND jtalkid IN ($in)");
return undef unless $res;
return 1;
}
sub screen_comment {
my $u = shift;
return undef unless LJ::isu($u);
my $itemid = shift(@_) + 0;
my $in = join (',', map { $_+0 } @_);
return unless $in;
my $userid = $u->{'userid'} + 0;
my $updated = $u->talk2_do("L", $itemid, undef,
"UPDATE talk2 SET state='S' ".
"WHERE journalid=$userid AND jtalkid IN ($in) ".
"AND nodetype='L' AND nodeid=$itemid ".
"AND state NOT IN ('S','D')");
return undef unless $updated;
if ($updated > 0) {
LJ::replycount_do($u, $itemid, "decr", $updated);
LJ::set_logprop($u, $itemid, { 'hasscreened' => 1 });
}
LJ::Talk::update_commentalter($u, $itemid);
return;
}
sub unscreen_comment {
my $u = shift;
return undef unless LJ::isu($u);
my $itemid = shift(@_) + 0;
my $in = join (',', map { $_+0 } @_);
return unless $in;
my $userid = $u->{'userid'} + 0;
my $prop = LJ::get_prop("log", "hasscreened");
my $updated = $u->talk2_do("L", $itemid, undef,
"UPDATE talk2 SET state='A' ".
"WHERE journalid=$userid AND jtalkid IN ($in) ".
"AND nodetype='L' AND nodeid=$itemid ".
"AND state='S'");
return undef unless $updated;
if ($updated > 0) {
LJ::replycount_do($u, $itemid, "incr", $updated);
my $dbcm = LJ::get_cluster_master($u);
my $hasscreened = $dbcm->selectrow_array("SELECT COUNT(*) FROM talk2 " .
"WHERE journalid=$userid AND nodeid=$itemid AND nodetype='L' AND state='S'");
LJ::set_logprop($u, $itemid, { 'hasscreened' => 0 }) unless $hasscreened;
}
LJ::Talk::update_commentalter($u, $itemid);
return;
}
# retrieves data from the talk2 table (but preferrably memcache)
# returns a hashref (key -> { 'talkid', 'posterid', 'datepost',
# 'parenttalkid', 'state' } , or undef on failure
sub get_talk_data
{
my ($u, $nodetype, $nodeid) = @_;
return undef unless LJ::isu($u);
return undef unless $nodetype =~ /^\w$/;
return undef unless $nodeid =~ /^\d+$/;
my $ret = {};
# check for data in memcache
my $DATAVER = "1"; # single character
my $memkey = [$u->{'userid'}, "talk2:$u->{'userid'}:$nodetype:$nodeid"];
my $lockkey = $memkey->[1];
my $packed = LJ::MemCache::get($memkey);
# we check the replycount in memcache, the value we count, and then fix it up
# if it seems necessary.
my $rp_memkey = $nodetype eq "L" ? [$u->{'userid'}, "rp:$u->{'userid'}:$nodeid"] : undef;
my $rp_count = $rp_memkey ? LJ::MemCache::get($rp_memkey) : 0;
my $rp_ourcount = 0;
my $fixup_rp = sub {
return unless $nodetype eq "L";
return if $rp_count == $rp_ourcount;
return unless @LJ::MEMCACHE_SERVERS;
return unless $u->writer;
# attempt to get a database lock to make sure that nobody else is in this section
# at the same time we are
my $db_key = "rp:fix:$u->{userid}:$nodetype:$nodeid";
my $got_lock = $u->selectrow_array("SELECT GET_LOCK(?, 1)", undef, $db_key);
return unless $got_lock;
# setup an unlock handler
my $unlock = sub {
$u->do("SELECT RELEASE_LOCK(?)", undef, $db_key);
return undef;
};
# check memcache to see if someone has previously fixed this entry in this journal
# with this reply count
my $fix_key = "rp_fixed:$u->{userid}:$nodetype:$nodeid:$rp_count";
my $was_fixed = LJ::MemCache::get($fix_key);
return $unlock->() if $was_fixed;
# if we're doing innodb, begin a transaction, else lock tables
my $sharedmode = "";
if ($LJ::INNODB_DB{$u->{clusterid}}) {
$sharedmode = "LOCK IN SHARE MODE";
$u->begin_work;
} else {
$u->do("LOCK TABLES log2 WRITE, talk2 READ");
}
# get count and then update. this should be totally safe because we've either
# locked the tables or we're in a transaction.
my $ct = $u->selectrow_array("SELECT COUNT(*) FROM talk2 WHERE ".
"journalid=? AND nodetype='L' AND nodeid=? ".
"AND state IN ('A','F') $sharedmode",
undef, $u->{'userid'}, $nodeid);
$u->do("UPDATE log2 SET replycount=? WHERE journalid=? AND jitemid=?",
undef, int($ct), $u->{'userid'}, $nodeid);
print STDERR "Fixing replycount for $u->{'userid'}/$nodeid from $rp_count to $ct\n"
if $LJ::DEBUG{'replycount_fix'};
# now, commit or unlock as appropriate
if ($LJ::INNODB_DB{$u->{clusterid}}) {
$u->commit;
} else {
$u->do("UNLOCK TABLES");
}
# mark it as fixed in memcache, so we don't do this again
LJ::MemCache::add($fix_key, 1, 60);
$unlock->();
LJ::MemCache::delete($rp_memkey);
};
my $memcache_good = sub {
return $packed && substr($packed,0,1) eq $DATAVER &&
length($packed) % 16 == 1;
};
my $memcache_decode = sub {
my $n = (length($packed) - 1) / 16;
for (my $i=0; $i<$n; $i++) {
my ($f1, $par, $poster, $time) = unpack("NNNN",substr($packed,$i*16+1,16));
my $state = chr($f1 & 255);
my $talkid = $f1 >> 8;
$ret->{$talkid} = {
talkid => $talkid,
state => $state,
posterid => $poster,
datepost => LJ::mysql_time($time),
parenttalkid => $par,
};
# comments are counted if they're 'A'pproved or 'F'rozen
$rp_ourcount++ if $state eq "A" || $state eq "F";
}
$fixup_rp->();
return $ret;
};
return $memcache_decode->() if $memcache_good->();
my $dbcr = LJ::get_cluster_def_reader($u);
return undef unless $dbcr;
my $lock = $dbcr->selectrow_array("SELECT GET_LOCK(?,10)", undef, $lockkey);
return undef unless $lock;
# it's quite likely (for a popular post) that the memcache was
# already populated while we were waiting for the lock
$packed = LJ::MemCache::get($memkey);
if ($memcache_good->()) {
$dbcr->selectrow_array("SELECT RELEASE_LOCK(?)", undef, $lockkey);
$memcache_decode->();
return $ret;
}
my $memval = $DATAVER;
my $sth = $dbcr->prepare("SELECT t.jtalkid AS 'talkid', t.posterid, ".
"t.datepost, t.parenttalkid, t.state ".
"FROM talk2 t ".
"WHERE t.journalid=? AND t.nodetype=? AND t.nodeid=?");
$sth->execute($u->{'userid'}, $nodetype, $nodeid);
die $dbcr->errstr if $dbcr->err;
while (my $r = $sth->fetchrow_hashref) {
$ret->{$r->{'talkid'}} = $r;
$memval .= pack("NNNN",
($r->{'talkid'} << 8) + ord($r->{'state'}),
$r->{'parenttalkid'},
$r->{'posterid'},
LJ::mysqldate_to_time($r->{'datepost'}));
$rp_ourcount++ if $r->{'state'} eq "A";
}
LJ::MemCache::set($memkey, $memval);
$dbcr->selectrow_array("SELECT RELEASE_LOCK(?)", undef, $lockkey);
$fixup_rp->();
return $ret;
}
# LJ::Talk::load_comments($u, $remote, $nodetype, $nodeid, $opts)
#
# nodetype: "L" (for log) ... nothing else has been used
# noteid: the jitemid for log.
# opts keys:
# thread -- jtalkid to thread from ($init->{'thread'} or $GET{'thread'} >> 8)
# page -- $GET{'page'}
# view -- $GET{'view'} (picks page containing view's ditemid)
# up -- [optional] hashref of user object who posted the thing being replied to
# only used to make things visible which would otherwise be screened?
# out_error -- set by us if there's an error code:
# nodb: database unavailable
# noposts: no posts to load
# out_pages: number of pages
# out_page: page number being viewed
# out_itemfirst: first comment number on page (1-based, not db numbers)
# out_itemlast: last comment number on page (1-based, not db numbers)
# out_pagesize: size of each page
# out_items: number of total top level items
#
# userpicref -- hashref to load userpics into, or undef to
# not load them.
# userref -- hashref to load users into, keyed by userid
#
# returns:
# array of hashrefs containing keys:
# - talkid (jtalkid)
# - posterid (or zero for anon)
# - userpost (string, or blank if anon)
# - upost ($u object, or undef if anon)
# - datepost (mysql format)
# - parenttalkid (or zero for top-level)
# - state ("A"=approved, "S"=screened, "D"=deleted stub)
# - userpic number
# - picid (if userpicref AND userref were given)
# - subject
# - body
# - props => { propname => value, ... }
# - children => [ hashrefs like these ]
# - _loaded => 1 (if fully loaded, subject & body)
# unknown items will never be _loaded
# - _show => {0|1}, if item is to be ideally shown (0 if deleted or screened)
sub load_comments
{
my ($u, $remote, $nodetype, $nodeid, $opts) = @_;
my $n = $u->{'clusterid'};
my $viewall = $opts->{viewall};
my $posts = get_talk_data($u, $nodetype, $nodeid); # hashref, talkid -> talk2 row, or undef
unless ($posts) {
$opts->{'out_error'} = "nodb";
return;
}
my %users_to_load; # userid -> 1
my @posts_to_load; # talkid scalars
my %children; # talkid -> [ childenids+ ]
my $uposterid = $opts->{'up'} ? $opts->{'up'}->{'userid'} : 0;
my $post_count = 0;
{
my %showable_children; # $id -> $count
foreach my $post (sort { $b->{'talkid'} <=> $a->{'talkid'} } values %$posts) {
# see if we should ideally show it or not. even if it's
# zero, we'll still show it if it has any children (but we won't show content)
my $should_show = $post->{'state'} eq 'D' ? 0 : 1;
unless ($viewall) {
$should_show = 0 if
$post->{'state'} eq "S" && ! ($remote && ($remote->{'userid'} == $u->{'userid'} ||
$remote->{'userid'} == $uposterid ||
$remote->{'userid'} == $post->{'posterid'} ||
LJ::can_manage($remote, $u) ));
}
$post->{'_show'} = $should_show;
$post_count += $should_show;
# make any post top-level if it says it has a parent but it isn't
# loaded yet which means either a) row in database is gone, or b)
# somebody maliciously/accidentally made their parent be a future
# post, which could result in an infinite loop, which we don't want.
$post->{'parenttalkid'} = 0
if $post->{'parenttalkid'} && ! $posts->{$post->{'parenttalkid'}};
$post->{'children'} = [ map { $posts->{$_} } @{$children{$post->{'talkid'}} || []} ];
# increment the parent post's number of showable children,
# which is our showability plus all those of our children
# which were already computed, since we're working new to old
# and children are always newer.
# then, if we or our children are showable, add us to the child list
my $sum = $should_show + $showable_children{$post->{'talkid'}};
if ($sum) {
$showable_children{$post->{'parenttalkid'}} += $sum;
unshift @{$children{$post->{'parenttalkid'}}}, $post->{'talkid'};
}
}
}
# with a wrong thread number, silently default to the whole page
my $thread = $opts->{'thread'}+0;
$thread = 0 unless $posts->{$thread};
unless ($thread || $children{$thread}) {
$opts->{'out_error'} = "noposts";
return;
}
my $page_size = $LJ::TALK_PAGE_SIZE || 25;
my $max_subjects = $LJ::TALK_MAX_SUBJECTS || 200;
my $threading_point = $LJ::TALK_THREAD_POINT || 50;
# we let the page size initially get bigger than normal for awhile,
# but if it passes threading_point, then everything's in page_size
# chunks:
$page_size = $threading_point if $post_count < $threading_point;
my $top_replies = $thread ? 1 : scalar(@{$children{$thread}});
my $pages = int($top_replies / $page_size);
if ($top_replies % $page_size) { $pages++; }
my @top_replies = $thread ? ($thread) : @{$children{$thread}};
my $page_from_view = 0;
if ($opts->{'view'} && !$opts->{'page'}) {
# find top-level comment that this comment is under
my $viewid = $opts->{'view'} >> 8;
while ($posts->{$viewid} && $posts->{$viewid}->{'parenttalkid'}) {
$viewid = $posts->{$viewid}->{'parenttalkid'};
}
for (my $ti = 0; $ti < @top_replies; ++$ti) {
if ($posts->{$top_replies[$ti]}->{'talkid'} == $viewid) {
$page_from_view = int($ti/$page_size)+1;
last;
}
}
}
my $page = int($opts->{'page'}) || $page_from_view || 1;
$page = $page < 1 ? 1 : $page > $pages ? $pages : $page;
my $itemfirst = $page_size * ($page-1) + 1;
my $itemlast = $page==$pages ? $top_replies : ($page_size * $page);
@top_replies = @top_replies[$itemfirst-1 .. $itemlast-1];
push @posts_to_load, @top_replies;
# mark child posts of the top-level to load, deeper
# and deeper until we've hit the page size. if too many loaded,
# just mark that we'll load the subjects;
my @check_for_children = @posts_to_load;
my (@subjects_to_load, @subjects_ignored);
while (@check_for_children) {
my $cfc = shift @check_for_children;
next unless defined $children{$cfc};
foreach my $child (@{$children{$cfc}}) {
if (@posts_to_load < $page_size) {
push @posts_to_load, $child;
} else {
if (@subjects_to_load < $max_subjects) {
push @subjects_to_load, $child;
} else {
push @subjects_ignored, $child;
}
}
push @check_for_children, $child;
}
}
$opts->{'out_pages'} = $pages;
$opts->{'out_page'} = $page;
$opts->{'out_itemfirst'} = $itemfirst;
$opts->{'out_itemlast'} = $itemlast;
$opts->{'out_pagesize'} = $page_size;
$opts->{'out_items'} = $top_replies;
# load text of posts
my ($posts_loaded, $subjects_loaded);
$posts_loaded = LJ::get_talktext2($u, @posts_to_load);
$subjects_loaded = LJ::get_talktext2($u, {'onlysubjects'=>1}, @subjects_to_load) if @subjects_to_load;
foreach my $talkid (@posts_to_load) {
next unless $posts->{$talkid}->{'_show'};
$posts->{$talkid}->{'_loaded'} = 1;
$posts->{$talkid}->{'subject'} = $posts_loaded->{$talkid}->[0];
$posts->{$talkid}->{'body'} = $posts_loaded->{$talkid}->[1];
$users_to_load{$posts->{$talkid}->{'posterid'}} = 1;
}
foreach my $talkid (@subjects_to_load) {
next unless $posts->{$talkid}->{'_show'};
$posts->{$talkid}->{'subject'} = $subjects_loaded->{$talkid}->[0];
$users_to_load{$posts->{$talkid}->{'posterid'}} ||= 0.5; # only care about username
}
foreach my $talkid (@subjects_ignored) {
next unless $posts->{$talkid}->{'_show'};
$posts->{$talkid}->{'subject'} = "...";
$users_to_load{$posts->{$talkid}->{'posterid'}} ||= 0.5; # only care about username
}
# load meta-data
{
my %props;
LJ::load_talk_props2($u->{'userid'}, \@posts_to_load, \%props);
foreach (keys %props) {
next unless $posts->{$_}->{'_show'};
$posts->{$_}->{'props'} = $props{$_};
}
}
if ($LJ::UNICODE) {
foreach (@posts_to_load) {
if ($posts->{$_}->{'props'}->{'unknown8bit'}) {
LJ::item_toutf8($u, \$posts->{$_}->{'subject'},
\$posts->{$_}->{'body'},
{});
}
}
}
# load users who posted
delete $users_to_load{0};
my %up = ();
if (%users_to_load) {
LJ::load_userids_multiple([ map { $_, \$up{$_} } keys %users_to_load ]);
# fill in the 'userpost' member on each post being shown
while (my ($id, $post) = each %$posts) {
my $up = $up{$post->{'posterid'}};
next unless $up;
$post->{'upost'} = $up;
$post->{'userpost'} = $up->{'user'};
}
}
# optionally give them back user refs
if (ref($opts->{'userref'}) eq "HASH") {
my %userpics = ();
# copy into their ref the users we've already loaded above.
while (my ($k, $v) = each %up) {
$opts->{'userref'}->{$k} = $v;
}
# optionally load userpics
if (ref($opts->{'userpicref'}) eq "HASH") {
my @load_pic;
foreach my $talkid (@posts_to_load) {
my $post = $posts->{$talkid};
my $kw;
if ($post->{'props'} && $post->{'props'}->{'picture_keyword'}) {
$kw = $post->{'props'}->{'picture_keyword'};
}
my $pu = $opts->{'userref'}->{$post->{'posterid'}};
my $id = LJ::get_picid_from_keyword($pu, $kw);
$post->{'picid'} = $id;
push @load_pic, [ $pu, $id ];
}
LJ::load_userpics($opts->{'userpicref'}, \@load_pic);
}
}
return map { $posts->{$_} } @top_replies;
}
sub talkform {
# Takes a hashref with the following keys / values:
# remote: optional remote u object
# journalu: prequired journal u object
# parpost: parent post object
# replyto: init->replyto
# ditemid: init->ditemid
# form: optional full form hashref
# do_captcha: optional toggle for creating a captcha challenge
# require_tos: optional toggle to include TOS requirement form
# errors: optional error arrayref
my $opts = shift;
return "Invalid talkform values." unless ref $opts eq 'HASH';
my $ret;
my ($remote, $journalu, $parpost, $form) =
map { $opts->{$_} } qw(remote journalu parpost form);
my $pics = LJ::Talk::get_subjecticons();
# early bail if the user can't be making comments yet
return $LJ::UNDERAGE_ERROR
if $remote && $remote->underage;
# once we clean out talkpost.bml, this will need to be changed.
BML::set_language_scope('/talkpost.bml');
# make sure journal isn't locked
return "Sorry, this journal is locked and comments cannot be posted to it at this time."
if $journalu->{statusvis} eq 'L';
# check max comments
my $jitemid = $opts->{'ditemid'} >> 8;
return "Sorry, this entry already has the maximum number of comments allowed."
if LJ::Talk::Post::over_maxcomments($journalu, $jitemid);
if ($parpost->{'state'} eq "S") {
$ret .= "