# 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 .= "
$pichtml$intro
\n"; } else { $body .= "
$intro
\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;