diff --git a/webtools/bugzilla/Bugzilla/Attachment.pm b/webtools/bugzilla/Bugzilla/Attachment.pm index e7b3ffe86e52..5f491f3155c2 100644 --- a/webtools/bugzilla/Bugzilla/Attachment.pm +++ b/webtools/bugzilla/Bugzilla/Attachment.pm @@ -33,6 +33,7 @@ package Bugzilla::Attachment; # Use the Flag module to handle flags. use Bugzilla::Flag; +use Bugzilla::Config qw(:locations); ############################################################################ # Functions @@ -92,6 +93,17 @@ sub query # Retrieve a list of flags for this attachment. $a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'}, 'is_active' => 1 }); + + # A zero size indicates that the attachment is stored locally. + if ($a{'datasize'} == 0) { + my $attachid = $a{'attachid'}; + my $hash = ($attachid % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + if (open(AH, "$attachdir/$hash/attachment.$attachid")) { + $a{'datasize'} = (stat(AH))[7]; + close(AH); + } + } # We will display the edit link if the user can edit the attachment; # ie the are the submitter, or they have canedit. diff --git a/webtools/bugzilla/Bugzilla/Config.pm b/webtools/bugzilla/Bugzilla/Config.pm index 5c070e3728e7..3849f146bd31 100644 --- a/webtools/bugzilla/Bugzilla/Config.pm +++ b/webtools/bugzilla/Bugzilla/Config.pm @@ -55,6 +55,7 @@ use Bugzilla::Util; our $libpath = '.'; our $localconfig = "$libpath/localconfig"; our $datadir = "$libpath/data"; +our $attachdir = "$datadir/attachments"; our $templatedir = "$libpath/template"; our $webdotdir = "$datadir/webdot"; @@ -72,7 +73,8 @@ our $webdotdir = "$datadir/webdot"; ( admin => [qw(GetParamList UpdateParams SetParam WriteParams)], db => [qw($db_driver $db_host $db_port $db_name $db_user $db_pass $db_sock)], - locations => [qw($libpath $localconfig $datadir $templatedir $webdotdir)], + locations => [qw($libpath $localconfig $attachdir + $datadir $templatedir $webdotdir)], ); Exporter::export_ok_tags('admin', 'db', 'locations'); diff --git a/webtools/bugzilla/attachment.cgi b/webtools/bugzilla/attachment.cgi index 3522f9e26f01..0a296609b723 100755 --- a/webtools/bugzilla/attachment.cgi +++ b/webtools/bugzilla/attachment.cgi @@ -40,6 +40,7 @@ use vars qw( # Include the Bugzilla CGI and general utility library. require "CGI.pl"; +use Bugzilla::Config qw(:locations); # Use these modules to handle flags. use Bugzilla::Constants; @@ -360,12 +361,18 @@ sub validateData { my $maxsize = $::FORM{'ispatch'} ? Param('maxpatchsize') : Param('maxattachmentsize'); $maxsize *= 1024; # Convert from K - - my $fh = $cgi->upload('data'); + my $fh; + # Skip uploading into a local variable if the user wants to upload huge + # attachments into local files. + if (!$::FORM{'bigfile'}) + { + $fh = $cgi->upload('data'); + } my $data; # We could get away with reading only as much as required, except that then # we wouldn't have a size to print to the error handler below. + if (!$::FORM{'bigfile'}) { # enable 'slurp' mode local $/; @@ -373,10 +380,11 @@ sub validateData } $data + || ($::FORM{'bigfile'}) || ThrowUserError("zero_length_file"); # Make sure the attachment does not exceed the maximum permitted size - my $len = length($data); + my $len = $data ? length($data) : 0; if ($maxsize && $len > $maxsize) { my $vars = { filesize => sprintf("%.0f", $len/1024) }; if ( $::FORM{'ispatch'} ) { @@ -504,6 +512,23 @@ sub view # Return the appropriate HTTP response headers. $filename =~ s/^.*[\/\\]//; my $filesize = length($thedata); + # A zero length attachment in the database means the attachment is + # stored in a local file + if ($filesize == 0) + { + my $attachid = $::FORM{'id'}; + my $hash = ($attachid % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + if (open(AH, "$attachdir/$hash/attachment.$attachid")) { + binmode AH; + $filesize = (stat(AH))[7]; + } + } + if ($filesize == 0) + { + ThrowUserError("attachment_removed"); + } + # escape quotes and backslashes in the filename, per RFCs 2045/822 $filename =~ s/\\/\\\\/g; # escape backslashes @@ -513,7 +538,15 @@ sub view -content_disposition=> "inline; filename=\"$filename\"", -content_length => $filesize); - print $thedata; + if ($thedata) { + print $thedata; + } else { + while () { + print $_; + } + close(AH); + } + } sub interdiff @@ -771,7 +804,7 @@ sub viewall $privacy = "AND isprivate < 1 "; } SendSQL("SELECT attach_id, DATE_FORMAT(creation_ts, '%Y.%m.%d %H:%i'), - mimetype, description, ispatch, isobsolete, isprivate, + mimetype, description, ispatch, isobsolete, isprivate, LENGTH(thedata) FROM attachments WHERE bug_id = $::FORM{'bugid'} $privacy ORDER BY attach_id"); @@ -779,7 +812,7 @@ sub viewall while (MoreSQLData()) { my %a; # the attachment hash - ($a{'attachid'}, $a{'date'}, $a{'contenttype'}, + ($a{'attachid'}, $a{'date'}, $a{'contenttype'}, $a{'description'}, $a{'ispatch'}, $a{'isobsolete'}, $a{'isprivate'}, $a{'datasize'}) = FetchSQLData(); $a{'isviewable'} = isViewable($a{'contenttype'}); @@ -889,11 +922,39 @@ sub insert # Retrieve the ID of the newly created attachment record. my $attachid = $dbh->bz_last_key('attachments', 'attach_id'); + # If the file is to be stored locally, stream the file from the webserver + # to the local file without reading it into a local variable. + if ($::FORM{'bigfile'}) + { + my $fh = $cgi->upload('data'); + my $hash = ($attachid % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + mkdir "$attachdir/$hash", 0770; + chmod 0770, "$attachdir/$hash"; + open(AH, ">$attachdir/$hash/attachment.$attachid"); + binmode AH; + my $sizecount = 0; + my $limit = (Param("maxlocalattachment") * 1048576); + while (<$fh>) { + print AH $_; + $sizecount += length($_); + if ($sizecount > $limit) { + close AH; + close $fh; + unlink "$attachdir/$hash/attachment.$attachid"; + ThrowUserError("local_file_too_large"); + } + } + close AH; + close $fh; + } + + # Insert a comment about the new attachment into the database. my $comment = "Created an attachment (id=$attachid)\n$::FORM{'description'}\n"; $comment .= ("\n" . $::FORM{'comment'}) if $::FORM{'comment'}; - AppendComment($::FORM{'bugid'}, + AppendComment($::FORM{'bugid'}, Bugzilla->user->login, $comment, $isprivate, @@ -906,7 +967,7 @@ sub insert SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) VALUES ($::FORM{'bugid'}, $obsolete_id, $::userid, $sql_timestamp, $fieldid, '0', '1')"); # If the obsolete attachment has pending flags, migrate them to the new attachment. - if (Bugzilla::Flag::count({ 'attach_id' => $obsolete_id , + if (Bugzilla::Flag::count({ 'attach_id' => $obsolete_id , 'status' => 'pending', 'is_active' => 1 })) { Bugzilla::Flag::migrate($obsolete_id, $attachid, $timestamp); @@ -1009,11 +1070,11 @@ sub edit # Get a list of flag types that can be set for this attachment. SendSQL("SELECT product_id, component_id FROM bugs WHERE bug_id = $bugid"); my ($product_id, $component_id) = FetchSQLData(); - my $flag_types = Bugzilla::FlagType::match({ 'target_type' => 'attachment' , - 'product_id' => $product_id , + my $flag_types = Bugzilla::FlagType::match({ 'target_type' => 'attachment' , + 'product_id' => $product_id , 'component_id' => $component_id }); foreach my $flag_type (@$flag_types) { - $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id' => $flag_type->{'id'}, + $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id' => $flag_type->{'id'}, 'attach_id' => $::FORM{'id'}, 'is_active' => 1 }); } @@ -1087,10 +1148,10 @@ sub update # Update the attachment record in the database. # Sets the creation timestamp to itself to avoid it being updated automatically. SendSQL("UPDATE attachments - SET description = $quoteddescription , - mimetype = $quotedcontenttype , + SET description = $quoteddescription , + mimetype = $quotedcontenttype , filename = $quotedfilename , - ispatch = $::FORM{'ispatch'} , + ispatch = $::FORM{'ispatch'}, isobsolete = $::FORM{'isobsolete'} , isprivate = $::FORM{'isprivate'} WHERE attach_id = $::FORM{'id'} @@ -1143,7 +1204,7 @@ sub update # Unlock all database tables now that we are finished updating the database. $dbh->bz_unlock_tables(); - # If the user submitted a comment while editing the attachment, + # If the user submitted a comment while editing the attachment, # add the comment to the bug. if ( $::FORM{'comment'} ) { diff --git a/webtools/bugzilla/checksetup.pl b/webtools/bugzilla/checksetup.pl index 9c35b5a982f7..2aa2a6cc1628 100755 --- a/webtools/bugzilla/checksetup.pl +++ b/webtools/bugzilla/checksetup.pl @@ -904,6 +904,14 @@ unless (-d $datadir && -e "$datadir/nomail") { open FILE, '>>', "$datadir/mail"; close FILE; } + + unless (-d $attachdir) { + print "Creating local attachments directory ...\n"; + # permissions for non-webservergroup are fixed later on + mkdir $attachdir, 0770; + } + + # 2000-12-14 New graphing system requires a directory to put the graphs in # This code copied from what happens for the data dir above. # If the graphs dir is not present, we assume that they have been using @@ -1088,6 +1096,17 @@ END } } + if (!-e "$attachdir/.htaccess") { + print "Creating $attachdir/.htaccess...\n"; + open HTACCESS, ">$attachdir/.htaccess"; + print HTACCESS <<'END'; +# nothing in this directory is retrievable unless overriden by an .htaccess +# in a subdirectory; +deny from all +END + close HTACCESS; + chmod $fileperm, "$attachdir/.htaccess"; + } if (!-e "Bugzilla/.htaccess") { print "Creating Bugzilla/.htaccess...\n"; open HTACCESS, '>', 'Bugzilla/.htaccess'; @@ -1428,6 +1447,7 @@ if ($^O !~ /MSWin32/i) { fixPerms("$datadir/duplicates", $<, $webservergid, 027, 1); fixPerms("$datadir/mining", $<, $webservergid, 027, 1); fixPerms("$datadir/template", $<, $webservergid, 007, 1); # webserver will write to these + fixPerms($attachdir, $<, $webservergid, 007, 1); # webserver will write to these fixPerms($webdotdir, $<, $webservergid, 007, 1); fixPerms("$webdotdir/.htaccess", $<, $webservergid, 027); fixPerms("$datadir/params", $<, $webservergid, 017); diff --git a/webtools/bugzilla/defparams.pl b/webtools/bugzilla/defparams.pl index 3f91aabe2546..99b942ce691e 100644 --- a/webtools/bugzilla/defparams.pl +++ b/webtools/bugzilla/defparams.pl @@ -1269,6 +1269,17 @@ Reason: %reason% checker => \&check_numeric }, + { + name => 'maxlocalattachment', + desc => 'The maximum size (in Megabytes) of attachments identified by ' . + 'the user as "Big Files" to be stored locally on the webserver. ' . + 'If set to zero, attachments will never be kept on the local ' . + 'filesystem.', + type => 't', + default => '0', + checker => \&check_numeric + }, + { name => 'chartgroup', desc => 'The name of the group of users who can use the "New Charts" ' . diff --git a/webtools/bugzilla/template/en/default/attachment/create.html.tmpl b/webtools/bugzilla/template/en/default/attachment/create.html.tmpl index 82ad73ce17b7..43af6e638b57 100644 --- a/webtools/bugzilla/template/en/default/attachment/create.html.tmpl +++ b/webtools/bugzilla/template/en/default/attachment/create.html.tmpl @@ -65,6 +65,18 @@ + [% IF Param("maxlocalattachment") %] + + BigFile: + + + + + + [% END %] diff --git a/webtools/bugzilla/template/en/default/global/user-error.html.tmpl b/webtools/bugzilla/template/en/default/global/user-error.html.tmpl index 6a29f975d53d..ac2cba6d3c92 100644 --- a/webtools/bugzilla/template/en/default/global/user-error.html.tmpl +++ b/webtools/bugzilla/template/en/default/global/user-error.html.tmpl @@ -156,6 +156,10 @@ [% title = "Access Denied" %] You are not authorized to access this attachment. + [% ELSIF error == "attachment_removed" %] + [% title = "Attachment Removed" %] + The attachment you are attempting to access has been removed. + [% ELSIF error == "bug_access_denied" %] [% title = "Access Denied" %] You are not authorized to access [% terms.bug %] #[% bug_id FILTER html %]. @@ -604,6 +608,11 @@ [% title = "Invalid Keyword Name" %] You may not use commas or whitespace in a keyword name. + [% ELSIF error == "local_file_too_large" %] + [% title = "Local File Too Large" %] + Local file uploads must not exceed + [% Param('maxlocalattachment') %] MB in size. + [% ELSIF error == "login_needed_for_password_change" %] [% title = "Login Name Required" %] You must enter a login name when requesting to change your password.