# 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 ''
        . $alttext
        . ''; } # FIXME: should have alt text, if it should be kept sub imgtag_lite { my $self = $_[0]; return ''; } # FIXME: should have alt text, if it should be kept sub imgtag_nosize { my $self = $_[0]; return ''; } # 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 ''; } # 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;