2914 lines
90 KiB
Perl
2914 lines
90 KiB
Perl
# 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;
|