#!/usr/bin/perl # # DW::Controller::EditIcons # # This controller is for creating and managing icons. # # Authors: # Mark Smith # Jen Griffin # # 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;