1553 lines
43 KiB
Perl
1553 lines
43 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.
|
||
|
|
# echo "# CONTROLLER_TEST_$(date)" >> /home/dw/dw/cgi-bin/DW/Controller/Media.pm
|
||
|
|
package LJ::Userpic;
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use v5.10;
|
||
|
|
use Log::Log4perl;
|
||
|
|
my $log = Log::Log4perl->get_logger(__PACKAGE__);
|
||
|
|
|
||
|
|
use Digest::MD5;
|
||
|
|
use Storable;
|
||
|
|
|
||
|
|
use DW::BlobStore;
|
||
|
|
use LJ::Event::NewUserpic;
|
||
|
|
use LJ::Global::Constants;
|
||
|
|
|
||
|
|
##
|
||
|
|
## Potential properties of an LJ::Userpic object
|
||
|
|
##
|
||
|
|
# picid : (accessors picid, id) unique identifier for the object, generated
|
||
|
|
# userid : (accessor userid) the userid associated with the object
|
||
|
|
# state : state
|
||
|
|
# comment : user submitted descriptive comment
|
||
|
|
# description : user submitted description
|
||
|
|
# keywords : user submitted keywords (all keywords in a single string)
|
||
|
|
# dim : (accessors width, height, dimensions) array:[width][height]
|
||
|
|
# pictime : pictime
|
||
|
|
# flags : flags
|
||
|
|
# md5base64 : md5sum of of the image bitstream to prevent duplication
|
||
|
|
# ext : file extension, corresponding to mime types
|
||
|
|
# location : whether the image is stored in database or mogile
|
||
|
|
|
||
|
|
##
|
||
|
|
## virtual accessors
|
||
|
|
##
|
||
|
|
# url : returns a URL directly to the userpic
|
||
|
|
# fullurl : returns the URL used at upload time, it if exists
|
||
|
|
# altext : "username: keyword, comment (description)"
|
||
|
|
# u, owner : return the user object indicated by the userid
|
||
|
|
|
||
|
|
# legal image types
|
||
|
|
my %MimeTypeMap = (
|
||
|
|
'image/gif' => 'gif',
|
||
|
|
'G' => 'gif',
|
||
|
|
'image/jpeg' => 'jpg',
|
||
|
|
'J' => 'jpg',
|
||
|
|
'image/png' => 'png',
|
||
|
|
'P' => 'png',
|
||
|
|
);
|
||
|
|
|
||
|
|
# all LJ::Userpics in memory
|
||
|
|
# userid -> picid -> LJ::Userpic
|
||
|
|
my %singletons;
|
||
|
|
|
||
|
|
sub reset_singletons {
|
||
|
|
%singletons = ();
|
||
|
|
}
|
||
|
|
|
||
|
|
=head1 NAME
|
||
|
|
|
||
|
|
LJ::Userpic
|
||
|
|
|
||
|
|
=head1 Class Methods
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
# LJ::Userpic constructor. Returns a LJ::Userpic object.
|
||
|
|
# Return existing with userid and picid populated, or make new.
|
||
|
|
sub instance {
|
||
|
|
my ( $class, $u, $picid ) = @_;
|
||
|
|
$picid = 0 unless defined $picid;
|
||
|
|
my $up;
|
||
|
|
|
||
|
|
# return existing one, if loaded
|
||
|
|
if ( my $us = $singletons{ $u->{userid} } ) {
|
||
|
|
return $up if $up = $us->{$picid};
|
||
|
|
}
|
||
|
|
|
||
|
|
# otherwise construct a new one with the given picid
|
||
|
|
$up = $class->_skeleton( $u, $picid );
|
||
|
|
$singletons{ $u->{userid} }->{$picid} = $up;
|
||
|
|
return $up;
|
||
|
|
}
|
||
|
|
*new = \&instance;
|
||
|
|
|
||
|
|
# LJ::Userpic accessor. Returns a LJ::Userpic object indicated by $picid, or
|
||
|
|
# undef if userpic doesn't exist in the db.
|
||
|
|
# TODO: add in lazy peer loading here?
|
||
|
|
sub get {
|
||
|
|
my ( $class, $u, $picid, $opts ) = @_;
|
||
|
|
return unless LJ::isu($u);
|
||
|
|
return if $u->is_expunged || $u->is_suspended;
|
||
|
|
return unless defined $picid;
|
||
|
|
|
||
|
|
my $obj = ref $class ? $class : $class->new( $u, $picid );
|
||
|
|
my @cache = $class->load_user_userpics($u);
|
||
|
|
|
||
|
|
foreach my $curr (@cache) {
|
||
|
|
return $obj->absorb_row($curr) if $curr->{picid} == $picid;
|
||
|
|
}
|
||
|
|
|
||
|
|
# check the database directly (for expunged userpics,
|
||
|
|
# which aren't included in load_user_userpics)
|
||
|
|
return undef if $opts && $opts->{no_expunged};
|
||
|
|
my $row = $u->selectrow_hashref(
|
||
|
|
"SELECT userid, picid, width, height, state, "
|
||
|
|
. "fmt, comment, description, location, url, "
|
||
|
|
. "UNIX_TIMESTAMP(picdate) AS 'pictime', flags, md5base64 "
|
||
|
|
. "FROM userpic2 WHERE userid=? AND picid=?",
|
||
|
|
undef, $u->userid, $picid
|
||
|
|
);
|
||
|
|
return $obj->absorb_row($row) if $row;
|
||
|
|
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub _skeleton {
|
||
|
|
my ( $class, $u, $picid ) = @_;
|
||
|
|
$picid = 0 unless defined $picid;
|
||
|
|
|
||
|
|
# starts out as a skeleton and gets loaded in over time, as needed:
|
||
|
|
return bless {
|
||
|
|
userid => $u->{userid},
|
||
|
|
picid => int($picid),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
# given a md5sum, load a userpic
|
||
|
|
# takes $u, $md5sum (base64)
|
||
|
|
# TODO: croak if md5sum is wrong number of bytes
|
||
|
|
sub new_from_md5 {
|
||
|
|
my ( $class, $u, $md5sum ) = @_;
|
||
|
|
die unless $u && length($md5sum) == 22;
|
||
|
|
|
||
|
|
my $sth = $u->prepare( "SELECT * FROM userpic2 WHERE userid=? " . "AND md5base64=?" );
|
||
|
|
$sth->execute( $u->{'userid'}, $md5sum );
|
||
|
|
my $row = $sth->fetchrow_hashref
|
||
|
|
or return undef;
|
||
|
|
return LJ::Userpic->new_from_row($row);
|
||
|
|
}
|
||
|
|
|
||
|
|
sub preload_default_userpics {
|
||
|
|
my ( $class, @us ) = @_;
|
||
|
|
|
||
|
|
foreach my $u (@us) {
|
||
|
|
my $up = $u->userpic or next;
|
||
|
|
$up->load_row;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sub new_from_row {
|
||
|
|
my ( $class, $row ) = @_;
|
||
|
|
die unless $row && $row->{userid} && $row->{picid};
|
||
|
|
my $self = LJ::Userpic->new( LJ::load_userid( $row->{userid} ), $row->{picid} );
|
||
|
|
$self->absorb_row($row);
|
||
|
|
return $self;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $class->new_from_keyword( $u, $kw ) >>
|
||
|
|
|
||
|
|
Returns the LJ::Userpic for the given keyword
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub new_from_keyword {
|
||
|
|
my ( $class, $u, $kw ) = @_;
|
||
|
|
return undef unless LJ::isu($u);
|
||
|
|
|
||
|
|
my $picid = $u->get_picid_from_keyword($kw);
|
||
|
|
|
||
|
|
return $picid ? $class->new( $u, $picid ) : undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $class->new_from_mapid( $u, $mapid ) >>
|
||
|
|
|
||
|
|
Returns the LJ::Userpic for the given mapid
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub new_from_mapid {
|
||
|
|
my ( $class, $u, $mapid ) = @_;
|
||
|
|
return undef unless LJ::isu($u);
|
||
|
|
|
||
|
|
my $picid = $u->get_picid_from_mapid($mapid);
|
||
|
|
|
||
|
|
return $picid ? $class->new( $u, $picid ) : undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head1 Instance Methods
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub valid {
|
||
|
|
return defined $_[0]->state;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub absorb_row {
|
||
|
|
my ( $self, $row ) = @_;
|
||
|
|
return $self unless $row && ref $row eq 'HASH';
|
||
|
|
for my $f (
|
||
|
|
qw(userid picid width height comment description location state url pictime flags md5base64)
|
||
|
|
)
|
||
|
|
{
|
||
|
|
$self->{$f} = $row->{$f};
|
||
|
|
}
|
||
|
|
my $key;
|
||
|
|
$key ||= $row->{fmt} if exists $row->{fmt};
|
||
|
|
$key ||= $row->{contenttype} if exists $row->{contenttype};
|
||
|
|
$self->{_ext} = $MimeTypeMap{$key} if defined $key;
|
||
|
|
return $self;
|
||
|
|
}
|
||
|
|
|
||
|
|
##
|
||
|
|
## accessors
|
||
|
|
##
|
||
|
|
|
||
|
|
# returns the picture ID associated with the object
|
||
|
|
sub picid {
|
||
|
|
return $_[0]->{picid};
|
||
|
|
}
|
||
|
|
|
||
|
|
*id = \&picid;
|
||
|
|
|
||
|
|
# returns the userid associated with the object
|
||
|
|
sub userid {
|
||
|
|
return $_[0]->{userid};
|
||
|
|
}
|
||
|
|
|
||
|
|
# given a userpic with a known userid, return the user object
|
||
|
|
sub u {
|
||
|
|
return LJ::load_userid( $_[0]->userid );
|
||
|
|
}
|
||
|
|
|
||
|
|
*owner = \&u;
|
||
|
|
|
||
|
|
sub inactive {
|
||
|
|
my $self = $_[0];
|
||
|
|
my $state = defined $self->state ? $self->state : '';
|
||
|
|
return $state eq 'I';
|
||
|
|
}
|
||
|
|
|
||
|
|
sub expunged {
|
||
|
|
my $self = $_[0];
|
||
|
|
my $state = defined $self->state ? $self->state : '';
|
||
|
|
return $state eq 'X';
|
||
|
|
}
|
||
|
|
|
||
|
|
sub state {
|
||
|
|
my $self = $_[0];
|
||
|
|
return $self->{state} if defined $self->{state};
|
||
|
|
$self->load_row;
|
||
|
|
return $self->{state};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub comment {
|
||
|
|
my $self = $_[0];
|
||
|
|
return $self->{comment} if exists $self->{comment};
|
||
|
|
$self->load_row;
|
||
|
|
return $self->{comment};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub description {
|
||
|
|
my $self = $_[0];
|
||
|
|
return $self->{description} if exists $self->{description};
|
||
|
|
$self->load_row;
|
||
|
|
return $self->{description};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub width {
|
||
|
|
my $self = $_[0];
|
||
|
|
my @dims = $self->dimensions;
|
||
|
|
return undef unless @dims;
|
||
|
|
return $dims[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
sub height {
|
||
|
|
my $self = $_[0];
|
||
|
|
my @dims = $self->dimensions;
|
||
|
|
return undef unless @dims;
|
||
|
|
return $dims[1];
|
||
|
|
}
|
||
|
|
|
||
|
|
sub picdate {
|
||
|
|
return LJ::mysql_time( $_[0]->pictime );
|
||
|
|
}
|
||
|
|
|
||
|
|
sub pictime {
|
||
|
|
return $_[0]->{pictime};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub flags {
|
||
|
|
return $_[0]->{flags};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub md5base64 {
|
||
|
|
return $_[0]->{md5base64};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub mimetype {
|
||
|
|
my $self = $_[0];
|
||
|
|
return {
|
||
|
|
gif => 'image/gif',
|
||
|
|
jpg => 'image/jpeg',
|
||
|
|
png => 'image/png'
|
||
|
|
}->{ $self->extension };
|
||
|
|
}
|
||
|
|
|
||
|
|
sub extension {
|
||
|
|
my $self = $_[0];
|
||
|
|
return $self->{_ext} if $self->{_ext};
|
||
|
|
$self->load_row;
|
||
|
|
return $self->{_ext};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub location {
|
||
|
|
my $self = $_[0];
|
||
|
|
return $self->{location} if $self->{location};
|
||
|
|
$self->load_row;
|
||
|
|
return $self->{location};
|
||
|
|
}
|
||
|
|
|
||
|
|
sub storage_key {
|
||
|
|
my ( $self, $userid, $picid ) = @_;
|
||
|
|
|
||
|
|
# If called on LJ::Userpic...
|
||
|
|
return 'up:' . $self->userid . ':' . $self->picid
|
||
|
|
if ref $self;
|
||
|
|
|
||
|
|
# Else...
|
||
|
|
$log->logcroak('Invalid usage of storage_key.')
|
||
|
|
unless defined $userid && defined $picid;
|
||
|
|
return 'up:' . ( $userid + 0 ) . ':' . ( $picid + 0 );
|
||
|
|
}
|
||
|
|
|
||
|
|
sub in_mogile {
|
||
|
|
my $self = $_[0];
|
||
|
|
return ( $self->location // '' ) eq 'mogile';
|
||
|
|
}
|
||
|
|
|
||
|
|
sub in_blobstore {
|
||
|
|
my $self = $_[0];
|
||
|
|
return ( $self->location // '' ) eq 'blobstore';
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns (width, height)
|
||
|
|
sub dimensions {
|
||
|
|
my $self = $_[0];
|
||
|
|
|
||
|
|
# width and height probably loaded from DB
|
||
|
|
return ( $self->{width}, $self->{height} ) if ( $self->{width} && $self->{height} );
|
||
|
|
|
||
|
|
# if not, load them explicitly
|
||
|
|
$self->load_row;
|
||
|
|
return ( $self->{width}, $self->{height} );
|
||
|
|
}
|
||
|
|
|
||
|
|
sub max_allowed_bytes {
|
||
|
|
my ( $class, $u ) = @_;
|
||
|
|
return 600000;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Returns the direct link to the uploaded userpic
|
||
|
|
sub url {
|
||
|
|
my $self = $_[0];
|
||
|
|
|
||
|
|
if ( my $hook_path = LJ::Hooks::run_hook( 'construct_userpic_url', $self ) ) {
|
||
|
|
return $hook_path;
|
||
|
|
}
|
||
|
|
|
||
|
|
return "$LJ::USERPIC_ROOT/$self->{picid}/$self->{userid}";
|
||
|
|
}
|
||
|
|
|
||
|
|
# Returns original URL used if userpic was originally uploaded
|
||
|
|
# via a URL.
|
||
|
|
# FIXME: should be renamed to source_url
|
||
|
|
sub fullurl {
|
||
|
|
my $self = $_[0];
|
||
|
|
return $self->{url} if $self->{url};
|
||
|
|
$self->load_row;
|
||
|
|
return $self->{url};
|
||
|
|
}
|
||
|
|
|
||
|
|
# given a userpic and a keyword, return the alt text
|
||
|
|
sub alttext {
|
||
|
|
my ( $self, $kw, $mark_default ) = @_;
|
||
|
|
|
||
|
|
# load the alttext.
|
||
|
|
# "username: description (keyword)"
|
||
|
|
# If any of those are not present leave them (and their
|
||
|
|
# affiliated punctuation) out.
|
||
|
|
|
||
|
|
# always include the username
|
||
|
|
my $u = $self->owner;
|
||
|
|
my $alt = $u->username . ":";
|
||
|
|
|
||
|
|
if ( $self->description ) {
|
||
|
|
$alt .= " " . $self->description;
|
||
|
|
}
|
||
|
|
|
||
|
|
# 1. If there is a keyword associated with the icon, use it.
|
||
|
|
if ( defined $kw ) {
|
||
|
|
$alt .= " (" . $kw . ")";
|
||
|
|
}
|
||
|
|
|
||
|
|
# 2. If it was chosen via the default icon, show "(Default)".
|
||
|
|
if ( $mark_default // !defined $kw ) {
|
||
|
|
$alt .= " (Default)";
|
||
|
|
}
|
||
|
|
|
||
|
|
return LJ::ehtml($alt);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
# given a userpic and a keyword, return the title text
|
||
|
|
sub titletext {
|
||
|
|
my ( $self, $kw, $mark_default ) = @_;
|
||
|
|
|
||
|
|
# load the titletext.
|
||
|
|
# "username: keyword (description)"
|
||
|
|
# If any of those are not present leave them (and their
|
||
|
|
# affiliated punctuation) out.
|
||
|
|
|
||
|
|
# always include the username
|
||
|
|
my $u = $self->owner;
|
||
|
|
my $title = $u->username . ":";
|
||
|
|
|
||
|
|
# 1. If there is a keyword associated with the icon, use it.
|
||
|
|
if ( defined $kw ) {
|
||
|
|
$title .= " " . $kw;
|
||
|
|
}
|
||
|
|
|
||
|
|
# 2. If it was chosen via the default icon, show "(Default)".
|
||
|
|
if ( $mark_default // !defined $kw ) {
|
||
|
|
$title .= " (Default)";
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( $self->description ) {
|
||
|
|
$title .= " (" . $self->description . ")";
|
||
|
|
}
|
||
|
|
|
||
|
|
return LJ::ehtml($title);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns an image tag of this userpic
|
||
|
|
# optional parameters (which must be explicitly passed) include
|
||
|
|
# width, keyword, and user (object)
|
||
|
|
sub imgtag {
|
||
|
|
my ( $self, %opts ) = @_;
|
||
|
|
|
||
|
|
# if the width and keyword have been passed in as explicit
|
||
|
|
# parameters, set them. Otherwise, take what ever is set in
|
||
|
|
# the userpic
|
||
|
|
my $width = $opts{width} || $self->width;
|
||
|
|
my $height = $opts{height} || $self->height;
|
||
|
|
my $keyword = $opts{keyword} || $self->keywords;
|
||
|
|
|
||
|
|
my $alttext = $self->alttext($keyword);
|
||
|
|
my $title = $self->titletext($keyword);
|
||
|
|
|
||
|
|
return
|
||
|
|
'<img src="'
|
||
|
|
. $self->url
|
||
|
|
. '" width="'
|
||
|
|
. $width
|
||
|
|
. '" height="'
|
||
|
|
. $height
|
||
|
|
. '" alt="'
|
||
|
|
. $alttext
|
||
|
|
. '" title="'
|
||
|
|
. $title
|
||
|
|
. '" class="userpic-img" />';
|
||
|
|
}
|
||
|
|
|
||
|
|
# FIXME: should have alt text, if it should be kept
|
||
|
|
sub imgtag_lite {
|
||
|
|
my $self = $_[0];
|
||
|
|
return
|
||
|
|
'<img src="'
|
||
|
|
. $self->url
|
||
|
|
. '" width="'
|
||
|
|
. $self->width
|
||
|
|
. '" height="'
|
||
|
|
. $self->height
|
||
|
|
. '" class="userpic-img" />';
|
||
|
|
}
|
||
|
|
|
||
|
|
# FIXME: should have alt text, if it should be kept
|
||
|
|
sub imgtag_nosize {
|
||
|
|
my $self = $_[0];
|
||
|
|
return '<img src="' . $self->url . '" class="userpic-img" />';
|
||
|
|
}
|
||
|
|
|
||
|
|
# pass the decimal version of a percentage that you want to shrink/increase the userpic by
|
||
|
|
# default is 50% of original size
|
||
|
|
sub imgtag_percentagesize {
|
||
|
|
my ( $self, $percentage ) = @_;
|
||
|
|
$percentage ||= 0.5;
|
||
|
|
|
||
|
|
my $width = int( $self->width * $percentage );
|
||
|
|
my $height = int( $self->height * $percentage );
|
||
|
|
|
||
|
|
return
|
||
|
|
'<img src="'
|
||
|
|
. $self->url
|
||
|
|
. '" width="'
|
||
|
|
. $width
|
||
|
|
. '" height="'
|
||
|
|
. $height
|
||
|
|
. '" class="userpic-img" />';
|
||
|
|
}
|
||
|
|
|
||
|
|
# pass a fixed height or width that you want to be the size of the userpic
|
||
|
|
# must include either a height or width, if both are given the smaller of the two is used
|
||
|
|
# returns the width and height attributes as a string to insert into an img tag
|
||
|
|
sub img_fixedsize {
|
||
|
|
my ( $self, %opts ) = @_;
|
||
|
|
|
||
|
|
my $width = $opts{width} || 0;
|
||
|
|
my $height = $opts{height} || 0;
|
||
|
|
|
||
|
|
if ( $width > 0
|
||
|
|
&& $width < $self->width
|
||
|
|
&& ( !$height || ( $width <= $height && $self->width >= $self->height ) ) )
|
||
|
|
{
|
||
|
|
my $ratio = $width / $self->width;
|
||
|
|
$height = int( $self->height * $ratio );
|
||
|
|
}
|
||
|
|
elsif ( $height > 0 && $height < $self->height ) {
|
||
|
|
my $ratio = $height / $self->height;
|
||
|
|
$width = int( $self->width * $ratio );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$width = $self->width;
|
||
|
|
$height = $self->height;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 'height="' . $height . '" width="' . $width . '"';
|
||
|
|
}
|
||
|
|
|
||
|
|
# in scalar context returns comma-seperated list of keywords or "pic#12345" if no keywords defined
|
||
|
|
# in list context returns list of keywords ( (pic#12345) if none defined )
|
||
|
|
# opts: 'raw' = return '' instead of 'pic#12345'
|
||
|
|
sub keywords {
|
||
|
|
my ( $self, %opts ) = @_;
|
||
|
|
|
||
|
|
my $raw = delete $opts{raw} || undef;
|
||
|
|
|
||
|
|
$log->logcroak("Invalid opts passed to LJ::Userpic::keywords")
|
||
|
|
if keys %opts;
|
||
|
|
|
||
|
|
my $u = $self->owner;
|
||
|
|
|
||
|
|
my $keywords = $u->get_userpic_kw_map();
|
||
|
|
|
||
|
|
# return keywords for this picid
|
||
|
|
my @pickeywords = $keywords->{ $self->id } ? @{ $keywords->{ $self->id } } : ();
|
||
|
|
|
||
|
|
if (wantarray) {
|
||
|
|
|
||
|
|
# if list context return the array
|
||
|
|
return ( $raw ? ('') : ( "pic#" . $self->id ) ) unless @pickeywords;
|
||
|
|
|
||
|
|
return sort { lc $a cmp lc $b } @pickeywords;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
# if scalar context return comma-seperated list of keywords, or "pic#12345" if no keywords
|
||
|
|
return ( $raw ? '' : "pic#" . $self->id ) unless @pickeywords;
|
||
|
|
|
||
|
|
return join( ', ', sort { lc $a cmp lc $b } @pickeywords );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sub imagedata {
|
||
|
|
my $self = $_[0];
|
||
|
|
$self->load_row or return undef;
|
||
|
|
return undef if $self->expunged;
|
||
|
|
|
||
|
|
my $data = DW::BlobStore->retrieve( userpics => $self->storage_key );
|
||
|
|
return $data ? $$data : undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
# get : class :: load_row : object
|
||
|
|
sub load_row {
|
||
|
|
my $self = $_[0];
|
||
|
|
|
||
|
|
# use class method
|
||
|
|
return $self->get( $self->owner, $self->picid );
|
||
|
|
}
|
||
|
|
|
||
|
|
# checks request cache and memcache,
|
||
|
|
# returns: undef if nothing in cache
|
||
|
|
# arrayref of LJ::Userpic instances if found in cache
|
||
|
|
sub get_cache {
|
||
|
|
my ( $class, $u ) = @_;
|
||
|
|
|
||
|
|
# check request cache first!
|
||
|
|
# -- this gets populated when a ->load_user_userpics call happens,
|
||
|
|
# so the actual guts of the LJ::Userpic objects is cached in
|
||
|
|
# the singletons
|
||
|
|
if ( $u->{_userpicids} ) {
|
||
|
|
return [ map { LJ::Userpic->instance( $u, $_ ) } @{ $u->{_userpicids} } ];
|
||
|
|
}
|
||
|
|
|
||
|
|
my $memkey = $class->memkey($u);
|
||
|
|
my $memval = LJ::MemCache::get($memkey);
|
||
|
|
|
||
|
|
# nothing found in cache, return undef
|
||
|
|
return undef unless $memval;
|
||
|
|
|
||
|
|
my @ret = ();
|
||
|
|
foreach my $row (@$memval) {
|
||
|
|
my $curr = LJ::MemCache::array_to_hash( 'userpic2', $row );
|
||
|
|
$curr->{userid} = $u->id;
|
||
|
|
push @ret, LJ::Userpic->new_from_row($curr);
|
||
|
|
}
|
||
|
|
|
||
|
|
# set cache of picids on $u since we got them from memcache
|
||
|
|
$u->{_userpicids} = [ map { $_->picid } @ret ];
|
||
|
|
|
||
|
|
# return arrayref of LJ::Userpic instances
|
||
|
|
return \@ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
# $class->memkey( $u )
|
||
|
|
sub memkey {
|
||
|
|
return [ $_[1]->id, "userpic2:" . $_[1]->id ];
|
||
|
|
}
|
||
|
|
|
||
|
|
sub set_cache {
|
||
|
|
my ( $class, $u, $rows ) = @_;
|
||
|
|
|
||
|
|
my $memkey = $class->memkey($u);
|
||
|
|
my @vals = map { LJ::MemCache::hash_to_array( 'userpic2', $_ ) } @$rows;
|
||
|
|
LJ::MemCache::set( $memkey, \@vals, 60 * 30 );
|
||
|
|
|
||
|
|
# set cache of picids on $u
|
||
|
|
$u->{_userpicids} = [ map { $_->{picid} } @$rows ];
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub load_user_userpics {
|
||
|
|
my ( $class, $u ) = @_;
|
||
|
|
local $LJ::THROW_ERRORS = 1;
|
||
|
|
|
||
|
|
my $cache = $class->get_cache($u);
|
||
|
|
return @$cache if $cache;
|
||
|
|
|
||
|
|
# select all of their userpics
|
||
|
|
my $data = $u->selectall_hashref(
|
||
|
|
"SELECT userid, picid, width, height, state, fmt, comment,"
|
||
|
|
. " description, location, url, UNIX_TIMESTAMP(picdate) AS 'pictime',"
|
||
|
|
. " flags, md5base64 FROM userpic2 WHERE userid=? AND state <> 'X'",
|
||
|
|
'picid', undef, $u->userid
|
||
|
|
);
|
||
|
|
die "Error loading userpics: clusterid=$u->{clusterid}, errstr=" . $u->errstr
|
||
|
|
if $u->err;
|
||
|
|
|
||
|
|
my @ret = sort { $a->{picid} <=> $b->{picid} } values %$data;
|
||
|
|
|
||
|
|
# set cache if reasonable
|
||
|
|
$class->set_cache( $u, \@ret );
|
||
|
|
|
||
|
|
return map { $class->new_from_row($_) } @ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub create {
|
||
|
|
my ( $class, $u, %opts ) = @_;
|
||
|
|
local $LJ::THROW_ERRORS = 1;
|
||
|
|
|
||
|
|
my $dataref = delete $opts{data};
|
||
|
|
my $maxbytesize = delete $opts{maxbytesize};
|
||
|
|
my $nonotify = delete $opts{nonotify};
|
||
|
|
$log->logcroak("dataref not a scalarref")
|
||
|
|
unless ref $dataref eq 'SCALAR';
|
||
|
|
$log->logcroak( "Unknown extra options: " . join( ", ", scalar keys %opts ) )
|
||
|
|
if %opts;
|
||
|
|
|
||
|
|
my $err = sub {
|
||
|
|
my $msg = $_[0];
|
||
|
|
};
|
||
|
|
|
||
|
|
# FIXME the filetype is supposed to be returned in the next call
|
||
|
|
# but according to the docs of Image::Size v3.2 it does not return that value
|
||
|
|
eval "use Image::Size;";
|
||
|
|
my ( $w, $h, $filetype ) = Image::Size::imgsize($dataref);
|
||
|
|
my $MAX_UPLOAD = $maxbytesize || LJ::Userpic->max_allowed_bytes($u);
|
||
|
|
|
||
|
|
my $size = length $$dataref;
|
||
|
|
my $fmterror = 0;
|
||
|
|
|
||
|
|
my @errors;
|
||
|
|
if ( $size > $MAX_UPLOAD ) {
|
||
|
|
push @errors,
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::Bytesize",
|
||
|
|
size => $size,
|
||
|
|
max => int( $MAX_UPLOAD / 1024 )
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
unless ( $filetype eq "GIF" || $filetype eq "JPG" || $filetype eq "PNG" ) {
|
||
|
|
push @errors, LJ::errobj( "Userpic::FileType", type => $filetype );
|
||
|
|
$fmterror = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# don't throw a dimensions error if it's the wrong file type because its dimensions will always
|
||
|
|
# be 0x0
|
||
|
|
unless ( $w && $w >= 1 && $w <= 100 && $h && $h >= 1 && $h <= 100 ) {
|
||
|
|
push @errors,
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::Dimensions",
|
||
|
|
w => $w,
|
||
|
|
h => $h
|
||
|
|
) unless $fmterror;
|
||
|
|
}
|
||
|
|
|
||
|
|
LJ::throw(@errors);
|
||
|
|
|
||
|
|
# see if it's a duplicate, return it if it is
|
||
|
|
my $base64 = Digest::MD5::md5_base64($$dataref);
|
||
|
|
if ( my $dup_up = LJ::Userpic->new_from_md5( $u, $base64 ) ) {
|
||
|
|
return $dup_up;
|
||
|
|
}
|
||
|
|
|
||
|
|
# start making a new onew
|
||
|
|
my $picid = LJ::alloc_global_counter('P');
|
||
|
|
|
||
|
|
my $contenttype = {
|
||
|
|
'GIF' => 'G',
|
||
|
|
'PNG' => 'P',
|
||
|
|
'JPG' => 'J',
|
||
|
|
}->{$filetype};
|
||
|
|
|
||
|
|
@errors = (); # TEMP: FIXME: remove... using exceptions
|
||
|
|
|
||
|
|
$u->do(
|
||
|
|
q{INSERT INTO userpic2 (
|
||
|
|
picid, userid, fmt, width, height, picdate, md5base64, location)
|
||
|
|
VALUES (?, ?, ?, ?, ?, NOW(), ?, ?)},
|
||
|
|
undef, $picid, $u->userid, $contenttype, $w, $h, $base64, 'blobstore'
|
||
|
|
);
|
||
|
|
push @errors, $err->( $u->errstr )
|
||
|
|
if $u->err;
|
||
|
|
|
||
|
|
# All pictures are now stored to blobstore
|
||
|
|
my $storage_key = LJ::Userpic->storage_key( $u->userid, $picid );
|
||
|
|
unless ( DW::BlobStore->store( userpics => $storage_key, $dataref ) ) {
|
||
|
|
$u->do( q{DELETE FROM userpic2 WHERE userid=? AND picid=?}, undef, $u->userid, $picid );
|
||
|
|
push @errors, 'Failed to store userpic in blobstore.';
|
||
|
|
}
|
||
|
|
LJ::throw(@errors);
|
||
|
|
|
||
|
|
# now that we've created a new pic, invalidate the user's memcached userpic info
|
||
|
|
LJ::Userpic->delete_cache($u);
|
||
|
|
|
||
|
|
# Fire ESN and return
|
||
|
|
my $pic = LJ::Userpic->new( $u, $picid )
|
||
|
|
or $log->logcroak('Error insantiating userpic after creation');
|
||
|
|
LJ::Event::NewUserpic->new($pic)->fire
|
||
|
|
if LJ::is_enabled('esn') && !$nonotify;
|
||
|
|
return $pic;
|
||
|
|
}
|
||
|
|
|
||
|
|
# this will return a user's userpicfactory image stored in mogile scaled down.
|
||
|
|
# if only $size is passed, will return image scaled so the largest dimension will
|
||
|
|
# not be greater than $size. If $x1, $y1... are set then it will return the image
|
||
|
|
# scaled so the largest dimension will not be greater than 100
|
||
|
|
# all parameters are optional, default size is 640.
|
||
|
|
#
|
||
|
|
# if maxfilesize option is passed, get_upf_scaled will decrease the image quality
|
||
|
|
# until it reaches maxfilesize, in kilobytes. (only applies to the 100x100 userpic)
|
||
|
|
#
|
||
|
|
# returns [imageref, mime, width, height] on success, undef on failure.
|
||
|
|
#
|
||
|
|
# note: this will always keep the image's original aspect ratio and not distort it.
|
||
|
|
sub get_upf_scaled {
|
||
|
|
my ( $class, %opts ) = @_;
|
||
|
|
my $size = delete $opts{size} || 640;
|
||
|
|
my $x1 = delete $opts{x1};
|
||
|
|
my $y1 = delete $opts{y1};
|
||
|
|
my $x2 = delete $opts{x2};
|
||
|
|
my $y2 = delete $opts{y2};
|
||
|
|
my $border = delete $opts{border} || 0;
|
||
|
|
my $maxfilesize = delete $opts{maxfilesize} || 38;
|
||
|
|
my $u = LJ::want_user( delete $opts{userid} || delete $opts{u} ) || LJ::get_remote();
|
||
|
|
my $mogkey = delete $opts{mogkey};
|
||
|
|
my $downsize_only = delete $opts{downsize_only};
|
||
|
|
$log->logcroak("No userid or remote")
|
||
|
|
unless $u || $mogkey;
|
||
|
|
|
||
|
|
$maxfilesize *= 1024;
|
||
|
|
|
||
|
|
$log->logcroak("Invalid parameters to get_upf_scaled")
|
||
|
|
if scalar keys %opts;
|
||
|
|
|
||
|
|
my $mode = ( $x1 || $y1 || $x2 || $y2 ) ? "crop" : "scale";
|
||
|
|
|
||
|
|
eval "use Image::Magick (); 1;"
|
||
|
|
or return undef;
|
||
|
|
|
||
|
|
eval "use Image::Size (); 1;"
|
||
|
|
or return undef;
|
||
|
|
|
||
|
|
$mogkey ||= 'upf:' . $u->{userid};
|
||
|
|
my $dataref = DW::BlobStore->retrieve( temp => $mogkey )
|
||
|
|
or return undef;
|
||
|
|
|
||
|
|
# original width/height
|
||
|
|
my ( $ow, $oh ) = Image::Size::imgsize($dataref);
|
||
|
|
return undef unless $ow && $oh;
|
||
|
|
|
||
|
|
# converts an ImageMagick object to the form returned to our callers
|
||
|
|
my $imageParams = sub {
|
||
|
|
my $im = $_[0];
|
||
|
|
my $blob = $im->ImageToBlob;
|
||
|
|
return [ \$blob, $im->Get('MIME'), $im->Get('width'), $im->Get('height') ];
|
||
|
|
};
|
||
|
|
|
||
|
|
# compute new width and height while keeping aspect ratio
|
||
|
|
my $getSizedCoords = sub {
|
||
|
|
my ( $newsize, $img ) = @_;
|
||
|
|
|
||
|
|
my $fromw = $img ? $img->Get('width') : $ow;
|
||
|
|
my $fromh = $img ? $img->Get('height') : $oh;
|
||
|
|
|
||
|
|
return ( int( $newsize * $fromw / $fromh ), $newsize ) if $fromh > $fromw;
|
||
|
|
return ( $newsize, int( $newsize * $fromh / $fromw ) );
|
||
|
|
};
|
||
|
|
|
||
|
|
# get the "medium sized" width/height. this is the size which
|
||
|
|
# the user selects from
|
||
|
|
my ( $medw, $medh ) = $getSizedCoords->($size);
|
||
|
|
return undef unless $medw && $medh;
|
||
|
|
|
||
|
|
# simple scaling mode
|
||
|
|
if ( $mode eq "scale" ) {
|
||
|
|
my $image = Image::Magick->new( size => "${medw}x${medh}" )
|
||
|
|
or return undef;
|
||
|
|
$image->BlobToImage($$dataref);
|
||
|
|
unless ( $downsize_only && ( $medw > $ow || $medh > $oh ) ) {
|
||
|
|
$image->Resize( width => $medw, height => $medh );
|
||
|
|
}
|
||
|
|
return $imageParams->($image);
|
||
|
|
}
|
||
|
|
|
||
|
|
# else, we're in 100x100 cropping mode
|
||
|
|
|
||
|
|
# scale user coordinates up from the medium pixelspace to full pixelspace
|
||
|
|
$x1 *= ( $ow / $medw );
|
||
|
|
$x2 *= ( $ow / $medw );
|
||
|
|
$y1 *= ( $oh / $medh );
|
||
|
|
$y2 *= ( $oh / $medh );
|
||
|
|
|
||
|
|
# cropping dimensions from the full pixelspace
|
||
|
|
my $tw = $x2 - $x1;
|
||
|
|
my $th = $y2 - $y1;
|
||
|
|
|
||
|
|
# but if their selected region in full pixelspace is 800x800 or something
|
||
|
|
# ridiculous, no point decoding the JPEG to its full size... we can
|
||
|
|
# decode to a smaller size so we get 100px when we crop
|
||
|
|
my $min_dim = $tw < $th ? $tw : $th;
|
||
|
|
my ( $decodew, $decodeh ) = ( $ow, $oh );
|
||
|
|
my $wanted_size = 100;
|
||
|
|
if ( $min_dim > $wanted_size ) {
|
||
|
|
|
||
|
|
# then let's not decode the full JPEG down from its huge size
|
||
|
|
my $de_scale = $wanted_size / $min_dim;
|
||
|
|
$decodew = int( $de_scale * $decodew );
|
||
|
|
$decodeh = int( $de_scale * $decodeh );
|
||
|
|
$_ *= $de_scale foreach ( $x1, $x2, $y1, $y2 );
|
||
|
|
}
|
||
|
|
|
||
|
|
$_ = int($_) foreach ( $x1, $x2, $y1, $y2, $tw, $th );
|
||
|
|
|
||
|
|
# make the pristine (uncompressed) 100x100 image
|
||
|
|
my $timage = Image::Magick->new( size => "${decodew}x${decodeh}" )
|
||
|
|
or return undef;
|
||
|
|
$timage->BlobToImage($$dataref);
|
||
|
|
$timage->Scale( width => $decodew, height => $decodeh );
|
||
|
|
|
||
|
|
my $w = ( $x2 - $x1 );
|
||
|
|
my $h = ( $y2 - $y1 );
|
||
|
|
my $foo = $timage->Mogrify( crop => "${w}x${h}+$x1+$y1" );
|
||
|
|
|
||
|
|
my $targetSize = $border ? 98 : 100;
|
||
|
|
|
||
|
|
my ( $nw, $nh ) = $getSizedCoords->( $targetSize, $timage );
|
||
|
|
$timage->Scale( width => $nw, height => $nh );
|
||
|
|
|
||
|
|
# add border if desired
|
||
|
|
$timage->Border( geometry => "1x1", color => 'black' ) if $border;
|
||
|
|
|
||
|
|
foreach my $qual (qw(100 90 85 75)) {
|
||
|
|
|
||
|
|
# work off a copy of the image so we aren't recompressing it
|
||
|
|
my $piccopy = $timage->Clone();
|
||
|
|
$piccopy->Set( 'quality' => $qual );
|
||
|
|
my $ret = $imageParams->($piccopy);
|
||
|
|
return $ret if length( ${ $ret->[0] } ) < $maxfilesize;
|
||
|
|
}
|
||
|
|
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
# make this picture the default
|
||
|
|
sub make_default {
|
||
|
|
my $self = shift;
|
||
|
|
my $u = $self->owner
|
||
|
|
or die;
|
||
|
|
|
||
|
|
$u->update_self( { defaultpicid => $self->id } );
|
||
|
|
$u->{'defaultpicid'} = $self->id;
|
||
|
|
}
|
||
|
|
|
||
|
|
# returns true if this picture is the default userpic
|
||
|
|
sub is_default {
|
||
|
|
my $self = $_[0];
|
||
|
|
my $u = $self->owner;
|
||
|
|
return unless defined $u->{'defaultpicid'};
|
||
|
|
|
||
|
|
return $u->{'defaultpicid'} == $self->id;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub delete_cache {
|
||
|
|
my ( $class, $u ) = @_;
|
||
|
|
my $memkey = [ $u->{'userid'}, "upicinf:$u->{'userid'}" ];
|
||
|
|
LJ::MemCache::delete($memkey);
|
||
|
|
$memkey = [ $u->{'userid'}, "upiccom:$u->{'userid'}" ];
|
||
|
|
LJ::MemCache::delete($memkey);
|
||
|
|
$memkey = [ $u->{'userid'}, "upicurl:$u->{'userid'}" ];
|
||
|
|
LJ::MemCache::delete($memkey);
|
||
|
|
$memkey = [ $u->{'userid'}, "upicdes:$u->{'userid'}" ];
|
||
|
|
LJ::MemCache::delete($memkey);
|
||
|
|
|
||
|
|
# userpic2 rows for a given $u
|
||
|
|
$memkey = LJ::Userpic->memkey($u);
|
||
|
|
LJ::MemCache::delete($memkey);
|
||
|
|
|
||
|
|
delete $u->{_userpicids};
|
||
|
|
|
||
|
|
# clear process cache
|
||
|
|
$LJ::CACHE_USERPIC_INFO{ $u->{'userid'} } = undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
# delete this userpic
|
||
|
|
# TODO: error checking/throw errors on failure
|
||
|
|
sub delete {
|
||
|
|
my $self = $_[0];
|
||
|
|
local $LJ::THROW_ERRORS = 1;
|
||
|
|
|
||
|
|
my $fail = sub {
|
||
|
|
LJ::errobj(
|
||
|
|
"WithSubError",
|
||
|
|
main => LJ::errobj("DeleteFailed"),
|
||
|
|
suberr => $@
|
||
|
|
)->throw;
|
||
|
|
};
|
||
|
|
|
||
|
|
my $u = $self->owner;
|
||
|
|
my $picid = $self->id;
|
||
|
|
|
||
|
|
# userpic keywords
|
||
|
|
eval {
|
||
|
|
if ( $u->userpic_have_mapid ) {
|
||
|
|
$u->do( "DELETE FROM userpicmap3 WHERE userid = ? AND picid = ? AND kwid=NULL",
|
||
|
|
undef, $u->userid, $picid )
|
||
|
|
or die;
|
||
|
|
$u->do( "UPDATE userpicmap3 SET picid=NULL WHERE userid=? AND picid=?",
|
||
|
|
undef, $u->userid, $picid )
|
||
|
|
or die;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$u->do( "DELETE FROM userpicmap2 WHERE userid=? AND picid=?",
|
||
|
|
undef, $u->userid, $picid )
|
||
|
|
or die;
|
||
|
|
}
|
||
|
|
$u->do( "DELETE FROM userpic2 WHERE picid=? AND userid=?", undef, $picid, $u->userid )
|
||
|
|
or die;
|
||
|
|
};
|
||
|
|
$fail->() if $@;
|
||
|
|
|
||
|
|
$u->log_event( 'delete_userpic', { picid => $picid } );
|
||
|
|
DW::BlobStore->delete( userpics => $self->storage_key );
|
||
|
|
LJ::Userpic->delete_cache($u);
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub set_comment {
|
||
|
|
my ( $self, $comment ) = @_;
|
||
|
|
local $LJ::THROW_ERRORS = 1;
|
||
|
|
|
||
|
|
my $u = $self->owner;
|
||
|
|
$comment = LJ::text_trim( $comment, LJ::BMAX_UPIC_COMMENT(), LJ::CMAX_UPIC_COMMENT() );
|
||
|
|
$u->do( "UPDATE userpic2 SET comment=? WHERE userid=? AND picid=?",
|
||
|
|
undef, $comment, $u->{'userid'}, $self->id )
|
||
|
|
or die;
|
||
|
|
$self->{comment} = $comment;
|
||
|
|
|
||
|
|
LJ::Userpic->delete_cache($u);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub set_description {
|
||
|
|
my ( $self, $description ) = @_;
|
||
|
|
local $LJ::THROW_ERRORS = 1;
|
||
|
|
|
||
|
|
my $u = $self->owner;
|
||
|
|
|
||
|
|
#return 0 unless LJ::Userpic->user_supports_descriptions($u);
|
||
|
|
$description =
|
||
|
|
LJ::text_trim( $description, LJ::BMAX_UPIC_DESCRIPTION, LJ::CMAX_UPIC_DESCRIPTION );
|
||
|
|
$u->do( "UPDATE userpic2 SET description=? WHERE userid=? AND picid=?",
|
||
|
|
undef, $description, $u->{'userid'}, $self->id )
|
||
|
|
or die;
|
||
|
|
$self->{description} = $description;
|
||
|
|
|
||
|
|
LJ::Userpic->delete_cache($u);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# instance method: takes a string of comma-separate keywords, or an array of keywords
|
||
|
|
sub set_keywords {
|
||
|
|
my $self = shift;
|
||
|
|
|
||
|
|
my @keywords;
|
||
|
|
if ( @_ > 1 ) {
|
||
|
|
@keywords = @_;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
@keywords = split( ',', $_[0] );
|
||
|
|
}
|
||
|
|
|
||
|
|
@keywords = grep { !/^pic\#\d+$/ } map { s/^\s+//; s/\s+$//; $_ } @keywords;
|
||
|
|
|
||
|
|
my $u = $self->owner;
|
||
|
|
my $have_mapid = $u->userpic_have_mapid;
|
||
|
|
|
||
|
|
my $sth;
|
||
|
|
my $dbh;
|
||
|
|
|
||
|
|
if ($have_mapid) {
|
||
|
|
$sth = $u->prepare("SELECT kwid FROM userpicmap3 WHERE userid=? AND picid=?");
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$sth = $u->prepare("SELECT kwid FROM userpicmap2 WHERE userid=? AND picid=?");
|
||
|
|
}
|
||
|
|
$sth->execute( $u->userid, $self->id );
|
||
|
|
|
||
|
|
my %exist_kwids;
|
||
|
|
while ( my ($kwid) = $sth->fetchrow_array ) {
|
||
|
|
|
||
|
|
# This is an edge case to catch keyword changes where the existing keyword
|
||
|
|
# is in the pic# format. In this case kwid is NULL and we want to
|
||
|
|
# delete any records from userpicmap3 that involve it.
|
||
|
|
unless ($kwid) {
|
||
|
|
$u->do( "DELETE FROM userpicmap3 WHERE userid=? AND picid=? AND kwid IS NULL",
|
||
|
|
undef, $u->id, $self->id );
|
||
|
|
}
|
||
|
|
|
||
|
|
$exist_kwids{$kwid} = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
my %kwid_to_mapid;
|
||
|
|
if ($have_mapid) {
|
||
|
|
$sth =
|
||
|
|
$u->prepare("SELECT mapid, kwid FROM userpicmap3 WHERE userid=? AND kwid IS NOT NULL");
|
||
|
|
$sth->execute( $u->userid );
|
||
|
|
|
||
|
|
while ( my ( $mapid, $kwid ) = $sth->fetchrow_array ) {
|
||
|
|
$kwid_to_mapid{$kwid} = $mapid;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
my ( @bind, @data, @kw_errors );
|
||
|
|
my $c = 0;
|
||
|
|
my $picid = $self->{picid};
|
||
|
|
|
||
|
|
foreach my $kw (@keywords) {
|
||
|
|
my $kwid = $u->get_keyword_id($kw);
|
||
|
|
|
||
|
|
next unless $kwid; # TODO: fire some warning that keyword was bogus
|
||
|
|
|
||
|
|
if ( ++$c > $LJ::MAX_USERPIC_KEYWORDS ) {
|
||
|
|
push @kw_errors, $kw;
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( exists $exist_kwids{$kwid} ) {
|
||
|
|
delete $exist_kwids{$kwid};
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
if ($have_mapid) {
|
||
|
|
$kwid_to_mapid{$kwid} ||= LJ::alloc_user_counter( $u, 'Y' );
|
||
|
|
|
||
|
|
push @bind, '(?, ?, ?, ?)';
|
||
|
|
push @data, $u->userid, $kwid_to_mapid{$kwid}, $kwid, $picid;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
push @bind, '(?, ?, ?)';
|
||
|
|
push @data, $u->userid, $kwid, $picid;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
LJ::Userpic->delete_cache($u);
|
||
|
|
|
||
|
|
foreach my $kwid ( keys %exist_kwids ) {
|
||
|
|
if ($have_mapid) {
|
||
|
|
$u->do( "UPDATE userpicmap3 SET picid=NULL WHERE userid=? AND picid=? AND kwid=?",
|
||
|
|
undef, $u->id, $self->id, $kwid );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$u->do( "DELETE FROM userpicmap2 WHERE userid=? AND picid=? AND kwid=?",
|
||
|
|
undef, $u->id, $self->id, $kwid );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# save data if any
|
||
|
|
if ( scalar @data ) {
|
||
|
|
my $bind = join( ',', @bind );
|
||
|
|
|
||
|
|
if ($have_mapid) {
|
||
|
|
$u->do( "REPLACE INTO userpicmap3 (userid, mapid, kwid, picid) VALUES $bind",
|
||
|
|
undef, @data );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$u->do( "REPLACE INTO userpicmap2 (userid, kwid, picid) VALUES $bind", undef, @data );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# clear the userpic-keyword map.
|
||
|
|
$u->clear_userpic_kw_map;
|
||
|
|
|
||
|
|
# Let the user know about any we didn't save
|
||
|
|
# don't throw until the end or nothing will be saved!
|
||
|
|
if (@kw_errors) {
|
||
|
|
my $num_words = scalar(@kw_errors);
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::TooManyKeywords",
|
||
|
|
userpic => $self,
|
||
|
|
lost => \@kw_errors
|
||
|
|
)->throw;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# instance method: takes two strings of comma-separated keywords, the first
|
||
|
|
# being the new set of keywords, the second being the old set of keywords.
|
||
|
|
#
|
||
|
|
# the new keywords must be the same number as the old keywords; that is,
|
||
|
|
# if the userpic has three keywords and you want to rename them, you must
|
||
|
|
# rename them to three keywords (some can match). otherwise there would be
|
||
|
|
# some ambiguity about which old keywords should match up with the new
|
||
|
|
# keywords. if the number of keywords don't match, then an error is thrown
|
||
|
|
# and no changes are made to the keywords for this userpic.
|
||
|
|
#
|
||
|
|
# all new keywords must not currently be in use; you can't rename a keyword
|
||
|
|
# to a keyword currently mapped to another (or the same) userpic. this will
|
||
|
|
# result in an error and no changes made to these keywords.
|
||
|
|
sub set_and_rename_keywords {
|
||
|
|
my ( $self, $new_keyword_string, $orig_keyword_string ) = @_;
|
||
|
|
|
||
|
|
my $u = $self->owner;
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::RenameKeywords",
|
||
|
|
origkw => $orig_keyword_string,
|
||
|
|
newkw => $new_keyword_string
|
||
|
|
)->throw
|
||
|
|
unless LJ::is_enabled("icon_renames") || $u->userpic_have_mapid;
|
||
|
|
|
||
|
|
my @keywords = split( ',', $new_keyword_string );
|
||
|
|
my @orig_keywords = split( ',', $orig_keyword_string );
|
||
|
|
|
||
|
|
if ( grep ( /^\s*pic\#\d+\s*$/, @keywords ) ) {
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::RenameBlankKeywords",
|
||
|
|
origkw => $orig_keyword_string,
|
||
|
|
newkw => $new_keyword_string
|
||
|
|
)->throw;
|
||
|
|
}
|
||
|
|
|
||
|
|
# compare sizes
|
||
|
|
if ( scalar @keywords ne scalar @orig_keywords ) {
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::MismatchRenameKeywords",
|
||
|
|
origkw => $orig_keyword_string,
|
||
|
|
newkw => $new_keyword_string
|
||
|
|
)->throw;
|
||
|
|
}
|
||
|
|
|
||
|
|
#interleave these into a map, excluding duplicates
|
||
|
|
my %keywordmap;
|
||
|
|
foreach my $newkw (@keywords) {
|
||
|
|
my $origkw = shift(@orig_keywords);
|
||
|
|
|
||
|
|
# clear whitespace
|
||
|
|
$newkw =~ s/^\s+//;
|
||
|
|
$newkw =~ s/\s+$//;
|
||
|
|
$origkw =~ s/^\s+//;
|
||
|
|
$origkw =~ s/\s+$//;
|
||
|
|
|
||
|
|
$keywordmap{$origkw} = $newkw if $origkw ne $newkw;
|
||
|
|
}
|
||
|
|
|
||
|
|
# make sure there is at least one change.
|
||
|
|
if ( keys(%keywordmap) ) {
|
||
|
|
|
||
|
|
#make sure that none of the target keywords already exist.
|
||
|
|
foreach my $kw ( values %keywordmap ) {
|
||
|
|
if ( $u && $u->get_picid_from_keyword( $kw, -1 ) != -1 ) {
|
||
|
|
LJ::errobj( "Userpic::RenameKeywordExisting", keyword => $kw )->throw;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
while ( my ( $origkw, $newkw ) = each(%keywordmap) ) {
|
||
|
|
|
||
|
|
# need to check if the kwid already has a mapid
|
||
|
|
my $mapid = $u->get_mapid_from_keyword($newkw);
|
||
|
|
|
||
|
|
# if it does, we have to remap it
|
||
|
|
if ($mapid) {
|
||
|
|
my $oldid = $u->get_mapid_from_keyword($origkw);
|
||
|
|
|
||
|
|
# redirect the old mapid to the new mapid
|
||
|
|
$u->do(
|
||
|
|
"UPDATE userpicmap3 SET kwid = NULL, picid = NULL, redirect_mapid = ? WHERE mapid = ? AND userid = ?",
|
||
|
|
undef, $mapid, $oldid, $u->id
|
||
|
|
);
|
||
|
|
if ( $u->err ) {
|
||
|
|
warn $u->errstr;
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::RenameKeywords",
|
||
|
|
origkw => $origkw,
|
||
|
|
newkw => $newkw
|
||
|
|
)->throw;
|
||
|
|
}
|
||
|
|
|
||
|
|
# change any redirects pointing to the old mapid to the new mapid
|
||
|
|
$u->do(
|
||
|
|
"UPDATE userpicmap3 SET redirect_mapid = ? WHERE redirect_mapid = ? AND userid = ?",
|
||
|
|
undef, $mapid, $oldid, $u->id
|
||
|
|
);
|
||
|
|
if ( $u->err ) {
|
||
|
|
warn $u->errstr;
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::RenameKeywords",
|
||
|
|
origkw => $origkw,
|
||
|
|
newkw => $newkw
|
||
|
|
)->throw;
|
||
|
|
}
|
||
|
|
|
||
|
|
# and set the new mapid to point to the picture
|
||
|
|
$u->do( "UPDATE userpicmap3 SET picid = ? WHERE mapid = ? AND userid = ?",
|
||
|
|
undef, $self->picid, $mapid, $u->id );
|
||
|
|
if ( $u->err ) {
|
||
|
|
warn $u->errstr;
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::RenameKeywords",
|
||
|
|
origkw => $origkw,
|
||
|
|
newkw => $newkw
|
||
|
|
)->throw;
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
if ( $origkw !~ /^\s*pic\#(\d+)\s*$/ ) {
|
||
|
|
$u->do(
|
||
|
|
"UPDATE userpicmap3 SET kwid = ? WHERE kwid = ? AND userid = ?",
|
||
|
|
undef,
|
||
|
|
$u->get_keyword_id($newkw),
|
||
|
|
$u->get_keyword_id($origkw), $u->id
|
||
|
|
);
|
||
|
|
if ( $u->err ) {
|
||
|
|
warn $u->errstr;
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::RenameKeywords",
|
||
|
|
origkw => $origkw,
|
||
|
|
newkw => $newkw
|
||
|
|
)->throw;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else { # pic#xx
|
||
|
|
my $picid = $1;
|
||
|
|
|
||
|
|
# get (or create) the mapid for picxx
|
||
|
|
my $mapid_for_picxx = $u->get_mapid_from_keyword($origkw);
|
||
|
|
$u->do(
|
||
|
|
"UPDATE userpicmap3 SET kwid = ? WHERE kwid is NULL AND userid = ? AND picid = ?",
|
||
|
|
undef, $u->get_keyword_id($newkw), $u->id, $picid
|
||
|
|
);
|
||
|
|
if ( $u->err ) {
|
||
|
|
warn $u->errstr;
|
||
|
|
LJ::errobj(
|
||
|
|
"Userpic::RenameKeywords",
|
||
|
|
origkw => $origkw,
|
||
|
|
newkw => $newkw
|
||
|
|
)->throw;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
LJ::Userpic->delete_cache($u);
|
||
|
|
$u->clear_userpic_kw_map;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub set_fullurl {
|
||
|
|
my ( $self, $url ) = @_;
|
||
|
|
my $u = $self->owner;
|
||
|
|
$u->do( "UPDATE userpic2 SET url=? WHERE userid=? AND picid=?",
|
||
|
|
undef, $url, $u->{'userid'}, $self->id );
|
||
|
|
$self->{url} = $url;
|
||
|
|
|
||
|
|
LJ::Userpic->delete_cache($u);
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Sorts the given list of Userpics.
|
||
|
|
sub sort {
|
||
|
|
my ( $class, $userpics ) = @_;
|
||
|
|
|
||
|
|
return () unless ( $userpics && ref $userpics );
|
||
|
|
|
||
|
|
my %kwhash;
|
||
|
|
my %nokwhash;
|
||
|
|
|
||
|
|
for my $pic (@$userpics) {
|
||
|
|
my $pickw = $pic->keywords( raw => 1 );
|
||
|
|
if ( defined $pickw ) {
|
||
|
|
$kwhash{$pickw} = $pic;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$pickw = $pic->keywords;
|
||
|
|
$nokwhash{$pickw} = $pic;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
my @sortedkw = sort { lc $a cmp lc $b } keys %kwhash;
|
||
|
|
my @sortednokw = sort { lc $a cmp lc $b } keys %nokwhash;
|
||
|
|
|
||
|
|
my @sortedpics;
|
||
|
|
foreach my $kw (@sortedkw) {
|
||
|
|
push @sortedpics, $kwhash{$kw};
|
||
|
|
}
|
||
|
|
foreach my $kw (@sortednokw) {
|
||
|
|
push @sortedpics, $nokwhash{$kw};
|
||
|
|
}
|
||
|
|
|
||
|
|
return @sortedpics;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Organizes the given userpics by keyword. Returns an array of hashes,
|
||
|
|
# with values of keyword and userpic.
|
||
|
|
sub separate_keywords {
|
||
|
|
my ( $class, $userpics ) = @_;
|
||
|
|
|
||
|
|
return () unless ( $userpics && ref $userpics );
|
||
|
|
|
||
|
|
my @userpic_array;
|
||
|
|
my @nokw_array;
|
||
|
|
|
||
|
|
foreach my $userpic (@$userpics) {
|
||
|
|
my @keywords = $userpic->keywords( raw => 0 );
|
||
|
|
foreach my $keyword (@keywords) {
|
||
|
|
if ( defined $keyword ) {
|
||
|
|
push @userpic_array, { keyword => $keyword, userpic => $userpic };
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$keyword = $userpic->keywords;
|
||
|
|
push @nokw_array, { keyword => $keyword, userpic => $userpic };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@userpic_array = sort { lc( $a->{keyword} ) cmp lc( $b->{keyword} ) } @userpic_array;
|
||
|
|
push @userpic_array, sort { $a->{keyword} cmp $b->{keyword} } @nokw_array;
|
||
|
|
|
||
|
|
return @userpic_array;
|
||
|
|
}
|
||
|
|
|
||
|
|
# convert to json
|
||
|
|
sub TO_JSON {
|
||
|
|
my $self = shift;
|
||
|
|
|
||
|
|
my $remote = LJ::get_remote();
|
||
|
|
my @keywords = $self->keywords;
|
||
|
|
my $returnval = {
|
||
|
|
username => $self->u->user,
|
||
|
|
picid => int( $self->picid ),
|
||
|
|
url => $self->url,
|
||
|
|
comment => $self->comment,
|
||
|
|
keywords => \@keywords,
|
||
|
|
};
|
||
|
|
|
||
|
|
if ( $remote && $remote eq $self->u ) {
|
||
|
|
$returnval->{inactive} = $self->inactive;
|
||
|
|
}
|
||
|
|
return $returnval;
|
||
|
|
}
|
||
|
|
|
||
|
|
####
|
||
|
|
# error classes:
|
||
|
|
|
||
|
|
package LJ::Error::Userpic::TooManyKeywords;
|
||
|
|
|
||
|
|
sub user_caused { 1 }
|
||
|
|
sub fields { qw(userpic lost); }
|
||
|
|
|
||
|
|
sub number_lost {
|
||
|
|
my $self = $_[0];
|
||
|
|
return scalar @{ $self->field("lost") };
|
||
|
|
}
|
||
|
|
|
||
|
|
sub lost_keywords_as_html {
|
||
|
|
my $self = $_[0];
|
||
|
|
return join( ", ", map { LJ::ehtml($_) } @{ $self->field("lost") } );
|
||
|
|
}
|
||
|
|
|
||
|
|
sub as_html {
|
||
|
|
my $self = $_[0];
|
||
|
|
return LJ::Lang::ml(
|
||
|
|
"error.editicons.toomanykeywords",
|
||
|
|
{
|
||
|
|
numwords => $self->number_lost,
|
||
|
|
words => $self->lost_keywords_as_html,
|
||
|
|
max => $LJ::MAX_USERPIC_KEYWORDS,
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
package LJ::Error::Userpic::Bytesize;
|
||
|
|
sub user_caused { 1 }
|
||
|
|
sub fields { qw(size max); }
|
||
|
|
|
||
|
|
sub as_html {
|
||
|
|
my $self = $_[0];
|
||
|
|
return LJ::Lang::ml(
|
||
|
|
'error.editicons.filetoolarge',
|
||
|
|
{
|
||
|
|
maxsize => $self->{'max'},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
package LJ::Error::Userpic::Dimensions;
|
||
|
|
sub user_caused { 1 }
|
||
|
|
sub fields { qw(w h); }
|
||
|
|
|
||
|
|
sub as_html {
|
||
|
|
my $self = $_[0];
|
||
|
|
return LJ::Lang::ml(
|
||
|
|
'error.editicons.imagetoolarge',
|
||
|
|
{
|
||
|
|
imagesize => $self->{'w'} . 'x' . $self->{'h'},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
package LJ::Error::Userpic::FileType;
|
||
|
|
sub user_caused { 1 }
|
||
|
|
sub fields { qw(type); }
|
||
|
|
|
||
|
|
sub as_html {
|
||
|
|
my $self = $_[0];
|
||
|
|
return LJ::Lang::ml(
|
||
|
|
"error.editicons.unsupportedtype",
|
||
|
|
{
|
||
|
|
filetype => $self->{'type'},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
package LJ::Error::Userpic::MismatchRenameKeywords;
|
||
|
|
sub user_caused { 1 }
|
||
|
|
sub fields { qw(origkw newkw); }
|
||
|
|
|
||
|
|
sub as_html {
|
||
|
|
my $self = $_[0];
|
||
|
|
return LJ::Lang::ml(
|
||
|
|
"error.iconkw.rename.mismatchedlength",
|
||
|
|
{
|
||
|
|
origkw => $self->{'origkw'},
|
||
|
|
newkw => $self->{'newkw'},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
package LJ::Error::Userpic::RenameBlankKeywords;
|
||
|
|
sub user_caused { 1 }
|
||
|
|
sub fields { qw(origkw newkw); }
|
||
|
|
|
||
|
|
sub as_html {
|
||
|
|
my $self = $_[0];
|
||
|
|
return LJ::Lang::ml(
|
||
|
|
"error.iconkw.rename.blankkw",
|
||
|
|
{
|
||
|
|
origkw => $self->{'origkw'},
|
||
|
|
newkw => $self->{'newkw'},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
package LJ::Error::Userpic::RenameKeywordExisting;
|
||
|
|
sub user_caused { 1 }
|
||
|
|
sub fields { qw(keyword); }
|
||
|
|
|
||
|
|
sub as_html {
|
||
|
|
my $self = $_[0];
|
||
|
|
return LJ::Lang::ml(
|
||
|
|
"error.iconkw.rename.keywordexists",
|
||
|
|
{
|
||
|
|
keyword => $self->{'keyword'},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
package LJ::Error::Userpic::RenameKeywords;
|
||
|
|
sub user_caused { 0 }
|
||
|
|
sub fields { qw(origkw newkw); }
|
||
|
|
|
||
|
|
sub as_html {
|
||
|
|
my $self = $_[0];
|
||
|
|
return LJ::Lang::ml(
|
||
|
|
"error.iconkw.rename.keywords",
|
||
|
|
{
|
||
|
|
origkw => $self->{'origkw'},
|
||
|
|
newkw => $self->{'newkw'},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
package LJ::Error::Userpic::DeleteFailed;
|
||
|
|
sub user_caused { 0 }
|
||
|
|
|
||
|
|
1;
|