# 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 entry object. # # Just framing right now, not much to see here! # package LJ::Entry; use strict; use v5.10; use Log::Log4perl; my $log = Log::Log4perl->get_logger(__PACKAGE__); our $AUTOLOAD; use Carp qw/ croak confess /; use DW::Task::DeleteEntry; use DW::Task::SphinxCopier; =head1 NAME LJ::Entry =head1 CLASS METHODS =cut # internal fields: # # u: object, always present # anum: lazily loaded, either by ctor or _loaded_row # ditemid: lazily loaded # jitemid: always present # props: hashref of props, loaded if _loaded_props # subject: text of subject, loaded if _loaded_text # event: text of log event, loaded if _loaded_text # subject_orig: text of subject without transcoding, present if unknown8bit # event_orig: text of log event without transcoding, present if unknown8bit # eventtime: mysql datetime of event, loaded if _loaded_row # logtime: mysql datetime of event, loaded if _loaded_row # security: "public", "private", "usemask", loaded if _loaded_row # allowmask: if _loaded_row # posterid: if _loaded_row # comments: arrayref of comment objects on this entry # talkdata: hash of raw comment data for this entry # userpic # _loaded_text: loaded subject/text # _loaded_row: loaded log2 row # _loaded_props: loaded props # _loaded_comments: loaded comments # _loaded_talkdata: loaded talkdata my %singletons = (); # journalid->jitemid->singleton sub reset_singletons { %singletons = (); } # # name: LJ::Entry::new # class: entry # des: Gets a journal entry. # args: uuserid, opts # des-uuserid: A user id or user object ($u ) to load the entry for. # des-opts: Hash of optional keypairs. # 'jitemid' => a journal itemid (no anum) # 'ditemid' => display itemid (a jitemid << 8 + anum) # 'anum' => the id passed was an ditemid, use the anum # to create a proper jitemid. # 'slug' => the slug in the URL to load from # returns: A new LJ::Entry object. undef on failure. # sub new { my $class = shift; my $self = bless {}; my $uuserid = shift; my $n_arg = scalar @_; croak("wrong number of arguments") unless $n_arg && ( $n_arg % 2 == 0 ); my %opts = @_; croak("can't supply both anum and ditemid") if defined $opts{anum} && defined $opts{ditemid}; croak("can't supply both itemid and ditemid") if defined $opts{ditemid} && defined $opts{jitemid}; croak("can't supply slug with anything else") if defined $opts{slug} && ( defined $opts{jitemid} || defined $opts{ditemid} ); # FIXME: don't store $u in here, or at least call LJ::load_userids() on all singletons # if LJ::want_user() would have been called $self->{u} = LJ::want_user($uuserid) or croak("invalid user/userid parameter: $uuserid"); $self->{anum} = delete $opts{anum}; $self->{ditemid} = delete $opts{ditemid}; $self->{jitemid} = delete $opts{jitemid}; $self->{slug} = LJ::canonicalize_slug( delete $opts{slug} ); # make arguments numeric for my $f (qw(ditemid jitemid anum)) { $self->{$f} = int( $self->{$f} ) if defined $self->{$f}; } croak("need to supply either a jitemid or ditemid or slug") unless defined $self->{ditemid} || defined $self->{jitemid} || defined $self->{slug}; croak( "Unknown parameters: " . join( ", ", keys %opts ) ) if %opts; if ( $self->{ditemid} ) { $self->{_untrusted_anum} = $self->{ditemid} & 255; $self->{jitemid} = $self->{ditemid} >> 8; } # If specified by slug, look it up in the database. # FIXME: This should be memcached in some efficient method. By slug? if ( defined $self->{slug} ) { my $jitemid = $self->{u} ->selectrow_array( q{SELECT jitemid FROM logslugs WHERE journalid = ? AND slug = ?}, undef, $self->{u}->id, $self->{slug} ); croak $self->{u}->errstr if $self->{u}->err; return undef unless $jitemid; return LJ::Entry->new( $self->{u}, jitemid => $jitemid ); } # do we have a singleton for this entry? { my $journalid = $self->{u}->{userid}; my $jitemid = $self->{jitemid}; $singletons{$journalid} ||= {}; return $singletons{$journalid}->{$jitemid} if $singletons{$journalid}->{$jitemid}; # save the singleton if it doesn't exist $singletons{$journalid}->{$jitemid} = $self; } return $self; } # sometimes item hashes don't have a journalid arg. # in those cases call as ($u, $item) and the $u will # be used sub new_from_item_hash { my ( $class, $arg1, $item ) = @_; if ( LJ::isu($arg1) ) { $item->{journalid} ||= $arg1->id; } else { $item = $arg1; } # some item hashes have 'jitemid', others have 'itemid' $item->{jitemid} ||= $item->{itemid}; croak "invalid item hash" unless $item && ref $item; croak "no journalid in item hash" unless $item->{journalid}; croak "no entry information in item hash" unless $item->{ditemid} || ( $item->{jitemid} && defined( $item->{anum} ) ); my $entry; # have a ditemid only? no problem. if ( $item->{ditemid} ) { $entry = LJ::Entry->new( $item->{journalid}, ditemid => $item->{ditemid} ); # jitemid/anum is okay too } elsif ( $item->{jitemid} && defined( $item->{anum} ) ) { $entry = LJ::Entry->new( $item->{journalid}, jitemid => $item->{jitemid}, anum => $item->{anum} ); } return $entry; } sub new_from_url { my ( $class, $url ) = @_; if ( $url =~ m!^(.+)/(\d+)\.html$! ) { my $u = LJ::User->new_from_url($1) or return undef; return LJ::Entry->new( $u, ditemid => $2 ); } elsif ( $url =~ m!^(.+)/(\d\d\d\d/\d\d/\d\d)/([a-z0-9_-]+)\.html$! ) { my $u = LJ::User->new_from_url($1) or return undef; # This hack validates that the YYYY/MM/DD given to us is correct. my $date = $2; my $ljentry = LJ::Entry->new( $u, slug => $3 ); if ( defined $ljentry ) { my $dt = join( '/', split( '-', substr( $ljentry->eventtime_mysql, 0, 10 ) ) ); return undef unless $dt eq $date; return $ljentry; } } return undef; } sub new_from_url_or_ditemid { my ( $class, $input, $u ) = @_; my $e = LJ::Entry->new_from_url($input); # couldn't be parsed as a URL, try as a ditemid $e ||= LJ::Entry->new( $u, ditemid => $input ) if $input =~ /^(?:\d+)$/; return $e && $e->valid ? $e : undef; } sub new_from_row { my ( $class, %row ) = @_; my $journalu = LJ::load_userid( $row{journalid} ); my $self = $class->new( $journalu, jitemid => $row{jitemid} ); $self->absorb_row(%row); return $self; } =head1 INSTANCE METHODS =cut # 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]; __PACKAGE__->preload_rows( [$self] ) unless $self->{_loaded_row}; return $self->{_loaded_row}; } sub jitemid { my $self = $_[0]; return $self->{jitemid}; } sub ditemid { my $self = $_[0]; return $self->{ditemid} ||= ( ( $self->{jitemid} << 8 ) + $self->anum ); } sub reply_url { my $self = $_[0]; return $self->url( mode => 'reply' ); } # returns permalink url sub url { my ( $self, %opts ) = @_; my %style_opts = %{ delete $opts{style_opts} || {} }; my %args = %opts; # used later @args{ keys %style_opts } = values %style_opts; my $u = $self->{u}; my $view = delete $opts{view}; my $anchor = delete $opts{anchor}; my $mode = delete $opts{mode}; croak "Unknown args passed to url: " . join( ",", keys %opts ) if %opts; my $override = LJ::Hooks::run_hook( "entry_permalink_override", $self, %opts ); return $override if $override; my $base_url = $self->ditemid; if ( my $slug = $self->slug ) { my $ymd = join( '/', split( '-', substr( $self->eventtime_mysql, 0, 10 ) ) ); $base_url = $ymd . "/" . $slug; } my $url = $u->journal_base . "/" . $base_url . ".html"; delete $args{anchor}; if (%args) { $url .= "?"; $url .= LJ::encode_url_string( \%args ); } $url .= "#$anchor" if $anchor; return $url; } # returns a url that will display the number of comments on the entry # as an image sub comment_image_url { my $self = $_[0]; my $u = $self->{u}; return "$LJ::SITEROOT/tools/commentcount?user=" . $self->journal->user . "&ditemid=" . $self->ditemid; } # returns a pre-generated comment img tag using the comment_image_url sub comment_imgtag { my $self = $_[0]; my $alttext = LJ::Lang::ml('setting.xpost.option.footer.vars.comment_image.alt'); return ''
        . $alttext
        . ''; } sub anum { my $self = $_[0]; return $self->{anum} if defined $self->{anum}; __PACKAGE__->preload_rows( [$self] ) unless $self->{_loaded_row}; return $self->{anum} if defined $self->{anum}; croak("couldn't retrieve anum for entry"); } # method: # $entry->correct_anum # $entry->correct_anum($given_anum) # if no given anum, gets it from the provided ditemid to constructor # Note: an anum parsed from the ditemid cannot be trusted which is what we're verifying here sub correct_anum { my ( $self, $given ) = @_; $given = defined $given ? int($given) : $self->{ditemid} ? $self->{_untrusted_anum} : $self->{anum}; return 0 unless $self->valid; return 0 unless defined $self->{anum} && defined $given; return $self->{anum} == $given; } # returns LJ::User object for the poster of this entry sub poster { my $self = $_[0]; return LJ::load_userid( $self->posterid ); } sub posterid { my $self = $_[0]; __PACKAGE__->preload_rows( [$self] ) unless $self->{_loaded_row}; return $self->{posterid}; } sub journalid { my $self = $_[0]; return $self->{u}{userid}; } sub journal { my $self = $_[0]; return $self->{u}; } sub eventtime_mysql { my $self = $_[0]; __PACKAGE__->preload_rows( [$self] ) unless $self->{_loaded_row}; return $self->{eventtime}; } sub logtime_mysql { my $self = $_[0]; __PACKAGE__->preload_rows( [$self] ) unless $self->{_loaded_row}; return $self->{logtime}; } sub logtime_unix { my $self = $_[0]; __PACKAGE__->preload_rows( [$self] ) unless $self->{_loaded_row}; return LJ::mysqldate_to_time( $self->{logtime}, 1 ); } sub modtime_unix { my $self = $_[0]; return $self->prop("revtime") || $self->logtime_unix; } sub security { my $self = $_[0]; __PACKAGE__->preload_rows( [$self] ) unless $self->{_loaded_row}; return $self->{security}; } sub allowmask { my $self = $_[0]; __PACKAGE__->preload_rows( [$self] ) unless $self->{_loaded_row}; return $self->{allowmask}; } sub preload { my ( $class, $entlist ) = @_; $class->preload_rows($entlist); $class->preload_props($entlist); # TODO: $class->preload_text($entlist); } # class method: sub preload_rows { my ( $class, $entlist ) = @_; foreach my $en (@$entlist) { next if $en->{_loaded_row}; my $lg = LJ::get_log2_row( $en->{u}, $en->{jitemid} ); next unless $lg; # absorb row into given LJ::Entry object $en->absorb_row(%$lg); } } sub absorb_row { my ( $self, %row ) = @_; $self->{$_} = $row{$_} foreach (qw(allowmask posterid eventtime logtime security anum)); $self->{_loaded_row} = 1; } # class method: sub preload_props { my ( $class, $entlist ) = @_; foreach my $en (@$entlist) { next if $en->{_loaded_props}; $en->_load_props; } } # method for preloading props into all outstanding singletons that haven't already # loaded properties. sub preload_props_all { foreach my $uid ( keys %singletons ) { my $hr = $singletons{$uid}; my @load; foreach my $jid ( keys %$hr ) { next if $hr->{$jid}->{_loaded_props}; push @load, $jid; } my $props = {}; LJ::load_log_props2( $uid, \@load, $props ); foreach my $jid ( keys %$props ) { $hr->{$jid}->{props} = $props->{$jid}; $hr->{$jid}->{_loaded_props} = 1; } } } # returns array of tags for this post sub tags { my $self = $_[0]; my $taginfo = LJ::Tags::get_logtags( $self->journal, $self->jitemid ); return () unless $taginfo; my $entry_taginfo = $taginfo->{ $self->jitemid }; return () unless $entry_taginfo; return values %$entry_taginfo; } # 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 $ret = LJ::get_logtext2( $self->{'u'}, $self->{'jitemid'} ); my $lt = $ret->{ $self->{jitemid} }; return 0 unless $lt; $self->{subject} = $lt->[0]; $self->{event} = $lt->[1]; if ( $self->prop("unknown8bit") ) { # save the old ones away, so we can get back at them if we really need to $self->{subject_orig} = $self->{subject}; $self->{event_orig} = $self->{event}; # FIXME: really convert all the props? what if we binary-pack some in the future? LJ::item_toutf8( $self->{u}, \$self->{'subject'}, \$self->{'event'}, $self->{props} ); } $self->{_loaded_text} = 1; return 1; } sub slug { my $self = $_[0]; my $u = $self->{u}; my $jid = $u->id; # Get the slug from ourself, memcache, or the database. Populate both if # we do get the data. if ( scalar @_ == 1 ) { return $self->{slug} if $self->{_loaded_slug}; $self->{_loaded_slug} = 1; my $mc = LJ::MemCache::get( [ $jid, "logslug:$jid:$self->{jitemid}" ] ); return $self->{slug} = $mc if defined $mc; my $db = $u->selectrow_array( q{SELECT slug FROM logslugs WHERE journalid = ? AND jitemid = ?}, undef, $jid, $self->{jitemid} ) || ''; croak $u->errstr if $u->err; LJ::MemCache::set( [ $jid, "logslug:$jid:$self->{jitemid}" ], $db ); return $self->{slug} = $db; } # If deletion... if ( !defined $_[1] ) { $u->do( 'DELETE FROM logslugs WHERE journalid = ? AND jitemid = ?', undef, $jid, $self->{jitemid} ); croak $u->errstr if $u->err; LJ::MemCache::set( [ $jid, "logslug:$jid:$self->{jitemid}" ], '' ); $self->{_loaded_slug} = 1; return $self->{slug} = undef; } # Set it... my $slug = LJ::canonicalize_slug( $_[1] ); croak 'Invalid slug' unless defined $slug && length $slug > 0; return $self->{slug} if defined $self->{slug} && $self->{slug} eq $slug; # Ensure this slug isn't already used... my $et = LJ::Entry->new( $u, slug => $slug ); croak 'Slug already in use' if defined $et; # Looks good, now update our slug database (REPLACE since we're updating) $u->do( 'REPLACE INTO logslugs (journalid, jitemid, slug) VALUES (?, ?, ?)', undef, $jid, $self->{jitemid}, $slug ); croak $u->errstr if $u->err; LJ::MemCache::set( [ $jid, "logslug:$jid:$self->{jitemid}" ], $slug ); $self->{_loaded_slug} = 1; return $self->{slug} = $slug; } sub prop { my ( $self, $prop ) = @_; $self->_load_props unless $self->{_loaded_props}; return $self->{props}{$prop}; } sub props { my ( $self, $prop ) = @_; $self->_load_props unless $self->{_loaded_props}; return $self->{props} || {}; } sub _load_props { my $self = $_[0]; return 1 if $self->{_loaded_props}; my $props = {}; LJ::load_log_props2( $self->{u}, [ $self->{jitemid} ], $props ); $self->{props} = $props->{ $self->{jitemid} }; $self->{_loaded_props} = 1; return 1; } sub set_prop { my ( $self, $prop, $val ) = @_; LJ::set_logprop( $self->journal, $self->jitemid, { $prop => $val } ); $self->{props}{$prop} = $val; return 1; } # called automatically on $event->comments # returns the same data as LJ::get_talk_data, with the addition # of 'subject' and 'event' keys. sub _load_comments { my $self = $_[0]; return 1 if $self->{_loaded_comments}; # need to load using talklib API my $comment_ref = $self->{_loaded_talkdata} ? $self->{talkdata} : LJ::Talk::get_talk_data( $self->journal, 'L', $self->jitemid ); die "unable to load comment data for entry" unless ref $comment_ref; my @comment_list; my $u = $self->journal; my $nodeid = $self->jitemid; # instantiate LJ::Comment singletons and set them on our $self foreach my $jtalkid ( keys %$comment_ref ) { my $row = $comment_ref->{$jtalkid}; # at this point we have data for this comment loaded in memory # -- instantiate an LJ::Comment object as a singleton and absorb # that data into the object my $comment = LJ::Comment->new( $u, jtalkid => $jtalkid ); # add important info to row $row->{nodetype} = "L"; $row->{nodeid} = $nodeid; $comment->absorb_row(%$row); push @comment_list, $comment; } $self->set_comment_list(@comment_list); return $self; } sub comment_list { my $self = $_[0]; $self->_load_comments unless $self->{_loaded_comments}; return @{ $self->{comments} || [] }; } sub set_comment_list { my ( $self, @args ) = @_; $self->{comments} = \@args; $self->{_loaded_comments} = 1; return 1; } sub set_talkdata { my ( $self, $talkdata ) = @_; $self->{talkdata} = $talkdata; $self->{_loaded_talkdata} = 1; return 1; } sub reply_count { my ( $self, %opts ) = @_; unless ( $opts{force_lookup} ) { my $rc = $self->prop('replycount'); return $rc if defined $rc; } return LJ::Talk::get_replycount( $self->journal, $self->jitemid ); } # returns "Leave a comment", "1 comment", "2 comments" etc sub comment_text { my $self = $_[0]; my $comments; my $comment_count = $self->reply_count; if ($comment_count) { $comments = $comment_count == 1 ? "1 Comment" : "$comment_count Comments"; } else { $comments = "Leave a comment"; } return $comments; } # returns data hashref suitable for use in S2 CommentInfo function sub comment_info { my ( $self, %opts ) = @_; return unless %opts; return unless exists $opts{u}; return unless exists $opts{remote}; return unless exists $opts{style_args}; my $u = $opts{u}; # the journal being viewed my $remote = $opts{remote}; # the person viewing the page my $style_args = $opts{style_args}; my $viewall = $opts{viewall}; my $journal = exists $opts{journal} ? $opts{journal} : $u; # journal entry was posted in # may be different from $u on a read page my $permalink = $self->url; my $comments_enabled = ( $viewall || ( $journal->{opt_showtalklinks} eq "Y" && !$self->comments_disabled ) ) ? 1 : 0; my $has_screened = ( $self->props->{hasscreened} && $remote && $journal && $remote->can_manage($journal) ) ? 1 : 0; my $screenedcount = $has_screened ? LJ::Talk::get_screenedcount( $journal, $self->jitemid ) : 0; my $replycount = $comments_enabled ? $self->reply_count : 0; my $nc = ""; $nc .= "nc=$replycount" if $replycount && $remote && $remote->{opt_nctalklinks}; return { read_url => LJ::Talk::talkargs( $permalink, $nc, $style_args ), post_url => LJ::Talk::talkargs( $permalink, "mode=reply", $style_args ), permalink_url => LJ::Talk::talkargs( $permalink, $style_args ), count => $replycount, maxcomments => ( $replycount >= $u->count_maxcomments ) ? 1 : 0, enabled => $comments_enabled, comments_disabled_maintainer => $self->comments_disabled_maintainer, screened => $has_screened, screened_count => $screenedcount, show_readlink => $comments_enabled && ( $replycount || $has_screened ), show_readlink_hidden => $comments_enabled, show_postlink => $comments_enabled, }; } # used in comment notification email headers sub email_messageid { my $self = $_[0]; return "<" . join( "-", "entry", $self->journal->id, $self->ditemid ) . "\@$LJ::DOMAIN>"; } sub atom_id { my $self = $_[0]; my $u = $self->{u}; my $ditemid = $self->ditemid; return $u->atomid . ":$ditemid"; } # returns an XML::Atom::Entry object for a feed # opts: synlevel ("full"), apilinks (bool) sub atom_entry { my ( $self, %opts ) = @_; my $atom_entry = XML::Atom::Entry->new( Version => 1 ); my $u = $self->{u}; my $make_link = sub { my ( $rel, $href, $type, $title ) = @_; my $link = XML::Atom::Link->new( Version => 1 ); $link->rel($rel); $link->href($href); $link->title($title) if $title; $link->type($type) if $type; return $link; }; $atom_entry->id( $self->atom_id ); $atom_entry->title( $self->subject_text ); $atom_entry->published( LJ::time_to_w3c( $self->logtime_unix, "Z" ) ); $atom_entry->updated( LJ::time_to_w3c( $self->modtime_unix, 'Z' ) ); my $author = XML::Atom::Person->new( Version => 1 ); $author->name( $self->poster->name_orig ); $atom_entry->author($author); $atom_entry->add_link( $make_link->( "alternate", $self->url, "text/html" ) ); $atom_entry->add_link( $make_link->( "edit", $self->atom_url, "application/atom+xml", "Edit this post" ) ) if $opts{apilinks}; foreach my $tag ( $self->tags ) { my $category = XML::Atom::Category->new( Version => 1 ); $category->term($tag); $atom_entry->add_category($category); } my $syn_level = $opts{synlevel} || $u->prop("opt_synlevel") || "full"; # if syndicating the complete entry # -print a content tag # elsif syndicating summaries # -print a summary tag # else (code omitted), we're syndicating title only # -print neither (the title has already been printed) # note: the $event was also emptied earlier, in make_feed # # a lack of a content element is allowed, as long # as we maintain a proper 'alternate' link (above) if ( $syn_level eq 'full' || $syn_level eq 'cut' ) { $atom_entry->content( $self->event_raw ); } elsif ( $syn_level eq 'summary' ) { $atom_entry->summary( $self->event_summary ); } return $atom_entry; } sub atom_url { my $self = $_[0]; return "" unless $self->journal; return $self->journal->atom_base . "/entries/" . $self->jitemid; } # returns the entry as an XML Atom string, without the XML prologue sub as_atom { my $self = $_[0]; my $entry = $self->atom_entry; my $xml = $entry->as_xml; $xml =~ s!^<\?xml.+?>\s*!!s; return $xml; } # 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 event_raw { my $self = $_[0]; $self->_load_text unless $self->{_loaded_text}; return $self->{event}; } # raw text as user sent us, without transcoding while correcting for unknown8bit sub event_orig { my $self = $_[0]; $self->_load_text unless $self->{_loaded_text}; return $self->{event_orig} || $self->{event}; } sub subject_html { my $self = $_[0]; $self->_load_text unless $self->{_loaded_text}; my $subject = $self->{subject}; LJ::CleanHTML::clean_subject( \$subject ) if $subject; return $subject; } sub subject_text { my $self = $_[0]; $self->_load_text unless $self->{_loaded_text}; my $subject = $self->{subject}; LJ::CleanHTML::clean_subject_all( \$subject ) if $subject; return $subject; } # instance method. returns HTML-cleaned/formatted version of the event # optional $opt may be: # undef: loads the opt_preformatted key and uses that for formatting options # 1: treats entry as preformatted (no breaks applied) # 0: treats entry as normal (newlines convert to HTML breaks) # hashref: extra arguments to LJ::CleanHTML::clean_event sub event_html { my ( $self, $opts ) = @_; $self->_load_props unless $self->{_loaded_props}; if ( !defined $opts ) { $opts = {}; } elsif ( !ref $opts ) { $opts = { preformatted => $opts }; } # Caller can override preformatted (but: plz don't.) if ( !defined $opts->{preformatted} ) { $opts->{preformatted} = $self->prop('opt_preformatted'); } my $remote = LJ::get_remote(); my $suspend_msg = $self->should_show_suspend_msg_to($remote) ? 1 : 0; $opts->{suspend_msg} = $suspend_msg; $opts->{journal} = $self->{u}->user; $opts->{ditemid} = $self->{ditemid}; $opts->{is_syndicated} = $self->{u}->is_syndicated; $opts->{is_imported} = defined $self->{props}{import_source}; $opts->{editor} = $self->prop('editor'); $opts->{logtime_mysql} = $self->logtime_mysql; # for format guessing $self->_load_text unless $self->{_loaded_text}; my $event = $self->{event}; LJ::CleanHTML::clean_event( \$event, $opts ); LJ::expand_embedded( $self->{u}, $self->ditemid, LJ::User->remote, \$event, sandbox => $opts->{sandbox}, ); return $event; } # like event_html, but trimmed to $char_max sub event_html_summary { my ( $self, $char_max, $opts, $trunc_ref ) = @_; return LJ::html_trim( $self->event_html($opts), $char_max, $trunc_ref ); } sub event_text { my $self = $_[0]; my $event = $self->event_raw; LJ::CleanHTML::clean_event( \$event, { textonly => 1 } ) if $event; return $event; } # like event_html, but truncated for summary mode in rss/atom sub event_summary { my $self = $_[0]; my $url = $self->url; my $readmore = "(Read more ...)"; return LJ::Entry->summarize( $self->event_html, $readmore ); } # class method for truncation sub summarize { my ( $class, $event, $readmore ) = @_; return '' unless defined $event; # assume the first paragraph is terminated by two
