1013 lines
34 KiB
Perl
1013 lines
34 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# DW::Controller::EditIcons
|
||
|
|
#
|
||
|
|
# This controller is for creating and managing icons.
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Mark Smith <mark@dreamwidth.org>
|
||
|
|
# Jen Griffin <kareila@livejournal.com>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2016-2017 by Dreamwidth Studios, LLC.
|
||
|
|
#
|
||
|
|
# This program is free software; you may redistribute it and/or modify it under
|
||
|
|
# the same terms as Perl itself. For a copy of the license, please reference
|
||
|
|
# 'perldoc perlartistic' or 'perldoc perlgpl'.
|
||
|
|
#
|
||
|
|
|
||
|
|
package DW::Controller::EditIcons;
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use v5.10;
|
||
|
|
use Log::Log4perl;
|
||
|
|
my $log = Log::Log4perl->get_logger(__PACKAGE__);
|
||
|
|
|
||
|
|
use Digest::MD5;
|
||
|
|
use File::Type;
|
||
|
|
|
||
|
|
use DW::BlobStore;
|
||
|
|
use LJ::Userpic;
|
||
|
|
|
||
|
|
use LJ::Global::Constants;
|
||
|
|
|
||
|
|
use DW::Controller;
|
||
|
|
use DW::Routing;
|
||
|
|
use DW::Template;
|
||
|
|
|
||
|
|
DW::Routing->register_string( "/manage/icons", \&editicons_handler, app => 1 );
|
||
|
|
DW::Routing->register_string( "/tools/userpicfactory", \&factory_handler, app => 1 );
|
||
|
|
DW::Routing->register_string( "/misc/mogupic", \&mogupic_handler, app => 1, formats => 1 );
|
||
|
|
|
||
|
|
sub editicons_handler {
|
||
|
|
|
||
|
|
# don't automatically check form_auth: causes problems with
|
||
|
|
# processing multipart/form-data where the post arguments
|
||
|
|
# have to be parsed out of $r->uploads
|
||
|
|
my ( $ok, $rv ) = controller( authas => 1, form_auth => 0 );
|
||
|
|
return $rv unless $ok;
|
||
|
|
|
||
|
|
my $u = $rv->{u}; # authas || remote
|
||
|
|
my $r = $rv->{r}; # DW::Request
|
||
|
|
|
||
|
|
# error pages must be returned from handler context, not subroutines!
|
||
|
|
# this is basically error_ml without the implicit ML call
|
||
|
|
my $err = sub {
|
||
|
|
return DW::Template->render_template( 'error.tt', { message => $_[0] } );
|
||
|
|
};
|
||
|
|
|
||
|
|
return $err->($LJ::MSG_READONLY_USER) if $u->is_readonly;
|
||
|
|
|
||
|
|
# update this user's activated pics
|
||
|
|
$u->activate_userpics;
|
||
|
|
|
||
|
|
# get userpics and count 'em
|
||
|
|
my @userpics = LJ::Userpic->load_user_userpics($u);
|
||
|
|
|
||
|
|
# get the maximum number of icons for this user
|
||
|
|
my $max = $u->count_max_userpics;
|
||
|
|
|
||
|
|
# keep track of fatal errors vs. non-fatal/informative messages
|
||
|
|
my $errors = DW::FormErrors->new;
|
||
|
|
my @info;
|
||
|
|
|
||
|
|
if ( $r->did_post ) {
|
||
|
|
my $post = $r->post_args;
|
||
|
|
return error_ml("error.utf8") unless LJ::text_in($post);
|
||
|
|
|
||
|
|
LJ::Hooks::run_hooks( 'spam_check', $u, $post, 'icons' );
|
||
|
|
|
||
|
|
### save changes to existing pics
|
||
|
|
if ( $post->{'action:save'} ) {
|
||
|
|
|
||
|
|
# form being posted isn't multipart, so check form_auth
|
||
|
|
return error_ml('error.invalidform')
|
||
|
|
unless LJ::check_form_auth( $post->{lj_form_auth} );
|
||
|
|
|
||
|
|
my $refresh = update_userpics( $post, \@info, \@userpics, $u );
|
||
|
|
|
||
|
|
# reload the pictures to account for deleted
|
||
|
|
@userpics = LJ::Userpic->load_user_userpics($u) if $refresh;
|
||
|
|
}
|
||
|
|
|
||
|
|
unless (%$post) {
|
||
|
|
### no post data, so we'll parse the multipart data -
|
||
|
|
### this means that we have a new pic to handle.
|
||
|
|
my $size = $r->header_in("Content-Length");
|
||
|
|
return error_ml("error.editicons.contentlength") unless $size;
|
||
|
|
|
||
|
|
my $MAX_UPLOAD = LJ::Userpic->max_allowed_bytes($u);
|
||
|
|
my $parsetomogile = $size > $MAX_UPLOAD + 2048;
|
||
|
|
|
||
|
|
# array of map references
|
||
|
|
my @uploaded_userpics;
|
||
|
|
|
||
|
|
# Three possibilities here: (a) small upload, in which case we
|
||
|
|
# just parse it; (b) large upload, in which case we save the
|
||
|
|
# files to temp storage; or (c) coming from the factory.
|
||
|
|
|
||
|
|
unless ($parsetomogile) { # for options (a) and (c)
|
||
|
|
my $uploads = eval { $r->uploads }; # multipart/form-data
|
||
|
|
return $err->($@) if $@;
|
||
|
|
|
||
|
|
$post->{ $_->{name} } = $_->{body} foreach @$uploads;
|
||
|
|
|
||
|
|
# parse the post parameters into an array of new pics
|
||
|
|
@uploaded_userpics = parse_post_uploads( $post, $u, $MAX_UPLOAD );
|
||
|
|
LJ::Hooks::run_hooks( 'spam_check', $u, $post, 'icons' );
|
||
|
|
}
|
||
|
|
|
||
|
|
# if we're (c) coming from the factory, then we don't
|
||
|
|
# need to save the image to temporary storage again
|
||
|
|
my $used_factory = $post->{src} && $post->{src} eq 'factory';
|
||
|
|
$parsetomogile = 0 if $used_factory;
|
||
|
|
|
||
|
|
if ($parsetomogile) {
|
||
|
|
|
||
|
|
# (b) save large images to temporary storage and populate %$post
|
||
|
|
my $error;
|
||
|
|
parse_large_upload( $post, \$error, $u );
|
||
|
|
|
||
|
|
# was there an error parsing the multipart form?
|
||
|
|
return $err->($error) if $error;
|
||
|
|
|
||
|
|
# parse the post parameters into an array of new pics
|
||
|
|
@uploaded_userpics = parse_post_uploads( $post, $u, $MAX_UPLOAD );
|
||
|
|
|
||
|
|
}
|
||
|
|
elsif ($used_factory) {
|
||
|
|
|
||
|
|
# (c) parse the data submitted from the factory
|
||
|
|
my $scaledsizemax = $post->{'scaledSizeMax'};
|
||
|
|
my ( $x1, $x2, $y1, $y2 ) = map { $_ + 0 } @$post{qw( x1 x2 y1 y2 )};
|
||
|
|
|
||
|
|
return error_ml("error.editicons.parse.factory")
|
||
|
|
unless $scaledsizemax && $x2;
|
||
|
|
|
||
|
|
my $picinfo = eval {
|
||
|
|
LJ::Userpic->get_upf_scaled(
|
||
|
|
x1 => $x1,
|
||
|
|
y1 => $y1,
|
||
|
|
x2 => $x2,
|
||
|
|
y2 => $y2,
|
||
|
|
border => $post->{border},
|
||
|
|
userid => $u->userid,
|
||
|
|
mogkey => mogkey( $u, $post->{index} ),
|
||
|
|
);
|
||
|
|
};
|
||
|
|
return error_ml( "error.editicons.get_upf_scaled", { err => $@ } )
|
||
|
|
unless $picinfo;
|
||
|
|
|
||
|
|
# create image data hash for userpic array
|
||
|
|
my %current_upload = (
|
||
|
|
key => 'userpic_0',
|
||
|
|
image => \${ $picinfo->[0] },
|
||
|
|
index => 0,
|
||
|
|
);
|
||
|
|
$current_upload{$_} = $post->{$_}
|
||
|
|
foreach qw/ keywords default comments descriptions make_default /;
|
||
|
|
push @uploaded_userpics, \%current_upload;
|
||
|
|
}
|
||
|
|
|
||
|
|
# throw an error if @uploaded_userpics is still empty
|
||
|
|
return error_ml(
|
||
|
|
$post->{src}
|
||
|
|
? "error.editicons.empty." . $post->{src}
|
||
|
|
: "error.editicons.parse.nodata"
|
||
|
|
) unless @uploaded_userpics;
|
||
|
|
|
||
|
|
# count how many icons the user already has
|
||
|
|
my $userpic_count = scalar @userpics;
|
||
|
|
|
||
|
|
my $factory_redirect;
|
||
|
|
my $success_count = 0;
|
||
|
|
|
||
|
|
my $index_sort = sub { $a->{index} cmp $b->{index} };
|
||
|
|
my $current_index = 0;
|
||
|
|
|
||
|
|
my $message_prefix = sub {
|
||
|
|
my $idx = $_[0];
|
||
|
|
return "" unless scalar @uploaded_userpics > 1;
|
||
|
|
return LJ::Lang::ml( "/edit/icons.tt.icon.msgprefix", { num => $idx } ) . " ";
|
||
|
|
};
|
||
|
|
|
||
|
|
# go through each userpic and try to create it
|
||
|
|
foreach my $cur_upload_ref ( sort $index_sort @uploaded_userpics ) {
|
||
|
|
$current_index++;
|
||
|
|
my %current_upload = %$cur_upload_ref;
|
||
|
|
|
||
|
|
## see if they are trying to go over their limit
|
||
|
|
if ( $userpic_count >= $max ) {
|
||
|
|
$errors->add( undef, "error.editicons.toomanyicons", { num => $max } );
|
||
|
|
last;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $mp = $message_prefix->($current_index);
|
||
|
|
my $err_add = sub { $errors->add_string( undef, $mp . $_[0] ) };
|
||
|
|
my $info_add = sub { push( @info, $mp . $_[0] ) };
|
||
|
|
|
||
|
|
if ( $current_upload{error} ) {
|
||
|
|
|
||
|
|
# error returned from parse_post_uploads
|
||
|
|
$err_add->( $current_upload{error} );
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( $current_upload{requires_factory} ) {
|
||
|
|
$factory_redirect = factory_prepare( \%current_upload, $u );
|
||
|
|
|
||
|
|
# go ahead and add this error message to @info; it
|
||
|
|
# will get displayed only if we can't do the redirect.
|
||
|
|
$info_add->( LJ::Lang::ml("error.editicons.toolarge") );
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
# save this userpic
|
||
|
|
my $userpic = eval { LJ::Userpic->create( $u, data => $current_upload{image} ); };
|
||
|
|
|
||
|
|
if ( !$userpic ) {
|
||
|
|
$@ = $@->as_html if $@->isa('LJ::Error');
|
||
|
|
$err_add->($@);
|
||
|
|
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$info_add->( LJ::Lang::ml("/edit/icons.tt.upload.success") );
|
||
|
|
$success_count++;
|
||
|
|
|
||
|
|
set_userpic_info( $userpic, \%current_upload );
|
||
|
|
$userpic_count++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# yay we (probably) created new pics, reload the @userpics
|
||
|
|
@userpics = LJ::Userpic->load_user_userpics($u);
|
||
|
|
|
||
|
|
# did we designate an image to redirect to the factory?
|
||
|
|
if ( $factory_redirect && !$errors->exist ) {
|
||
|
|
$factory_redirect->{successcount} = $success_count
|
||
|
|
if $success_count;
|
||
|
|
|
||
|
|
return $r->redirect(
|
||
|
|
LJ::create_url(
|
||
|
|
"/tools/userpicfactory",
|
||
|
|
keep_args => ['authas'],
|
||
|
|
args => $factory_redirect
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# make the processed post data accessible to the template
|
||
|
|
# in case there was an error and they need to resubmit
|
||
|
|
$rv->{formdata} = $post if $errors->exist;
|
||
|
|
}
|
||
|
|
|
||
|
|
# finished post processing
|
||
|
|
|
||
|
|
$rv->{errors} = $errors;
|
||
|
|
$rv->{messages} = \@info;
|
||
|
|
|
||
|
|
my $args = $r->get_args;
|
||
|
|
$rv->{sort_by_kw} = $args->{'keywordSort'} ? 1 : 0;
|
||
|
|
|
||
|
|
$rv->{selflink} = sub {
|
||
|
|
my $want_kw = $_[0] // $args->{'keywordSort'};
|
||
|
|
my $keyword_sort = $want_kw ? { 'keywordSort' => 1 } : {};
|
||
|
|
return LJ::create_url(
|
||
|
|
"/manage/icons",
|
||
|
|
keep_args => ['authas'],
|
||
|
|
args => $keyword_sort
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
$rv->{icons} =
|
||
|
|
$args->{'keywordSort'}
|
||
|
|
? [ LJ::Userpic->sort( \@userpics ) ]
|
||
|
|
: \@userpics;
|
||
|
|
$rv->{num_icons} = scalar @userpics;
|
||
|
|
$rv->{max_icons} = $max;
|
||
|
|
|
||
|
|
# Check for default userpic keywords
|
||
|
|
foreach my $pic (@userpics) {
|
||
|
|
if ( substr( $pic->keywords, 0, 4 ) eq "pic#" ) {
|
||
|
|
$rv->{uses_default_keywords} = 1;
|
||
|
|
last;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$rv->{display_rename} = LJ::is_enabled("icon_renames") ? 1 : 0;
|
||
|
|
$rv->{help_icon} = sub { LJ::help_icon_html(@_) };
|
||
|
|
$rv->{alttext_faq} = sub { LJ::Hooks::run_hook( 'faqlink', 'alttext', $_[0] ) };
|
||
|
|
|
||
|
|
$rv->{maxlength} = {
|
||
|
|
comment => LJ::CMAX_UPIC_COMMENT,
|
||
|
|
description => LJ::CMAX_UPIC_DESCRIPTION
|
||
|
|
};
|
||
|
|
|
||
|
|
return DW::Template->render_template( 'edit/icons.tt', $rv );
|
||
|
|
}
|
||
|
|
|
||
|
|
sub factory_handler {
|
||
|
|
my ( $ok, $rv ) = controller( authas => 1 );
|
||
|
|
return $rv unless $ok;
|
||
|
|
|
||
|
|
my $u = $rv->{u}; # authas || remote
|
||
|
|
my $args = $rv->{r}->get_args;
|
||
|
|
|
||
|
|
my $w = int( $args->{'imageWidth'} // 0 );
|
||
|
|
my $h = int( $args->{'imageHeight'} // 0 );
|
||
|
|
|
||
|
|
my $mogkey = mogkey( $u, $args->{index} );
|
||
|
|
|
||
|
|
# make sure index is given and points to a valid file
|
||
|
|
my $has_index = defined $args->{index} && length $args->{index} ? 1 : 0;
|
||
|
|
$has_index &&= DW::BlobStore->exists( temp => $mogkey );
|
||
|
|
|
||
|
|
$rv->{no_index} = $has_index ? 0 : 1;
|
||
|
|
|
||
|
|
if ( $has_index && !( $w && $h ) ) {
|
||
|
|
|
||
|
|
# we do not have the width and height passed in, must compute it
|
||
|
|
my $upf = LJ::Userpic->get_upf_scaled(
|
||
|
|
userid => $u->id,
|
||
|
|
mogkey => $mogkey
|
||
|
|
);
|
||
|
|
( $w, $h ) = ( $upf->[2], $upf->[3] )
|
||
|
|
if $upf && $upf->[2];
|
||
|
|
}
|
||
|
|
|
||
|
|
$rv->{upf_w} = $w;
|
||
|
|
$rv->{upf_h} = $h;
|
||
|
|
|
||
|
|
$rv->{scaledSizeMax} = 640;
|
||
|
|
|
||
|
|
$rv->{successcount} = $args->{successcount};
|
||
|
|
|
||
|
|
my %keepargs =
|
||
|
|
map { $_ => $args->{$_} } qw( keywords comments descriptions make_default index );
|
||
|
|
$rv->{form_keepargs} = LJ::html_hidden(%keepargs);
|
||
|
|
|
||
|
|
return DW::Template->render_template( 'tools/userpicfactory.tt', $rv );
|
||
|
|
}
|
||
|
|
|
||
|
|
sub mogupic_handler {
|
||
|
|
my ( $ok, $rv ) = controller( authas => 1 );
|
||
|
|
return $rv unless $ok;
|
||
|
|
|
||
|
|
my $u = $rv->{u}; # authas || remote
|
||
|
|
my $r = $rv->{r}; # DW::Request
|
||
|
|
my $args = $r->get_args;
|
||
|
|
|
||
|
|
return $r->FORBIDDEN
|
||
|
|
unless $r->header_in("Referer") # can't load page directly
|
||
|
|
&& LJ::check_referer('/tools/userpicfactory');
|
||
|
|
|
||
|
|
my $mogkey = mogkey( $u, $args->{index} );
|
||
|
|
my $size = int( $args->{size} // 0 );
|
||
|
|
$size = 640 if $size <= 0 || $size > 640;
|
||
|
|
|
||
|
|
my $upf = LJ::Userpic->get_upf_scaled(
|
||
|
|
size => $size,
|
||
|
|
userid => $u->id,
|
||
|
|
mogkey => $mogkey
|
||
|
|
);
|
||
|
|
|
||
|
|
return $r->NOT_FOUND unless $upf;
|
||
|
|
|
||
|
|
my $blob = $upf->[0];
|
||
|
|
my $mime = $upf->[1];
|
||
|
|
|
||
|
|
# return the image
|
||
|
|
$r->content_type($mime);
|
||
|
|
$r->print($$blob);
|
||
|
|
|
||
|
|
return $r->OK;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub mogkey {
|
||
|
|
my ( $u, $index ) = @_;
|
||
|
|
$log->logcroak("No user given") unless LJ::isu($u);
|
||
|
|
|
||
|
|
my $key = 'upf';
|
||
|
|
$key .= "_$index" if defined $index && length $index;
|
||
|
|
$key .= ':' . $u->id;
|
||
|
|
|
||
|
|
return $key;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub set_userpic_info {
|
||
|
|
my ( $userpic, $upload ) = @_;
|
||
|
|
|
||
|
|
$userpic->make_default if $upload->{make_default};
|
||
|
|
$userpic->set_keywords( $upload->{keywords} )
|
||
|
|
if defined $upload->{keywords};
|
||
|
|
$userpic->set_comment( $upload->{comments} )
|
||
|
|
if $upload->{comments};
|
||
|
|
$userpic->set_description( $upload->{descriptions} )
|
||
|
|
if $upload->{descriptions};
|
||
|
|
$userpic->set_fullurl( $upload->{url} ) if $upload->{url};
|
||
|
|
}
|
||
|
|
|
||
|
|
# prepare to send a particular upload to the factory
|
||
|
|
sub factory_prepare {
|
||
|
|
my ( $upload, $u ) = @_;
|
||
|
|
|
||
|
|
# save the file
|
||
|
|
DW::BlobStore->store(
|
||
|
|
temp => mogkey( $u, $upload->{index} ),
|
||
|
|
$upload->{image}
|
||
|
|
);
|
||
|
|
|
||
|
|
# save the arguments for the factory URL
|
||
|
|
my $args = {
|
||
|
|
imageWidth => $upload->{imagew},
|
||
|
|
imageHeight => $upload->{imageh}
|
||
|
|
};
|
||
|
|
|
||
|
|
$args->{$_} = $upload->{$_} foreach qw/ keywords comments descriptions
|
||
|
|
make_default index /;
|
||
|
|
|
||
|
|
return $args;
|
||
|
|
}
|
||
|
|
|
||
|
|
# save changes to existing userpics
|
||
|
|
sub update_userpics {
|
||
|
|
my ( $POST, $errors, $userpicsref, $u ) = @_;
|
||
|
|
my @userpics = @$userpicsref;
|
||
|
|
my $display_rename = LJ::is_enabled("icon_renames") ? 1 : 0;
|
||
|
|
|
||
|
|
my @delete; # userpic objects to delete
|
||
|
|
my @inactive_picids;
|
||
|
|
my %picid_of_kwid;
|
||
|
|
my %used_keywords;
|
||
|
|
|
||
|
|
# we need to count keywords based on what the user provided, in order
|
||
|
|
# to find duplicates. $up->keywords doesn't work, because re-using a
|
||
|
|
# keyword will remove it from the other userpic without our knowing
|
||
|
|
my $count_keywords = sub {
|
||
|
|
my $kwlist = $_[0];
|
||
|
|
$used_keywords{$_}++ foreach split( /,\s*/, $kwlist );
|
||
|
|
};
|
||
|
|
|
||
|
|
foreach my $up (@userpics) {
|
||
|
|
my $picid = $up->id;
|
||
|
|
|
||
|
|
# delete this pic
|
||
|
|
if ( $POST->{"delete_$picid"} ) {
|
||
|
|
push @delete, $up;
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
# we're only going to modify keywords/comments on active pictures
|
||
|
|
if ( $up->inactive || $POST->{"pic_inactive_$picid"} ) {
|
||
|
|
|
||
|
|
# use 'orig' because we don't POST disabled fields
|
||
|
|
$count_keywords->( $POST->{"kw_orig_$picid"} );
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
$count_keywords->( $POST->{"kw_$picid"} );
|
||
|
|
|
||
|
|
# only modify if changing the data, make sure not colliding with other edits, etc
|
||
|
|
if ( $POST->{"kw_$picid"} ne $POST->{"kw_orig_$picid"} ) {
|
||
|
|
my $kws = $POST->{"kw_$picid"};
|
||
|
|
|
||
|
|
if ( $POST->{"rename_keyword_$picid"} ) {
|
||
|
|
if ( $display_rename && $u->userpic_have_mapid ) {
|
||
|
|
eval { $up->set_and_rename_keywords( $kws, $POST->{"kw_orig_$picid"} ); }
|
||
|
|
or push @$errors, $@->as_html;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
push @$errors, LJ::Lang::ml('error.iconkw.rename.disabled');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
eval { $up->set_keywords($kws); } or push @$errors, $@->as_html;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
eval {
|
||
|
|
$up->set_comment( $POST->{"com_$picid"} )
|
||
|
|
unless ( $POST->{"com_$picid"} // '' ) eq ( $POST->{"com_orig_$picid"} // '' );
|
||
|
|
} or push @$errors, $@;
|
||
|
|
|
||
|
|
eval {
|
||
|
|
$up->set_description( $POST->{"desc_$picid"} )
|
||
|
|
unless ( $POST->{"desc_$picid"} // '' ) eq ( $POST->{"desc_orig_$picid"} // '' );
|
||
|
|
} or push @$errors, $@;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach my $kw ( keys %used_keywords ) {
|
||
|
|
next unless $used_keywords{$kw} > 1;
|
||
|
|
push @$errors, LJ::Lang::ml( 'error.iconkw.rename.multiple', { ekw => $kw } );
|
||
|
|
}
|
||
|
|
|
||
|
|
if (@delete) {
|
||
|
|
|
||
|
|
# delete pics
|
||
|
|
foreach my $up (@delete) {
|
||
|
|
eval { $up->delete; } or push @$errors, $@;
|
||
|
|
}
|
||
|
|
|
||
|
|
# if any of the userpics they want to delete are active, then we want to
|
||
|
|
# re-run activate_userpics() - turns out it's faster to not check to
|
||
|
|
# see if we need to do this
|
||
|
|
$u->activate_userpics;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $new_default = $POST->{'defaultpic'} + 0;
|
||
|
|
if ( $POST->{"delete_${new_default}"} ) {
|
||
|
|
|
||
|
|
# deleting default
|
||
|
|
$new_default = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( $new_default && $new_default != $u->{'defaultpicid'} ) {
|
||
|
|
my ($up) = grep { $_->id == $new_default } @userpics;
|
||
|
|
|
||
|
|
# see if they are trying to make an inactive userpic their default
|
||
|
|
if ( $up && !$up->inactive ) {
|
||
|
|
$up->make_default;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
elsif ( $new_default eq '0' && $u->{'defaultpicid'} ) {
|
||
|
|
|
||
|
|
# selected the "no default picture" option
|
||
|
|
$u->update_self( { defaultpicid => 0 } );
|
||
|
|
$u->{'defaultpicid'} = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
return scalar @delete;
|
||
|
|
}
|
||
|
|
|
||
|
|
# parse the post parameters into an array of new pics
|
||
|
|
sub parse_post_uploads {
|
||
|
|
my ( $POST, $u, $MAX_UPLOAD ) = @_;
|
||
|
|
my @uploads;
|
||
|
|
|
||
|
|
# if we find a userpic that requires the factory, save it here.
|
||
|
|
# we can only have one.
|
||
|
|
my $requires_factory;
|
||
|
|
|
||
|
|
# go through each key and create a %current_upload for it, then
|
||
|
|
# put it in @uploads.
|
||
|
|
foreach my $userpic_key ( keys %$POST ) {
|
||
|
|
next unless $userpic_key =~ /^(?:userpic|urlpic)_\d+$/;
|
||
|
|
|
||
|
|
my @tokens = split( /_/, $userpic_key );
|
||
|
|
my $counter = $tokens[1];
|
||
|
|
my $make_default = $POST->{make_default} // '';
|
||
|
|
|
||
|
|
my %current_upload = (
|
||
|
|
comments => $POST->{"comments_$counter"},
|
||
|
|
descriptions => $POST->{"descriptions_$counter"},
|
||
|
|
index => $counter,
|
||
|
|
key => $userpic_key,
|
||
|
|
keywords => $POST->{"keywords_$counter"},
|
||
|
|
make_default => $make_default eq $counter,
|
||
|
|
);
|
||
|
|
|
||
|
|
# check input text for comments, descriptions, and keywords
|
||
|
|
unless ( LJ::text_in( \%current_upload ) ) {
|
||
|
|
$current_upload{error} = LJ::Lang::ml("error.utf8");
|
||
|
|
push @uploads, \%current_upload;
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
# uploaded pics
|
||
|
|
if ( $userpic_key =~ /userpic_.*/ ) {
|
||
|
|
|
||
|
|
# only use userpic_0 if we selected file for the source
|
||
|
|
next if $userpic_key eq "userpic_0" && $POST->{"src"} ne "file";
|
||
|
|
|
||
|
|
# Some callers to the function pass data, others pass
|
||
|
|
# a reference to data. Figure out which type we got.
|
||
|
|
$current_upload{image} =
|
||
|
|
ref $POST->{$userpic_key}
|
||
|
|
? $POST->{$userpic_key}
|
||
|
|
: \$POST->{$userpic_key};
|
||
|
|
|
||
|
|
my $size = length ${ $current_upload{image} };
|
||
|
|
if ( $size == 0 ) {
|
||
|
|
$current_upload{error} = LJ::Lang::ml('error.editicons.empty.file');
|
||
|
|
push @uploads, \%current_upload;
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
my ( $imagew, $imageh, $filetype ) =
|
||
|
|
Image::Size::imgsize( $current_upload{image} );
|
||
|
|
|
||
|
|
# couldn't parse the file
|
||
|
|
if ( !$imagew || !$imageh ) {
|
||
|
|
$current_upload{error} = LJ::Lang::ml(
|
||
|
|
'error.editicons.unsupportedtype',
|
||
|
|
{
|
||
|
|
filetype => $filetype,
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
# file is too big, no matter what.
|
||
|
|
}
|
||
|
|
elsif ( $imagew > 5000 || $imageh > 5000 ) {
|
||
|
|
$current_upload{error} = LJ::Lang::ml('error.editicons.dimstoolarge');
|
||
|
|
|
||
|
|
# let's try to use the factory
|
||
|
|
}
|
||
|
|
elsif ( int($imagew) > 100 || int($imageh) > 100 || $size > $MAX_UPLOAD ) {
|
||
|
|
|
||
|
|
# file wrong type for factory
|
||
|
|
if ( $filetype ne 'JPG' && $filetype ne 'PNG' ) {
|
||
|
|
|
||
|
|
# factory only works on jpegs and pngs
|
||
|
|
# because Image::Magick has issues
|
||
|
|
if ( int($imagew) > 100 || int($imageh) > 100 ) {
|
||
|
|
$current_upload{error} = LJ::Lang::ml('error.editicons.giffiledimensions');
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$current_upload{error} = LJ::Lang::ml(
|
||
|
|
'error.editicons.filetoolarge',
|
||
|
|
{
|
||
|
|
maxsize => int( $MAX_UPLOAD / 1024 ),
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
# if it's the right size, just too large a file,
|
||
|
|
# see if we can resize it down
|
||
|
|
}
|
||
|
|
elsif ( $imagew <= 100 && $imageh <= 100 ) {
|
||
|
|
|
||
|
|
# have to store the file, this is the interface that
|
||
|
|
# the userpic factory uses to get files between
|
||
|
|
# the N different web processes you might talk to
|
||
|
|
my $mogkey = mogkey( $u, $counter );
|
||
|
|
my $rv = DW::BlobStore->store(
|
||
|
|
temp => $mogkey,
|
||
|
|
$current_upload{image}
|
||
|
|
);
|
||
|
|
unless ($rv) {
|
||
|
|
$current_upload{error} = LJ::Lang::ml('error.editicons.blobstore');
|
||
|
|
push @uploads, \%current_upload;
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
eval {
|
||
|
|
my $picinfo = LJ::Userpic->get_upf_scaled(
|
||
|
|
mogkey => $mogkey,
|
||
|
|
size => 100,
|
||
|
|
u => $u,
|
||
|
|
);
|
||
|
|
|
||
|
|
# success! don't go to the factory, and
|
||
|
|
# pretend the user just uploaded the file
|
||
|
|
# and continue on normally
|
||
|
|
$current_upload{image} = $picinfo->[0];
|
||
|
|
};
|
||
|
|
|
||
|
|
if ( $@ || length ${ $current_upload{image} } > $MAX_UPLOAD ) {
|
||
|
|
$current_upload{error} = LJ::Lang::ml(
|
||
|
|
'error.editicons.filetoolarge',
|
||
|
|
{
|
||
|
|
maxsize => int( $MAX_UPLOAD / 1024 ),
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
# this is a candidate for the userpicfactory.
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
# we can only do a single pic in the factory, so if there are two,
|
||
|
|
# then error out for both.
|
||
|
|
if ($requires_factory) {
|
||
|
|
$requires_factory->{error} = $current_upload{error} =
|
||
|
|
LJ::Lang::ml('error.editicons.multipleresize');
|
||
|
|
$requires_factory->{requires_factory} = 0;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$current_upload{requires_factory} = 1;
|
||
|
|
$current_upload{imageh} = $imageh;
|
||
|
|
$current_upload{imagew} = $imagew;
|
||
|
|
if ( $POST->{"spool_data_$counter"} ) {
|
||
|
|
$current_upload{spool_data} = $POST->{"spool_data_$counter"};
|
||
|
|
}
|
||
|
|
$requires_factory = \%current_upload;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
push @uploads, \%current_upload;
|
||
|
|
|
||
|
|
}
|
||
|
|
elsif ( $userpic_key =~ /urlpic_.*/ ) {
|
||
|
|
|
||
|
|
# go through the URL uploads
|
||
|
|
next if $userpic_key eq "urlpic_0" && $POST->{src} ne "url";
|
||
|
|
|
||
|
|
if ( !$POST->{$userpic_key} ) {
|
||
|
|
$current_upload{error} = LJ::Lang::ml('error.editicons.empty.url');
|
||
|
|
}
|
||
|
|
elsif ( $POST->{$userpic_key} !~ /^https?:\/\// ) {
|
||
|
|
$current_upload{error} = LJ::Lang::ml('error.editicons.url.format');
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
my $ua = LJ::get_useragent(
|
||
|
|
role => 'userpic',
|
||
|
|
max_size => $MAX_UPLOAD + 1024,
|
||
|
|
timeout => 10,
|
||
|
|
);
|
||
|
|
my $res = $ua->get( $POST->{$userpic_key} );
|
||
|
|
$current_upload{image} = \$res->content
|
||
|
|
if $res && $res->is_success;
|
||
|
|
$current_upload{error} = LJ::Lang::ml('error.editicons.url.fetch')
|
||
|
|
unless $current_upload{image};
|
||
|
|
$current_upload{error} = LJ::Lang::ml('error.editicons.url.filetoolarge')
|
||
|
|
if $current_upload{image}
|
||
|
|
&& length ${ $current_upload{image} } > $MAX_UPLOAD;
|
||
|
|
$current_upload{url} = $POST->{$userpic_key};
|
||
|
|
}
|
||
|
|
push @uploads, \%current_upload;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return @uploads;
|
||
|
|
}
|
||
|
|
|
||
|
|
# save large images to temporary storage and populate post parameters
|
||
|
|
sub parse_large_upload {
|
||
|
|
my ( $POST, $errref, $u ) = @_;
|
||
|
|
|
||
|
|
my %upload = (); # { spool_data, spool_file_name, filename, bytes, md5sum, md5ctx, mime }
|
||
|
|
my @uploaded_files = ();
|
||
|
|
my $curr_name;
|
||
|
|
|
||
|
|
# subref to set $$errref and return false
|
||
|
|
my $err = sub { $$errref = LJ::Lang::ml(@_); return 0 };
|
||
|
|
|
||
|
|
# called when the beginning of an upload is encountered
|
||
|
|
my $hook_newheaders = sub {
|
||
|
|
my ( $name, $filename ) = @_;
|
||
|
|
$curr_name = $name;
|
||
|
|
$POST->{$curr_name} = '';
|
||
|
|
return 1 unless $curr_name =~ /userpic.*/;
|
||
|
|
|
||
|
|
# new file, need to create a filehandle, etc
|
||
|
|
%upload = ();
|
||
|
|
$upload{filename} = $filename;
|
||
|
|
$upload{md5ctx} = new Digest::MD5;
|
||
|
|
|
||
|
|
my @tokens = split( /_/, $curr_name );
|
||
|
|
my $counter = $tokens[1];
|
||
|
|
|
||
|
|
$upload{spool_file_name} = mogkey( $u, $counter );
|
||
|
|
$upload{spool_data} = '';
|
||
|
|
|
||
|
|
push @uploaded_files, $upload{spool_file_name};
|
||
|
|
return 1;
|
||
|
|
};
|
||
|
|
|
||
|
|
# called as data is received
|
||
|
|
my $hook_data = sub {
|
||
|
|
my ( $len, $data ) = @_;
|
||
|
|
unless ( $curr_name =~ /userpic.*/ ) {
|
||
|
|
$POST->{$curr_name} .= $data;
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# check that we've not exceeded the max read limit
|
||
|
|
my $max_read = ( 1 << 20 ) * 5; # 5 MiB
|
||
|
|
$upload{bytes} += $len;
|
||
|
|
if ( $upload{bytes} > $max_read ) {
|
||
|
|
return $err->(
|
||
|
|
"error.editicons.parse.maxread",
|
||
|
|
{ max_read => $max_read, bytes => $upload{bytes} }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
$upload{md5ctx}->add($data);
|
||
|
|
$upload{spool_data} .= $data;
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
};
|
||
|
|
|
||
|
|
# called when the end of an upload is encountered
|
||
|
|
my $hook_enddata = sub {
|
||
|
|
return 1 unless $curr_name =~ /userpic.*/;
|
||
|
|
|
||
|
|
# since we've just finished a potentially slow upload, we need to
|
||
|
|
# make sure the database handles in DBI::Role's cache haven't expired,
|
||
|
|
# so we'll just trigger a revalidation now so that subsequent database
|
||
|
|
# calls will be safe.
|
||
|
|
$LJ::DBIRole->clear_req_cache();
|
||
|
|
|
||
|
|
# don't try to operate on 0-length spoolfiles
|
||
|
|
unless ( $upload{bytes} ) {
|
||
|
|
%upload = ();
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
unless ( length $upload{spool_data} > 0 ) {
|
||
|
|
return $err->("error.editicons.parse.nosize");
|
||
|
|
}
|
||
|
|
|
||
|
|
# Get MIME type from magic bytes
|
||
|
|
$upload{mime} = File::Type->new->mime_type( $upload{spool_data} );
|
||
|
|
unless ( $upload{mime} ) {
|
||
|
|
return $err->("error.editicons.parse.unknowntype");
|
||
|
|
}
|
||
|
|
|
||
|
|
# finished adding data for md5, create digest (but don't destroy original)
|
||
|
|
$upload{md5sum} = $upload{md5ctx}->digest;
|
||
|
|
$POST->{$curr_name} = \$upload{spool_data};
|
||
|
|
return 1;
|
||
|
|
};
|
||
|
|
|
||
|
|
# parse multipart-mime submission, one chunk at a time, calling
|
||
|
|
# our hooks as we go to put uploads in temporary file storage
|
||
|
|
my $retval = eval {
|
||
|
|
parse_multipart_interactive(
|
||
|
|
$errref,
|
||
|
|
{
|
||
|
|
newheaders => $hook_newheaders,
|
||
|
|
data => $hook_data,
|
||
|
|
enddata => $hook_enddata,
|
||
|
|
}
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
unless ($retval) {
|
||
|
|
|
||
|
|
# if we hit a parse error, delete the uploaded files
|
||
|
|
foreach my $mogkey (@uploaded_files) {
|
||
|
|
DW::BlobStore->delete( temp => $mogkey );
|
||
|
|
}
|
||
|
|
|
||
|
|
# $errref is already set by parse_multipart_interactive
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $retval;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub parse_multipart_interactive {
|
||
|
|
my ( $errref, $hooks ) = @_;
|
||
|
|
my $apache_r = DW::Request->get;
|
||
|
|
|
||
|
|
# subref to set $@ and $$errref, then return false
|
||
|
|
my $err = sub { $$errref = $@ = $_[0]; return 0 };
|
||
|
|
|
||
|
|
my $run_hook = sub {
|
||
|
|
my $name = shift;
|
||
|
|
my $ret = eval { $hooks->{$name}->(@_) };
|
||
|
|
return $err->($@) if $@;
|
||
|
|
|
||
|
|
# return a default hook error if the hook didn't set $$errref
|
||
|
|
return $err->( $$errref ? $$errref : "Hook: '$name' returned false" )
|
||
|
|
unless $ret;
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
};
|
||
|
|
|
||
|
|
my $mimetype = $apache_r->header_in("Content-Type");
|
||
|
|
my $size = $apache_r->header_in("Content-length");
|
||
|
|
|
||
|
|
unless ( $mimetype =~ m!^multipart/form-data;\s*boundary=(\S+)! ) {
|
||
|
|
return $err->( LJ::Lang::ml( "error.editicons.parse.boundary", { type => $mimetype } ) );
|
||
|
|
}
|
||
|
|
my $sep = "--$1";
|
||
|
|
my $seplen = length($sep) + 2; # plus \r\n
|
||
|
|
|
||
|
|
my $window = '';
|
||
|
|
my $to_read = $size;
|
||
|
|
my $max_read = 8192;
|
||
|
|
|
||
|
|
my $seen_chunk = 0; # have we seen any chunk yet?
|
||
|
|
|
||
|
|
my $state = 0; # what we last parsed
|
||
|
|
# 0 = nothing (looking for a separator)
|
||
|
|
# 1 = separator (looking for headers)
|
||
|
|
# 0 = headers (looking for data)
|
||
|
|
# 0 = data (looking for a separator)
|
||
|
|
|
||
|
|
while (1) {
|
||
|
|
my $read = -1;
|
||
|
|
if ($to_read) {
|
||
|
|
$read = $apache_r->read( $window, $to_read < $max_read ? $to_read : $max_read,
|
||
|
|
length($window) );
|
||
|
|
$to_read -= $read;
|
||
|
|
|
||
|
|
# prevent loops. Opera, in particular, alerted us to
|
||
|
|
# this bug, since it doesn't upload proper MIME on
|
||
|
|
# reload and its Content-Length header is correct,
|
||
|
|
# but its body tiny
|
||
|
|
if ( $read == 0 ) {
|
||
|
|
return $err->( LJ::Lang::ml("error.editicons.parse.nodata") );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# starting case, or data-reading case (looking for separator)
|
||
|
|
if ( $state == 0 ) {
|
||
|
|
my $idx = index( $window, $sep );
|
||
|
|
|
||
|
|
# didn't find a separator. emit the previous data
|
||
|
|
# which we know for sure is data and not a possible
|
||
|
|
# new separator
|
||
|
|
if ( $idx == -1 ) {
|
||
|
|
|
||
|
|
# bogus if we're done reading and didn't find what we're
|
||
|
|
# looking for:
|
||
|
|
if ( $read == -1 ) {
|
||
|
|
return $err->(
|
||
|
|
LJ::Lang::ml( "error.editicons.parse.notfound", { what => 'separator' } ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($seen_chunk) {
|
||
|
|
|
||
|
|
# data hook is required
|
||
|
|
my $len = length($window) - $seplen;
|
||
|
|
$run_hook->( 'data', $len, substr( $window, 0, $len, '' ) )
|
||
|
|
or return 0;
|
||
|
|
}
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
# we found a separator. emit the previous read's
|
||
|
|
# data and enddata.
|
||
|
|
if ($seen_chunk) {
|
||
|
|
my $len = $idx - 2;
|
||
|
|
if ( $len > 0 ) {
|
||
|
|
|
||
|
|
# data hook is required
|
||
|
|
$run_hook->( 'data', $len, substr( $window, 0, $len ) )
|
||
|
|
or return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
# enddata hook is required
|
||
|
|
substr( $window, 0, $idx, '' );
|
||
|
|
$run_hook->('enddata')
|
||
|
|
or return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
# we're now looking for a header
|
||
|
|
$seen_chunk = 1;
|
||
|
|
$state = 1;
|
||
|
|
|
||
|
|
# have we hit the end?
|
||
|
|
return 1 if $to_read <= 2 && length($window) <= $seplen + 4;
|
||
|
|
}
|
||
|
|
|
||
|
|
# read a separator, looking for headers
|
||
|
|
if ( $state == 1 ) {
|
||
|
|
my $idx = index( $window, "\r\n\r\n" );
|
||
|
|
if ( $idx == -1 ) {
|
||
|
|
if ( length($window) > 8192 ) {
|
||
|
|
return $err->(
|
||
|
|
LJ::Lang::ml( "error.editicons.parse.window", { len => length($window) } )
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
# bogus if we're done reading and didn't find what we're
|
||
|
|
# looking for:
|
||
|
|
if ( $read == -1 ) {
|
||
|
|
return $err->(
|
||
|
|
LJ::Lang::ml( "error.editicons.parse.notfound", { what => 'headers' } ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
# +4 is \r\n\r\n
|
||
|
|
my $header = substr( $window, 0, $idx + 4, '' );
|
||
|
|
my @lines = split( /\r\n/, $header );
|
||
|
|
|
||
|
|
my %hdval;
|
||
|
|
my $lasthd;
|
||
|
|
foreach (@lines) {
|
||
|
|
if (/^(\S+?):\s*(.+)/) {
|
||
|
|
$lasthd = lc($1);
|
||
|
|
$hdval{$lasthd} = $2;
|
||
|
|
}
|
||
|
|
elsif (/^\s+.+/) {
|
||
|
|
$hdval{$lasthd} .= $&;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
my ( $name, $filename );
|
||
|
|
if ( $hdval{'content-disposition'} =~ /\bNAME=\"(.+?)\"/i ) {
|
||
|
|
$name = $1;
|
||
|
|
}
|
||
|
|
if ( $hdval{'content-disposition'} =~ /\bFILENAME=\"(.+?)\"/i ) {
|
||
|
|
$filename = $1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# newheaders hook is required
|
||
|
|
$run_hook->( 'newheaders', $name, $filename )
|
||
|
|
or return 0;
|
||
|
|
|
||
|
|
$state = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
1;
|