#!/usr/bin/perl -wT # -*- Mode: perl; indent-tabs-mode: nil -*- # # The contents of this file are subject to the Mozilla Public # License Version 1.1 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of # the License at http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or # implied. See the License for the specific language governing # rights and limitations under the License. # # The Original Code is the Bugzilla Bug Tracking System. # # The Initial Developer of the Original Code is Netscape Communications # Corporation. Portions created by Netscape are # Copyright (C) 1998 Netscape Communications Corporation. All # Rights Reserved. # # Contributor(s): Terry Weissman # Myk Melez # Daniel Raichle # Dave Miller # Alexander J. Vincent # Max Kanat-Alexander ################################################################################ # Script Initialization ################################################################################ # Make it harder for us to do dangerous things in Perl. use strict; use lib qw(.); use vars qw( $template $vars ); # 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; use Bugzilla::Flag; use Bugzilla::FlagType; use Bugzilla::User; use Bugzilla::Util; use Bugzilla::Bug; Bugzilla->login(); my $cgi = Bugzilla->cgi; ################################################################################ # Main Body Execution ################################################################################ # All calls to this script should contain an "action" variable whose # value determines what the user wants to do. The code below checks # the value of that variable and runs the appropriate code. If none is # supplied, we default to 'view'. # Determine whether to use the action specified by the user or the default. my $action = $cgi->param('action') || 'view'; if ($action eq "view") { view(); } elsif ($action eq "interdiff") { interdiff(); } elsif ($action eq "diff") { diff(); } elsif ($action eq "viewall") { viewall(); } elsif ($action eq "enter") { Bugzilla->login(LOGIN_REQUIRED); enter(); } elsif ($action eq "insert") { Bugzilla->login(LOGIN_REQUIRED); insert(); } elsif ($action eq "edit") { edit(); } elsif ($action eq "update") { Bugzilla->login(LOGIN_REQUIRED); update(); } else { ThrowCodeError("unknown_action", { action => $action }); } exit; ################################################################################ # Data Validation / Security Authorization ################################################################################ # Validates an attachment ID. Optionally takes a parameter of a form # variable name that contains the ID to be validated. If not specified, # uses 'id'. # # Will throw an error if 1) attachment ID is not a valid number, # 2) attachment does not exist, or 3) user isn't allowed to access the # attachment. # # Returns a list, where the first item is the validated, detainted # attachment id, and the 2nd item is the bug id corresponding to the # attachment. # sub validateID { my $param = @_ ? $_[0] : 'id'; my $user = Bugzilla->user; # If we're not doing interdiffs, check if id wasn't specified and # prompt them with a page that allows them to choose an attachment. # Happens when calling plain attachment.cgi from the urlbar directly if ($param eq 'id' && !$cgi->param('id')) { print Bugzilla->cgi->header(); $template->process("attachment/choose.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; } my $attach_id = $cgi->param($param); # Validate the specified attachment id. detaint kills $attach_id if # non-natural, so use the original value from $cgi in our exception # message here. detaint_natural($attach_id) || ThrowUserError("invalid_attach_id", { attach_id => $cgi->param($param) }); # Make sure the attachment exists in the database. SendSQL("SELECT bug_id, isprivate, submitter_id FROM attachments WHERE attach_id = $attach_id"); MoreSQLData() || ThrowUserError("invalid_attach_id", { attach_id => $attach_id }); # Make sure the user is authorized to access this attachment's bug. my ($bugid, $isprivate, $submitter_id) = FetchSQLData(); ValidateBugID($bugid); if ($isprivate && $user->id != $submitter_id && !$user->is_insider) { ThrowUserError("auth_failure", {action => "access", object => "attachment"}); } return ($attach_id,$bugid); } # Validates format of a diff/interdiff. Takes a list as an parameter, which # defines the valid format values. Will throw an error if the format is not # in the list. Returns either the user selected or default format. sub validateFormat { # receives a list of legal formats; first item is a default my $format = $cgi->param('format') || $_[0]; if ( lsearch(\@_, $format) == -1) { ThrowUserError("invalid_format", { format => $format, formats => \@_ }); } return $format; } # Validates context of a diff/interdiff. Will throw an error if the context # is not number, "file" or "patch". Returns the validated, detainted context. sub validateContext { my $context = $cgi->param('context') || "patch"; if ($context ne "file" && $context ne "patch") { detaint_natural($context) || ThrowUserError("invalid_context", { context => $cgi->param('context') }); } return $context; } sub validateCanEdit { my ($attach_id) = (@_); my $dbh = Bugzilla->dbh; my $user = Bugzilla->user; my ($is_private, $submitter_id) = $dbh->selectrow_array('SELECT isprivate, submitter_id FROM attachments WHERE attach_id = ?', undef, $attach_id); # Bug 97729 - the submitter can edit their attachments return if ($submitter_id == $user->id); # Only people in the insider group can view private attachments. if ($is_private && !$user->is_insider) { ThrowUserError('illegal_attachment_edit', {attach_id => $attach_id}); } # People in editbugs can edit all attachments return if UserInGroup("editbugs"); # If we come here, then this attachment cannot be seen by the user. ThrowUserError("illegal_attachment_edit", { attach_id => $attach_id }); } sub validateCanChangeAttachment { my ($attachid) = @_; SendSQL("SELECT product_id FROM attachments INNER JOIN bugs ON bugs.bug_id = attachments.bug_id WHERE attach_id = $attachid"); my $productid = FetchOneColumn(); CanEditProductId($productid) || ThrowUserError("illegal_attachment_edit", { attach_id => $attachid }); } sub validateCanChangeBug { my ($bugid) = @_; SendSQL("SELECT product_id FROM bugs WHERE bug_id = $bugid"); my $productid = FetchOneColumn(); CanEditProductId($productid) || ThrowUserError("illegal_attachment_edit_bug", { bug_id => $bugid }); } sub validateDescription { $cgi->param('description') || ThrowUserError("missing_attachment_description"); } sub validateIsPatch { # Set the ispatch flag to zero if it is undefined, since the UI uses # an HTML checkbox to represent this flag, and unchecked HTML checkboxes # do not get sent in HTML requests. $cgi->param('ispatch', $cgi->param('ispatch') ? 1 : 0); # Set the content type to text/plain if the attachment is a patch. $cgi->param('contenttype', 'text/plain') if $cgi->param('ispatch'); } sub validateContentType { if (!defined $cgi->param('contenttypemethod')) { ThrowUserError("missing_content_type_method"); } elsif ($cgi->param('contenttypemethod') eq 'autodetect') { my $contenttype = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'}; # The user asked us to auto-detect the content type, so use the type # specified in the HTTP request headers. if ( !$contenttype ) { ThrowUserError("missing_content_type"); } $cgi->param('contenttype', $contenttype); } elsif ($cgi->param('contenttypemethod') eq 'list') { # The user selected a content type from the list, so use their selection. $cgi->param('contenttype', $cgi->param('contenttypeselection')); } elsif ($cgi->param('contenttypemethod') eq 'manual') { # The user entered a content type manually, so use their entry. $cgi->param('contenttype', $cgi->param('contenttypeentry')); } else { ThrowCodeError("illegal_content_type_method", { contenttypemethod => $cgi->param('contenttypemethod') }); } if ( $cgi->param('contenttype') !~ /^(application|audio|image|message|model|multipart|text|video)\/.+$/ ) { ThrowUserError("invalid_content_type", { contenttype => $cgi->param('contenttype') }); } } sub validateIsObsolete { # Set the isobsolete flag to zero if it is undefined, since the UI uses # an HTML checkbox to represent this flag, and unchecked HTML checkboxes # do not get sent in HTML requests. $cgi->param('isobsolete', $cgi->param('isobsolete') ? 1 : 0); } sub validatePrivate { # Set the isprivate flag to zero if it is undefined, since the UI uses # an HTML checkbox to represent this flag, and unchecked HTML checkboxes # do not get sent in HTML requests. $cgi->param('isprivate', $cgi->param('isprivate') ? 1 : 0); } sub validateData { my $maxsize = $cgi->param('ispatch') ? Param('maxpatchsize') : Param('maxattachmentsize'); $maxsize *= 1024; # Convert from K my $fh; # Skip uploading into a local variable if the user wants to upload huge # attachments into local files. if (!$cgi->param('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 (!$cgi->param('bigfile')) { # enable 'slurp' mode local $/; $data = <$fh>; } $data || ($cgi->param('bigfile')) || ThrowUserError("zero_length_file"); # Make sure the attachment does not exceed the maximum permitted size my $len = $data ? length($data) : 0; if ($maxsize && $len > $maxsize) { my $vars = { filesize => sprintf("%.0f", $len/1024) }; if ($cgi->param('ispatch')) { ThrowUserError("patch_too_large", $vars); } else { ThrowUserError("file_too_large", $vars); } } return $data || ''; } sub validateFilename { defined $cgi->upload('data') || ThrowUserError("file_not_specified"); my $filename = $cgi->upload('data'); # Remove path info (if any) from the file name. The browser should do this # for us, but some are buggy. This may not work on Mac file names and could # mess up file names with slashes in them, but them's the breaks. We only # use this as a hint to users downloading attachments anyway, so it's not # a big deal if it munges incorrectly occasionally. $filename =~ s/^.*[\/\\]//; # Truncate the filename to 100 characters, counting from the end of the string # to make sure we keep the filename extension. $filename = substr($filename, -100, 100); return $filename; } sub validateObsolete { my @obsolete_ids = (); # Make sure the attachment id is valid and the user has permissions to view # the bug to which it is attached. foreach my $attachid ($cgi->param('obsolete')) { my $vars = {}; $vars->{'attach_id'} = $attachid; detaint_natural($attachid) || ThrowCodeError("invalid_attach_id_to_obsolete", $vars); SendSQL("SELECT bug_id, isobsolete, description FROM attachments WHERE attach_id = $attachid"); # Make sure the attachment exists in the database. MoreSQLData() || ThrowUserError("invalid_attach_id", $vars); my ($bugid, $isobsolete, $description) = FetchSQLData(); # Check that the user can modify this attachment validateCanEdit($attachid); $vars->{'description'} = $description; if ($bugid != $cgi->param('bugid')) { $vars->{'my_bug_id'} = $cgi->param('bugid'); $vars->{'attach_bug_id'} = $bugid; ThrowCodeError("mismatched_bug_ids_on_obsolete", $vars); } if ( $isobsolete ) { ThrowCodeError("attachment_already_obsolete", $vars); } push(@obsolete_ids, $attachid); } return @obsolete_ids; } # Returns 1 if the parameter is a content-type viewable in this browser # Note that we don't use $cgi->Accept()'s ability to check if a content-type # matches, because this will return a value even if it's matched by the generic # */* which most browsers add to the end of their Accept: headers. sub isViewable { my $contenttype = trim(shift); # We assume we can view all text and image types if ($contenttype =~ /^(text|image)\//) { return 1; } # Mozilla can view XUL. Note the trailing slash on the Gecko detection to # avoid sending XUL to Safari. if (($contenttype =~ /^application\/vnd\.mozilla\./) && ($cgi->user_agent() =~ /Gecko\//)) { return 1; } # If it's not one of the above types, we check the Accept: header for any # types mentioned explicitly. my $accept = join(",", $cgi->Accept()); if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/) { return 1; } return 0; } ################################################################################ # Functions ################################################################################ # Display an attachment. sub view { # Retrieve and validate parameters my ($attach_id) = validateID(); # Retrieve the attachment content and its content type from the database. SendSQL("SELECT mimetype, filename, thedata FROM attachments " . "WHERE attach_id = $attach_id"); my ($contenttype, $filename, $thedata) = FetchSQLData(); # Bug 111522: allow overriding content-type manually in the posted form # params. if (defined $cgi->param('content_type')) { $cgi->param('contenttypemethod', 'manual'); $cgi->param('contenttypeentry', $cgi->param('content_type')); validateContentType(); $contenttype = $cgi->param('content_type'); } # 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 $hash = ($attach_id % 100) + 100; $hash =~ s/.*(\d\d)$/group.$1/; if (open(AH, "$attachdir/$hash/attachment.$attach_id")) { 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 $filename =~ s/"/\\"/g; # escape quotes print Bugzilla->cgi->header(-type=>"$contenttype; name=\"$filename\"", -content_disposition=> "inline; filename=\"$filename\"", -content_length => $filesize); if ($thedata) { print $thedata; } else { while () { print $_; } close(AH); } } sub interdiff { # Retrieve and validate parameters my ($old_id) = validateID('oldid'); my ($new_id) = validateID('newid'); my $format = validateFormat('html', 'raw'); my $context = validateContext(); # Get old patch data my ($old_bugid, $old_description, $old_filename, $old_file_list) = get_unified_diff($old_id); # Get new patch data my ($new_bugid, $new_description, $new_filename, $new_file_list) = get_unified_diff($new_id); my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list); # # send through interdiff, send output directly to template # # Must hack path so that interdiff will work. # $ENV{'PATH'} = $::diffpath; open my $interdiff_fh, "$::interdiffbin $old_filename $new_filename|"; binmode $interdiff_fh; my ($reader, $last_reader) = setup_patch_readers("", $context); if ($format eq 'raw') { require PatchReader::DiffPrinter::raw; $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw()); # Actually print out the patch print $cgi->header(-type => 'text/plain', -expires => '+3M'); } else { $vars->{warning} = $warning if $warning; $vars->{bugid} = $new_bugid; $vars->{oldid} = $old_id; $vars->{old_desc} = $old_description; $vars->{newid} = $new_id; $vars->{new_desc} = $new_description; delete $vars->{attachid}; delete $vars->{do_context}; delete $vars->{context}; setup_template_patch_reader($last_reader, $format, $context); } $reader->iterate_fh($interdiff_fh, "interdiff #$old_id #$new_id"); close $interdiff_fh; $ENV{'PATH'} = ''; # # Delete temporary files # unlink($old_filename) or warn "Could not unlink $old_filename: $!"; unlink($new_filename) or warn "Could not unlink $new_filename: $!"; } sub get_unified_diff { my ($id) = @_; # Bring in the modules we need require PatchReader::Raw; require PatchReader::FixPatchRoot; require PatchReader::DiffPrinter::raw; require PatchReader::PatchInfoGrabber; require File::Temp; # Get the patch SendSQL("SELECT bug_id, description, ispatch, thedata FROM attachments WHERE attach_id = $id"); my ($bugid, $description, $ispatch, $thedata) = FetchSQLData(); if (!$ispatch) { $vars->{'attach_id'} = $id; ThrowUserError("must_be_patch"); } # Reads in the patch, converting to unified diff in a temp file my $reader = new PatchReader::Raw; my $last_reader = $reader; # fixes patch root (makes canonical if possible) if (Param('cvsroot')) { my $fix_patch_root = new PatchReader::FixPatchRoot(Param('cvsroot')); $last_reader->sends_data_to($fix_patch_root); $last_reader = $fix_patch_root; } # Grabs the patch file info my $patch_info_grabber = new PatchReader::PatchInfoGrabber(); $last_reader->sends_data_to($patch_info_grabber); $last_reader = $patch_info_grabber; # Prints out to temporary file my ($fh, $filename) = File::Temp::tempfile(); my $raw_printer = new PatchReader::DiffPrinter::raw($fh); $last_reader->sends_data_to($raw_printer); $last_reader = $raw_printer; # Iterate! $reader->iterate_string($id, $thedata); return ($bugid, $description, $filename, $patch_info_grabber->patch_info()->{files}); } sub warn_if_interdiff_might_fail { my ($old_file_list, $new_file_list) = @_; # Verify that the list of files diffed is the same my @old_files = sort keys %{$old_file_list}; my @new_files = sort keys %{$new_file_list}; if (@old_files != @new_files || join(' ', @old_files) ne join(' ', @new_files)) { return "interdiff1"; } # Verify that the revisions in the files are the same foreach my $file (keys %{$old_file_list}) { if ($old_file_list->{$file}{old_revision} ne $new_file_list->{$file}{old_revision}) { return "interdiff2"; } } return undef; } sub setup_patch_readers { my ($diff_root, $context) = @_; # # Parameters: # format=raw|html # context=patch|file|0-n # collapsed=0|1 # headers=0|1 # # Define the patch readers # The reader that reads the patch in (whatever its format) require PatchReader::Raw; my $reader = new PatchReader::Raw; my $last_reader = $reader; # Fix the patch root if we have a cvs root if (Param('cvsroot')) { require PatchReader::FixPatchRoot; $last_reader->sends_data_to(new PatchReader::FixPatchRoot(Param('cvsroot'))); $last_reader->sends_data_to->diff_root($diff_root) if defined($diff_root); $last_reader = $last_reader->sends_data_to; } # Add in cvs context if we have the necessary info to do it if ($context ne "patch" && $::cvsbin && Param('cvsroot_get')) { require PatchReader::AddCVSContext; $last_reader->sends_data_to( new PatchReader::AddCVSContext($context, Param('cvsroot_get'))); $last_reader = $last_reader->sends_data_to; } return ($reader, $last_reader); } sub setup_template_patch_reader { my ($last_reader, $format, $context) = @_; require PatchReader::DiffPrinter::template; # Define the vars for templates if (defined $cgi->param('headers')) { $vars->{headers} = $cgi->param('headers'); } else { $vars->{headers} = 1 if !defined $cgi->param('headers'); } $vars->{collapsed} = $cgi->param('collapsed'); $vars->{context} = $context; $vars->{do_context} = $::cvsbin && Param('cvsroot_get') && !$vars->{'newid'}; # Print everything out print $cgi->header(-type => 'text/html', -expires => '+3M'); $last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template, "attachment/diff-header.$format.tmpl", "attachment/diff-file.$format.tmpl", "attachment/diff-footer.$format.tmpl", { %{$vars}, bonsai_url => Param('bonsai_url'), lxr_url => Param('lxr_url'), lxr_root => Param('lxr_root'), })); } sub diff { # Retrieve and validate parameters my ($attach_id) = validateID(); my $user = Bugzilla->user; my $format = validateFormat('html', 'raw'); my $context = validateContext(); # Get patch data SendSQL("SELECT bug_id, description, ispatch, thedata FROM attachments " . "WHERE attach_id = $attach_id"); my ($bugid, $description, $ispatch, $thedata) = FetchSQLData(); # If it is not a patch, view normally if (!$ispatch) { view(); return; } my ($reader, $last_reader) = setup_patch_readers(undef,$context); if ($format eq 'raw') { require PatchReader::DiffPrinter::raw; $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw()); # Actually print out the patch use vars qw($cgi); print $cgi->header(-type => 'text/plain', -expires => '+3M'); $reader->iterate_string("Attachment $attach_id", $thedata); } else { $vars->{other_patches} = []; if ($::interdiffbin && $::diffpath) { # Get the list of attachments that the user can view in this bug. # Ignore the current patch, but select the one right before it # chronologically. my $and_isprivate = ''; unless ($user->is_insider) { $and_isprivate = 'AND (isprivate = 0 OR submitter_id = ' . $user->id . ')'; } SendSQL("SELECT attach_id, description FROM attachments WHERE bug_id = $bugid AND ispatch = 1 $and_isprivate ORDER BY creation_ts DESC"); my $select_next_patch = 0; while (my ($other_id, $other_desc) = FetchSQLData()) { if ($other_id eq $attach_id) { $select_next_patch = 1; } else { push @{$vars->{other_patches}}, { id => $other_id, desc => $other_desc, selected => $select_next_patch }; if ($select_next_patch) { $select_next_patch = 0; } } } } $vars->{bugid} = $bugid; $vars->{attachid} = $attach_id; $vars->{description} = $description; setup_template_patch_reader($last_reader, $format, $context); # Actually print out the patch $reader->iterate_string("Attachment $attach_id", $thedata); } } # Display all attachments for a given bug in a series of IFRAMEs within one # HTML page. sub viewall { # Retrieve and validate parameters my $bugid = $cgi->param('bugid'); ValidateBugID($bugid); # Retrieve the attachments from the database and write them into an array # of hashes where each hash represents one attachment. my $dbh = Bugzilla->dbh; my $user = Bugzilla->user; # By default, private attachments are not accessible, unless the user # is in the insider group or submitted the attachment. my $privacy = ''; unless ($user->is_insider) { $privacy = 'AND (isprivate = 0 OR submitter_id = ' . $user->id . ')'; } SendSQL("SELECT attach_id, " . $dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . ", mimetype, description, ispatch, isobsolete, isprivate, LENGTH(thedata) FROM attachments WHERE bug_id = $bugid $privacy ORDER BY attach_id"); my @attachments; # the attachments array while (MoreSQLData()) { my %a; # the attachment hash ($a{'attachid'}, $a{'date'}, $a{'contenttype'}, $a{'description'}, $a{'ispatch'}, $a{'isobsolete'}, $a{'isprivate'}, $a{'datasize'}) = FetchSQLData(); $a{'isviewable'} = isViewable($a{'contenttype'}); $a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'}, 'is_active' => 1 }); # Add the hash representing the attachment to the array of attachments. push @attachments, \%a; } # Retrieve the bug summary (for displaying on screen) and assignee. SendSQL("SELECT short_desc, assigned_to FROM bugs " . "WHERE bug_id = $bugid"); my ($bugsummary, $assignee_id) = FetchSQLData(); # Define the variables and functions that will be passed to the UI template. $vars->{'bugid'} = $bugid; $vars->{'attachments'} = \@attachments; $vars->{'bugassignee_id'} = $assignee_id; $vars->{'bugsummary'} = $bugsummary; $vars->{'GetBugLink'} = \&GetBugLink; print Bugzilla->cgi->header(); # Generate and return the UI (HTML page) from the appropriate template. $template->process("attachment/show-multiple.html.tmpl", $vars) || ThrowTemplateError($template->error()); } # Display a form for entering a new attachment. sub enter { # Retrieve and validate parameters my $bugid = $cgi->param('bugid'); ValidateBugID($bugid); validateCanChangeBug($bugid); # Retrieve the attachments the user can edit from the database and write # them into an array of hashes where each hash represents one attachment. my $canEdit = ""; if (!UserInGroup("editbugs")) { $canEdit = "AND submitter_id = " . Bugzilla->user->id; } SendSQL("SELECT attach_id, description, isprivate FROM attachments WHERE bug_id = $bugid AND isobsolete = 0 $canEdit ORDER BY attach_id"); my @attachments; # the attachments array while ( MoreSQLData() ) { my %a; # the attachment hash ($a{'id'}, $a{'description'}, $a{'isprivate'}) = FetchSQLData(); # Add the hash representing the attachment to the array of attachments. push @attachments, \%a; } # Retrieve the bug summary (for displaying on screen) and assignee. SendSQL("SELECT short_desc, assigned_to FROM bugs WHERE bug_id = $bugid"); my ($bugsummary, $assignee_id) = FetchSQLData(); # Define the variables and functions that will be passed to the UI template. $vars->{'bugid'} = $bugid; $vars->{'attachments'} = \@attachments; $vars->{'bugassignee_id'} = $assignee_id; $vars->{'bugsummary'} = $bugsummary; $vars->{'GetBugLink'} = \&GetBugLink; 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, 'component_id' => $component_id}); $vars->{'flag_types'} = $flag_types; $vars->{'any_flags_requesteeble'} = grep($_->{'is_requesteeble'}, @$flag_types); print Bugzilla->cgi->header(); # Generate and return the UI (HTML page) from the appropriate template. $template->process("attachment/create.html.tmpl", $vars) || ThrowTemplateError($template->error()); } # Insert a new attachment into the database. sub insert { my $dbh = Bugzilla->dbh; my $userid = Bugzilla->user->id; # Retrieve and validate parameters my $bugid = $cgi->param('bugid'); ValidateBugID($bugid); validateCanChangeBug($bugid); ValidateComment(scalar $cgi->param('comment')); my $filename = validateFilename(); validateIsPatch(); my $data = validateData(); validateDescription(); validateContentType() unless $cgi->param('ispatch'); my @obsolete_ids = (); @obsolete_ids = validateObsolete() if $cgi->param('obsolete'); # The order of these function calls is important, as both Flag::validate # and FlagType::validate assume User::match_field has ensured that the # values in the requestee fields are legitimate user email addresses. my $match_status = Bugzilla::User::match_field($cgi, { '^requestee(_type)?-(\d+)$' => { 'type' => 'single' }, }, MATCH_SKIP_CONFIRM); $vars->{'match_field'} = 'requestee'; if ($match_status == USER_MATCH_FAILED) { $vars->{'message'} = 'user_match_failed'; } elsif ($match_status == USER_MATCH_MULTIPLE) { $vars->{'message'} = 'user_match_multiple'; } # FlagType::validate() and Flag::validate() should not detect # any reference to existing flags when creating a new attachment. # Setting the third param to -1 will force this function to check this point. Bugzilla::Flag::validate($cgi, $bugid, -1); Bugzilla::FlagType::validate($cgi, $bugid, -1); # Escape characters in strings that will be used in SQL statements. my $sql_filename = SqlQuote($filename); my $description = SqlQuote($cgi->param('description')); my $contenttype = SqlQuote($cgi->param('contenttype')); my $isprivate = $cgi->param('isprivate') ? 1 : 0; # Figure out when the changes were made. my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); my $sql_timestamp = SqlQuote($timestamp); # Insert the attachment into the database. my $sth = $dbh->prepare("INSERT INTO attachments (thedata, bug_id, creation_ts, filename, description, mimetype, ispatch, isprivate, submitter_id) VALUES (?, $bugid, $sql_timestamp, $sql_filename, $description, $contenttype, " . $cgi->param('ispatch') . ", $isprivate, $userid)"); # We only use $data here in this INSERT with a placeholder, # so it's safe. trick_taint($data); $sth->bind_param(1, $data, $dbh->BLOB_TYPE); $sth->execute(); # 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 ($cgi->param('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" . $cgi->param('description') . "\n"; $comment .= ("\n" . $cgi->param('comment')) if defined $cgi->param('comment'); AppendComment($bugid, $userid, $comment, $isprivate, $timestamp); # Make existing attachments obsolete. my $fieldid = GetFieldID('attachments.isobsolete'); foreach my $obsolete_id (@obsolete_ids) { # If the obsolete attachment has request flags, cancel them. # This call must be done before updating the 'attachments' table. Bugzilla::Flag::CancelRequests($bugid, $obsolete_id, $sql_timestamp); SendSQL("UPDATE attachments SET isobsolete = 1 " . "WHERE attach_id = $obsolete_id"); SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) VALUES ($bugid, $obsolete_id, $userid, $sql_timestamp, $fieldid, '0', '1')"); } # Assign the bug to the user, if they are allowed to take it my $owner = ""; if ($cgi->param('takebug') && UserInGroup("editbugs")) { my @fields = ("assigned_to", "bug_status", "resolution", "everconfirmed", "login_name"); # Get the old values, for the bugs_activity table SendSQL("SELECT " . join(", ", @fields) . " " . "FROM bugs " . "INNER JOIN profiles " . "ON profiles.userid = bugs.assigned_to " . "WHERE bugs.bug_id = $bugid"); my @oldvalues = FetchSQLData(); my @newvalues = ($userid, "ASSIGNED", "", 1, Bugzilla->user->login); # Make sure the person we are taking the bug from gets mail. $owner = $oldvalues[4]; @oldvalues = map(SqlQuote($_), @oldvalues); @newvalues = map(SqlQuote($_), @newvalues); # Update the bug record. Note that this doesn't involve login_name. SendSQL("UPDATE bugs SET delta_ts = $sql_timestamp, " . join(", ", map("$fields[$_] = $newvalues[$_]", (0..3))) . " WHERE bug_id = $bugid"); # If the bug was a dupe, we have to remove its entry from the # 'duplicates' table. $dbh->do('DELETE FROM duplicates WHERE dupe = ?', undef, $bugid); # We store email addresses in the bugs_activity table rather than IDs. $oldvalues[0] = $oldvalues[4]; $newvalues[0] = $newvalues[4]; # Add the changes to the bugs_activity table for (my $i = 0; $i < 4; $i++) { if ($oldvalues[$i] ne $newvalues[$i]) { my $fieldid = GetFieldID($fields[$i]); SendSQL("INSERT INTO bugs_activity " . "(bug_id, who, bug_when, fieldid, removed, added) " . "VALUES ($bugid, $userid, $sql_timestamp, " . "$fieldid, $oldvalues[$i], $newvalues[$i])"); } } } # Create flags. my $target = Bugzilla::Flag::GetTarget(undef, $attachid); Bugzilla::Flag::process($target, $timestamp, $cgi); # Define the variables and functions that will be passed to the UI template. $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login, 'owner' => $owner }; $vars->{'bugid'} = $bugid; $vars->{'attachid'} = $attachid; $vars->{'description'} = $description; $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod'); $vars->{'contenttype'} = $cgi->param('contenttype'); print Bugzilla->cgi->header(); # Generate and return the UI (HTML page) from the appropriate template. $template->process("attachment/created.html.tmpl", $vars) || ThrowTemplateError($template->error()); } # Displays a form for editing attachment properties. # Any user is allowed to access this page, unless the attachment # is private and the user does not belong to the insider group. # Validations are done later when the user submits changes. sub edit { # Retrieve and validate parameters my ($attach_id) = validateID(); my $user = Bugzilla->user; # Retrieve the attachment from the database. SendSQL("SELECT description, mimetype, filename, bug_id, ispatch, isobsolete, isprivate, LENGTH(thedata) FROM attachments WHERE attach_id = $attach_id"); my ($description, $contenttype, $filename, $bugid, $ispatch, $isobsolete, $isprivate, $datasize) = FetchSQLData(); my $isviewable = isViewable($contenttype); # Retrieve a list of attachments for this bug as well as a summary of the bug # to use in a navigation bar across the top of the screen. my $and_isprivate = ''; unless ($user->is_insider) { $and_isprivate = 'AND (isprivate = 0 OR submitter_id = ' . $user->id . ')'; } SendSQL("SELECT attach_id FROM attachments WHERE bug_id = $bugid $and_isprivate ORDER BY attach_id"); my @bugattachments; push(@bugattachments, FetchSQLData()) while (MoreSQLData()); SendSQL("SELECT short_desc FROM bugs WHERE bug_id = $bugid"); my ($bugsummary) = FetchSQLData(); # 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 , 'component_id' => $component_id }); foreach my $flag_type (@$flag_types) { $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id' => $flag_type->{'id'}, 'attach_id' => $attach_id, 'is_active' => 1 }); } $vars->{'flag_types'} = $flag_types; $vars->{'any_flags_requesteeble'} = grep($_->{'is_requesteeble'}, @$flag_types); # Define the variables and functions that will be passed to the UI template. $vars->{'attachid'} = $attach_id; $vars->{'description'} = $description; $vars->{'contenttype'} = $contenttype; $vars->{'filename'} = $filename; $vars->{'bugid'} = $bugid; $vars->{'bugsummary'} = $bugsummary; $vars->{'ispatch'} = $ispatch; $vars->{'isobsolete'} = $isobsolete; $vars->{'isprivate'} = $isprivate; $vars->{'datasize'} = $datasize; $vars->{'isviewable'} = $isviewable; $vars->{'attachments'} = \@bugattachments; $vars->{'GetBugLink'} = \&GetBugLink; # Determine if PatchReader is installed eval { require PatchReader; $vars->{'patchviewerinstalled'} = 1; }; print Bugzilla->cgi->header(); # Generate and return the UI (HTML page) from the appropriate template. $template->process("attachment/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); } # Updates an attachment record. Users with "editbugs" privileges, (or the # original attachment's submitter) can edit the attachment's description, # content type, ispatch and isobsolete flags, and statuses, and they can # also submit a comment that appears in the bug. # Users cannot edit the content of the attachment itself. sub update { my $dbh = Bugzilla->dbh; my $user = Bugzilla->user; my $userid = $user->id; # Retrieve and validate parameters ValidateComment(scalar $cgi->param('comment')); my ($attach_id, $bugid) = validateID(); validateCanEdit($attach_id); validateCanChangeAttachment($attach_id); validateDescription(); validateIsPatch(); validateContentType() unless $cgi->param('ispatch'); validateIsObsolete(); validatePrivate(); # If the submitter of the attachment is not in the insidergroup, # be sure that he cannot overwrite the private bit. # This check must be done before calling Bugzilla::Flag*::validate(), # because they will look at the private bit when checking permissions. # XXX - This is a ugly hack. Ideally, we shouldn't have to look at the # old private bit twice (first here, and then below again), but this is # the less risky change. unless ($user->is_insider) { my $oldisprivate = $dbh->selectrow_array('SELECT isprivate FROM attachments WHERE attach_id = ?', undef, $attach_id); $cgi->param('isprivate', $oldisprivate); } # The order of these function calls is important, as both Flag::validate # and FlagType::validate assume User::match_field has ensured that the # values in the requestee fields are legitimate user email addresses. Bugzilla::User::match_field($cgi, { '^requestee(_type)?-(\d+)$' => { 'type' => 'single' } }); Bugzilla::Flag::validate($cgi, $bugid, $attach_id); Bugzilla::FlagType::validate($cgi, $bugid, $attach_id); # Lock database tables in preparation for updating the attachment. $dbh->bz_lock_tables('attachments WRITE', 'flags WRITE' , 'flagtypes READ', 'fielddefs READ', 'bugs_activity WRITE', 'flaginclusions AS i READ', 'flagexclusions AS e READ', # cc, bug_group_map, user_group_map, and groups are in here so we # can check the permissions of flag requestees and email addresses # on the flag type cc: lists via the CanSeeBug # function call in Flag::notify. group_group_map is in here in case # Bugzilla::User needs to rederive groups. profiles and # user_group_map would be READ locks instead of WRITE locks if it # weren't for derive_groups, which needs to write to those tables. 'bugs WRITE', 'profiles WRITE', 'email_setting READ', 'cc READ', 'bug_group_map READ', 'user_group_map WRITE', 'group_group_map READ', 'groups READ'); # Get a copy of the attachment record before we make changes # so we can record those changes in the activity table. SendSQL("SELECT description, mimetype, filename, ispatch, isobsolete, isprivate FROM attachments WHERE attach_id = $attach_id"); my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch, $oldisobsolete, $oldisprivate) = FetchSQLData(); # Quote the description and content type for use in the SQL UPDATE statement. my $quoteddescription = SqlQuote($cgi->param('description')); my $quotedcontenttype = SqlQuote($cgi->param('contenttype')); my $quotedfilename = SqlQuote($cgi->param('filename')); # Figure out when the changes were made. SendSQL("SELECT NOW()"); my $timestamp = FetchOneColumn(); # Update flags. We have to do this before committing changes # to attachments so that we can delete pending requests if the user # is obsoleting this attachment without deleting any requests # the user submits at the same time. my $target = Bugzilla::Flag::GetTarget(undef, $attach_id); Bugzilla::Flag::process($target, $timestamp, $cgi); # Update the attachment record in the database. SendSQL("UPDATE attachments SET description = $quoteddescription , mimetype = $quotedcontenttype , filename = $quotedfilename , ispatch = " . $cgi->param('ispatch') . ", isobsolete = " . $cgi->param('isobsolete') . ", isprivate = " . $cgi->param('isprivate') . " WHERE attach_id = $attach_id "); # Record changes in the activity table. my $sql_timestamp = SqlQuote($timestamp); if ($olddescription ne $cgi->param('description')) { my $quotedolddescription = SqlQuote($olddescription); my $fieldid = GetFieldID('attachments.description'); SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) VALUES ($bugid, $attach_id, $userid, $sql_timestamp, $fieldid, $quotedolddescription, $quoteddescription)"); } if ($oldcontenttype ne $cgi->param('contenttype')) { my $quotedoldcontenttype = SqlQuote($oldcontenttype); my $fieldid = GetFieldID('attachments.mimetype'); SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) VALUES ($bugid, $attach_id, $userid, $sql_timestamp, $fieldid, $quotedoldcontenttype, $quotedcontenttype)"); } if ($oldfilename ne $cgi->param('filename')) { my $quotedoldfilename = SqlQuote($oldfilename); my $fieldid = GetFieldID('attachments.filename'); SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) VALUES ($bugid, $attach_id, $userid, $sql_timestamp, $fieldid, $quotedoldfilename, $quotedfilename)"); } if ($oldispatch ne $cgi->param('ispatch')) { my $fieldid = GetFieldID('attachments.ispatch'); SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) VALUES ($bugid, $attach_id, $userid, $sql_timestamp, $fieldid, $oldispatch, " . $cgi->param('ispatch') . ")"); } if ($oldisobsolete ne $cgi->param('isobsolete')) { my $fieldid = GetFieldID('attachments.isobsolete'); SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) VALUES ($bugid, $attach_id, $userid, $sql_timestamp, $fieldid, $oldisobsolete, " . $cgi->param('isobsolete') . ")"); } if ($oldisprivate ne $cgi->param('isprivate')) { my $fieldid = GetFieldID('attachments.isprivate'); SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) VALUES ($bugid, $attach_id, $userid, $sql_timestamp, $fieldid, $oldisprivate, " . $cgi->param('isprivate') . ")"); } # 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, # add the comment to the bug. if ($cgi->param('comment')) { # Prepend a string to the comment to let users know that the comment came # from the "edit attachment" screen. my $comment = qq|(From update of attachment $attach_id)\n| . $cgi->param('comment'); # Append the comment to the list of comments in the database. AppendComment($bugid, $userid, $comment, $cgi->param('isprivate'), $timestamp); } # Define the variables and functions that will be passed to the UI template. $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login }; $vars->{'attachid'} = $attach_id; $vars->{'bugid'} = $bugid; print Bugzilla->cgi->header(); # Generate and return the UI (HTML page) from the appropriate template. $template->process("attachment/updated.html.tmpl", $vars) || ThrowTemplateError($template->error()); }