or a

# valid XML tags should be handled, even though it makes an uglier regex if ( $event =~ m!(.*?(?:(?:(?:)?\s*){2}|))!i ) { # everything before the matched tag + the tag itself # + a link to read more $event = $1 . $readmore; } return $event; } sub comments_manageable_by { my ( $self, $remote ) = @_; return 0 unless $self->valid; return 0 unless $remote; my $u = $self->{u}; return $remote->userid == $self->posterid || $remote->can_manage($u); } # instance method: returns bool, if remote user can edit this entry # use this to determine whether to, e.g., show edit buttons or an edit form # but don't use this when saving stuff to the database -- those need to pass through the protocol # does not care about readonly status, just about permissions sub editable_by { my ( $self, $remote ) = @_; return 0 unless LJ::isu($remote); return 0 unless $self->visible_to($remote); # remote is editing their own entry return 1 if $self->posterid == $remote->userid; # editing an entry that's not your personal journal return 1 if $self->journalid != $self->posterid && $remote->can_manage( $self->journal ); return 0; } # instance method: returns bool, if remote user can view this entry sub visible_to { my ( $self, $remote, $canview ) = @_; return 0 unless $self->valid; my ( $viewall, $viewsome ) = ( 0, 0 ); if ( LJ::isu($remote) && $canview ) { $viewall = $remote->has_priv( 'canview', '*' ); $viewsome = $viewall || $remote->has_priv( 'canview', 'suspended' ); } # can see anything with viewall return 1 if $viewall; # can't see anything unless the journal is visible # unless you have viewsome. then, other restrictions apply unless ($viewsome) { return 0 if $self->journal->is_inactive; # can't see anything by suspended users return 0 if $self->poster->is_suspended; # can't see suspended entries return 0 if $self->is_suspended_for($remote); } # public is okay return 1 if $self->security eq "public"; # must be logged in otherwise return 0 unless $remote; my $userid = int( $self->{u}{userid} ); my $remoteid = int( $remote->{userid} ); # owners can always see their own. return 1 if $userid == $remoteid; # should be 'usemask' or 'private' security from here out, otherwise # assume it's something new and return 0 return 0 unless $self->security eq "usemask" || $self->security eq "private"; return 0 unless $remote->is_individual; if ( $self->security eq "private" ) { # other people can't read private on personal journals return 0 if $self->journal->is_individual; # but community administrators can read private entries on communities return 1 if $self->journal->is_community && $remote->can_manage( $self->journal ); # private entry on a community; we're not allowed to see this return 0; } if ( $self->security eq "usemask" ) { # check if it's a community and they're a member return 1 if $self->journal->is_community && $remote->member_of( $self->journal ); my $gmask = $self->journal->trustmask($remote); my $allowed = ( int($gmask) & int( $self->{'allowmask'} ) ); return $allowed ? 1 : 0; # no need to return matching mask } return 0; } # returns hashref of (kwid => tag) for tags on the entry sub tag_map { my $self = $_[0]; my $tags = LJ::Tags::get_logtags( $self->{u}, $self->jitemid ); return {} unless $tags; return $tags->{ $self->jitemid } || {}; } =head2 C<< $entry->admin_post >> Returns true if this post is an official administrator post. =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] ) { return $_[0]->set_prop( 'admin_post', $_[1] ? 1 : 0 ); } else { return $_[0]->prop('admin_post') ? 1 : 0; } } =head2 C<< $entry->userpic >> Returns a LJ::Userpic object for this post, or undef. If called in a list context, returns ( LJ::Userpic object, keyword ) See userpic_kw. =cut # FIXME: add a context option for friends page, and perhaps # respect $remote's userpic viewing preferences (community shows poster # vs community's picture) sub userpic { my $up = $_[0]->poster; my $kw = $_[0]->userpic_kw; my $pic = LJ::Userpic->new_from_keyword( $up, $kw ) || $up->userpic; return wantarray ? ( $pic, $kw ) : $pic; } =head2 C<< $entry->userpic_kw >> Returns the keyword to use for the entry. If a keyword is specified, it uses that. =cut sub userpic_kw { my $self = $_[0]; my $up = $self->poster; my $key; # try their entry-defined userpic keyword if ( $up->userpic_have_mapid ) { my $mapid = $self->prop('picture_mapid'); $key = $up->get_keyword_from_mapid($mapid) if $mapid; } else { $key = $self->prop('picture_keyword'); } return $key; } # returns true if the user is allowed to share an entry via Tell a Friend # $u is the logged-in user # $item is a hash containing Entry info sub can_tellafriend { my ( $entry, $u ) = @_; # this is undefined in preview my $seclevel = $entry->security // ''; return 1 if $seclevel eq 'public'; return 0 if $seclevel eq 'private'; # friends only return 0 unless $entry->journal->is_person; return 0 unless $u && $u->equals( $entry->poster ); return 1; } # defined by the entry poster sub adult_content { my $self = $_[0]; return $self->prop('adult_content'); } # defined by a community maintainer sub adult_content_maintainer { my $self = $_[0]; my $userLevel = $self->adult_content; my $maintLevel = $self->prop('adult_content_maintainer'); return undef unless $maintLevel; return $maintLevel if $userLevel eq $maintLevel; return $maintLevel if !$userLevel || $userLevel eq "none"; return $maintLevel if $userLevel eq "concepts" && $maintLevel eq "explicit"; return undef; } # defined by a community maintainer sub adult_content_maintainer_reason { my $self = $_[0]; return $self->prop('adult_content_maintainer_reason'); } # defined by the entry poster sub adult_content_reason { my $self = $_[0]; return $self->prop('adult_content_reason'); } # uses both poster- and maintainer-defined props to figure out the adult content level sub adult_content_calculated { my $self = $_[0]; return $self->adult_content_maintainer if $self->adult_content_maintainer; return $self->adult_content; } # returns who marked the entry as the 'adult_content_calculated' adult content level sub adult_content_marker { my $self = $_[0]; return "community" if $self->adult_content_maintainer; return "poster" if $self->adult_content; return $self->journal->adult_content_marker; } # return whether this entry has comment emails enabled or not sub comment_email_disabled { my $self = $_[0]; my $entry_no_email = $self->prop('opt_noemail'); return $entry_no_email if $entry_no_email; #my $journal_no_email = $self-> return 0; } # return whether this entry has comments disabled, either by the poster or by the maintainer sub comments_disabled { my $self = $_[0]; return $self->prop('opt_nocomments') || $self->prop('opt_nocomments_maintainer'); } # return whether comments were disabled by the entry poster sub comments_disabled_poster { return $_[0]->prop('opt_nocomments'); } # return whether this post had its comments disabled by a community maintainer (not by the poster, who can override the community moderator) sub comments_disabled_maintainer { my $self = $_[0]; return $self->prop('opt_nocomments_maintainer') && !$self->comments_disabled_poster; } sub should_block_robots { my $self = $_[0]; return 1 if $self->journal->prop('opt_blockrobots'); return 0 unless LJ::is_enabled('adult_content'); my $adult_content = $self->adult_content_calculated; return 1 if $adult_content && $LJ::CONTENT_FLAGS{$adult_content} && $LJ::CONTENT_FLAGS{$adult_content}->{block_robots}; return 0; } sub syn_link { my $self = $_[0]; return $self->prop('syn_link'); } # group names to be displayed with this entry # returns nothing if remote is not the poster of the entry # returns names as links to the /security/ URLs if the user can use those URLs # returns names as plaintext otherwise sub group_names { my $self = $_[0]; my $remote = LJ::get_remote(); my $poster = $self->poster; return "" unless $remote && $poster && $poster->equals($remote); return $poster->security_group_display( $self->allowmask ); } sub statusvis { my $self = $_[0]; my $vis = $self->prop("statusvis") || ''; return $vis eq "S" ? "S" : "V"; } sub is_backdated { my $self = $_[0]; return $self->prop('opt_backdated') ? 1 : 0; } sub is_visible { my $self = $_[0]; return $self->statusvis eq "V" ? 1 : 0; } sub is_suspended { my $self = $_[0]; return $self->statusvis eq "S" ? 1 : 0; } # same as is_suspended, except that it returns 0 if the given user can see the suspended entry sub is_suspended_for { my ( $self, $u ) = @_; return 0 unless $self->is_suspended; return 1 unless LJ::isu($u); # see if $u has access return 0 if $u->has_priv( 'canview', 'suspended' ); return 0 if $u->equals( $self->poster ); return 1; } sub should_show_suspend_msg_to { my ( $self, $u ) = @_; return $self->is_suspended && !$self->is_suspended_for($u) ? 1 : 0; } # some entry props must keep all their history sub put_logprop_in_history { my ( $self, $prop, $old_value, $new_value, $note ) = @_; my $p = LJ::get_prop( "log", $prop ); return undef unless $p; my $propid = $p->{id}; my $u = $self->journal; $u->do( "INSERT INTO logprop_history (journalid, jitemid, propid, change_time, old_value, new_value, note) VALUES (?, ?, ?, unix_timestamp(), ?, ?, ?)", undef, $self->journalid, $self->jitemid, $propid, $old_value, $new_value, $note ); return undef if $u->err; return 1; } package LJ; use Carp qw(confess); use LJ::Poll; use LJ::EmbedModule; use DW::External::Account; # # name: LJ::get_posts_raw # des: Gets raw post data (text and props) efficiently from clusters. # info: Fetches posts from clusters, trying memcache and slaves first if available. # returns: hashref with keys 'text', 'prop', or 'replycount', and values being # hashrefs with keys "jid:jitemid". values of that are as follows: # text: [ $subject, $body ], props: { ... }, and replycount: scalar # args: opts?, id # des-opts: An optional hashref of options: # - memcache_only: Don't fall back on the database. # - text_only: Retrieve only text, no props (used to support old API). # - prop_only: Retrieve only props, no text (used to support old API). # des-id: An arrayref of [ clusterid, ownerid, itemid ]. # sub get_posts_raw { my $opts = ref $_[0] eq "HASH" ? shift : {}; my $ret = {}; my $sth; LJ::load_props('log') unless $opts->{text_only}; # throughout this function, the concept of an "id" # is the key to identify a single post. # it is of the form "$jid:$jitemid". # build up a list for each cluster of what we want to get, # as well as a list of all the keys we want from memcache. my %cids; # cid => 1 my $needtext; # text needed: $cid => $id => 1 my $needprop; # props needed: $cid => $id => 1 my $needrc; # replycounts needed: $cid => $id => 1 my @mem_keys; # if we're loading entries for a friends page, # silently failing to load a cluster is acceptable. # but for a single user, we want to die loudly so they don't think # we just lost their journal. my $single_user; # because the memcache keys for logprop don't contain # which cluster they're in, we also need a map to get the # cid back from the jid so we can insert into the needfoo hashes. # the alternative is to not key the needfoo hashes on cluster, # but that means we need to grep out each cluster's jids when # we do per-cluster queries on the databases. my %cidsbyjid; foreach my $post (@_) { my ( $cid, $jid, $jitemid ) = @{$post}; my $id = "$jid:$jitemid"; if ( not defined $single_user ) { $single_user = $jid; } elsif ( $single_user and $jid != $single_user ) { # multiple users $single_user = 0; } $cids{$cid} = 1; $cidsbyjid{$jid} = $cid; unless ( $opts->{prop_only} ) { $needtext->{$cid}{$id} = 1; push @mem_keys, [ $jid, "logtext:$cid:$id" ]; } unless ( $opts->{text_only} ) { $needprop->{$cid}{$id} = 1; push @mem_keys, [ $jid, "logprop:$id" ]; $needrc->{$cid}{$id} = 1; push @mem_keys, [ $jid, "rp:$id" ]; } } # first, check memcache. my $mem = LJ::MemCache::get_multi(@mem_keys) || {}; while ( my ( $k, $v ) = each %$mem ) { next unless defined $v; next unless $k =~ /(\w+):(?:\d+:)?(\d+):(\d+)/; my ( $type, $jid, $jitemid ) = ( $1, $2, $3 ); my $cid = $cidsbyjid{$jid}; my $id = "$jid:$jitemid"; if ( $type eq "logtext" ) { delete $needtext->{$cid}{$id}; $ret->{text}{$id} = $v; } elsif ( $type eq "logprop" && ref $v eq "HASH" ) { delete $needprop->{$cid}{$id}; $ret->{prop}{$id} = $v; } elsif ( $type eq "rp" ) { delete $needrc->{$cid}{$id}; $ret->{replycount}{$id} = int($v); # remove possible spaces } } # we may be done already. return $ret if $opts->{memcache_only}; return $ret unless values %$needtext or values %$needprop or values %$needrc; # otherwise, hit the database. foreach my $cid ( keys %cids ) { # for each cluster, get the text/props we need from it. my $cneedtext = $needtext->{$cid} || {}; my $cneedprop = $needprop->{$cid} || {}; my $cneedrc = $needrc->{$cid} || {}; next unless %$cneedtext or %$cneedprop or %$cneedrc; my $make_in = sub { my @in; foreach my $id (@_) { my ( $jid, $jitemid ) = map { $_ + 0 } split( /:/, $id ); push @in, "(journalid=$jid AND jitemid=$jitemid)"; } return join( " OR ", @in ); }; # now load from each cluster. my $fetchtext = sub { my $db = $_[0]; return unless %$cneedtext; my $in = $make_in->( keys %$cneedtext ); $sth = $db->prepare( "SELECT journalid, jitemid, subject, event " . "FROM logtext2 WHERE $in" ); $sth->execute; while ( my ( $jid, $jitemid, $subject, $event ) = $sth->fetchrow_array ) { LJ::text_uncompress( \$event ); my $id = "$jid:$jitemid"; my $val = [ $subject, $event ]; $ret->{text}{$id} = $val; LJ::MemCache::add( [ $jid, "logtext:$cid:$id" ], $val ); delete $cneedtext->{$id}; } }; my $fetchprop = sub { my $db = $_[0]; return unless %$cneedprop; my $in = $make_in->( keys %$cneedprop ); $sth = $db->prepare( "SELECT journalid, jitemid, propid, value " . "FROM logprop2 WHERE $in" ); $sth->execute; my %gotid; while ( my ( $jid, $jitemid, $propid, $value ) = $sth->fetchrow_array ) { my $id = "$jid:$jitemid"; my $propname = $LJ::CACHE_PROPID{'log'}->{$propid}{name}; $ret->{prop}{$id}{$propname} = $value; $gotid{$id} = 1; } foreach my $id ( keys %gotid ) { my ( $jid, $jitemid ) = map { $_ + 0 } split( /:/, $id ); LJ::MemCache::add( [ $jid, "logprop:$id" ], $ret->{prop}{$id} ); delete $cneedprop->{$id}; } }; my $fetchrc = sub { my $db = $_[0]; return unless %$cneedrc; my $in = $make_in->( keys %$cneedrc ); $sth = $db->prepare("SELECT journalid, jitemid, replycount FROM log2 WHERE $in"); $sth->execute; while ( my ( $jid, $jitemid, $rc ) = $sth->fetchrow_array ) { my $id = "$jid:$jitemid"; $ret->{replycount}{$id} = $rc; LJ::MemCache::add( [ $jid, "rp:$id" ], $rc ); delete $cneedrc->{$id}; } }; my $dberr = sub { die "Couldn't connect to database" if $single_user; next; }; # run the fetch functions on the proper databases, with fallbacks if necessary. my ( $dbcm, $dbcr ); if ( @LJ::MEMCACHE_SERVERS or $opts->{use_master} ) { $dbcm ||= LJ::get_cluster_master($cid) or $dberr->(); $fetchtext->($dbcm) if %$cneedtext; $fetchprop->($dbcm) if %$cneedprop; $fetchrc->($dbcm) if %$cneedrc; } else { $dbcr ||= LJ::get_cluster_reader($cid); if ($dbcr) { $fetchtext->($dbcr) if %$cneedtext; $fetchprop->($dbcr) if %$cneedprop; $fetchrc->($dbcr) if %$cneedrc; } # if we still need some data, switch to the master. if ( %$cneedtext or %$cneedprop ) { $dbcm ||= LJ::get_cluster_master($cid) or $dberr->(); $fetchtext->($dbcm); $fetchprop->($dbcm); $fetchrc->($dbcm); } } # and finally, if there were no errors, # insert into memcache the absence of props # for all posts that didn't have any props. foreach my $id ( keys %$cneedprop ) { my ( $jid, $jitemid ) = map { $_ + 0 } split( /:/, $id ); LJ::MemCache::set( [ $jid, "logprop:$id" ], {} ); } } return $ret; } sub get_posts { my $opts = ref $_[0] eq "HASH" ? shift : {}; my $rawposts = get_posts_raw( $opts, @_ ); # fix up posts as needed for display, following directions given in opts. # XXX this function is incomplete. it should also HTML clean, etc. # XXX we need to load users when we have unknown8bit data, but that # XXX means we have to load users. while ( my ( $id, $rp ) = each %$rawposts ) { if ( $rp->{props}{unknown8bit} ) { #LJ::item_toutf8($u, \$rp->{text}[0], \$rp->{text}[1], $rp->{props}); } } return $rawposts; } # # returns a row from log2, trying memcache # accepts $u + $jitemid # returns hash with: posterid, eventtime, logtime, # security, allowmask, journalid, jitemid, anum. sub get_log2_row { my ( $u, $jitemid ) = @_; my $jid = $u->{'userid'}; my $memkey = [ $jid, "log2:$jid:$jitemid" ]; my ( $row, $item ); $row = LJ::MemCache::get($memkey); if ($row) { @$item{ 'posterid', 'eventtime', 'logtime', 'allowmask', 'ditemid' } = unpack( $LJ::LOGMEMCFMT, $row ); $item->{'security'} = ( $item->{'allowmask'} == 0 ? 'private' : ( $item->{'allowmask'} == $LJ::PUBLICBIT ? 'public' : 'usemask' ) ); $item->{'journalid'} = $jid; @$item{ 'jitemid', 'anum' } = ( $item->{'ditemid'} >> 8, $item->{'ditemid'} % 256 ); $item->{'eventtime'} = LJ::mysql_time( $item->{'eventtime'}, 1 ); $item->{'logtime'} = LJ::mysql_time( $item->{'logtime'}, 1 ); return $item; } my $db = LJ::get_cluster_def_reader($u); return undef unless $db; my $sql = "SELECT posterid, eventtime, logtime, security, allowmask, " . "anum FROM log2 WHERE journalid=? AND jitemid=?"; $item = $db->selectrow_hashref( $sql, undef, $jid, $jitemid ); return undef unless $item; $item->{'journalid'} = $jid; $item->{'jitemid'} = $jitemid; $item->{'ditemid'} = $jitemid * 256 + $item->{'anum'}; my ( $sec, $eventtime, $logtime ); $sec = $item->{'allowmask'}; $sec = 0 if $item->{'security'} eq 'private'; $sec = $LJ::PUBLICBIT if $item->{'security'} eq 'public'; $eventtime = LJ::mysqldate_to_time( $item->{'eventtime'}, 1 ); $logtime = LJ::mysqldate_to_time( $item->{'logtime'}, 1 ); # note: this cannot distinguish between security == private and security == usemask with allowmask == 0 (no groups) # both should have the same display behavior, but we don't store the security value in memcache $row = pack( $LJ::LOGMEMCFMT, $item->{'posterid'}, $eventtime, $logtime, $sec, $item->{'ditemid'} ); LJ::MemCache::set( $memkey, $row ); return $item; } # get 2 weeks worth of recent items, in rlogtime order, # using memcache # accepts $u or ($jid, $clusterid) + $notafter - max value for rlogtime # $update is the timeupdate for this user, as far as the caller knows, # in UNIX time. # returns hash keyed by $jitemid, fields: # posterid, eventtime, rlogtime, # security, allowmask, journalid, jitemid, anum. sub get_log2_recent_log { my ( $u, $cid, $update, $notafter, $events_date ) = @_; my $jid = LJ::want_userid($u); $cid ||= $u->{'clusterid'} if ref $u; my $DATAVER = "4"; # 1 char my $use_cache = 1; # timestamp $events_date = ( !defined $events_date || $events_date eq "" ) ? 0 : int $events_date; $use_cache = 0 if $events_date; # do not use memcache for dayly friends log my $memkey = [ $jid, "log2lt:$jid" ]; my $lockkey = $memkey->[1]; my ( $rows, $ret ); $rows = LJ::MemCache::get($memkey) if $use_cache; $ret = []; my $construct_singleton = sub { foreach my $row (@$ret) { $row->{journalid} = $jid; # FIX: # logtime param should be datetime, not unixtimestamp. # $row->{logtime} = LJ::mysql_time( $LJ::EndOfTime - $row->{rlogtime}, 1 ); # construct singleton for later LJ::Entry->new_from_row(%$row); } return $ret; }; my $rows_decode = sub { return 0 unless $rows && substr( $rows, 0, 1 ) eq $DATAVER; my $tu = unpack( "N", substr( $rows, 1, 4 ) ); # if update time we got from upstream is newer than recorded # here, this data is unreliable return 0 if $update > $tu; my $n = ( length($rows) - 5 ) / 24; for ( my $i = 0 ; $i < $n ; $i++ ) { my ( $posterid, $eventtime, $rlogtime, $allowmask, $ditemid ) = unpack( $LJ::LOGMEMCFMT, substr( $rows, $i * 24 + 5, 24 ) ); next if $notafter and $rlogtime > $notafter; $eventtime = LJ::mysql_time( $eventtime, 1 ); my $security = $allowmask == 0 ? 'private' : ( $allowmask == $LJ::PUBLICBIT ? 'public' : 'usemask' ); my ( $jitemid, $anum ) = ( $ditemid >> 8, $ditemid % 256 ); my $item = {}; @$item{ 'posterid', 'eventtime', 'rlogtime', 'allowmask', 'ditemid', 'security', 'journalid', 'jitemid', 'anum' } = ( $posterid, $eventtime, $rlogtime, $allowmask, $ditemid, $security, $jid, $jitemid, $anum ); $item->{'ownerid'} = $jid; $item->{'itemid'} = $jitemid; push @$ret, $item; } return 1; }; return $construct_singleton->() if $rows_decode->(); $rows = ""; my $db = LJ::get_cluster_def_reader($cid); # if we use slave or didn't get some data, don't store in memcache my $dont_store = 0; unless ($db) { $db = LJ::get_cluster_reader($cid); $dont_store = 1; return undef unless $db; } # my $lock = $db->selectrow_array( "SELECT GET_LOCK(?,10)", undef, $lockkey ); return undef unless $lock; if ($use_cache) { # try to get cached data in exclusive context $rows = LJ::MemCache::get($memkey); if ( $rows_decode->() ) { $db->selectrow_array( "SELECT RELEASE_LOCK(?)", undef, $lockkey ); return $construct_singleton->(); } } # ok. fetch data directly from DB. $rows = ""; # get reliable update time from the db # TODO: check userprop first my $tu; my $dbh = LJ::get_db_writer(); if ($dbh) { $tu = $dbh->selectrow_array( "SELECT UNIX_TIMESTAMP(timeupdate) " . "FROM userusage WHERE userid=?", undef, $jid ); # if no mistake, treat absence of row as tu==0 (new user) $tu = 0 unless $tu || $dbh->err; LJ::MemCache::set( [ $jid, "tu:$jid" ], pack( "N", $tu ), 30 * 60 ) if defined $tu; # TODO: update userprop if necessary } # if we didn't get tu, don't bother to memcache $dont_store = 1 unless defined $tu; # get reliable log2lt data from the db my $max_age = $LJ::MAX_FRIENDS_VIEW_AGE || 3600 * 24 * 14; # 2 weeks default my $sql = " SELECT jitemid, posterid, eventtime, rlogtime, security, allowmask, anum, replycount FROM log2 USE INDEX (rlogtime) WHERE journalid=? " . ( $events_date ? "AND rlogtime <= ($LJ::EndOfTime - $events_date) AND rlogtime >= ($LJ::EndOfTime - " . ( $events_date + 24 * 3600 ) . ")" : "AND rlogtime <= ($LJ::EndOfTime - UNIX_TIMESTAMP()) + $max_age" ); my $sth = $db->prepare($sql); $sth->execute($jid); my @row = (); push @row, $_ while $_ = $sth->fetchrow_hashref; @row = sort { $a->{'rlogtime'} <=> $b->{'rlogtime'} } @row; my $itemnum = 0; foreach my $item (@row) { $item->{'ownerid'} = $item->{'journalid'} = $jid; $item->{'itemid'} = $item->{'jitemid'}; push @$ret, $item; my ( $sec, $ditemid, $eventtime, $logtime ); $sec = $item->{'allowmask'}; $sec = 0 if $item->{'security'} eq 'private'; $sec = $LJ::PUBLICBIT if $item->{'security'} eq 'public'; $ditemid = $item->{'jitemid'} * 256 + $item->{'anum'}; $eventtime = LJ::mysqldate_to_time( $item->{'eventtime'}, 1 ); $rows .= pack( $LJ::LOGMEMCFMT, $item->{'posterid'}, $eventtime, $item->{'rlogtime'}, $sec, $ditemid ); if ( $use_cache && $itemnum++ < 50 ) { LJ::MemCache::add( [ $jid, "rp:$jid:$item->{'jitemid'}" ], $item->{'replycount'} ); } } $rows = $DATAVER . pack( "N", $tu ) . $rows; # store journal log in cache LJ::MemCache::set( $memkey, $rows ) if $use_cache and not $dont_store; $db->selectrow_array( "SELECT RELEASE_LOCK(?)", undef, $lockkey ); return $construct_singleton->(); } # get recent entries for a user sub get_log2_recent_user { my $opts = $_[0]; my $ret = []; my $log = LJ::get_log2_recent_log( $opts->{'userid'}, $opts->{'clusterid'}, $opts->{'update'}, $opts->{'notafter'}, $opts->{events_date} ); my $left = $opts->{'itemshow'}; my $notafter = $opts->{'notafter'}; my $remote = $opts->{'remote'}; my $filter = $opts->{filter}; my %mask_for_remote = (); # jid => mask for $remote foreach my $item (@$log) { last unless $left; last if $notafter and $item->{'rlogtime'} > $notafter; next unless $remote || $item->{'security'} eq 'public'; next if defined( $opts->{security} ) && !( ( $opts->{security} eq 'access' && $item->{security} eq 'usemask' && $item->{allowmask} + 0 != 0 ) || ( $opts->{security} eq 'private' && $item->{security} eq 'usemask' && $item->{allowmask} + 0 == 0 ) || ( $opts->{security} eq $item->{security} ) ); if ( $item->{security} eq 'private' and $item->{journalid} != $remote->{userid} ) { my $ju = LJ::load_userid( $item->{journalid} ); next unless $remote->can_manage($ju); } if ( $item->{'security'} eq 'usemask' ) { next unless $remote->is_individual; my $permit = ( $item->{journalid} == $remote->userid ); unless ($permit) { # $mask for $item{journalid} should always be the same since get_log2_recent_log # selects based on the $u we pass in; $u->id == $item->{journalid} from what I can see # -- we'll store in a per-journalid hash to be safe, but still avoid # extra memcache calls my $mask = $mask_for_remote{ $item->{journalid} }; unless ( defined $mask ) { my $ju = LJ::load_userid( $item->{journalid} ); if ( $ju->is_community ) { # communities don't have masks towards users, so fake it $mask = $remote->member_of($ju) ? 1 : 0; } else { $mask = $ju->trustmask($remote); } $mask_for_remote{ $item->{journalid} } = $mask; } $permit = $item->{'allowmask'} + 0 & $mask + 0; } next unless $permit; } # date conversion if ( !$opts->{'dateformat'} || $opts->{'dateformat'} eq "S2" ) { $item->{'alldatepart'} = LJ::alldatepart_s2( $item->{'eventtime'} ); # conversion to get the system time of this entry my $logtime = LJ::mysql_time( $LJ::EndOfTime - $item->{rlogtime}, 1 ); $item->{'system_alldatepart'} = LJ::alldatepart_s2($logtime); } else { confess "We removed S1 support, sorry."; } # now see if this item matches the filter next if $filter && !$filter->show_entry($item); push @$ret, $item; } return @$ret; } ## ## see subs 'get_itemid_after2' and 'get_itemid_before2' ## sub get_itemid_near2 { my ( $u, $jitemid, $tagnav, $after_before ) = @_; $jitemid += 0; my ( $order, $cmp1, $cmp3, $cmp4 ); if ( $after_before eq "after" ) { ( $order, $cmp1, $cmp3, $cmp4 ) = ( "DESC", "<=", sub { $a->[0] <=> $b->[0] }, sub { $b->[2] <=> $a->[2] } ); } elsif ( $after_before eq "before" ) { ( $order, $cmp1, $cmp3, $cmp4 ) = ( "ASC", ">=", sub { $b->[0] <=> $a->[0] }, sub { $a->[2] <=> $b->[2] } ); } else { return 0; } my $dbr = LJ::get_cluster_reader($u) or return 0; my $jid = $u->{'userid'} + 0; my $field = $u->is_person ? "revttime" : "rlogtime"; my $stime = $dbr->selectrow_array( "SELECT $field FROM log2 WHERE " . "journalid=$jid AND jitemid=$jitemid" ); return 0 unless $stime; my $secwhere = "AND security='public'"; my $remote = LJ::get_remote(); if ($remote) { if ( $remote->equals($u) || ( $u->is_community && $remote->can_manage($u) ) ) { $secwhere = ""; # see everything } elsif ( $remote->is_individual ) { my $gmask = $u->is_community ? $remote->member_of($u) : $u->trustmask($remote); $secwhere = "AND (security='public' OR (security='usemask' AND allowmask & $gmask))" if $gmask; } } ## ## We need a next/prev record in journal before/after a given time ## Since several records may have the same time (time is rounded to 1 minute), ## we're ordering them by jitemid. So, the SQL we need is ## SELECT * FROM log2 ## WHERE journalid=? AND rlogtime>? AND jitmemidselectall_arrayref( "SELECT log2.jitemid, anum, $field FROM log2 use index (rlogtime,revttime), logtagsrecent " . "WHERE log2.journalid=? AND $field $cmp1 ? AND log2.jitemid <> ? " . "AND log2.journalid=logtagsrecent.journalid AND log2.jitemid=logtagsrecent.jitemid AND logtagsrecent.kwid=$tagnav " . $secwhere . " " . "ORDER BY $field $order LIMIT $limit", undef, $jid, $stime, $jitemid ); } else { $result_ref = $dbr->selectall_arrayref( "SELECT jitemid, anum, $field FROM log2 use index (rlogtime,revttime) " . "WHERE journalid=? AND $field $cmp1 ? AND jitemid <> ? " . $secwhere . " " . "ORDER BY $field $order LIMIT $limit", undef, $jid, $stime, $jitemid ); } my %hash_times = (); map { $hash_times{ $_->[2] } = 1 } @$result_ref; # If we has one the only 'time' in $limit fetched rows, # may be $limit cuts off our record. Increase the limit and repeat. if ( ( ( scalar keys %hash_times ) > 1 ) || ( scalar @$result_ref ) < $limit ) { my @result; # Remove results with the same time but the jitemid is too high or low if ( $after_before eq "after" ) { @result = grep { $_->[2] != $stime || $_->[0] > $jitemid } @$result_ref; } elsif ( $after_before eq "before" ) { @result = grep { $_->[2] != $stime || $_->[0] < $jitemid } @$result_ref; } # Sort result by jitemid and get our id from a top. @result = sort $cmp3 @result; # Sort result by revttime @result = sort $cmp4 @result; my ( $id, $anum ) = ( $result[0]->[0], $result[0]->[1] ); return 0 unless $id; return wantarray() ? ( $id, $anum ) : ( $id * 256 + $anum ); } } return 0; } ## ## Returns ID (a pair in list context, ditmeid in scalar context) ## of a journal record that follows/preceeds the given record. ## Input: $u, $jitemid ## sub get_itemid_after2 { return get_itemid_near2( @_, "after" ); } sub get_itemid_before2 { return get_itemid_near2( @_, "before" ); } sub set_logprop { my ( $u, $jitemid, $hashref, $logprops ) = @_; # hashref to set, hashref of what was done $jitemid += 0; my $uid = $u->{'userid'} + 0; my $kill_mem = 0; my $del_ids; my $ins_values; while ( my ( $k, $v ) = each %{ $hashref || {} } ) { my $prop = LJ::get_prop( "log", $k ); next unless $prop; $kill_mem = 1 unless $prop eq "commentalter"; if ($v) { $ins_values .= "," if $ins_values; $ins_values .= "($uid, $jitemid, $prop->{'id'}, " . $u->quote($v) . ")"; $logprops->{$k} = $v; } else { $del_ids .= "," if $del_ids; $del_ids .= $prop->{'id'}; } } $u->do( "REPLACE INTO logprop2 (journalid, jitemid, propid, value) " . "VALUES $ins_values" ) if $ins_values; $u->do( "DELETE FROM logprop2 WHERE journalid=? AND jitemid=? " . "AND propid IN ($del_ids)", undef, $u->userid, $jitemid ) if $del_ids; LJ::MemCache::delete( [ $uid, "logprop:$uid:$jitemid" ] ) if $kill_mem; } # # name: LJ::load_log_props2 # class: # des: # info: # args: db?, uuserid, listref, hashref # des-: # returns: # sub load_log_props2 { my $db = LJ::DB::isdb( $_[0] ) ? shift @_ : undef; my ( $uuserid, $listref, $hashref ) = @_; my $userid = want_userid($uuserid); return unless ref $hashref eq "HASH"; my %needprops; my %needrc; my %rc; my @memkeys; foreach (@$listref) { my $id = $_ + 0; $needprops{$id} = 1; $needrc{$id} = 1; push @memkeys, [ $userid, "logprop:$userid:$id" ]; push @memkeys, [ $userid, "rp:$userid:$id" ]; } return unless %needprops || %needrc; my $mem = LJ::MemCache::get_multi(@memkeys) || {}; while ( my ( $k, $v ) = each %$mem ) { next unless $k =~ /(\w+):(\d+):(\d+)/; if ( $1 eq 'logprop' ) { next unless ref $v eq "HASH"; delete $needprops{$3}; $hashref->{$3} = $v; } if ( $1 eq 'rp' ) { delete $needrc{$3}; $rc{$3} = int($v); # change possible "0 " (true) to "0" (false) } } foreach ( keys %rc ) { $hashref->{$_}{'replycount'} = $rc{$_}; } return unless %needprops || %needrc; unless ($db) { my $u = LJ::load_userid($userid); $db = @LJ::MEMCACHE_SERVERS ? LJ::get_cluster_def_reader($u) : LJ::get_cluster_reader($u); return unless $db; } if (%needprops) { LJ::load_props("log"); my $in = join( ",", keys %needprops ); my $sth = $db->prepare( "SELECT jitemid, propid, value FROM logprop2 " . "WHERE journalid=? AND jitemid IN ($in)" ); $sth->execute($userid); while ( my ( $jitemid, $propid, $value ) = $sth->fetchrow_array ) { $hashref->{$jitemid}->{ $LJ::CACHE_PROPID{'log'}->{$propid}->{'name'} } = $value; } foreach my $id ( keys %needprops ) { LJ::MemCache::set( [ $userid, "logprop:$userid:$id" ], $hashref->{$id} || {} ); } } if (%needrc) { my $in = join( ",", keys %needrc ); my $sth = $db->prepare( "SELECT jitemid, replycount FROM log2 WHERE journalid=? AND jitemid IN ($in)"); $sth->execute($userid); while ( my ( $jitemid, $rc ) = $sth->fetchrow_array ) { $hashref->{$jitemid}->{'replycount'} = $rc; LJ::MemCache::add( [ $userid, "rp:$userid:$jitemid" ], $rc ); } } } # # name: LJ::load_talk_props2 # class: # des: # info: # args: # des-: # returns: # sub load_talk_props2 { my $db = LJ::DB::isdb( $_[0] ) ? shift @_ : undef; my ( $uuserid, $listref, $hashref ) = @_; my $userid = want_userid($uuserid); my $u = ref $uuserid ? $uuserid : undef; $hashref = {} unless ref $hashref eq "HASH"; my %need; my @memkeys; foreach (@$listref) { my $id = $_ + 0; $need{$id} = 1; push @memkeys, [ $userid, "talkprop:$userid:$id" ]; } return $hashref unless %need; my $mem = LJ::MemCache::get_multi(@memkeys) || {}; # allow hooks to count memcaches in this function for testing if ($LJ::_T_GET_TALK_PROPS2_MEMCACHE) { $LJ::_T_GET_TALK_PROPS2_MEMCACHE->(); } while ( my ( $k, $v ) = each %$mem ) { next unless $k =~ /(\d+):(\d+)/ && ref $v eq "HASH"; delete $need{$2}; $hashref->{$2}->{ $_[0] } = $_[1] while @_ = each %$v; } return $hashref unless %need; if ( !$db || @LJ::MEMCACHE_SERVERS ) { $u ||= LJ::load_userid($userid); $db = @LJ::MEMCACHE_SERVERS ? LJ::get_cluster_def_reader($u) : LJ::get_cluster_reader($u); return $hashref unless $db; } LJ::load_props("talk"); my $in = join( ',', keys %need ); my $sth = $db->prepare( "SELECT jtalkid, tpropid, value FROM talkprop2 " . "WHERE journalid=? AND jtalkid IN ($in)" ); $sth->execute($userid); while ( my ( $jtalkid, $propid, $value ) = $sth->fetchrow_array ) { my $p = $LJ::CACHE_PROPID{'talk'}->{$propid}; next unless $p; $hashref->{$jtalkid}->{ $p->{'name'} } = $value; } foreach my $id ( keys %need ) { LJ::MemCache::set( [ $userid, "talkprop:$userid:$id" ], $hashref->{$id} || {} ); } return $hashref; } # # name: LJ::delete_all_comments # des: deletes all comments from a post, permanently, for when a post is deleted # info: The tables [dbtable[talk2]], [dbtable[talkprop2]], [dbtable[talktext2]], # are deleted from, immediately. # args: u, nodetype, nodeid # des-nodetype: The thread nodetype (probably 'L' for log items). # des-nodeid: The thread nodeid for the given nodetype (probably the jitemid # from the [dbtable[log2]] row). # returns: boolean; success value # sub delete_all_comments { my ( $u, $nodetype, $nodeid ) = @_; my $dbcm = LJ::get_cluster_master($u); return 0 unless $dbcm && $u->writer; # delete comments my ( $t, $loop ) = ( undef, 1 ); my $chunk_size = 200; while ( $loop && ( $t = $dbcm->selectcol_arrayref( "SELECT jtalkid FROM talk2 WHERE " . "nodetype=? AND journalid=? " . "AND nodeid=? LIMIT $chunk_size", undef, $nodetype, $u->userid, $nodeid ) ) && $t && @$t ) { my $in = join( ',', map { $_ + 0 } @$t ); return 1 unless $in; foreach my $table (qw(talkprop2 talktext2 talk2)) { $u->do( "DELETE FROM $table WHERE journalid=? AND jtalkid IN ($in)", undef, $u->userid ); } my $ct = scalar @$t; DW::Stats::increment( 'dw.action.comment.delete', $ct, [ "journal_type:" . $u->journaltype_readable, 'method:delete_all_comments' ] ); # decrement memcache LJ::MemCache::decr( [ $u->userid, "talk2ct:" . $u->userid ], $ct ); $loop = 0 unless $ct == $chunk_size; } return 1; } # # name: LJ::delete_comments # des: deletes comments, but not the relational information, so threading doesn't break # info: The tables [dbtable[talkprop2]] and [dbtable[talktext2]] are deleted from. [dbtable[talk2]] # just has its state column modified, to 'D'. # args: u, nodetype, nodeid, talkids # des-nodetype: The thread nodetype (probably 'L' for log items) # des-nodeid: The thread nodeid for the given nodetype (probably the jitemid # from the [dbtable[log2]] row). # des-talkids: List array of talkids to delete. # returns: scalar integer; number of items deleted. # sub delete_comments { my ( $u, $nodetype, $nodeid, @talkids ) = @_; return 0 unless $u->writer; my $jid = $u->id + 0; my $in = join ',', map { $_ + 0 } @talkids; # invalidate talk2row memcache LJ::Talk::invalidate_talk2row_memcache( $jid, @talkids ); return 1 unless $in; my $where = "WHERE journalid=$jid AND jtalkid IN ($in)"; my $num = $u->talk2_do( $nodetype, $nodeid, undef, "UPDATE talk2 SET state='D' $where" ); return 0 unless $num; $num = 0 if $num == -1; if ( $num > 0 ) { DW::Stats::increment( 'dw.action.comment.delete', $num, [ "journal_type:" . $u->journaltype_readable, 'method:delete_comments' ] ); $u->do("UPDATE talktext2 SET subject=NULL, body=NULL $where"); $u->do("DELETE FROM talkprop2 $where"); } foreach my $talkid (@talkids) { LJ::Hooks::run_hooks( 'delete_comment', $jid, $nodeid, $talkid ); # jitemid, jtalkid } $u->memc_delete('activeentries'); LJ::MemCache::delete( [ $jid, "screenedcount:$jid:$nodeid" ] ); return $num; } # # name: LJ::delete_entry # des: Deletes a user's journal entry # args: uuserid, jitemid, quick?, anum? # des-uuserid: Journal itemid or $u object of journal to delete entry from # des-jitemid: Journal itemid of item to delete. # des-quick: Optional boolean. If set, only [dbtable[log2]] table # is deleted from and the rest of the content is deleted # later via DW::TaskQueue. # des-anum: The log item's anum, which'll be needed to delete lazily # some data in tables which includes the anum, but the # log row will already be gone so we'll need to store it for later. # returns: boolean; 1 on success, 0 on failure. # sub delete_entry { my ( $uuserid, $jitemid, $quick, $anum ) = @_; my $jid = LJ::want_userid($uuserid); my $u = ref $uuserid ? $uuserid : LJ::load_userid($jid); $jitemid += 0; my $and; if ( defined $anum ) { $and = "AND anum=" . ( $anum + 0 ); } # delete tags LJ::Tags::delete_logtags( $u, $jitemid ); my $dc = $u->log2_do( undef, "DELETE FROM log2 WHERE journalid=$jid AND jitemid=$jitemid $and" ); LJ::MemCache::delete( [ $jid, "log2:$jid:$jitemid" ] ); LJ::MemCache::delete( [ $jid, "activeentries:$jid" ] ); LJ::MemCache::decr( [ $jid, "log2ct:$jid" ] ) if $dc > 0; LJ::memcache_kill( $jid, "dayct2" ); LJ::Hooks::run_hooks( "deletepost", $jid, $jitemid, $anum ); # if this is running the second time (started by the cmd buffer), # the log2 row will already be gone and we shouldn't check for it. if ($quick) { return 1 if $dc < 1; # already deleted? return 1 if DW::TaskQueue->dispatch( DW::Task::DeleteEntry->new( { uid => $jid, jitemid => $jitemid, anum => $anum, } ) ); return 0; } DW::Stats::increment( 'dw.action.entry.delete', 1, [ "journal_type:" . $u->journaltype_readable ] ); # delete from clusters foreach my $t (qw(logtext2 logprop2 logsec2 logslugs)) { $u->do("DELETE FROM $t WHERE journalid=$jid AND jitemid=$jitemid"); } $u->dudata_set( 'L', $jitemid, 0 ); # delete all comments LJ::delete_all_comments( $u, 'L', $jitemid ); # fired to delete the post from the Sphinx search database if (@LJ::SPHINX_SEARCHD) { DW::TaskQueue->dispatch( DW::Task::SphinxCopier->new( { userid => $u->id, jitemid => $jitemid, source => "entrydel" } ) ); } return 1; } # # name: LJ::mark_entry_as_spam # class: web # des: Copies an entry in a community into the global [dbtable[spamreports]] table. # args: journalu_uid, jitemid # des-journalu_uid: User object of journal (community) entry was posted in, or the userid of it. # des-jitemid: ID of this entry. # returns: 1 for success, 0 for failure # sub mark_entry_as_spam { my ( $journalu, $jitemid ) = @_; $journalu = LJ::want_user($journalu); $jitemid += 0; return 0 unless $journalu && $jitemid; return 0 if LJ::sysban_check( 'spamreport', $journalu->user ); my $dbcr = LJ::get_cluster_def_reader($journalu); my $dbh = LJ::get_db_writer(); return 0 unless $dbcr && $dbh; my $item = LJ::get_log2_row( $journalu, $jitemid ); return 0 unless $item; # step 1: get info we need my $logtext = LJ::get_logtext2( $journalu, $jitemid ); my ( $subject, $body, $posterid ) = ( $logtext->{$jitemid}[0], $logtext->{$jitemid}[1], $item->{posterid} ); return 0 unless $body; # step 2: insert into spamreports $dbh->do( 'INSERT INTO spamreports (reporttime, posttime, journalid, posterid, subject, body, report_type) ' . 'VALUES (UNIX_TIMESTAMP(), UNIX_TIMESTAMP(?), ?, ?, ?, ?, \'entry\')', undef, $item->{logtime}, $journalu->{userid}, $posterid, $subject, $body ); return 0 if $dbh->err; return 1; } # Same as previous, but mark as spam moderated event selected by modid. sub reject_entry_as_spam { my ( $journalu, $modid ) = @_; $journalu = LJ::want_user($journalu); $modid += 0; return 0 unless $journalu && $modid; return 0 if LJ::sysban_check( 'spamreport', $journalu->user ); my $dbcr = LJ::get_cluster_def_reader($journalu); my $dbh = LJ::get_db_writer(); return 0 unless $dbcr && $dbh; # step 1: get info we need my ( $posterid, $logtime ) = $dbcr->selectrow_array( "SELECT posterid, logtime FROM modlog WHERE journalid=? AND modid=?", undef, $journalu->userid, $modid ); my $frozen = $dbcr->selectrow_array( "SELECT request_stor FROM modblob WHERE journalid=? AND modid=?", undef, $journalu->userid, $modid ); use Storable; my $req = $frozen ? Storable::thaw($frozen) : undef; my ( $subject, $body ) = ( $req->{subject}, $req->{event} ); return 0 unless $body; # step 2: insert into spamreports $dbh->do( 'INSERT INTO spamreports (reporttime, posttime, journalid, posterid, subject, body, report_type) ' . 'VALUES (UNIX_TIMESTAMP(), UNIX_TIMESTAMP(?), ?, ?, ?, ?, \'entry\')', undef, $logtime, $journalu->{userid}, $posterid, $subject, $body ); return 0 if $dbh->err; return 1; } # replycount_do # input: $u, $jitemid, $action, $value # action is one of: "init", "incr", "decr" # $value is amount to incr/decr, 1 by default sub replycount_do { my ( $u, $jitemid, $action, $value ) = @_; $value = 1 unless defined $value; my $uid = $u->{'userid'}; my $memkey = [ $uid, "rp:$uid:$jitemid" ]; # "init" is easiest and needs no lock (called before the entry is live) if ( $action eq 'init' ) { LJ::MemCache::set( $memkey, "0 " ); return 1; } return 0 unless $u->writer; my $lockkey = $memkey->[1]; $u->selectrow_array( "SELECT GET_LOCK(?,10)", undef, $lockkey ); my $ret; if ( $action eq 'decr' ) { $ret = LJ::MemCache::decr( $memkey, $value ); $u->do( "UPDATE log2 SET replycount=replycount-$value WHERE journalid=$uid AND jitemid=$jitemid" ); } if ( $action eq 'incr' ) { $ret = LJ::MemCache::incr( $memkey, $value ); $u->do( "UPDATE log2 SET replycount=replycount+$value WHERE journalid=$uid AND jitemid=$jitemid" ); } if ( @LJ::MEMCACHE_SERVERS && !defined $ret ) { my $rc = $u->selectrow_array( "SELECT replycount FROM log2 WHERE journalid=$uid AND jitemid=$jitemid"); if ( defined $rc ) { $rc = sprintf( "%-4d", $rc ); LJ::MemCache::set( $memkey, $rc ); } } $u->selectrow_array( "SELECT RELEASE_LOCK(?)", undef, $lockkey ); return 1; } # # name: LJ::get_logtext2 # des: Efficiently retrieves a large number of journal entry text, trying first # slave database servers for recent items, then the master in # cases of old items the slaves have already disposed of. See also: # [func[LJ::get_talktext2]]. # args: u, opts?, jitemid* # returns: hashref with keys being jitemids, values being [ $subject, $body ] # des-opts: Optional hashref of special options. NOW IGNORED (2005-09-14) # des-jitemid: List of jitemids to retrieve the subject & text for. # sub get_logtext2 { my $u = shift; my $clusterid = $u->{'clusterid'}; my $journalid = $u->{'userid'} + 0; my $opts = ref $_[0] ? shift : {}; # this is now ignored # return structure. my $lt = {}; return $lt unless $clusterid; # keep track of itemids we still need to load. my %need; my @mem_keys; foreach (@_) { my $id = $_ + 0; $need{$id} = 1; push @mem_keys, [ $journalid, "logtext:$clusterid:$journalid:$id" ]; } # pass 1: memcache my $mem = LJ::MemCache::get_multi(@mem_keys) || {}; while ( my ( $k, $v ) = each %$mem ) { next unless $v; $k =~ /:(\d+):(\d+):(\d+)/; delete $need{$3}; $lt->{$3} = $v; } return $lt unless %need; # pass 2: databases my $db = LJ::get_cluster_def_reader($clusterid); die "Can't get database handle loading entry text" unless $db; my $jitemid_in = join( ", ", keys %need ); my $sth = $db->prepare( "SELECT jitemid, subject, event FROM logtext2 " . "WHERE journalid=$journalid AND jitemid IN ($jitemid_in)" ); $sth->execute; while ( my ( $id, $subject, $event ) = $sth->fetchrow_array ) { LJ::text_uncompress( \$event ); my $val = [ $subject, $event ]; $lt->{$id} = $val; LJ::MemCache::add( [ $journalid, "logtext:$clusterid:$journalid:$id" ], $val ); delete $need{$id}; } return $lt; } # # name: LJ::get_talktext2 # des: Retrieves comment text. Tries slave servers first, then master. # info: Efficiently retrieves batches of comment text. Will try alternate # servers first. See also [func[LJ::get_logtext2]]. # returns: Hashref with the talkids as keys, values being [ $subject, $event ]. # args: u, opts?, jtalkids # des-opts: A hashref of options. 'onlysubjects' will only retrieve subjects. # des-jtalkids: A list of talkids to get text for. # sub get_talktext2 { my $u = shift; my $clusterid = $u->{'clusterid'}; my $journalid = $u->{'userid'} + 0; my $opts = ref $_[0] ? shift : {}; # return structure. my $lt = {}; return $lt unless $clusterid; # keep track of itemids we still need to load. my %need; my @mem_keys; foreach (@_) { my $id = $_ + 0; $need{$id} = 1; push @mem_keys, [ $journalid, "talksubject:$clusterid:$journalid:$id" ]; unless ( $opts->{'onlysubjects'} ) { push @mem_keys, [ $journalid, "talkbody:$clusterid:$journalid:$id" ]; } } # try the memory cache my $mem = LJ::MemCache::get_multi(@mem_keys) || {}; if ($LJ::_T_GET_TALK_TEXT2_MEMCACHE) { $LJ::_T_GET_TALK_TEXT2_MEMCACHE->(); } while ( my ( $k, $v ) = each %$mem ) { $k =~ /^talk(.*):(\d+):(\d+):(\d+)/; if ( $opts->{'onlysubjects'} && $1 eq "subject" ) { delete $need{$4}; $lt->{$4} = [$v]; } if ( !$opts->{'onlysubjects'} && $1 eq "body" && exists $mem->{"talksubject:$2:$3:$4"} ) { delete $need{$4}; $lt->{$4} = [ $mem->{"talksubject:$2:$3:$4"}, $v ]; } } return $lt unless %need; my $bodycol = $opts->{'onlysubjects'} ? "" : ", body"; # pass 1 (slave) and pass 2 (master) foreach my $pass ( 1, 2 ) { next unless %need; my $db = $pass == 1 ? LJ::get_cluster_reader($clusterid) : LJ::get_cluster_def_reader($clusterid); unless ($db) { next if $pass == 1; die "Could not get db handle"; } my $in = join( ",", keys %need ); my $sth = $db->prepare( "SELECT jtalkid, subject $bodycol FROM talktext2 " . "WHERE journalid=$journalid AND jtalkid IN ($in)" ); $sth->execute; while ( my ( $id, $subject, $body ) = $sth->fetchrow_array ) { $subject = "" unless defined $subject; LJ::text_uncompress( \$subject ); $body = "" unless defined $body; LJ::text_uncompress( \$body ); $lt->{$id} = [ $subject, $body ]; LJ::MemCache::add( [ $journalid, "talkbody:$clusterid:$journalid:$id" ], $body ) unless $opts->{'onlysubjects'}; LJ::MemCache::add( [ $journalid, "talksubject:$clusterid:$journalid:$id" ], $subject ); delete $need{$id}; } } return $lt; } # # name: LJ::item_link # class: component # des: Returns URL to view an individual journal item. # info: The returned URL may have an ampersand in it. In an HTML/XML attribute, # these must first be escaped by, say, [func[LJ::ehtml]]. This # function doesn't return it pre-escaped because the caller may # use it in, say, a plain-text e-mail message. # args: u, itemid, anum? # des-itemid: Itemid of entry to link to. # des-anum: If present, $u is assumed to be on a cluster and itemid is assumed # to not be a $ditemid already, and the $itemid will be turned into one # by multiplying by 256 and adding $anum. # returns: scalar; unescaped URL string # sub item_link { my ( $u, $itemid, $anum, $args ) = @_; my $ditemid = $itemid * 256 + $anum; $u = LJ::load_user($u) unless LJ::isu($u); $args = $args ? "?$args" : ""; return $u->journal_base . "/$ditemid.html$args"; } # # name: LJ::expand_embedded # class: # des: Used for expanding embedded content like polls, for entries. # info: The u-object of the journal in question transmits to the function # and its hooks. # args: u, ditemid, remote, eventref, opts? # des-eventref: # des-opts: # returns: # sub expand_embedded { my ( $u, $ditemid, $remote, $eventref, %opts ) = @_; LJ::Poll->expand_entry( $eventref, %opts ) unless $opts{preview}; LJ::EmbedModule->expand_entry( $u, $eventref, %opts ); LJ::Hooks::run_hooks( "expand_embedded", $u, $ditemid, $remote, $eventref, %opts ); } # # name: LJ::item_toutf8 # des: convert one item's subject, text and props to UTF-8. # item can be an entry or a comment (in which cases props can be # left empty, since there are no 8bit talkprops). # args: u, subject, text, props # des-u: user hashref of the journal's owner # des-subject: ref to the item's subject # des-text: ref to the item's text # des-props: hashref of the item's props # returns: nothing. # sub item_toutf8 { my ( $u, $subject, $text, $props ) = @_; $props ||= {}; my $convert = sub { my $rtext = $_[0]; my $error = 0; return unless defined $$rtext; my $res = LJ::text_convert( $$rtext, $u, \$error ); if ($error) { LJ::text_out($rtext); } else { $$rtext = $res; } return; }; $convert->($subject); $convert->($text); # FIXME: Have some logprop flag for what props are binary foreach ( keys %$props ) { next if $_ eq 'xpost' || $_ eq 'xpostdetail'; $convert->( \$props->{$_} ); } return; } # function to fill in hash for basic currents sub currents { my ( $props, $u, $opts ) = @_; return unless ref $props eq 'HASH'; my %current; my ( $key, $entry, $s2imgref ) = ( "", undef, undef ); if ( $opts && ref $opts ) { $key = $opts->{key} || ''; $entry = $opts->{entry}; $s2imgref = $opts->{s2imgref}; } # Mood if ( $props->{"${key}current_mood"} || $props->{"${key}current_moodid"} ) { my $moodid = $props->{"${key}current_moodid"}; my $mood = $props->{"${key}current_mood"}; my ( $moodname, $moodpic ) = ( '', '' ); # favor custom mood over system mood if ( my $val = $mood ) { LJ::CleanHTML::clean_subject( \$val ); $moodname = $val; } if ( my $val = $moodid ) { $moodname ||= DW::Mood->mood_name($val); if ( defined $u ) { my $themeid = LJ::isu($u) ? $u->moodtheme : undef; # $u might be a hashref instead of a user object? $themeid ||= ref $u ? $u->{moodthemeid} : undef; my $theme = DW::Mood->new($themeid); my %pic; if ( $theme && $theme->get_picture( $val, \%pic ) ) { if ( $s2imgref && ref $s2imgref ) { # return argument array for S2::Image $$s2imgref = [ $pic{pic}, $pic{w}, $pic{h} ]; } else { $moodpic = " "; } } } } $current{Mood} = "$moodpic$moodname"; } # Music if ( $props->{"${key}current_music"} ) { $current{Music} = $props->{"${key}current_music"}; LJ::CleanHTML::clean_subject( \$current{Music} ); } # Location if ( $props->{"${key}current_location"} || $props->{"${key}current_coords"} ) { my $loc = eval { LJ::Location->new( coords => $props->{"${key}current_coords"}, location => $props->{"${key}current_location"} ); }; $current{Location} = $loc->as_current if $loc; LJ::CleanHTML::clean_subject( \$current{Location} ); } # Crossposts if ( my $xpost = $props->{"${key}xpostdetail"} ) { my $xposthash = DW::External::Account->xpost_string_to_hash($xpost); my $xpostlinks = ""; foreach my $xpostvalue ( values %$xposthash ) { if ( $xpostvalue->{url} ) { my $xpost_url = LJ::no_utf8_flag( $xpostvalue->{url} ); $xpostlinks .= " " if $xpostlinks; $xpostlinks .= "$xpost_url"; } } $current{Xpost} = $xpostlinks if $xpostlinks; } if ($entry) { # Groups my $group_names = $entry->group_names; $current{Groups} = $group_names if $group_names; # Tags my $u = $entry->journal; my $base = $u->journal_base; my $itemid = $entry->jitemid; my $logtags = LJ::Tags::get_logtags( $u, $itemid ); if ( $logtags->{$itemid} ) { my @tags = map { "" . LJ::ehtml($_) . "" } sort values %{ $logtags->{$itemid} }; $current{Tags} = join( ', ', @tags ) if @tags; } } return %current; } # function to format table for currents display sub currents_table { my (%current) = @_; my $ret = ''; return $ret unless %current; $ret .= "\n"; foreach ( sort keys %current ) { next unless $current{$_}; my $curkey = "talk.curname_" . $_; my $curname = LJ::Lang::ml($curkey); $curname = "Current $_:" unless $curname; $ret .= ""; $ret .= "\n"; } $ret .= "
$curname$current{$_}
\n"; return $ret; } # Same, but stop it with the tables sub currents_div { my (%current) = @_; my $ret = ''; return $ret unless %current; $ret .= "
\n"; $ret .= "\n"; $ret .= "
\n"; return $ret; } 1;