# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.
#
# LiveJournal comment object.
#
# Just framing right now, not much to see here!
#
package LJ::Comment;
use strict;
use Carp qw/ croak /;
use LJ::Entry;
use LJ::HTMLControls;
use LJ::Talk;
=head1 NAME
LJ::Comment
=head1 CLASS METHODS
=cut
# internal fields:
#
# journalid: journalid where the comment was
# posted, always present
# jtalkid: jtalkid identifying this comment
# within the journal_u, always present
#
# nodetype: single-char nodetype identifier, loaded if _loaded_row
# nodeid: nodeid to which this comment
# applies (often an entry itemid), loaded if _loaded_row
#
# parenttalkid: talkid of parent comment, loaded if _loaded_row
# posterid: userid of posting user lazily loaded at access
# datepost_unix: unixtime from the 'datepost' loaded if _loaded_row
# state: comment state identifier, loaded if _loaded_row
# body: text of comment, loaded if _loaded_text
# body_orig: text of comment w/o transcoding, present if unknown8bit
# subject: subject of comment, loaded if _loaded_text
# subject_orig subject of comment w/o transcoding, present if unknown8bit
# props: hashref of props, loaded if _loaded_props
# childids: arrayref of child ids loaded if _loaded_childids
# _loaded_text: loaded talktext2 row
# _loaded_row: loaded talk2 row
# _loaded_props: loaded props
# _loaded_childids: loaded childids
my %singletons = (); # journalid->jtalkid->singleton
# singletons still to be loaded
my %unloaded_singletons = ();
my %unloaded_text_singletons = ();
my %unloaded_prop_singletons = ();
sub reset_singletons {
%singletons = ();
%unloaded_singletons = ();
%unloaded_text_singletons = ();
%unloaded_prop_singletons = ();
}
#
# name: LJ::Comment::new
# class: comment
# des: Gets a comment given journal_u entry and jtalkid.
# args: uobj, opts?
# des-uobj: A user id or user object ($u) to load the comment for.
# des-opts: Hash of optional keypairs.
# jtalkid => talkid journal itemid (no anum)
# returns: A new LJ::Comment object. Returns undef on failure.
#
sub instance {
croak("wrong number of arguments")
unless scalar @_ == 4;
my ( $class, $uuserid, $which, $value ) = @_;
my $journalid = LJ::want_userid($uuserid)
or croak("invalid journalid parameter");
my $jtalkid = $which eq 'jtalkid' ? $value + 0 : ( $value + 0 >> 8 )
or croak("need to supply jtalkid or dtalkid");
# do we have a singleton for this comment?
$singletons{$journalid} ||= {};
return $singletons{$journalid}->{$jtalkid}
if $singletons{$journalid}->{$jtalkid};
# save the singleton if it doesn't exist
my $self = bless {
journalid => $journalid,
jtalkid => $jtalkid,
}, $class;
$unloaded_singletons{ $self->singletonkey } = $self;
$unloaded_text_singletons{ $self->singletonkey } = $self;
$unloaded_prop_singletons{ $self->singletonkey } = $self;
return $singletons{$journalid}->{$jtalkid} = $self;
}
*new = \&instance;
# class method. takes a ?thread= or ?replyto= URL
# to a comment, and returns that comment object
sub new_from_url {
my ( $class, $url ) = @_;
$url =~ s!#.*!!;
if ( $url =~ /(.+?)\?(?:thread|replyto)\=(\d+)/ ) {
my $entry = LJ::Entry->new_from_url($1);
return undef unless $entry;
return LJ::Comment->new( $entry->journal, dtalkid => $2 );
}
return undef;
}
#
# name: LJ::Comment::create
# class: comment
# des: Create a new comment. Add them to DB.
# args:
# returns: A new LJ::Comment object. Returns undef on failure.
#
sub create {
my ( $class, %opts ) = @_;
my $need_captcha = delete( $opts{need_captcha} ) || 0;
my $err_ref = delete $opts{err_ref};
my $err = sub {
$$err_ref = {
code => $_[0],
msg => $_[1]
};
return undef;
};
# %talk_opts emulates parameters received from web form.
# Fill it with nessesary options.
my %talk_opts = map { $_ => delete $opts{$_} } qw(nodetype parenttalkid body subject props);
# poster and journal should be $u objects
my $journalu = delete $opts{journal};
return $err->( "bad_journal", "invalid journal for new comment: $journalu" )
unless LJ::isu($journalu);
my $posteru = delete $opts{poster};
return $err->( "bad_poster", "invalid poster for new comment: $posteru" )
unless LJ::isu($posteru);
# to prepare the comment, we need an LJ::Entry object, so go get one.
my $ditemid = delete $opts{ditemid};
return $err->( "no_entry", "No ditemid provided" ) unless $ditemid;
my $entry = LJ::Entry->new( $journalu, ditemid => $ditemid );
# Strictly parameters check. Do not allow any unused params to be passed in.
return $err->(
"bad_args", __PACKAGE__ . "->create: Unsupported params: " . join " " => keys %opts
) if %opts;
# Move props values to the talk_opts hash so it resembles the reply form
# fields, since that's what prepare_and_validate_comment expects.
foreach my $key ( keys %{ $talk_opts{props} } ) {
my $talk_key = "prop_$key";
$talk_opts{$talk_key} = delete $talk_opts{props}->{$key}
if not exists $talk_opts{$talk_key};
}
## init. this handles all the error-checking, as well.
my @errors = ();
my $comment = LJ::Talk::Post::prepare_and_validate_comment( \%talk_opts, $posteru, $entry,
\$need_captcha, \@errors );
# Special double-checking for max comments limit error before we return a
# generic error, because, it's VERY SPECIAL, I guess, and there's a faint
# possibility some consumer of LJ::Protocol Cares.
return $err->(
"too_many_comments", "Sorry, this entry already has the maximum number of comments allowed."
) if LJ::Talk::Post::over_maxcomments( $journalu, $entry->jitemid );
# OK, now bail for generic errors.
return $err->( "init_comment", join "\n" => @errors )
unless defined $comment;
## insertion
my ( $ok, $talkid_or_err ) = LJ::Talk::Post::post_comment($comment);
unless ($ok) {
return $err->( "post_comment", $talkid_or_err );
}
return LJ::Comment->new( $journalu, jtalkid => $talkid_or_err );
}
=head1 INSTANCE METHODS
=cut
sub absorb_row {
my ( $self, %row ) = @_;
$self->{$_} = $row{$_} foreach (qw(nodetype nodeid parenttalkid posterid datepost state));
$self->{_loaded_row} = 1;
delete $unloaded_singletons{ $self->singletonkey };
}
sub url {
my ( $self, $url_args ) = @_;
my $dtalkid = $self->dtalkid;
my $entry = $self->entry;
my $url = $entry->url;
return
"$url?thread=$dtalkid"
. ( $url_args ? "&$url_args" : "" )
. LJ::Talk::comment_anchor($dtalkid);
}
*thread_url = \&url;
=head2 C<< $self->threadroot_url >>
URL to the thread root. It would be unnecessarily expensive to look up the thread
root, since it is only rarely needed. So we set up a redirect then look up the
thread root only if the user clicks the link.
=cut
sub threadroot_url {
my ( $self, $url_args ) = @_;
my $dtalkid = $self->dtalkid;
my $jitemid = $self->entry->jitemid;
my $journal = $self->entry->journal->user;
return "$LJ::SITEROOT/go?redir_type=threadroot&journal=$journal&talkid=$dtalkid"
. ( $url_args ? "&$url_args" : "" );
}
sub reply_url {
my $self = $_[0];
my $dtalkid = $self->dtalkid;
my $entry = $self->entry;
my $url = $entry->url;
return "$url?replyto=$dtalkid";
}
sub parent_url {
my ( $self, $url_args ) = @_;
my $parent = $self->parent;
return undef unless $parent;
return $parent->url($url_args);
}
sub unscreen_url {
my $self = $_[0];
my $dtalkid = $self->dtalkid;
my $entry = $self->entry;
my $journal = $entry->journal->{user};
return "$LJ::SITEROOT/talkscreen" . "?mode=unscreen&journal=$journal" . "&talkid=$dtalkid";
}
sub delete_url {
my $self = $_[0];
my $dtalkid = $self->dtalkid;
my $entry = $self->entry;
my $journal = $entry->journal->{user};
return "$LJ::SITEROOT/delcomment" . "?journal=$journal&id=$dtalkid";
}
sub edit_url {
my $self = $_[0];
my $dtalkid = $self->dtalkid;
my $entry = $self->entry;
my $url = $entry->url;
return "$url?edit=$dtalkid";
}
# return LJ::User of journal comment is in
sub journal {
return LJ::load_userid( $_[0]->{journalid} );
}
sub journalid {
return $_[0]->{journalid};
}
sub singletonkey {
return $_[0]->{journalid} . "-" . $_[0]->{jtalkid};
}
# return LJ::Entry of entry comment is in, or undef if it's not
# a nodetype of L
sub entry {
my $self = $_[0];
return undef unless $self && $self->valid;
return LJ::Entry->new( $self->journal, jitemid => $self->nodeid );
}
sub jtalkid {
return $_[0]->{jtalkid};
}
sub dtalkid {
my $self = $_[0];
my $entry = $self->entry or return undef;
return ( $self->jtalkid * 256 ) + $entry->anum;
}
sub nodeid {
__PACKAGE__->preload_rows();
return $_[0]->{nodeid};
}
sub nodetype {
__PACKAGE__->preload_rows();
return $_[0]->{nodetype};
}
=head2 C<< $self->threadrootid >>
Gets the id of the topmost comment in the thread this comment is part of.
If you just want to create a link, do not call this directly. Instead, use
$self->threadroot_url.
=cut
sub threadrootid {
my ($self) = @_;
# if this has no parent, then this is the thread root
return $self->jtalkid unless $self->parenttalkid;
# if we have the information already, then just return it
return $self->{threadrootid} if $self->{threadrootid};
my $entry = $self->entry;
# if it is in memcache, then use the cached value
my $jid = $entry->journalid;
my $memkey = [ $jid, "talkroot:$jid:" . $self->jtalkid ];
my $cached_threadrootid = LJ::MemCache::get($memkey);
if ($cached_threadrootid) {
$self->{threadrootid} = $cached_threadrootid;
return $cached_threadrootid;
}
# not cached anywhere; let's look it up
# get all comments to post
my $comments = LJ::Talk::get_talk_data( $entry->journal, 'L', $entry->jitemid ) || {};
# see if our comment exists
return undef unless $comments->{ $self->jtalkid };
# walk up the tree
my $id = $self->jtalkid;
while ( $comments->{$id} && $comments->{$id}->{parenttalkid} ) {
# avoid (the unlikely chance of) an infinite loop
$id = delete $comments->{$id}->{parenttalkid};
}
# cache the value, for future lookup
$self->{threadrootid} = $id;
LJ::MemCache::set( $memkey, $id );
return $id;
}
sub parenttalkid {
__PACKAGE__->preload_rows();
return $_[0]->{parenttalkid};
}
# returns a LJ::Comment object for the parent
sub parent {
my $self = $_[0];
my $ptalkid = $self->parenttalkid or return undef;
return LJ::Comment->new( $self->journal, jtalkid => $ptalkid );
}
# returns an array of LJ::Comment objects with parentid == $self->jtalkid
sub children {
my $self = $_[0];
if ( $self->{_loaded_childids} ) {
my @children = ();
my $u = $self->journal;
if ( $self->{childids} && scalar @{ $self->{childids} } ) {
my @childids = @{ $self->{childids} };
foreach my $talkid (@childids) {
my $child = LJ::Comment->new( $u, jtalkid => $talkid );
push @children, $child;
}
}
return @children;
}
my $entry = $self->entry;
return grep { $_->{parenttalkid} == $self->{jtalkid} } $entry->comment_list;
# FIXME: It might be a good idea to check to see if the entry object had
# comments cached above, then fall back to a query to select a list
# from db or memcache
}
sub has_children {
return $_[0]->children ? 1 : 0;
}
sub has_nondeleted_children {
my $nondeleted_children = grep { !$_->is_deleted } $_[0]->children;
return $nondeleted_children ? 1 : 0;
}
# returns true if entry currently exists. (it's possible for a given
# $u, to make a fake jitemid and that'd be a valid skeleton LJ::Entry
# object, even though that jitemid hasn't been created yet, or was
# previously deleted)
sub valid {
my $self = $_[0];
my $u = $self->journal;
return 0 unless $u && $u->{clusterid};
__PACKAGE__->preload_rows();
return $self->{_loaded_row};
}
# when was this comment left?
sub unixtime {
__PACKAGE__->preload_rows();
return LJ::mysqldate_to_time( $_[0]->{datepost}, 0 );
}
# returns LJ::User object for the poster of this entry, or undef for anonymous
sub poster {
return LJ::load_userid( $_[0]->posterid );
}
sub posterid {
__PACKAGE__->preload_rows();
return $_[0]->{posterid};
}
sub all_singletons {
my $self = $_[0];
my @singletons;
push @singletons, values %{ $singletons{$_} } foreach keys %singletons;
return @singletons;
}
# class method:
sub preload_rows {
my @to_load = ();
push @to_load, map { [ $_->journal, $_->jtalkid ] } values %unloaded_singletons;
# already loaded?
return 1 unless @to_load;
# args: ([ journalid, jtalkid ], ...)
my @rows = LJ::Talk::get_talk2_row_multi(@to_load);
# make a mapping of journalid-jtalkid => $row
my %row_map = map { join( "-", $_->{journalid}, $_->{jtalkid} ) => $_ } @rows;
foreach my $obj ( values %unloaded_singletons ) {
my $u = $obj->journal;
my $row = $row_map{ join( "-", $u->id, $obj->jtalkid ) };
next unless $row;
# absorb row into the given LJ::Comment object
$obj->absorb_row(%$row);
}
%unloaded_singletons = ();
return 1;
}
# returns true if loaded, zero if not.
# also sets _loaded_text and subject and event.
sub _load_text {
my $self = $_[0];
return 1 if $self->{_loaded_text};
my $entry = $self->entry;
my $entryu = $entry->journal;
my $entry_uid = $entryu->id;
# find singletons which don't already have text loaded
my @to_load;
foreach my $c_obj ( values %unloaded_text_singletons ) {
if ( $c_obj->journalid == $entry_uid ) {
push @to_load, $c_obj;
}
}
my $ret = LJ::get_talktext2( $entryu, map { $_->jtalkid } @to_load );
return 0 unless $ret && ref $ret;
# iterate over comment objects we retrieved and set their subject/body/loaded members
foreach my $c_obj (@to_load) {
my $tt = $ret->{ $c_obj->jtalkid };
next unless ( $tt && ref $tt );
# raw subject and body
$c_obj->{subject} = $tt->[0];
$c_obj->{body} = $tt->[1];
if ( $c_obj->prop("unknown8bit") ) {
# save the old ones away, so we can get back at them if we really need to
$c_obj->{subject_orig} = $c_obj->{subject};
$c_obj->{body_orig} = $c_obj->{body};
# FIXME: really convert all the props? what if we binary-pack some in the future?
LJ::item_toutf8( $c_obj->journal, \$c_obj->{subject}, \$c_obj->{body},
$c_obj->{props} );
}
$c_obj->{_loaded_text} = 1;
delete $unloaded_text_singletons{ $self->singletonkey };
}
return 1;
}
sub _set_text {
my ( $self, %opts ) = @_;
my $jtalkid = $self->jtalkid;
die "can't set text on unsaved comment"
unless $jtalkid;
my %doing = ();
my %original = ();
my %compressed = ();
foreach my $part (qw(subject body)) {
next unless exists $opts{$part};
$original{$part} = delete $opts{$part};
die "$part is not utf-8" unless LJ::is_utf8( $original{$part} );
$doing{$part}++;
$compressed{$part} = LJ::text_compress( $original{$part} );
}
croak "must set either body or subject" unless %doing;
# if the comment is unknown8bit, then we must be setting both subject and body,
# else we'll have one side utf-8 and the other side unknown, but no metadata
# capable of expressing "subject is unknown8bit, but not body".
if ( $self->prop('unknown8bit') ) {
die "Can't set text on unknown8bit comments unless both subject and body are specified"
unless $doing{subject} && $doing{body};
}
my $journalu = $self->journal;
my $journalid = $self->journalid;
# need to set new values in the database
my $set_sql = join( ", ", map { "$_=?" } grep { $doing{$_} } qw(subject body) );
my @set_vals = map { $compressed{$_} } grep { $doing{$_} } qw(subject body);
# update is okay here because we verified we have a jtalkid, presumably from this table
# -- compressed versions of the text here
$journalu->do( "UPDATE talktext2 SET $set_sql WHERE journalid=? AND jtalkid=?",
undef, @set_vals, $journalid, $jtalkid );
die $journalu->errstr if $journalu->err;
# need to also update memcache
# -- uncompressed versions here
my $memkey = join( ":", $journalu->clusterid, $journalid, $jtalkid );
foreach my $part (qw(subject body)) {
next unless $doing{$part};
LJ::MemCache::set( [ $journalid, "talk$part:$memkey" ], $original{$part} );
}
# got this far in setting text, and we know we used to be unknown8bit, except the text
# we just set was utf8, so clear the unknown8bit flag
if ( $self->prop('unknown8bit') ) {
# set to 0 instead of delete so we can find these records later
$self->set_prop( 'unknown8bit', '0' );
}
# if text is already loaded, then we can just set whatever we've modified in $self
if ( $doing{subject} && $doing{body} ) {
$self->{$_} = $original{$_} foreach qw(subject body);
$self->{_loaded_text} = 1;
}
else {
$self->{$_} = undef foreach qw(subject body);
$self->{_loaded_text} = 0;
$unloaded_text_singletons{ $self->singletonkey } = $self;
}
# otherwise _loaded_text=0 and we won't do any optimizations
return 1;
}
sub set_subject {
my ( $self, $text ) = @_;
return $self->_set_text( subject => $text );
}
sub set_body {
my ( $self, $text ) = @_;
return $self->_set_text( body => $text );
}
sub set_subject_and_body {
my ( $self, $subject, $body ) = @_;
return $self->_set_text( subject => $subject, body => $body );
}
sub prop {
my ( $self, $prop ) = @_;
$self->_load_props unless $self->{_loaded_props};
return $self->{props}{$prop};
}
sub set_prop {
my ( $self, $prop, $val ) = @_;
return $self->set_props( $prop => $val );
}
# allows the caller to pass raw SQL to set a prop (e.g. UNIX_TIMESTAMP())
# do not use this if setting a value given by the user
sub set_prop_raw {
my ( $self, $prop, $val ) = @_;
return $self->set_props_raw( $prop => $val );
}
sub delete_prop {
my ( $self, $prop ) = @_;
return $self->set_props( $prop => undef );
}
sub props {
my ( $self, $prop ) = @_;
$self->_load_props unless $self->{_loaded_props};
return $self->{props} || {};
}
# class method: preloads the props on the provided list of Comment objects.
sub preload_props {
my ( $class, $journalid, @to_load ) = @_;
my $prop_ret = {};
LJ::load_talk_props2( $journalid, [ map { $_->jtalkid } @to_load ], $prop_ret );
# iterate over comment objects to load and fill in their props members
foreach my $c_obj (@to_load) {
$c_obj->{props} = $prop_ret->{ $c_obj->jtalkid } || {};
$c_obj->{_loaded_props} = 1;
delete $unloaded_prop_singletons{ $c_obj->singletonkey };
}
return 1;
}
sub _load_props {
my $self = $_[0];
return 1 if $self->{_loaded_props};
my $journalid = $self->journalid;
# find singletons which don't already have props loaded
my @to_load;
foreach my $c_obj ( values %unloaded_prop_singletons ) {
if ( $c_obj->journalid == $journalid ) {
push @to_load, $c_obj;
}
}
my $prop_ret = {};
LJ::load_talk_props2( $journalid, [ map { $_->jtalkid } @to_load ], $prop_ret );
# iterate over comment objects to load and fill in their props members
foreach my $c_obj (@to_load) {
$c_obj->{props} = $prop_ret->{ $c_obj->jtalkid } || {};
$c_obj->{_loaded_props} = 1;
delete $unloaded_prop_singletons{ $c_obj->singletonkey };
}
return 1;
}
sub set_props {
my ( $self, %props ) = @_;
# call this so that get_prop() calls below will be cached
LJ::load_props("talk");
my $set_raw = delete $props{_raw} ? 1 : 0;
my $journalid = $self->journalid;
my $journalu = $self->journal;
my $jtalkid = $self->jtalkid;
my @vals = ();
my @to_del = ();
my %tprops = ();
my @prop_vals = ();
foreach my $key ( keys %props ) {
my $p = LJ::get_prop( "talk", $key );
next unless $p;
my $val = $props{$key};
# build lists for inserts and deletes, also update $self
if ( defined $val ) {
if ($set_raw) {
push @vals, ( $journalid, $jtalkid, $p->{tpropid} );
push @prop_vals, $val;
$tprops{ $p->{tpropid} } = $key;
}
else {
push @vals, ( $journalid, $jtalkid, $p->{tpropid}, $val );
$self->{props}->{$key} = $props{$key};
}
}
else {
push @to_del, $p->{tpropid};
delete $self->{props}->{$key};
}
}
if (@vals) {
my $bind;
if ($set_raw) {
my @binds;
foreach my $prop_val (@prop_vals) {
push @binds, "(?,?,?,$prop_val)";
}
$bind = join( ",", @binds );
}
else {
$bind = join( ",", map { "(?,?,?,?)" } 1 .. ( @vals / 4 ) );
}
$journalu->do(
"REPLACE INTO talkprop2 (journalid, jtalkid, tpropid, value) " . "VALUES $bind",
undef, @vals );
die $journalu->errstr if $journalu->err;
# get the raw prop values back out of the database to store on the object
if ($set_raw) {
my $bind = join( ",", map { "?" } keys %tprops );
my $sth = $journalu->prepare(
"SELECT tpropid, value FROM talkprop2 WHERE journalid = ? AND jtalkid = ? AND tpropid IN ($bind)"
);
$sth->execute( $journalid, $jtalkid, keys %tprops );
while ( my $row = $sth->fetchrow_hashref ) {
my $tpropid = $row->{tpropid};
$self->{props}->{ $tprops{$tpropid} } = $row->{value};
}
}
if ($LJ::_T_COMMENT_SET_PROPS_INSERT) {
$LJ::_T_COMMENT_SET_PROPS_INSERT->();
}
}
if (@to_del) {
my $bind = join( ",", map { "?" } @to_del );
$journalu->do(
"DELETE FROM talkprop2 WHERE journalid=? AND jtalkid=? AND tpropid IN ($bind)",
undef, $journalid, $jtalkid, @to_del );
die $journalu->errstr if $journalu->err;
if ($LJ::_T_COMMENT_SET_PROPS_DELETE) {
$LJ::_T_COMMENT_SET_PROPS_DELETE->();
}
}
if ( @vals || @to_del ) {
LJ::MemCache::delete( [ $journalid, "talkprop:$journalid:$jtalkid" ] );
}
return 1;
}
sub set_props_raw {
my ( $self, %props ) = @_;
return $self->set_props( %props, _raw => 1 );
}
# raw utf8 text, with no HTML cleaning
sub subject_raw {
my $self = $_[0];
$self->_load_text unless $self->{_loaded_text};
return $self->{subject};
}
# raw text as user sent us, without transcoding while correcting for unknown8bit
sub subject_orig {
my $self = $_[0];
$self->_load_text unless $self->{_loaded_text};
return $self->{subject_orig} || $self->{subject};
}
# raw utf8 text, with no HTML cleaning
sub body_raw {
my $self = $_[0];
$self->_load_text unless $self->{_loaded_text};
# die if we didn't load any body text
die "Couldn't load body text" unless $self->{_loaded_text};
return $self->{body};
}
# raw text as user sent us, without transcoding while correcting for unknown8bit
sub body_orig {
my $self = $_[0];
$self->_load_text unless $self->{_loaded_text};
return $self->{body_orig} || $self->{body};
}
# comment body, cleaned
sub body_html {
my ( $self, %extra_opts ) = @_;
my $opts;
$opts->{preformatted} = $self->prop('opt_preformatted');
$opts->{anon_comment} = LJ::Talk::treat_as_anon( $self->poster, $self->journal );
$opts->{nocss} = $opts->{anon_comment};
$opts->{editor} = $self->prop('editor');
$opts->{is_imported} = defined $self->prop('import_source') ? 1 : 0;
$opts->{datepost} = $self->{datepost}; # for format guessing
$opts->{journal} = $self->journal->user;
$opts->{ditemid} = $self->entry->ditemid;
my $body = $self->body_raw;
LJ::CleanHTML::clean_comment( \$body, $opts ) if $body;
return $body;
}
# comement body, but trimmed to $char_max
sub body_html_summary {
my ( $self, $char_max, %opts ) = @_;
return LJ::html_trim( $self->body_html(%opts), $char_max );
}
# comment body, plaintext
sub body_text {
my $self = $_[0];
my $body = $self->body_html;
return LJ::strip_html($body);
}
sub subject_html {
my $self = $_[0];
$self->_load_text unless $self->{_loaded_text};
return LJ::ehtml( $self->{subject} );
}
sub subject_text {
my $self = $_[0];
my $subject = $self->subject_raw;
return LJ::ehtml($subject);
}
sub state {
__PACKAGE__->preload_rows();
return $_[0]->{state} || '';
}
sub is_active {
return $_[0]->state eq 'A' ? 1 : 0;
}
sub is_screened {
return $_[0]->state eq 'S' ? 1 : 0;
}
sub is_deleted {
return $_[0]->state eq 'D' ? 1 : 0;
}
sub is_frozen {
return $_[0]->state eq 'F' ? 1 : 0;
}
sub viewable_by_others {
my ($self) = @_;
# Is the comment attached to a visible entry?
my $remote = LJ::get_remote();
return 0 unless $self->entry && $self->entry->visible_to($remote);
# If the entry is visible, the comment should be generally viewable unless
# the comment is deleted, screened, or posted by a suspended user.
return 0 if $self->is_deleted;
return 0 if $self->is_screened;
return 0 if $self->poster && $self->poster->is_suspended;
return 1;
}
sub visible_to {
my ( $self, $u ) = @_;
return 0 unless LJ::isu($u);
return 0 unless $self->entry && $self->entry->visible_to($u);
my $posted_comment = $self->poster && $u->equals( $self->poster );
my $posted_entry = $self->entry->poster
&& $u->equals( $self->entry->poster );
my $posted_parent =
$self->parent
&& $self->parent->poster
&& $u->equals( $self->parent->poster );
my $posted_by_admin = $self->poster
&& $self->poster->can_manage( $self->journal );
# screened comment
return 0 if $self->is_screened && ! # allowed viewers:
(
$u->can_manage( $self->journal ) # owns the journal
|| $posted_comment || $posted_entry # owns the content
|| ( $posted_parent && $posted_by_admin )
);
# person this is in reply to,
# as long as this comment was by a moderator
# comments from suspended users aren't visible
return 0 if $self->poster && $self->poster->is_suspended;
return 1;
}
sub remote_can_delete {
my $self = $_[0];
my $remote = LJ::User->remote;
return $self->user_can_delete($remote);
}
sub user_can_delete {
my ( $self, $targetu ) = @_;
return 0 unless LJ::isu($targetu);
my $journalu = $self->journal;
my $posteru = $self->poster;
my $poster = $posteru ? $posteru->{user} : undef;
return LJ::Talk::can_delete( $targetu, $journalu, $posteru, $poster );
}
sub remote_can_edit {
my ( $self, $errref ) = @_;
my $remote = LJ::get_remote();
return $self->user_can_edit( $remote, $errref );
}
sub user_can_edit {
my ( $self, $u, $errref ) = @_;
return 0 unless $u;
$$errref = LJ::Lang::ml('talk.error.cantedit.invalid');
return 0 unless $self && $self->valid;
# comment editing must be enabled and the user must have the cap
$$errref = LJ::Lang::ml('talk.error.cantedit');
return 0 unless LJ::is_enabled("edit_comments");
return 0 unless $u->can_edit_comments;
# entry cannot be suspended
return 0 if $self->entry->is_suspended;
# user must be the poster of the comment
unless ( $u->equals( $self->poster ) ) {
$$errref = LJ::Lang::ml('talk.error.cantedit.notyours');
return 0;
}
# user cannot be read-only
return 0 if $u->is_readonly;
my $journal = $self->journal;
# journal owner must have commenting enabled
if ( $journal->prop('opt_showtalklinks') eq "N" ) {
$$errref = LJ::Lang::ml('talk.error.cantedit.commentingdisabled');
return 0;
}
# user cannot be banned from commenting
if ( $journal->has_banned($u) ) {
$$errref = LJ::Lang::ml('talk.error.cantedit.banned');
return 0;
}
# user must be a friend if friends-only commenting is on
if ( $journal->prop('opt_whocanreply') eq "friends" && !$journal->trusts_or_has_member($u) ) {
$$errref = LJ::Lang::ml('talk.error.cantedit.notfriend');
return 0;
}
# comment cannot have any replies; deleted comments don't count
if ( $self->has_nondeleted_children ) {
$$errref = LJ::Lang::ml('talk.error.cantedit.haschildren');
return 0;
}
# comment cannot be deleted
if ( $self->is_deleted ) {
$$errref = LJ::Lang::ml('talk.error.cantedit.isdeleted');
return 0;
}
# comment cannot be frozen
if ( $self->is_frozen ) {
$$errref = LJ::Lang::ml('talk.error.cantedit.isfrozen');
return 0;
}
# comment must be visible to the user
unless ( $self->visible_to($u) ) {
$$errref = LJ::Lang::ml('talk.error.cantedit.notvisible');
return 0;
}
$$errref = "";
return 1;
}
sub mark_as_spam {
my $self = $_[0];
LJ::Talk::mark_comment_as_spam( $self->poster, $self->jtalkid );
}
# returns comment action buttons (screen, freeze, delete, etc...)
sub manage_buttons {
my $self = $_[0];
my $dtalkid = $self->dtalkid;
my $journal = $self->journal;
my $jargent = "journal=$journal->{'user'}&";
my $remote = LJ::get_remote() or return '';
my $managebtns = '';
return '' unless $self->entry->poster;
my $poster = $self->poster ? $self->poster->user : "";
# Hack to strip the protocol off the return from LJ::img, so the JS is happy
# see https://github.com/dreamwidth/dreamwidth/commit/317619ee029f39ecd206cec484e0bfb7fa7c4ef1
sub no_proto_img { my $var = LJ::img(@_); $var =~ s/^https?://; return $var; }
if ( $self->remote_can_edit ) {
$managebtns .=
""
. no_proto_img( "editcomment", "", { align => 'absmiddle', hspace => 2 } ) . "";
}
if ( LJ::Talk::can_delete( $remote, $self->journal, $self->entry->poster, $poster ) ) {
$managebtns .= ""
. no_proto_img( "btn_del", "", { align => 'absmiddle', hspace => 2 } ) . "";
}
if ( LJ::Talk::can_freeze( $remote, $self->journal, $self->entry->poster, $poster ) ) {
unless ( $self->is_frozen ) {
$managebtns .=
""
. no_proto_img( "btn_freeze", "", { align => 'absmiddle', hspace => 2 } ) . "";
}
else {
$managebtns .=
""
. no_proto_img( "btn_unfreeze", "", { align => 'absmiddle', hspace => 2 } )
. "";
}
}
if ( LJ::Talk::can_screen( $remote, $self->journal, $self->entry->poster, $poster ) ) {
unless ( $self->is_screened ) {
$managebtns .=
""
. no_proto_img( "btn_scr", "", { align => 'absmiddle', hspace => 2 } ) . "";
}
else {
$managebtns .=
""
. no_proto_img( "btn_unscr", "", { align => 'absmiddle', hspace => 2 } ) . "";
}
}
return $managebtns;
}
# returns info for javascript comment management
# can be used as a class method if $journal is passed explicitly
sub info {
my ( $self, $journal ) = @_;
my $remote = LJ::get_remote();
$journal ||= $self->journal or return {};
return {
canAdmin => $remote && $remote->can_manage($journal),
canSpam => !LJ::sysban_check( 'spamreport', $journal->user ),
journal => $journal->user,
remote => $remote ? $remote->user : '',
};
}
sub thread_has_subscription {
my ( $comment, $remote, $u ) = @_;
my @unknown_tracking_status;
my $watched = 0;
while ( $comment && $comment->valid && $comment->parenttalkid ) {
# check cache
$comment->{_watchedby} ||= {};
my $thread_watched = $comment->{_watchedby}->{ $u->{userid} };
my $had_cached = defined $thread_watched;
unless ($had_cached) {
$thread_watched = $remote->has_subscription(
event => "JournalNewComment",
journal => $u,
arg2 => $comment->parenttalkid,
require_active => 1,
);
}
if ($thread_watched) {
$watched = 1;
# we had to go up a couple of levels before we could figure out
# whether we were watching or not
# so fix the status of intervening levels
foreach (@unknown_tracking_status) {
my $c = LJ::Comment->new( $u, dtalkid => $_ );
$c->{_watchedby}->{ $u->{userid} } = $thread_watched;
}
@unknown_tracking_status = ();
}
# cache in this comment object if it's being watched by this user
$comment->{_watchedby}->{ $u->{userid} } = $thread_watched;
# shortcircuit and stop going up the tree because:
last if $thread_watched; # current comment is watched
last if $had_cached; # we've been to this section of the ancestor tree already
push @unknown_tracking_status, $comment->dtalkid;
$comment = $comment->parent;
}
return $watched;
}
sub indent {
return LJ::Talk::Post::indent(@_);
}
sub blockquote {
return LJ::Talk::Post::blockquote(@_);
}
# used for comment email notification headers
sub email_messageid {
my $self = $_[0];
return "<" . join( "-", "comment", $self->journal->id, $self->dtalkid ) . "\@$LJ::DOMAIN>";
}
my @_ml_strings_en = (
'esn.journal_new_comment.subject', # 'Subject:',
'esn.journal_new_comment.message', # 'Message',
'esn.screened', # 'This comment was screened.',
'esn.you_must_unscreen', # 'You must respond to it or unscreen it before others can see it.',
'esn.here_you_can', # 'From here, you can:',
'esn.reply_at_webpage', # '[[openlink]]Reply[[closelink]] at the webpage',
'esn.unscreen_comment', # '[[openlink]]Unscreen the comment[[closelink]]',
'esn.edit_comment', # '[[openlink]]Edit the comment[[closelink]]',
'esn.delete_comment', # '[[openlink]]Delete the comment[[closelink]]',
'esn.view_comments', # '[[openlink]]View all comments[[closelink]] to this entry',
'esn.view_threadroot'
, # '[[openlink]]Go to the top of the thread[[closelink]] this comment is part of',
'esn.view_thread', # '[[openlink]]View the thread[[closelink]] beginning with this comment',
'esn.if_suport_form', # 'If your mail client supports it, you can also reply here:',
'esn.journal_new_comment.anonymous.comment', # 'Their reply was:',
'esn.journal_new_comment.anonymous.reply_to.anonymous_comment.to_your_post3'
, # 'Somebody replied to another comment somebody left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.anonymous.reply_to.user_comment.to_your_post3'
, # 'Somebody replied to another comment [[pwho]] left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.anonymous.reply_to.your_comment.to_post3'
, # 'Somebody replied to another comment you left in [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.anonymous.reply_to.your_comment.to_your_post3'
, # 'Somebody replied to another comment you left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.anonymous.reply_to.your_post3'
, # 'Somebody replied to [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]] in which you said:',
'esn.journal_new_comment.edit_reason',
'esn.journal_new_comment.user.comment', # 'Their reply was:',
'esn.journal_new_comment.user.edit_reply_to.anonymous_comment.to_your_post3'
, # '[[who]] edited a reply to another comment somebody left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.user.edit_reply_to.user_comment.to_your_post3'
, # '[[who]] edited a reply to another comment [[pwho]] left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.user.edit_reply_to.your_comment.to_post3'
, # '[[who]] edited a reply to another comment you left in [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.user.edit_reply_to.your_comment.to_your_post3'
, # '[[who]] edited a reply to another comment you left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.user.edit_reply_to.your_post3'
, # '[[who]] edited a reply to [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]] in which you said:',
'esn.journal_new_comment.user.new_comment', # 'Their new reply was:',
'esn.journal_new_comment.user.reply_to.anonymous_comment.to_your_post3'
, # '[[who]] replied to another comment somebody left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.user.reply_to.user_comment.to_your_post3'
, # '[[who]] replied to another comment [[pwho]] left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.user.reply_to.your_comment.to_post3'
, # '[[who]] replied to another comment you left in [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.user.reply_to.your_comment.to_your_post3'
, # '[[who]] replied to another comment you left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment they replied to was:',
'esn.journal_new_comment.user.reply_to.your_post3'
, # '[[who]] replied to [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]] in which you said:',
'esn.journal_new_comment.you.edit_reply_to.anonymous_comment.to_post3'
, # 'You edited a reply to another comment somebody left in [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.edit_reply_to.anonymous_comment.to_your_post3'
, # 'You edited a reply to another comment somebody left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.edit_reply_to.post3'
, # 'You edited a reply to [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]] in which [[pwho]] said:',
'esn.journal_new_comment.you.edit_reply_to.user_comment.to_post3'
, # 'You edited a reply to another comment [[pwho]] left in [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.edit_reply_to.user_comment.to_your_post3'
, # 'You edited a reply to another comment [[pwho]] left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.edit_reply_to.your_comment.to_post3'
, # 'You edited a reply to another comment you left in [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.edit_reply_to.your_comment.to_your_post3'
, # 'You edited a reply to another comment you left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.edit_reply_to.your_post3'
, # 'You edited a reply to [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]] in which you said:',
'esn.journal_new_comment.you.reply_to.anonymous_comment.to_post3'
, # 'You replied to another comment somebody left in [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.reply_to.anonymous_comment.to_your_post3'
, # 'You replied to another comment somebody left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.reply_to.post3'
, # 'You replied to [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]] in which [[pwho]] said:',
'esn.journal_new_comment.you.reply_to.user_comment.to_post3'
, # 'You replied to another comment [[pwho]] left in [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.reply_to.user_comment.to_your_post3'
, # 'You replied to another comment [[pwho]] left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.reply_to.your_comment.to_post3'
, # 'You replied to another comment you left in [[openlink]]a [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.reply_to.your_comment.to_your_post3'
, # 'You replied to another comment you left in [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]]. The comment you replied to was:',
'esn.journal_new_comment.you.reply_to.your_post3'
, # 'You replied to [[openlink]]your [[sitenameshort]] post[[postsubject]][[closelink]][[postsecurity]] in which you said:',
'esn.journal_new_comment.your.comment', # 'Your reply was:',
'esn.journal_new_comment.your.new_comment', # 'Your new reply was:',
);
# Implementation for both format_text_mail and format_html_mail.
sub _format_mail_both {
my ( $self, $targetu, $is_html ) = @_;
my $parent = $self->parent;
my $entry = $self->entry;
my $posteru = $self->poster;
my $edited = $self->is_edited;
my $who = ''; # Empty means anonymous
my ( $k_who, $k_what, $k_reply_edit );
if ($posteru) {
if ( $posteru->{name} eq $posteru->display_username ) {
if ($is_html) {
my $profile_url = $posteru->profile_url;
$who = " " . $posteru->display_username . "";
}
else {
$who = $posteru->display_username;
}
}
else {
if ($is_html) {
my $profile_url = $posteru->profile_url;
$who =
LJ::ehtml( $posteru->{name} )
. " ("
. $posteru->display_username . ")";
}
else {
$who = $posteru->{name} . " (" . $posteru->display_username . ")";
}
}
if ( $targetu->equals($posteru) ) {
if ($edited) {
# 'You edit your comment to...';
$k_who = 'you.edit_reply_to';
$k_reply_edit = 'your.new_comment';
}
else {
# 'You replied to...'
$k_who = 'you.reply_to';
$k_reply_edit = 'your.comment';
}
}
else {
if ($edited) {
# 'LJ-user ' . $posteru->{name} . ' edit reply to...';
$k_who = 'user.edit_reply_to';
$k_reply_edit = 'user.new_comment';
}
else {
# 'LJ-user ' . $posteru->{name} . ' replied to...';
$k_who = 'user.reply_to';
$k_reply_edit = 'user.comment';
}
}
}
else {
# 'Somebody replied to';
$k_who = 'anonymous.reply_to';
$k_reply_edit = 'anonymous.comment';
}
# Parent post author. Empty string means 'You'.
my $parentu = $entry->journal;
my $pwho = ''; #author of the commented post/comment. If empty - it's you or anonymous
if ($is_html) {
if ( !$parent && !$parentu->equals($targetu) ) {
# comment to a post and e-mail is going to be sent to not-AUTHOR of the journal
my $p_profile_url = $entry->poster->profile_url;
# pwho - author of the post
# If the user's name hasn't been set (it's the same as display_username), then
# don't display both
if ( $entry->poster->{name} eq $entry->poster->display_username ) {
$pwho = "" . $entry->poster->display_username . "";
}
else {
$pwho =
LJ::ehtml( $entry->poster->{name} )
. " ("
. $entry->poster->display_username . ")";
}
}
elsif ($parent) {
my $threadu = $parent->poster;
if ( $threadu && !$threadu->equals($targetu) ) {
my $p_profile_url = $threadu->profile_url;
if ( $threadu->{name} eq $threadu->display_username ) {
$pwho = "" . $threadu->display_username . "";
}
else {
$pwho =
LJ::ehtml( $threadu->{name} )
. " ("
. $threadu->display_username . ")";
}
}
}
}
else {
if ( !$parent && !$parentu->equals($targetu) ) {
if ( $entry->poster->{name} eq $entry->poster->display_username ) {
$pwho = $entry->poster->display_username;
}
else {
$pwho = $entry->poster->{name} . " (" . $entry->poster->display_username . ")";
}
}
elsif ($parent) {
my $threadu = $parent->poster;
if ( $threadu && !$threadu->equals($targetu) ) {
if ( $threadu->{name} eq $threadu->display_username ) {
$pwho = $threadu->display_username;
}
else {
$pwho = $threadu->{name} . " (" . $threadu->display_username . ")";
}
}
}
}
# Parent post security. Only include if post is locked/filtered.
my $postsecurity = ''; # if empty, the post is public or private
if ( $self->entry->security eq 'usemask' ) {
$postsecurity = ' [locked]';
}
# ESN directed to comment poster
if ( $targetu->equals( $self->poster ) ) {
# ->parent returns undef/0 if parent is an entry.
if ($parent) {
if ($pwho) {
# '... a comment ' . $pwho . ' left in post.';
$k_what = 'user_comment';
}
else {
# '... a comment you left in post.';
if ( $parent->poster ) {
$k_what = 'your_comment';
}
else {
$k_what = 'anonymous_comment';
}
}
if ( $targetu->equals( $entry->journal ) ) {
$k_what .= '.to_your_post3';
}
else {
$k_what .= '.to_post3';
}
}
else {
if ($pwho) {
$k_what = 'post3';
}
else {
$k_what = 'your_post3';
}
}
# ESN directed to entry author
}
elsif ( $targetu->equals( $entry->journal ) ) {
if ($parent) {
if ($pwho) {
# '... another comment ' . $pwho . ' left in your post.';
$k_what = 'user_comment.to_your_post3';
}
else {
if ( $parent->poster ) {
$k_what = 'your_comment.to_your_post3';
}
else {
# '... another comment you left in your post.';
$k_what = 'anonymous_comment.to_your_post3';
}
}
}
else {
$k_what = 'your_post3';
}
# ESN directed to author parent comment or post
}
else {
if ($parent) {
if ( $parent->poster ) {
if ($pwho) {
$k_what = 'user_comment.to_post3';
}
else {
$k_what = 'your_comment.to_post3';
}
}
else {
# '... another comment you left in your post.';
$k_what = 'anonymous_comment.to_post3';
}
}
else {
if ($pwho) {
$k_what = 'post3';
}
else {
$k_what = 'your_post3';
}
}
}
# Precache text lines, using DEFAULT_LANG for $targetu
my $lang = $LJ::DEFAULT_LANG;
LJ::Lang::get_text_multi( $lang, undef, \@_ml_strings_en );
my $body = '';
$body = "
"
if $is_html;
my $vars = {
who => $who,
pwho => $pwho,
sitenameshort => $LJ::SITENAMESHORT,
postsecurity => $postsecurity
};
# make hyperlinks for post
my $talkurl = $entry->url;
if ($is_html) {
$vars->{openlink} = "";
$vars->{closelink} = "";
}
else {
$vars->{openlink} = '';
$vars->{closelink} = " ( $talkurl )";
}
my $subject = $is_html ? $entry->subject_html : $entry->subject_text;
$subject = " \"$subject\""
if ($subject);
$vars->{postsubject} = $subject;
my $ml_prefix = "esn.journal_new_comment.";
$k_who = $ml_prefix . $k_who;
$k_reply_edit = $ml_prefix . $k_reply_edit;
my $intro = LJ::Lang::get_text( $lang, $k_who . '.' . $k_what, undef, $vars );
if ($is_html) {
my $pichtml;
if ($posteru) {
my ( $pic, $pic_kw ) = $self->userpic;
if ( $pic && $pic->load_row ) {
$pichtml =
"
{picid}/$pic->{userid}\" align='absmiddle' "
. "width='$pic->{width}' height='$pic->{height}' "
. "hspace='1' vspace='2' alt='"
. $pic->alttext($pic_kw) . "' /> ";
}
}
if ($pichtml) {
$body .=
"\n";
}
else {
$body .=
"\n";
}
$body .= blockquote( $parent ? $parent->body_html : $entry->event_html );
}
else {
$body .= $intro . "\n\n" . indent( $parent ? $parent->body_raw : $entry->event_raw, ">" );
}
# reason for editing, if applicable
if ($edited) {
my $reason = $self->edit_reason;
if ($is_html) {
$body .= "
"
. LJ::Lang::get_text(
$lang, "esn.journal_new_comment.edit_reason",
undef, { reason => LJ::ehtml($reason) }
)
. "
"
if $reason;
}
else {
$body .= "\n\n"
. LJ::Lang::get_text( $lang, "esn.journal_new_comment.edit_reason",
undef, { reason => $reason } )
if $reason;
}
}
$body .= "\n\n" . LJ::Lang::get_text( $lang, $k_reply_edit, undef, $vars ) . "\n\n";
if ($is_html) {
my $subjecticon = LJ::Talk::print_subjecticon_by_id( $self->prop('subjecticon') );
my $heading;
if ( $self->subject_raw ) {
$heading = ""
. LJ::Lang::get_text( $lang, $ml_prefix . 'subject', undef ) . " "
. $self->subject_html;
}
$heading .= $subjecticon;
$heading .= "
" if $heading;
# this needs to be one string so blockquote handles it properly.
if ( $self->admin_post ) {
$body .= '
'
. LJ::Lang::get_text(
$lang, "esn.journal_new_entry.admin_post",
undef, { img => LJ::img('admin-post') }
) . '
';
}
$body .= blockquote( "$heading" . $self->body_html );
$body .= "
";
}
else {
if ( my $subj = $self->subject_raw ) {
$body .= Text::Wrap::wrap(
" " . LJ::Lang::get_text( $lang, $ml_prefix . 'subject', undef ) . " ",
"", $subj )
. "\n\n";
}
if ( $self->admin_post ) {
$body .= LJ::Lang::get_text( $lang, "esn.journal_new_entry.admin_post.text" ) . "\n\n";
}
$body .= indent( $self->body_raw ) . "\n\n";
# Don't wrap options, only text.
$body = Text::Wrap::wrap( "", "", $body ) . "\n";
}
my $can_unscreen = $self->is_screened
&& LJ::Talk::can_unscreen( $targetu, $entry->journal, $entry->poster,
$posteru ? $posteru->{user} : undef );
if ( $self->is_screened ) {
$body .= LJ::Lang::get_text( $lang, 'esn.screened', undef ) . " ";
$body .= LJ::Lang::get_text( $lang, 'esn.you_must_unscreen', undef )
if $can_unscreen;
$body .= "\n";
}
my $commentsurl = $talkurl . "#comments";
$body .= LJ::Lang::get_text( $lang, 'esn.here_you_can', undef, $vars );
$body .= LJ::Event::format_options(
undef, $is_html, $lang, $vars,
{
'esn.reply_at_webpage' => [ 1, $self->reply_url ],
'esn.unscreen_comment' => [ $can_unscreen ? 2 : 0, $self->unscreen_url ],
'esn.edit_comment' => [ $self->user_can_edit($targetu) ? 3 : 0, $self->edit_url ],
'esn.delete_comment' => [ $self->user_can_delete($targetu) ? 4 : 0, $self->delete_url ],
'esn.view_comments' => [ 5, $commentsurl ],
'esn.view_threadroot' => [ $self->parenttalkid != 0 ? 6 : 0, $self->threadroot_url ],
'esn.view_thread' => [ 7, $self->thread_url ],
}
);
my $open_link = "";
my $close_link = "";
my $reset_link = "$LJ::SITEROOT/manage/emailpost";
if ($is_html) {
$open_link = qq{};
$close_link = q{};
}
else {
$close_link = " ($reset_link)";
}
$body .= "\n"
. LJ::Lang::get_text( $lang, 'esn.reply_to_email2', undef,
{ openlink => $open_link, closelink => $close_link } )
. "\n";
$body .= "
\n" if $is_html;
return $body;
}
sub format_text_mail {
my ( $self, $targetu ) = @_;
croak "invalid targetu passed to format_text_mail"
unless LJ::isu($targetu);
return _format_mail_both( $self, $targetu, 0 );
}
sub format_html_mail {
my ( $self, $targetu ) = @_;
croak "invalid targetu passed to format_html_mail"
unless LJ::isu($targetu);
return _format_mail_both( $self, $targetu, 1 );
}
sub delete {
my $self = $_[0];
return LJ::Talk::delete_comment(
$self->journal,
$self->nodeid, # jitemid
$self->jtalkid,
$self->state
);
}
sub delete_thread {
my $self = $_[0];
return LJ::Talk::delete_thread(
$self->journal,
$self->nodeid, # jitemid
$self->jtalkid
);
}
=head2 C<< $cmt->userpic >>
Returns a LJ::Userpic object for the poster of the comment, or undef.
If called in a list context, returns ( LJ::Userpic object, keyword )
=cut
sub userpic {
my $self = $_[0];
my $up = $self->poster;
return unless $up;
# return the picture from keyword, if defined
# else return poster's default userpic
my $kw = $_[0]->userpic_kw;
my $pic = LJ::Userpic->new_from_keyword( $up, $kw ) || $up->userpic;
return wantarray ? ( $pic, $kw ) : $pic;
}
=head2 C<< $cmt->userpic_kw >>
Returns the userpic keyword used on this comment, or undef.
=cut
sub userpic_kw {
my $self = $_[0];
my $up = $self->poster;
return unless $up;
if ( $up->userpic_have_mapid ) {
my $mapid = $self->prop('picture_mapid');
return $up->get_keyword_from_mapid($mapid) if $mapid;
}
else {
return $self->prop('picture_keyword');
}
}
=head2 C<< $cmt->admin_post
Returns true if this comment is an official administrator comment.
=cut
sub admin_post {
my $self = $_[0];
return 0 unless $self->journal->is_community;
return 0
unless $self->poster && $self->poster->can_manage( $self->journal );
if ( exists $_[1] ) {
$_[0]->set_prop( 'admin_post', $_[1] ? 1 : 0 );
}
else {
return $_[0]->prop('admin_post') ? 1 : 0;
}
}
sub poster_ip {
return $_[0]->prop("poster_ip");
}
# sets the new poster IP and returns the value that was set
sub set_poster_ip {
my $self = $_[0];
return "" unless LJ::is_web_context();
my $current_ip = $self->poster_ip;
my $new_ip = BML::get_remote_ip();
my $forwarded = BML::get_client_header('X-Forwarded-For');
$new_ip = "$forwarded, via $new_ip" if $forwarded && $forwarded ne $new_ip;
if ( !$current_ip || $new_ip eq $current_ip ) {
$self->set_prop( poster_ip => $new_ip );
return $new_ip;
}
if ( $current_ip =~ /\(originally ([\w\.]+)\)/ ) {
if ( $new_ip eq $1 ) {
$self->set_prop( poster_ip => $new_ip );
return $new_ip;
}
$new_ip = "$new_ip (originally $1)";
}
else {
$new_ip = "$new_ip (originally $current_ip)";
}
$self->set_prop( poster_ip => $new_ip );
return $new_ip;
}
sub edit_reason {
return $_[0]->prop("edit_reason");
}
sub edit_time {
return $_[0]->prop("edit_time");
}
sub is_edited {
return $_[0]->edit_time ? 1 : 0;
}
1;