mourningdove/cgi-bin/LJ/Entry.pm

2915 lines
90 KiB
Perl
Raw Permalink Normal View History

2026-05-24 01:03:05 +00:00
# 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 = ();
}
# <LJFUNC>
# 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.
# </LJFUNC>
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
'<img src="'
. $self->comment_image_url
. '" width="30" height="12" alt="'
. $alttext
. '" style="vertical-align: middle;"/>';
}
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 = "<b>(<a href=\"$url\">Read more ...</a>)</b>";
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 <br> or a </p>
# valid XML tags should be handled, even though it makes an uglier regex
if ( $event =~ m!(.*?(?:(?:<br\s*/?>(?:</br\s*>)?\s*){2}|</p\s*>))!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;
# <LJFUNC>
# 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 ].
# </LJFUNC>
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 jitmemid<?
## ORDER BY rlogtime, jitemid DESC
## LIMIT 1
## Alas, MySQL tries to do filesort for the query.
## So, we sort by rlogtime only and fetch all (2, 10, 50) records
## with the same rlogtime (we skip records if rlogtime is different from the first one).
## If rlogtime of all fetched records is the same, increase the LIMIT and retry.
## Then we sort them in Perl by jitemid and takes just one.
##
my $result_ref;
foreach my $limit ( 2, 10, 50, 100 ) {
if ($tagnav) {
$result_ref = $dbr->selectall_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 <jitemid, anum> 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;
}
# <LJFUNC>
# name: LJ::load_log_props2
# class:
# des:
# info:
# args: db?, uuserid, listref, hashref
# des-:
# returns:
# </LJFUNC>
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 );
}
}
}
# <LJFUNC>
# name: LJ::load_talk_props2
# class:
# des:
# info:
# args:
# des-:
# returns:
# </LJFUNC>
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;
}
# <LJFUNC>
# 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
# </LJFUNC>
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;
}
# <LJFUNC>
# 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.
# </LJFUNC>
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;
}
# <LJFUNC>
# 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.
# </LJFUNC>
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;
}
# <LJFUNC>
# 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
# </LJFUNC>
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;
}
# <LJFUNC>
# 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.
# </LJFUNC>
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;
}
# <LJFUNC>
# 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.
# </LJFUNC>
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;
}
# <LJFUNC>
# 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
# </LJFUNC>
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";
}
# <LJFUNC>
# 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:
# </LJFUNC>
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 );
}
# <LJFUNC>
# 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.
# </LJFUNC>
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 =
"<img class='moodpic' src=\"$pic{pic}\" "
. "width='$pic{w}' height='$pic{h}' "
. "align='absmiddle' vspace='1' alt='' /> ";
}
}
}
}
$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 .= "<a href='$xpost_url'>$xpost_url</a>";
}
}
$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 { "<a href='$base/tag/" . LJ::eurl($_) . "'>" . LJ::ehtml($_) . "</a>" }
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 .= "<table summary='' class='currents' border=0>\n";
foreach ( sort keys %current ) {
next unless $current{$_};
my $curkey = "talk.curname_" . $_;
my $curname = LJ::Lang::ml($curkey);
$curname = "<b>Current $_:</b>" unless $curname;
$ret .= "<tr><td align='right'>$curname</td>";
$ret .= "<td>$current{$_}</td></tr>\n";
}
$ret .= "</table>\n";
return $ret;
}
# Same, but stop it with the tables
sub currents_div {
my (%current) = @_;
my $ret = '';
return $ret unless %current;
$ret .= "<div class='currents'>\n";
$ret .= "<div class='metadata bottom-metadata'>\n";
$ret .= "<ul>\n";
foreach ( sort keys %current ) {
next unless $current{$_};
my $curkey = "talk.curname_" . $_;
my $curname = LJ::Lang::ml($curkey);
$curname = "<b>Current $_:</b>" unless $curname;
$ret .= "<li><span class='metadata-label'>$curname</span> ";
$ret .= "<span class='metadata-item'>$current{$_}</span></li>\n";
}
$ret .= "</ul>\n";
$ret .= "</div>\n";
$ret .= "</div>\n";
return $ret;
}
1;