#!/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 ""; } # 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 .= "
$BML::ML{'.warnscreened'}
"; } $ret .= "
"; # Login challenge/response my $authchal = LJ::challenge_generate(900); # 15 minute auth token $ret .= ""; $ret .= ""; if ($opts->{errors} && @{$opts->{errors}}) { $ret .= ''; $ret .= "
"; } # hidden values my $parent = $opts->{replyto}+0; $ret .= LJ::html_hidden("replyto", $opts->{replyto}, "parenttalkid", $parent, "itemid", $opts->{ditemid}, "journal", $journalu->{'user'}); # rate limiting challenge { my ($time, $secret) = LJ::get_secret(); my $rchars = LJ::rand_chars(20); my $chal = $opts->{ditemid} . "-$journalu->{userid}-$time-$rchars"; my $res = Digest::MD5::md5_hex($secret . $chal); $ret .= LJ::html_hidden("chrp1", "$chal-$res"); } # if we know the user who is posting (error on talkpost_do POST action), # then see if we if ($opts->{require_tos}) { $ret .= LJ::tosagree_html('comment', $form->{agree_tos}, BML::ml('tos.error')); } my $oid_identity = $remote ? $remote->openid_identity : undef; # Default radio button # 4 possible scenarios: # remote - initial form load, error and redisplay # no remote - initial load, error and redisplay my $whocheck = sub { my $type = shift; my $default = " checked='checked'"; # Initial page load (no remote) return $default if $type eq 'anonymous' && ! $form->{'usertype'} && ! $remote && ! $oid_identity; # Anonymous return $default if $type eq 'anonymous' && $form->{'usertype'} eq 'anonymous'; if (LJ::OpenID::consumer_enabled()) { # OpenID return $default if $type eq 'openid' && $form->{'usertype'} eq 'openid'; return $default if $type eq 'openid_cookie' && ($form->{'usertype'} eq 'openid_cookie' || (defined $oid_identity)); } # Remote user, remote equals userpost return $default if $type eq 'remote' && ($form->{'usertype'} eq 'cookieuser' || $form->{'userpost'} eq $form->{'cookieuser'}); # Possible remote, using ljuser field if ($type eq 'ljuser') { return $default if # Remote user posting as someone else. ($form->{'userpost'} && $form->{'userpost'} ne $form->{'cookieuser'} && $form->{'usertype'} ne 'anonymous') || ($form->{'usertype'} eq 'user' && ! $form->{'userpost'}); } return; }; # from registered user or anonymous? $ret .= "\n"; $ret .= ""; $ret .= "
$BML::ML{'.opt.from'}"; $ret .= ""; # Internal for "From" options my $screening = LJ::Talk::screening_level($journalu, $opts->{ditemid} >> 8); if ($journalu->{'opt_whocanreply'} eq "all") { $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "\n"; if (LJ::OpenID::consumer_enabled()) { # OpenID!! # Logged in if (defined $oid_identity) { # Don't worry about a real href since js hides the row anyway my $other_user = ""; $ret .= ""; $ret .= "\n"; } # logged out $ret .= ""; $ret .= "\n"; # URL: [ ] Verify? [ ] my $url_def = $form->{'oidurl'} || $oid_identity if defined $oid_identity; $ret .= "\n"; } } if ($journalu->{'opt_whocanreply'} eq "reg") { $ret .= ""; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "\n"; if (LJ::OpenID::consumer_enabled()) { # OpenID - At some point we will include "trusted" $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "\n"; } } if ($journalu->{'opt_whocanreply'} eq 'friends') { $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "\n"; if (LJ::OpenID::consumer_enabled()) { # OpenID - At some point we will include "trusted" $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "\n"; } } if ($remote && !defined $oid_identity) { $ret .= ""; my $logged_in = LJ::ehtml($remote->display_name); # Don't worry about a real href since js hides the row anyway my $other_user = ""; if (LJ::is_banned($remote, $journalu)) { $ret .= ""; $ret .= ""; $ret .= ""; } else { $ret .= ""; $ret .= ""; } $ret .= "\n"; } # ( ) LiveJournal user: $ret .= ""; $ret .= "\n"; if ($remote && ! defined $oid_identity) { $ret .= ""; } # Username: [ ] Password: [ ] Login? [ ] $ret .= "\n"; # textarea for their message body $ret .= "
('anonymous') . " />"; $ret .= " " . $BML::ML{'.opt.willscreen'} if $screening; $ret .= "
('openid_cookie') . "/>"; $ret .= " "; $ret .= "" . $remote->display_name . ""; $ret .= $other_user . " "; $ret .= $BML::ML{'.opt.willscreen'} if $screening; $ret .= "
('openid') . "/>"; $ret .= " "; if (defined $LJ::HELPURL{'openid'}) { $ret .= "$BML::ML{ "; } $ret .= $BML::ML{'.opt.willscreen'} if $screening; $ret .= "
"; $ret .= "Identity URL:  "; $ret .= "
{'oiddo_login'}; $ret .= "/>
$BML::ML{'.opt.from'}( )$BML::ML{'.opt.anonymous'}$BML::ML{'.opt.noanonpost'}
( )OpenID"; if (defined $LJ::HELPURL{'openid'}) { $ret .= "$BML::ML{ "; } $ret .= "
( )$BML::ML{'.opt.anonymous'}"; $ret .= BML::ml(".opt.friendsonly", {'username'=>"$journalu->{'user'}"}); $ret .= "
( )OpenID"; if (defined $LJ::HELPURL{'openid'}) { $ret .= "$BML::ML{ "; } $ret .= "
( )" . BML::ml(".opt.loggedin", {'username'=>"$logged_in"}) . "" . BML::ml(".opt.bannedfrom", {'journal'=>$journalu->{'user'}}) . $other_user . "('remote') . " />\n"; $ret .= $other_user; $ret .= "\n"; if ($screening eq 'A' || ($screening eq 'F' && !LJ::is_friend($journalu, $remote))) { $ret .= " " . $BML::ML{'.opt.willscreen'}; } $ret .= "
('ljuser') . "/>"; $ret .= " "; $ret .= $BML::ML{'.opt.willscreenfriend'} if $screening eq 'F'; $ret .= $BML::ML{'.opt.willscreen'} if $screening eq 'A'; $ret .= "
"; my $ljuser_def = ($form->{'userpost'} ne $form->{'cookieuser'} && $form->{'usertype'} ne 'anonymous') ? BML::eall($form->{userpost}) : "$remote->{'user'}" unless $oid_identity; $ret .= "\n"; } my $basesubject = $form->{subject} || ""; if ($opts->{replyto} && !$basesubject && $parpost->{'subject'}) { $basesubject = $parpost->{'subject'}; $basesubject =~ s/^Re:\s*//i; $basesubject = "Re: $basesubject"; } # Closing internal "From" table $ret .= "
"; $ret .= "$BML::ML{'Username'}:"; $ret .= " "href='$LJ::SITEROOT/create.bml'"}); $ret .= "
"; # subject $basesubject = BML::eall($basesubject) if $basesubject; $ret .= "
$BML::ML{'.opt.subject'}\n"; # Subject Icon toggle button { my $subjicon = $form->{subjecticon} || 'none'; my $foundicon = 0; $ret .= "\n"; $ret .= "\n"; } # finish off subject line $ret .= "
$BML::ML{'.nosubjecthtml'}
\n"; $ret .= "
"; my %res; if ($remote) { LJ::do_request({ "mode" => "login", "ver" => ($LJ::UNICODE ? "1" : "0"), "user" => $remote->{'user'}, "getpickws" => 1, }, \%res, { "noauth" => 1, "userid" => $remote->{'userid'} }); } if ($res{'pickw_count'}) { $ret .= BML::ml('.label.picturetouse2', { 'aopts'=>"href='$LJ::SITEROOT/allpics.bml?user=$remote->{'user'}'"}) . " "; my @pics; for (my $i=1; $i<=$res{'pickw_count'}; $i++) { push @pics, $res{"pickw_$i"}; } @pics = sort { lc($a) cmp lc($b) } @pics; $ret .= LJ::html_select({'name' => 'prop_picture_keyword', 'selected' => $form->{'prop_picture_keyword'}, }, ("", $BML::ML{'.opt.defpic'}, map { ($_, $_) } @pics)); if (defined $LJ::HELPURL{'userpics'}) { $ret .= " $BML::ML{"; } } $ret .= "
"; $ret .= "
$BML::ML{'.opt.message'}
"; # only show on initial compostion my $quickquote; unless ($opts->{errors} && @{$opts->{errors}}) { # quick quote button $quickquote = "
" . LJ::ejs(''); } $ret .= "\n"; $ret .= "
"; $ret .= ""; # Display captcha challenge if over rate limits. if ($opts->{do_captcha}) { my ($wants_audio, $captcha_sess, $captcha_chal); $wants_audio = 1 if lc($form->{answer}) eq 'audio'; # Captcha sessions my $cid = $journalu->{clusterid}; $captcha_chal = $form->{captcha_chal} || LJ::challenge_generate(900); $captcha_sess = LJ::get_challenge_attributes($captcha_chal); my $dbcr = LJ::get_cluster_reader($journalu); my $try = 0; if ($form->{captcha_chal}) { $try = $dbcr->selectrow_array('SELECT trynum FROM captcha_session ' . 'WHERE sess=?', undef, $captcha_sess); } $ret .= '

'; # Visual challenge if (! $wants_audio && ! $form->{audio_chal}) { $ret .= "
$BML::ML{'/create.bml.captcha.desc'}
"; $ret .= ""; $ret .= "

$BML::ML{'/create.bml.captcha.answer'}"; } # Audio challenge else { $ret .= "
$BML::ML{'/create.bml.captcha.audiodesc'}
"; $ret .= "$BML::ML{'/create.bml.captcha.play'}   "; $ret .= LJ::html_hidden(audio_chal => 1); } $ret .= LJ::html_text({ name =>'answer', size =>15 }); $ret .= LJ::html_hidden(captcha_chal => $captcha_chal); $ret .= '
'; } if ($LJ::SPELLER) { $ret .= " "; } $ret .= ""; $ret .= LJ::html_check( { name => 'prop_opt_preformatted', id => 'prop_opt_preformatted', value => 1, selected => $form->{'prop_opt_preformatted'} } ); if (defined $LJ::HELPURL{'noautoformat'}) { $ret .= " $BML::ML{"; } # post and preview buttons my $limit = LJ::CMAX_COMMENT; # javascript String.length uses characters $ret .= < LOGIN if ($journalu->{'opt_logcommentips'} eq "A") { $ret .= "
$BML::ML{'.logyourip'}"; $ret .= LJ::help_icon("iplogging", " "); } if ($journalu->{'opt_logcommentips'} eq "S") { $ret .= "
$BML::ML{'.loganonip'}"; $ret .= LJ::help_icon("iplogging", " "); } $ret .= "
\n"; # Some JavaScript to help the UI out $ret .= ""; $ret .= "\n"; return $ret; } # # name: LJ::record_anon_comment_ip # class: web # des: Records the IP address of an anonymous comment # args: journalu, jtalkid, ip # des-journalu: User object of journal comment was posted in. # des-jtalkid: ID of this comment. # des-ip: IP address of the poster. # returns: 1 for success, 0 for failure # sub record_anon_comment_ip { my ($journalu, $jtalkid, $ip) = @_; $journalu = LJ::want_user($journalu); $jtalkid += 0; return 0 unless LJ::isu($journalu) && $jtalkid && $ip; $journalu->do("INSERT INTO tempanonips (reporttime, journalid, jtalkid, ip) VALUES (UNIX_TIMESTAMP(),?,?,?)", undef, $journalu->{userid}, $jtalkid, $ip); return 0 if $journalu->err; return 1; } # # name: LJ::mark_comment_as_spam # class: web # des: Copies a comment into the global spamreports table # args: journalu, jtalkid # des-journalu: User object of journal comment was posted in. # des-jtalkid: ID of this comment. # returns: 1 for success, 0 for failure # sub mark_comment_as_spam { my ($journalu, $jtalkid) = @_; $journalu = LJ::want_user($journalu); $jtalkid += 0; return 0 unless $journalu && $jtalkid; my $dbcr = LJ::get_cluster_def_reader($journalu); my $dbh = LJ::get_db_writer(); # step 1: get info we need my $row = LJ::Talk::get_talk2_row($dbcr, $journalu->{userid}, $jtalkid); my $temp = LJ::get_talktext2($journalu, $jtalkid); my ($subject, $body, $posterid) = ($temp->{$jtalkid}[0], $temp->{$jtalkid}[1], $row->{posterid}); return 0 unless $body; # step 2: get ip if anon my $ip; unless ($posterid) { $ip = $dbcr->selectrow_array('SELECT ip FROM tempanonips WHERE journalid=? AND jtalkid=?', undef, $journalu->{userid}, $jtalkid); return 0 if $dbcr->err; # we want to fail out if we have no IP address and this is anonymous, because otherwise # we have a completely useless spam report. pretend we were successful, too. return 1 unless $ip; } # step 3: insert into spamreports $dbh->do('INSERT INTO spamreports (reporttime, posttime, ip, journalid, posterid, subject, body) ' . 'VALUES (UNIX_TIMESTAMP(), UNIX_TIMESTAMP(?), ?, ?, ?, ?, ?)', undef, $row->{datepost}, $ip, $journalu->{userid}, $posterid, $subject, $body); return 0 if $dbh->err; return 1; } # # name: LJ::Talk::get_talk2_row # class: web # des: Gets a row of data from talk2. # args: dbcr, journalid, jtalkid # des-dbcr: Database handle to read from. # des-journalid: Journal id that comment is posted in. # des-jtalkid: Journal talkid of comment. # returns: Hashref of row data, or undef on error. # sub get_talk2_row { my ($dbcr, $journalid, $jtalkid) = @_; return $dbcr->selectrow_hashref('SELECT journalid, jtalkid, nodetype, nodeid, parenttalkid, ' . ' posterid, datepost, state ' . 'FROM talk2 WHERE journalid = ? AND jtalkid = ?', undef, $journalid+0, $jtalkid+0); } # get a comment count for a journal entry. sub get_replycount { my ($ju, $jitemid) = @_; $jitemid += 0; return undef unless $ju && $jitemid; my $memkey = [$ju->{'userid'}, "rp:$ju->{'userid'}:$jitemid"]; my $count = LJ::MemCache::get($memkey); return $count if $count; my $dbcr = LJ::get_cluster_def_reader($ju); return unless $dbcr; $count = $dbcr->selectrow_array("SELECT replycount FROM log2 WHERE " . "journalid=? AND jitemid=?", undef, $ju->{'userid'}, $jitemid); LJ::MemCache::add($memkey, $count); return $count; } package LJ::Talk::Post; sub format_text_mail { my ($targetu, $parent, $comment, $talkurl, $item) = @_; my $dtalkid = $comment->{talkid}*256 + $item->{anum}; $Text::Wrap::columns = 76; my $who = "Somebody"; if ($comment->{u}) { $who = "$comment->{u}{name} ($comment->{u}{user})"; } my $text = ""; if (LJ::u_equals($targetu, $comment->{u})) { if ($parent->{ispost}) { $who = "$parent->{u}{name} ($parent->{u}{user})"; $text .= "You left a comment in a post by $who. "; $text .= "The entry you replied to was:"; } else { $text .= "You left a comment in reply to another comment. "; $text .= "The comment you replied to was:"; } } elsif (LJ::u_equals($targetu, $item->{entryu})) { if ($parent->{ispost}) { $text .= "$who replied to your $LJ::SITENAMESHORT post in which you said:"; } else { $text .= "$who replied to another comment somebody left in your $LJ::SITENAMESHORT post. "; $text .= "The comment they replied to was:"; } } else { $text .= "$who replied to your $LJ::SITENAMESHORT comment in which you said:"; } $text .= "\n\n"; $text .= indent($parent->{body}, ">") . "\n\n"; $text .= (LJ::u_equals($targetu, $comment->{u}) ? 'Your' : 'Their') . " reply was:\n\n"; if ($comment->{subject}) { $text .= Text::Wrap::wrap(" Subject: ", " ", $comment->{subject}) . "\n\n"; } $text .= indent($comment->{body}); $text .= "\n\n"; my $can_unscreen = $comment->{state} eq 'S' && LJ::Talk::can_unscreen($targetu, $item->{journalu}, $item->{entryu}, $comment->{u} ? $comment->{u}{user} : undef); if ($comment->{state} eq 'S') { $text .= "This comment was screened. "; $text .= $can_unscreen ? "You must respond to it or unscreen it before others can see it.\n\n" : "Someone else must unscreen it before you can reply to it.\n\n"; } my $opts = ""; $opts .= "Options:\n\n"; $opts .= " - View the discussion:\n"; $opts .= " " . LJ::Talk::talkargs($talkurl, "thread=$dtalkid") . "\n"; $opts .= " - View all comments on the entry:\n"; $opts .= " $talkurl\n"; $opts .= " - Reply to the comment:\n"; $opts .= " " . LJ::Talk::talkargs($talkurl, "replyto=$dtalkid") . "\n"; if ($can_unscreen) { $opts .= " - Unscreen the comment:\n"; $opts .= " $LJ::SITEROOT/talkscreen.bml?mode=unscreen&journal=$item->{journalu}{user}&talkid=$dtalkid\n"; } if (LJ::Talk::can_delete($targetu, $item->{journalu}, $item->{entryu}, $comment->{u} ? $comment->{u}{user} : undef)) { $opts .= " - Delete the comment:\n"; $opts .= " $LJ::SITEROOT/delcomment.bml?journal=$item->{journalu}{user}&id=$dtalkid\n"; } my $footer = ""; $footer .= "-- $LJ::SITENAME\n\n"; $footer .= "(If you'd prefer to not get these updates, go to $LJ::SITEROOT/editinfo.bml and turn off the relevant options.)"; return Text::Wrap::wrap("", "", $text) . "\n" . $opts . "\n" . Text::Wrap::wrap("", "", $footer); } sub format_html_mail { my ($targetu, $parent, $comment, $encoding, $talkurl, $item) = @_; my $ditemid = $item->{itemid}*256 + $item->{anum}; my $dtalkid = $comment->{talkid}*256 + $item->{anum}; my $threadurl = LJ::Talk::talkargs($talkurl, "thread=$dtalkid"); my $who = "Somebody"; if ($comment->{u}) { $who = "$comment->{u}{name} ". "({u}{user}\">$comment->{u}{user})"; } my $html = ""; $html .= "\n\n"; my $intro; my $cleanbody = $parent->{body}; if (LJ::u_equals($targetu, $comment->{u})) { if ($parent->{ispost}) { $who = "$parent->{u}{name} " . "({u}{user}\">$parent->{u}{user})"; $intro = "You replied to a $LJ::SITENAMESHORT post in which $who said:"; LJ::CleanHTML::clean_event(\$cleanbody, {preformatted => $parent->{preformat}}); } else { $intro = "You replied to a comment somebody left in "; $intro .= "a $LJ::SITENAMESHORT post. "; $intro .= "The comment you replied to was:"; LJ::CleanHTML::clean_comment(\$cleanbody, { 'preformatted' => $parent->{preformat}, 'anon_comment' => !$comment->{u} }); } } elsif (LJ::u_equals($targetu, $item->{entryu})) { if ($parent->{ispost}) { $intro = "$who replied to your $LJ::SITENAMESHORT post in which you said:"; LJ::CleanHTML::clean_comment(\$cleanbody, { 'preformatted' => $parent->{preformat}, 'anon_comment' => !$comment->{u} }); } else { $intro = "$who replied to another comment somebody left in "; $intro .= "your $LJ::SITENAMESHORT post. "; $intro .= "The comment they replied to was:"; LJ::CleanHTML::clean_comment(\$cleanbody, { 'preformatted' => $parent->{preformat}, 'anon_comment' => !$comment->{u} }); } } else { $intro = "$who replied to your $LJ::SITENAMESHORT comment "; $intro .= "in which you said:"; LJ::CleanHTML::clean_comment(\$cleanbody, { 'preformatted' => $parent->{preformat}, 'anon_comment' => !$comment->{u} }); } my $pichtml; if ($comment->{u} && $comment->{u}{defaultpicid} || $comment->{pic}) { my $picid = $comment->{pic} ? $comment->{pic}{'picid'} : $comment->{u}{'defaultpicid'}; unless ($comment->{pic}) { my %pics; LJ::load_userpics(\%pics, [ $comment->{u}, $comment->{u}{'defaultpicid'} ]); $comment->{pic} = $pics{$picid}; # load_userpics doesn't return picid, but we rely on it above $comment->{pic}{'picid'} = $picid; } if ($comment->{pic}) { $pichtml = "{pic}{'userid'}\" align='absmiddle' ". "width='$comment->{pic}{'width'}' height='$comment->{pic}{'height'}' ". "hspace='1' vspace='2' alt='' /> "; } } if ($pichtml) { $html .= "
$pichtml$intro
\n"; } else { $html .= "
$intro
\n"; } $html .= blockquote($cleanbody); $html .= "\n\n" . (LJ::u_equals($targetu, $comment->{u}) ? 'Your' : 'Their') . " reply was:\n\n"; $cleanbody = $comment->{body}; LJ::CleanHTML::clean_comment(\$cleanbody, $comment->{preformat}); my $pics = LJ::Talk::get_subjecticons(); my $icon = LJ::Talk::show_image($pics, $comment->{subjecticon}); my $heading; if ($comment->{subject}) { $heading = "Subject: " . LJ::ehtml($comment->{subject}); } $heading .= $icon; $heading .= "
" if $heading; # this needs to be one string so blockquote handles it properly. $html .= blockquote("$heading$cleanbody"); my $can_unscreen = $comment->{state} eq 'S' && LJ::Talk::can_unscreen($targetu, $item->{journalu}, $item->{entryu}, $comment->{u} ? $comment->{u}{user} : undef); if ($comment->{state} eq 'S') { $html .= "

This comment was screened. "; $html .= $can_unscreen ? "You must respond to it or unscreen it before others can see it.

\n" : "Someone else must unscreen it before you can reply to it.

\n"; } $html .= "

From here, you can:\n"; $html .= "

"; my $want_form = $comment->{state} eq 'A' || $can_unscreen; # this should probably be a preference, or maybe just always off. if ($want_form) { $html .= "If your mail client supports it, you can also reply here:\n"; $html .= "
\n"; $html .= LJ::html_hidden( usertype => "user", parenttalkid => $comment->{talkid}, itemid => $ditemid, journal => $item->{journalu}{user}, userpost => $targetu->{user}, ecphash => LJ::Talk::ecphash($item->{itemid}, $comment->{talkid}, $targetu->{password}) ); $html .= "" unless $encoding eq "UTF-8"; my $newsub = $comment->{subject}; unless (!$newsub || $newsub =~ /^Re:/) { $newsub = "Re: $newsub"; } $html .= "Subject: "; $html .= "

Message
"; $html .= "
"; $html .= "

\n"; } $html .= "

(If you'd prefer to not get these updates, go to your user profile page and turn off the relevant options.)

\n"; $html .= "\n"; return $html; } sub indent { my $a = shift; my $leadchar = shift || " "; $Text::Wrap::columns = 76; return Text::Wrap::fill("$leadchar ", "$leadchar ", $a); } sub blockquote { my $a = shift; return "
$a
"; } sub generate_messageid { my ($type, $journalu, $did) = @_; # $type = {"entry" | "comment"} # $journalu = $u of journal # $did = display id of comment/entry my $jid = $journalu->{userid}; return "<$type-$jid-$did\@$LJ::DOMAIN>"; } # entryu : user who posted the entry this comment is under. # journalu : journal this entry is in. # parent : comment/entry this post is in response to. # comment : the comment itself. # item : entry this comment falls under. sub mail_comments { my ($entryu, $journalu, $parent, $comment, $item) = @_; my $itemid = $item->{itemid}; my $ditemid = $itemid*256 + $item->{anum}; my $dtalkid = $comment->{talkid}*256 + $item->{anum}; my $talkurl = LJ::journal_base($journalu) . "/$ditemid.html"; my $threadurl = LJ::Talk::talkargs($talkurl, "thread=$dtalkid"); # check to see if parent post is from a registered livejournal user, and # mail them the response my $parentcomment = ""; my $parentmailed = ""; # who if anybody was just mailed # message ID of the mythical top-level journal entry (which # currently is never emailed) so mail clients can group things # together with a comment ancestor if parents are missing my $top_msgid = generate_messageid("entry", $journalu, $ditemid); # find first parent my $par_msgid; if (my $ptid = $parent->{talkid}) { $par_msgid = generate_messageid("comment", $journalu, $ptid * 256 + $item->{anum}); } else { # is a reply to the top-level $par_msgid = $top_msgid; $top_msgid = ""; # so it's not duplicated } # and this message ID my $this_msgid = generate_messageid("comment", $journalu, $dtalkid); # if a response to another comment, send a mail to the parent commenter. if ($parent->{talkid}) { my $dbcr = LJ::get_cluster_def_reader($journalu); # get row of data my $row = LJ::Talk::get_talk2_row($dbcr, $journalu->{userid}, $parent->{talkid}); my $paruserid = $row->{posterid}; # now get body of comment my $temp = LJ::get_talktext2($journalu, $parent->{talkid}); my $parbody = $temp->{$parent->{talkid}}[1]; LJ::text_uncompress(\$parbody); $parentcomment = $parbody; my %props = ($parent->{talkid} => {}); LJ::load_talk_props2($dbcr, $journalu->{'userid'}, [$parent->{talkid}], \%props); $parent->{preformat} = $props{$parent->{talkid}}->{'opt_preformatted'}; # convert to UTF-8 if necessary my $parentsubject = $parent->{subject}; if ($LJ::UNICODE && $props{$parent->{talkid}}->{'unknown8bit'}) { LJ::item_toutf8($journalu, \$parentsubject, \$parentcomment, {}); } if ($paruserid) { my $paru = LJ::load_userid($paruserid); LJ::load_user_props($paru, 'mailencoding'); LJ::load_codes({ "encoding" => \%LJ::CACHE_ENCODINGS } ) unless %LJ::CACHE_ENCODINGS; # we don't want to send email to a parent if the email address on the # parent's user is the same as the email address on this comment's user # is_diff_email: also so we don't auto-vivify $comment->{u} my $is_diff_email = !$comment->{u} || $paru->{'email'} ne $comment->{u}{'email'}; if ($paru->{'opt_gettalkemail'} eq "Y" && $is_diff_email && $paru->{'status'} eq "A") { $parentmailed = $paru->{'email'}; my $encoding = $paru->{'mailencoding'} ? $LJ::CACHE_ENCODINGS{$paru->{'mailencoding'}} : "UTF-8"; my $part; my $headersubject = $comment->{subject}; if ($LJ::UNICODE && $encoding ne "UTF-8") { $headersubject = Unicode::MapUTF8::from_utf8({-string=>$headersubject, -charset=>$encoding}); } if (!LJ::is_ascii($headersubject)) { $headersubject = MIME::Words::encode_mimeword($headersubject, 'B', $encoding); } my $fromname = $comment->{u} ? "$comment->{u}{'user'} - $LJ::SITENAMEABBREV Comment" : "$LJ::SITENAMESHORT Comment"; my $msg = new MIME::Lite ('From' => "$LJ::BOGUS_EMAIL ($fromname)", 'To' => $paru->{'email'}, 'Subject' => ($headersubject || "Reply to your comment..."), 'Type' => 'multipart/alternative', 'Message-Id' => $this_msgid, 'In-Reply-To:' => $par_msgid, 'References' => "$top_msgid $par_msgid", ); $msg->add('X-LJ-JOURNAL' => $journalu->{'user'}); # for mail filters $parent->{u} = $paru; $parent->{body} = $parentcomment; $parent->{ispost} = 0; $item->{entryu} = $entryu; $item->{journalu} = $journalu; my $text = format_text_mail($paru, $parent, $comment, $talkurl, $item); if ($LJ::UNICODE && $encoding ne "UTF-8") { $text = Unicode::MapUTF8::from_utf8({-string=>$text, -charset=>$encoding}); } $part = $msg->attach('Type' => 'TEXT', 'Data' => $text, 'Encoding' => 'quoted-printable', ); $part->attr("content-type.charset" => $encoding) if $LJ::UNICODE; if ($paru->{'opt_htmlemail'} eq "Y") { my $html = format_html_mail($paru, $parent, $comment, $encoding, $talkurl, $item); if ($LJ::UNICODE && $encoding ne "UTF-8") { $html = Unicode::MapUTF8::from_utf8({-string=>$html, -charset=>$encoding}); } $part = $msg->attach('Type' => 'text/html', 'Data' => $html, 'Encoding' => 'quoted-printable', ); $part->attr("content-type.charset" => $encoding) if $LJ::UNICODE; } LJ::send_mail($msg); } } } # send mail to the poster of the entry if ($entryu->{'opt_gettalkemail'} eq "Y" && !$item->{props}->{'opt_noemail'} && !LJ::u_equals($comment->{u}, $entryu) && $entryu->{'email'} ne $parentmailed && $entryu->{'status'} eq "A") { LJ::load_user_props($entryu, 'mailencoding'); LJ::load_codes({ "encoding" => \%LJ::CACHE_ENCODINGS } ) unless %LJ::CACHE_ENCODINGS; my $encoding = $entryu->{'mailencoding'} ? $LJ::CACHE_ENCODINGS{$entryu->{'mailencoding'}} : "UTF-8"; my $part; my $headersubject = $comment->{subject}; if ($LJ::UNICODE && $encoding ne "UTF-8") { $headersubject = Unicode::MapUTF8::from_utf8({-string=>$headersubject, -charset=>$encoding}); } if (!LJ::is_ascii($headersubject)) { $headersubject = MIME::Words::encode_mimeword($headersubject, 'B', $encoding); } my $fromname = $comment->{u} ? "$comment->{u}{'user'} - $LJ::SITENAMEABBREV Comment" : "$LJ::SITENAMESHORT Comment"; my $msg = new MIME::Lite ('From' => "$LJ::BOGUS_EMAIL ($fromname)", 'To' => $entryu->{'email'}, 'Subject' => ($headersubject || "Reply to your post..."), 'Type' => 'multipart/alternative', 'Message-Id' => $this_msgid, 'In-Reply-To:' => $par_msgid, 'References' => "$top_msgid $par_msgid", ); $msg->add('X-LJ-JOURNAL' => $journalu->{'user'}); # for mail filters my $quote = $parentcomment ? $parentcomment : $item->{'event'}; # if this is a response to a comment inside our journal, # we don't know who made the parent comment # (and it's potentially anonymous). if ($parentcomment) { $parent->{u} = undef; $parent->{body} = $parentcomment; $parent->{ispost} = 0; } else { $parent->{u} = $entryu; $parent->{body} = $item->{'event'}, $parent->{ispost} = 1; $parent->{preformat} = $item->{'props'}->{'opt_preformatted'}; } $item->{entryu} = $entryu; $item->{journalu} = $journalu; my $text = format_text_mail($entryu, $parent, $comment, $talkurl, $item); if ($LJ::UNICODE && $encoding ne "UTF-8") { $text = Unicode::MapUTF8::from_utf8({-string=>$text, -charset=>$encoding}); } $part = $msg->attach('Type' => 'TEXT', 'Data' => $text, 'Encoding' => 'quoted-printable', ); $part->attr("content-type.charset" => $encoding) if $LJ::UNICODE; if ($entryu->{'opt_htmlemail'} eq "Y") { my $html = format_html_mail($entryu, $parent, $comment, $encoding, $talkurl, $item); if ($LJ::UNICODE && $encoding ne "UTF-8") { $html = Unicode::MapUTF8::from_utf8({-string=>$html, -charset=>$encoding}); } $part = $msg->attach('Type' => 'text/html', 'Data' => $html, 'Encoding' => 'quoted-printable', ); $part->attr("content-type.charset" => $encoding) if $LJ::UNICODE; } LJ::send_mail($msg); } # now send email to the person who posted the comment we're using? only if userprop # opt_getselfemail is turned on. no need to check for active/suspended accounts, as # they couldn't have posted if they were. (and if they did somehow, we're just emailing # them, so it shouldn't matter.) my $u = $comment->{u}; LJ::load_user_props($u, 'opt_getselfemail', 'mailencoding') if $u; if ($u && $u->{'opt_getselfemail'} && LJ::get_cap($u, 'getselfemail')) { LJ::load_codes({ "encoding" => \%LJ::CACHE_ENCODINGS } ) unless %LJ::CACHE_ENCODINGS; my $encoding = $u->{'mailencoding'} ? $LJ::CACHE_ENCODINGS{$u->{'mailencoding'}} : "UTF-8"; my $part; my $headersubject = $comment->{subject}; if ($LJ::UNICODE && $encoding ne "UTF-8") { $headersubject = Unicode::MapUTF8::from_utf8({-string=>$headersubject, -charset=>$encoding}); } if (!LJ::is_ascii($headersubject)) { $headersubject = MIME::Words::encode_mimeword($headersubject, 'B', $encoding); } my $msg = new MIME::Lite ('From' => "$LJ::BOGUS_EMAIL ($u->{'user'} - $LJ::SITENAMEABBREV Comment)", 'To' => $u->{'email'}, 'Subject' => ($headersubject || "Comment you posted..."), 'Type' => 'multipart/alternative', 'Message-Id' => $this_msgid, 'In-Reply-To:' => $par_msgid, 'References' => "$top_msgid $par_msgid", ); $msg->add('X-LJ-JOURNAL' => $journalu->{'user'}); # for mail filters my $quote = $parentcomment ? $parentcomment : $item->{'event'}; # if this is a response to a comment inside our journal, # we don't know who made the parent comment # (and it's potentially anonymous). if ($parentcomment) { $parent->{u} = undef; $parent->{body} = $parentcomment; $parent->{ispost} = 0; } else { $parent->{u} = $entryu; $parent->{body} = $item->{'event'}, $parent->{ispost} = 1; $parent->{preformat} = $item->{'props'}->{'opt_preformatted'}; } $item->{entryu} = $entryu; $item->{journalu} = $journalu; my $text = format_text_mail($u, $parent, $comment, $talkurl, $item); if ($LJ::UNICODE && $encoding ne "UTF-8") { $text = Unicode::MapUTF8::from_utf8({-string=>$text, -charset=>$encoding}); } $part = $msg->attach('Type' => 'TEXT', 'Data' => $text, 'Encoding' => 'quoted-printable', ); $part->attr("content-type.charset" => $encoding) if $LJ::UNICODE; if ($u->{'opt_htmlemail'} eq "Y") { my $html = format_html_mail($u, $parent, $comment, $encoding, $talkurl, $item); if ($LJ::UNICODE && $encoding ne "UTF-8") { $html = Unicode::MapUTF8::from_utf8({-string=>$html, -charset=>$encoding}); } $part = $msg->attach('Type' => 'text/html', 'Data' => $html, 'Encoding' => 'quoted-printable', ); $part->attr("content-type.charset" => $encoding) if $LJ::UNICODE; } LJ::send_mail($msg); } } sub enter_comment { my ($journalu, $parent, $item, $comment, $errref) = @_; my $partid = $parent->{talkid}; my $itemid = $item->{itemid}; my $err = sub { $$errref = join(": ", @_); return 0; }; return $err->("Invalid user object passed.") unless LJ::isu($journalu); my $jtalkid = LJ::alloc_user_counter($journalu, "T"); return $err->("Database Error", "Could not generate a talkid necessary to post this comment.") unless $jtalkid; # insert the comment my $posterid = $comment->{u} ? $comment->{u}{userid} : 0; my $errstr; $journalu->talk2_do("L", $itemid, \$errstr, "INSERT INTO talk2 ". "(journalid, jtalkid, nodetype, nodeid, parenttalkid, posterid, datepost, state) ". "VALUES (?,?,'L',?,?,?,NOW(),?)", $journalu->{userid}, $jtalkid, $itemid, $partid, $posterid, $comment->{state}); if ($errstr) { return $err->("Database Error", "There was an error posting your comment to the database. " . "Please report this. The error is: $errstr"); } LJ::MemCache::incr([$journalu->{'userid'}, "talk2ct:$journalu->{'userid'}"]); $comment->{talkid} = $jtalkid; # record IP if anonymous LJ::Talk::record_anon_comment_ip($journalu, $comment->{talkid}, LJ::get_remote_ip()) unless $posterid; # add to poster's talkleft table, or the xfer place if ($posterid) { my $table; my $db = LJ::get_cluster_master($comment->{u}); if ($db) { # remote's cluster is writable $table = "talkleft"; } else { # log to global cluster, another job will move it later. $db = LJ::get_db_writer(); $table = "talkleft_xfp"; } my $pub = $item->{'security'} eq "public" ? 1 : 0; if ($db) { $db->do("INSERT INTO $table (userid, posttime, journalid, nodetype, ". "nodeid, jtalkid, publicitem) VALUES (?, UNIX_TIMESTAMP(), ". "?, 'L', ?, ?, ?)", undef, $posterid, $journalu->{userid}, $itemid, $jtalkid, $pub); LJ::MemCache::incr([$posterid, "talkleftct:$posterid"]); } else { # both primary and backup talkleft hosts down. can't do much now. } } $journalu->do("INSERT INTO talktext2 (journalid, jtalkid, subject, body) ". "VALUES (?, ?, ?, ?)", undef, $journalu->{userid}, $jtalkid, $comment->{subject}, LJ::text_compress($comment->{body})); die $journalu->errstr if $journalu->err; my $memkey = "$journalu->{'clusterid'}:$journalu->{'userid'}:$jtalkid"; LJ::MemCache::set([$journalu->{'userid'},"talksubject:$memkey"], $comment->{subject}); LJ::MemCache::set([$journalu->{'userid'},"talkbody:$memkey"], $comment->{body}); # dudata my $bytes = length($comment->{subject}) + length($comment->{body}); # we used to do a LJ::dudata_set(..) on 'T' here, but decided # we could defer that. to find size of a journal, summing # bytes in dudata is too slow (too many seeks) my %talkprop; # propname -> value # meta-data $talkprop{'unknown8bit'} = 1 if $comment->{unknown8bit}; $talkprop{'subjecticon'} = $comment->{subjecticon}; $talkprop{'picture_keyword'} = $comment->{picture_keyword}; $talkprop{'opt_preformatted'} = $comment->{preformat} ? 1 : 0; if ($journalu->{'opt_logcommentips'} eq "A" || ($journalu->{'opt_logcommentips'} eq "S" && $comment->{usertype} ne "user")) { my $ip = BML::get_remote_ip(); my $forwarded = BML::get_client_header('X-Forwarded-For'); $ip = "$forwarded, via $ip" if $forwarded && $forwarded ne $ip; $talkprop{'poster_ip'} = $ip; } # remove blank/0 values (defaults) foreach (keys %talkprop) { delete $talkprop{$_} unless $talkprop{$_}; } # update the talkprops LJ::load_props("talk"); if (%talkprop) { my $values; my $hash = {}; foreach (keys %talkprop) { my $p = LJ::get_prop("talk", $_); next unless $p; $hash->{$_} = $talkprop{$_}; my $tpropid = $p->{'tpropid'}; my $qv = $journalu->quote($talkprop{$_}); $values .= "($journalu->{'userid'}, $jtalkid, $tpropid, $qv),"; } if ($values) { chop $values; $journalu->do("INSERT INTO talkprop2 (journalid, jtalkid, tpropid, value) ". "VALUES $values"); die $journalu->errstr if $journalu->err; } LJ::MemCache::set([$journalu->{'userid'}, "talkprop:$journalu->{'userid'}:$jtalkid"], $hash); } # record up to 25 (or $LJ::TALK_MAX_URLS) urls from a comment my (%urls, $dbh); if ($LJ::TALK_MAX_URLS && ( %urls = map { $_ => 1 } LJ::get_urls($comment->{body}) ) && ( $dbh = LJ::get_db_writer() )) # don't log if no db available { my (@bind, @vals); my $ip = LJ::get_remote_ip(); while (my ($url, undef) = each %urls) { push @bind, '(?,?,?,?,UNIX_TIMESTAMP(),?)'; push @vals, $posterid, $journalu->{userid}, $ip, $jtalkid, $url; last if @bind >= $LJ::TALK_MAX_URLS; } my $bind = join(',', @bind); my $sql = qq{ INSERT DELAYED INTO commenturls (posterid, journalid, ip, jtalkid, timecreate, url) VALUES $bind }; $dbh->do($sql, undef, @vals); } # update the "replycount" summary field of the log table if ($comment->{state} eq 'A') { LJ::replycount_do($journalu, $itemid, "incr"); } # update the "hasscreened" property of the log item if needed if ($comment->{state} eq 'S') { LJ::set_logprop($journalu, $itemid, { 'hasscreened' => 1 }); } # update the comment alter property LJ::Talk::update_commentalter($journalu, $itemid); return $jtalkid; } # XXX these strings should be in talk, but moving them means we have # to retranslate. so for now we're just gonna put it off. my $SC = '/talkpost_do.bml'; sub init { my ($form, $remote, $need_captcha, $errret) = @_; my $sth; my $err = sub { my $error = shift; push @$errret, $error; return undef; }; my $bmlerr = sub { return $err->($BML::ML{$_[0]}); }; my $init = LJ::Talk::init($form); return $err->($init->{error}) if $init->{error}; my $journalu = $init->{'journalu'}; return $bmlerr->('talk.error.nojournal') unless $journalu; return $err->($LJ::MSG_READONLY_USER) if LJ::get_cap($journalu, "readonly"); return $err->("Account is locked, unable to post comment.") if $journalu->{statusvis} eq 'L'; my $r = Apache->request; $r->notes("journalid" => $journalu->{'userid'}); my $dbcr = LJ::get_cluster_def_reader($journalu); return $bmlerr->('error.nodb') unless $dbcr; my $itemid = $init->{'itemid'}+0; my $item = LJ::Talk::get_journal_item($journalu, $itemid); if ($init->{'oldurl'} && $item) { $init->{'anum'} = $item->{'anum'}; $init->{'ditemid'} = $init->{'itemid'}*256 + $item->{'anum'}; } unless ($item && $item->{'anum'} == $init->{'anum'}) { return $bmlerr->('talk.error.noentry'); } my $iprops = $item->{'props'}; my $ditemid = $init->{'ditemid'}+0; my $talkurl = LJ::journal_base($journalu) . "/$ditemid.html"; $init->{talkurl} = $talkurl; ### load users LJ::load_userids_multiple([ $item->{'posterid'} => \$init->{entryu}, ], [ $journalu ]); LJ::load_user_props($journalu, "opt_logcommentips"); if ($form->{'userpost'} && $form->{'usertype'} ne "user") { unless ($form->{'usertype'} eq "cookieuser" && $form->{'userpost'} eq $form->{'cookieuser'}) { $bmlerr->("$SC.error.confused_identity"); } } # anonymous/cookie users cannot authenticate with ecphash if ($form->{'ecphash'} && $form->{'usertype'} ne "user") { $bmlerr->("$SC.error.badusername"); return undef; } my $cookie_auth; if ($form->{'usertype'} eq "cookieuser") { $bmlerr->("$SC.error.lostcookie") unless ($remote && $remote->{'user'} eq $form->{'cookieuser'}); return undef if @$errret; $cookie_auth = 1; $form->{'userpost'} = $remote->{'user'}; $form->{'usertype'} = "user"; } # XXXevan hack: remove me when we fix preview. $init->{cookie_auth} = $cookie_auth; # test accounts may only comment on other test accounts. if ((grep { $form->{'userpost'} eq $_ } @LJ::TESTACCTS) && !(grep { $journalu->{'user'} eq $_ } @LJ::TESTACCTS)) { $bmlerr->("$SC.error.testacct"); } my $userpost = lc($form->{'userpost'}); my $up; # user posting my $exptype; # set to long if ! after username my $ipfixed; # set to remote ip if < after username my $used_ecp; # ecphash was validated and used if ($form->{'usertype'} eq "user") { if ($form->{'userpost'}) { # parse inline login opts if ($form->{'userpost'} =~ s/[!<]{1,2}$//) { $exptype = 'long' if index($&, "!") >= 0; $ipfixed = LJ::get_remote_ip() if index($&, "<") >= 0; } $up = LJ::load_user($form->{'userpost'}); if ($up) { ### see if the user is banned from posting here if (LJ::is_banned($up, $journalu)) { $bmlerr->("$SC.error.banned"); } unless ($up->{'journaltype'} eq "P" || ($up->{'journaltype'} eq "I" && $cookie_auth)) { $bmlerr->("$SC.error.postshared"); } # if we're already authenticated via cookie, then userpost was set # to the authenticated username, so we got into this block, but we # don't want to re-authenticate, so just skip this unless ($cookie_auth) { # if ecphash present, authenticate on that if ($form->{'ecphash'}) { if ($form->{'ecphash'} eq LJ::Talk::ecphash($itemid, $form->{'parenttalkid'}, $up->{'password'})) { $used_ecp = 1; } else { $bmlerr->("$SC.error.badpassword"); } # otherwise authenticate on username/password } else { my $ok; if ($form->{response}) { $ok = LJ::challenge_check_login($up, $form->{chal}, $form->{response}); } else { $ok = LJ::auth_okay($up, $form->{'password'}, $form->{'hpassword'}); } $bmlerr->("$SC.error.badpassword") unless $ok; } } # if the user chooses to log in, do so if ($form->{'do_login'} && ! @$errret) { $init->{didlogin} = $up->make_login_session($exptype, $ipfixed); } } else { $bmlerr->("$SC.error.badusername"); } } else { $bmlerr->("$SC.error.nousername"); } } # OpenID if (LJ::OpenID::consumer_enabled() && ($form->{'usertype'} eq 'openid' || $form->{'usertype'} eq 'openid_cookie')) { return $err->("No OpenID identity URL entered") unless $form->{'oidurl'}; use LJ::OpenID; # to-TOP if ($remote && defined $remote->openid_identity) { $up = $remote; if ($form->{'oiddo_login'}) { $up->make_login_session($form->{'exptype'}, $form->{'ipfixed'}); } } else { # First time through my $csr = LJ::OpenID::consumer(); my $exptype = 'short'; my $ipfixed = 0; my $etime = 0; # parse inline login opts if ($form->{'oidurl'} =~ s/[!<]{1,2}$//) { if (index($&, "!") >= 0) { $exptype = 'long'; $etime = time()+60*60*24*60; } $ipfixed = LJ::get_remote_ip() if index($&, "<") >= 0; } my $tried_local_ref = LJ::OpenID::blocked_hosts($csr); my $claimed_id = $csr->claimed_identity($form->{'oidurl'}); unless ($claimed_id) { return $err->("You can't use a $LJ::SITENAMESHORT OpenID account on $LJ::SITENAME — ". "just go login with your actual $LJ::SITENAMESHORT account.") if $$tried_local_ref; return $err->("No claimed id: ".$csr->err); } # Store their cleaned up identity url vs what they # actually typed in $form->{'oidurl'} = $claimed_id->claimed_url(); # Store the entry my $pendcid = LJ::alloc_user_counter($journalu, "C"); $err->("Unable to allocate pending id") unless $pendcid; # Since these were gotten from the oidurl and won't # persist in the form data $form->{'exptype'} = $exptype; $form->{'etime'} = $etime; $form->{'ipfixed'} = $ipfixed; my $penddata = Storable::freeze($form); $err->("Unable to get database handle to store pending comment") unless $journalu->writer; $journalu->do("INSERT INTO pendcomments (jid, pendcid, data, datesubmit) VALUES (?, ?, ?, UNIX_TIMESTAMP())", undef, $journalu->{'userid'}, $pendcid, $penddata); $err->($journalu->errstr) if $journalu->err; my $check_url = $claimed_id->check_url( return_to => "$LJ::SITEROOT/talkpost_do.bml?jid=$journalu->{'userid'}&pendcid=$pendcid", trust_root => "http://*.$LJ::DOMAIN/", delayed_return => 1, ); # Don't redirect them if errors return undef if @$errret; return BML::redirect($check_url); } } # validate the challenge/response value (anti-spammer) unless ($used_ecp) { my $chrp_err; if (my $chrp = $form->{'chrp1'}) { my ($c_ditemid, $c_uid, $c_time, $c_chars, $c_res) = split(/\-/, $chrp); my $chal = "$c_ditemid-$c_uid-$c_time-$c_chars"; my $secret = LJ::get_secret($c_time); my $res = Digest::MD5::md5_hex($secret . $chal); if ($res ne $c_res) { $chrp_err = "invalid"; } elsif ($c_time < time() - 2*60*60) { $chrp_err = "too_old" if $LJ::REQUIRE_TALKHASH_NOTOLD; } } else { $chrp_err = "missing"; } if ($chrp_err) { my $ip = LJ::get_remote_ip(); if ($LJ::DEBUG_TALKSPAM) { my $ruser = $remote ? $remote->{user} : "[nonuser]"; print STDERR "talkhash error: from $ruser \@ $ip - $chrp_err - $talkurl\n"; } if ($LJ::REQUIRE_TALKHASH) { return $err->("Sorry, form expired. Press back, copy text, reload form, paste into new form, and re-submit.") if $chrp_err eq "too_old"; return $err->("Missing parameters"); } } } # check that user can even view this post, which is required # to reply to it #### Check security before viewing this post unless (LJ::can_view($up, $item)) { $bmlerr->("$SC.error.mustlogin") unless (defined $up); $bmlerr->("$SC.error.noauth"); return undef; } # If the reply is to a comment, check that it exists. # if it's screened, check that the user has permission to # reply and unscreen it my $parpost; my $partid = $form->{'parenttalkid'}+0; if ($partid) { $parpost = LJ::Talk::get_talk2_row($dbcr, $journalu->{userid}, $partid); unless ($parpost) { $bmlerr->("$SC.error.noparent"); } # can't use $remote because we may get here # with a reply from email. so use $up instead of $remote # in the call below. if ($parpost && $parpost->{'state'} eq "S" && !LJ::Talk::can_unscreen($up, $journalu, $init->{entryu}, $init->{entryu}{'user'})) { $bmlerr->("$SC.error.screened"); } } $init->{parpost} = $parpost; # don't allow anonymous comments on syndicated items if ($journalu->{'journaltype'} eq "Y" && $journalu->{'opt_whocanreply'} eq "all") { $journalu->{'opt_whocanreply'} = "reg"; } if ($form->{'usertype'} ne "user" && $journalu->{'opt_whocanreply'} ne "all") { $bmlerr->("$SC.error.noanon"); } if ($iprops->{'opt_nocomments'}) { $bmlerr->("$SC.error.nocomments"); } if ($up) { if ($up->{'status'} eq "N" && $up->{'journaltype'} ne "I") { $bmlerr->("$SC.error.noverify"); } if ($up->{'statusvis'} eq "D") { $bmlerr->("$SC.error.deleted"); } elsif ($up->{'statusvis'} eq "S") { $bmlerr->("$SC.error.suspended"); } } if ($journalu->{'opt_whocanreply'} eq "friends") { if ($up) { if ($up->{'userid'} != $journalu->{'userid'}) { unless (LJ::is_friend($journalu, $up)) { $err->(BML::ml("$SC.error.notafriend", {'user'=>$journalu->{'user'}})); } } } else { $err->(BML::ml("$SC.error.friendsonly", {'user'=>$journalu->{'user'}})); } } $bmlerr->("$SC.error.blankmessage") unless $form->{'body'} =~ /\S/; # in case this post comes directly from the user's mail client, it # may have an encoding field for us. if ($form->{'encoding'}) { $form->{'body'} = Unicode::MapUTF8::to_utf8({-string=>$form->{'body'}, -charset=>$form->{'encoding'}}); $form->{'subject'} = Unicode::MapUTF8::to_utf8({-string=>$form->{'subject'}, -charset=>$form->{'encoding'}}); } # unixify line-endings $form->{'body'} =~ s/\r\n/\n/g; # now check for UTF-8 correctness, it must hold return $err->("") unless LJ::text_in($form); $init->{unknown8bit} = 0; unless (LJ::is_ascii($form->{'body'}) && LJ::is_ascii($form->{'subject'})) { if ($LJ::UNICODE) { # no need to check if they're well-formed, we did that above } else { # so rest of site can change chars to ? marks until # default user's encoding is set. (legacy support) $init->{unknown8bit} = 1; } } my ($bl, $cl) = LJ::text_length($form->{'body'}); if ($cl > LJ::CMAX_COMMENT) { $err->(BML::ml("$SC.error.manychars", {'current'=>$cl, 'limit'=>LJ::CMAX_COMMENT})); } elsif ($bl > LJ::BMAX_COMMENT) { $err->(BML::ml("$SC.error.manybytes", {'current'=>$bl, 'limit'=>LJ::BMAX_COMMENT})); } # the Subject can be silently shortened, no need to reject the whole comment $form->{'subject'} = LJ::text_trim($form->{'subject'}, 100, 100); my $subjecticon = ""; if ($form->{'subjecticon'} ne "none" && $form->{'subjecticon'} ne "") { $subjecticon = LJ::trim(lc($form->{'subjecticon'})); } # figure out whether to post this comment screened my $state = 'A'; my $screening = LJ::Talk::screening_level($journalu, $ditemid >> 8); if ($screening eq 'A' || ($screening eq 'R' && ! $up) || ($screening eq 'F' && !($up && LJ::is_friend($journalu, $up)))) { $state = 'S'; } $state = 'A' if LJ::Talk::can_unscreen($up, $journalu, $init->{entryu}, $init->{entryu}{user}); my $parent = { state => $parpost->{state}, talkid => $partid, }; my $comment = { u => $up, usertype => $form->{'usertype'}, subject => $form->{'subject'}, body => $form->{'body'}, unknown8bit => $init->{unknown8bit}, subjecticon => $subjecticon, preformat => $form->{'prop_opt_preformatted'}, picture_keyword => $form->{'prop_picture_keyword'}, state => $state, }; $init->{item} = $item; $init->{parent} = $parent; $init->{comment} = $comment; # anti-spam captcha check if (ref $need_captcha eq 'SCALAR') { # see if they're in the second+ phases of a captcha check. # are they sending us a response? if ($form->{captcha_chal}) { # assume they won't pass and re-set the flag $$need_captcha = 1; # if they typed "audio", we don't double-check if they still need # a captcha (they still do), they just want an audio version. if (lc($form->{answer}) eq 'audio') { return; } my ($capid, $anum) = LJ::Captcha::session_check_code($form->{captcha_chal}, $form->{answer}, $journalu); return $err->("Incorrect response to spam robot challenge.") unless $capid && $anum; my $expire_u = $comment->{'u'} || LJ::load_user('system'); LJ::Captcha::expire($capid, $anum, $expire_u->{userid}); } else { my $show_captcha = sub { return 1 if $LJ::HUMAN_CHECK{'comment_html_auth'}; # Anonymous commenter return 1 if $LJ::HUMAN_CHECK{'comment_html_anon'} && ! LJ::isu($comment->{'u'}); # Identity commenter return 1 if $LJ::HUMAN_CHECK{'comment_html_anon'} && $comment->{'u'}->identity() && ! LJ::is_friend($journalu, $comment->{'u'}); }; $$need_captcha = ($LJ::HUMAN_CHECK{anonpost} || $LJ::HUMAN_CHECK{authpost}) && ! LJ::Talk::Post::check_rate($comment->{'u'}, $journalu); if ($show_captcha->()) { # see if they have any tags or URLs if ($form->{'body'} =~ /<[a-z]/i) { # strip white-listed bare tags w/o attributes, # then see if they still have HTML. if so, it's # questionable. (can do evil spammy-like stuff w/ # attributes and other elements) my $body_copy = $form->{'body'}; $body_copy =~ s/<(?:q|blockquote|b|strong|i|em|cite|sub|sup|var|del|tt|code|pre|p)>//ig; $$need_captcha = 1 if $body_copy =~ /<[a-z]/i; } # multiple URLs is questionable too $$need_captcha = 1 if $form->{'body'} =~ /\b(?:http|ftp)\b.+\b(?:http|ftp)\b/s; } # if the user is anonymous and the IP is marked, ignore rates and always human test. $$need_captcha = 1 if $LJ::HUMAN_CHECK{anonpost} && ! $comment->{'u'} && LJ::sysban_check('talk_ip_test', LJ::get_remote_ip()); if ($$need_captcha) { return $err->("Please confirm you are a human below."); } } } return undef if @$errret; return $init; } # returns 1 on success. 0 on fail (with $$errref set) sub post_comment { my ($entryu, $journalu, $comment, $parent, $item, $errref) = @_; # unscreen the parent comment if needed if ($parent->{state} eq 'S') { LJ::Talk::unscreen_comment($journalu, $item->{itemid}, $parent->{talkid}); $parent->{state} = 'A'; } # make sure they're not underage if ($comment->{u} && $comment->{u}->underage) { $$errref = $LJ::UNDERAGE_ERROR; return 0; } # check for duplicate entry (double submission) # Note: we don't do it inside a locked section like ljprotocol.pl's postevent, # so it's not perfect, but it works pretty well. my $posterid = $comment->{u} ? $comment->{u}{userid} : 0; my $jtalkid; # check for dup ID in memcache. my $memkey; if (@LJ::MEMCACHE_SERVERS) { my $md5_b64 = Digest::MD5::md5_base64( join(":", ($comment->{body}, $comment->{subject}, $comment->{subjecticon}, $comment->{preformat}, $comment->{picture_keyword}))); $memkey = [$journalu->{userid}, "tdup:$journalu->{userid}:$item->{itemid}-$parent->{talkid}-$posterid-$md5_b64" ]; $jtalkid = LJ::MemCache::get($memkey); } # they don't have a duplicate... unless ($jtalkid) { # XXX do select and delete $talkprop{'picture_keyword'} if they're lying my $pic = LJ::get_pic_from_keyword($comment->{u}, $comment->{picture_keyword}); delete $comment->{picture_keyword} unless $pic && $pic->{'state'} eq 'N'; $comment->{pic} = $pic; # put the post in the database my $ditemid = $item->{itemid}*256 + $item->{anum}; $jtalkid = enter_comment($journalu, $parent, $item, $comment, $errref); return 0 unless $jtalkid; # save its identifying characteristics to protect against duplicates. LJ::MemCache::set($memkey, $jtalkid+0, time()+60*10); # send some emails mail_comments($entryu, $journalu, $parent, $comment, $item); # log the event # this function doesn't do anything. # LJ::event_register($dbcm, "R", $journalu->{'userid'}, $ditemid); # FUTURE: log events type 'T' (thread) up to root } # the caller wants to know the comment's talkid. $comment->{talkid} = $jtalkid; # cluster tracking LJ::mark_user_active($comment->{u}, 'comment'); return 1; } # XXXevan: this function should have its functionality migrated to talkpost. # because of that, it's probably not worth the effort to make it not mangle $form... sub make_preview { my ($talkurl, $cookie_auth, $form) = @_; my $ret = ""; my $cleansubject = $form->{'subject'}; LJ::CleanHTML::clean_subject(\$cleansubject); $ret .= ""; $ret .= ""; my $event = $form->{'body'}; my $spellcheck_html; if ($LJ::SPELLER && $form->{'do_spellcheck'}) { my $s = new LJ::SpellCheck { 'spellcommand' => $LJ::SPELLER, 'color' => '', }; $spellcheck_html = $s->check_html(\$event); } LJ::CleanHTML::clean_comment(\$event, $form->{'prop_opt_preformatted'}); $ret .= "$BML::ML{'/talkpost_do.bml.preview.subject'} " . LJ::ehtml($cleansubject) . "
\n"; if ($spellcheck_html) { $ret .= $spellcheck_html; $ret .= "

"; } else { $ret .= $event; } $ret .= "


"; $ret .= "

\n"; $ret .= "
"; $ret .= "

"; # change mode: delete $form->{'submitpreview'}; $form->{'submitpost'} = 1; if ($cookie_auth) { $form->{'usertype'} = "cookieuser"; delete $form->{'userpost'}; } delete $form->{'do_spellcheck'}; foreach (keys %$form) { $ret .= LJ::html_hidden($_, $form->{$_}) unless $_ eq 'body' || $_ eq 'subject' || $_ eq 'prop_opt_preformatted'; } $ret .= "
\n"; $ret .= "\n"; if ($LJ::SPELLER) { $ret .= " "; } $ret .= "

"; $ret .= "$BML::ML{'/talkpost.bml.opt.noautoformat'} ". LJ::html_check({ 'name' => 'prop_opt_preformatted', selected => $form->{'prop_opt_preformatted'} }); $ret .= LJ::help_icon("noautoformat", " "); $ret .= "

"; $ret .= "

"; $ret .= "
"; return $ret; } # given a journalu and jitemid, return 1 if the entry # is over the maximum comments allowed. sub over_maxcomments { my ($journalu, $jitemid) = @_; $journalu = LJ::want_user($journalu); $jitemid += 0; return 0 unless $journalu && $jitemid; my $count = LJ::Talk::get_replycount($journalu, $jitemid); return ($count >= LJ::get_cap($journalu, 'maxcomments')) ? 1 : 0; } # more anti-spammer rate limiting. returns 1 if rate is okay, 0 if too fast. sub check_rate { my ($remote, $journalu) = @_; # we require memcache to do rate limiting efficiently return 1 unless @LJ::MEMCACHE_SERVERS; # return right away if the account is suspended return 0 if $remote && $remote->{'statusvis'} =~ /[SD]/; my $ip = LJ::get_remote_ip(); my $now = time(); my @watch; if ($remote) { # registered human (or human-impersonating robot) push @watch, [ "talklog:$remote->{userid}", $LJ::RATE_COMMENT_AUTH || [ [ 200, 3600 ], [ 20, 60 ] ], ]; } else { # anonymous, per IP address (robot or human) push @watch, [ "talklog:$ip", $LJ::RATE_COMMENT_ANON || [ [ 300, 3600 ], [ 200, 1800 ], [ 150, 900 ], [ 15, 60 ] ] ]; # anonymous, per journal. # this particular limit is intended to combat flooders, instead # of the other 'spammer-centric' limits. push @watch, [ "talklog:anonin:$journalu->{userid}", $LJ::RATE_COMMENT_ANON || [ [ 300, 3600 ], [ 200, 1800 ], [ 150, 900 ], [ 15, 60 ] ] ]; } WATCH: foreach my $watch (@watch) { my ($key, $rates) = ($watch->[0], $watch->[1]); my $max_period = $rates->[0]->[1]; my $log = LJ::MemCache::get($key); my $DATAVER = "1"; # parse the old log my @times; if (length($log) % 4 == 1 && substr($log,0,1) eq $DATAVER) { my $ct = (length($log)-1) / 4; for (my $i=0; $i<$ct; $i++) { my $time = unpack("N", substr($log,$i*4+1,4)); push @times, $time if $time > $now - $max_period; } } # add this event push @times, $now; # check rates foreach my $rate (@$rates) { my ($allowed, $period) = ($rate->[0], $rate->[1]); my $events = scalar grep { $_ > $now-$period } @times; if ($events > $allowed) { my $ruser = (exists $remote->{'user'}) ? $remote->{'user'} : 'Not logged in'; my $nowtime = localtime($now); my $body = < $allowed allowed / $period secs Remote user: $ruser Remote IP: $ip Time caught: $nowtime Posting to: $journalu->{'user'} EOM if ($LJ::DEBUG_TALK_RATE && LJ::MemCache::add("warn:$key", 1, 600)) { LJ::send_mail({ 'to' => $LJ::DEBUG_TALK_RATE, 'from' => $LJ::ADMIN_EMAIL, 'fromname' => $LJ::SITENAME, 'charset' => 'utf-8', 'subject' => "talk spam: $key", 'body' => $body, }); } return 0 if $LJ::ANTI_TALKSPAM; last WATCH; } } # build the new log my $newlog = $DATAVER; foreach (@times) { $newlog .= pack("N", $_); } LJ::MemCache::set($key, $newlog, $max_period); } return 1; } 1;