1 #!/usr/bin/env perl -wT
2 # -*- Mode: perl; indent-tabs-mode: nil -*-
4 # The contents of this file are subject to the Mozilla Public
5 # License Version 1.1 (the "License"); you may not use this file
6 # except in compliance with the License. You may obtain a copy of
7 # the License at http://www.mozilla.org/MPL/
9 # Software distributed under the License is distributed on an "AS
10 # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
11 # implied. See the License for the specific language governing
12 # rights and limitations under the License.
14 # The Original Code is the Bugzilla Bug Tracking System.
16 # The Initial Developer of the Original Code is Netscape Communications
17 # Corporation. Portions created by Netscape are
18 # Copyright (C) 1998 Netscape Communications Corporation. All
21 # Contributor(s): Terry Weissman <terry@mozilla.org>
22 # Myk Melez <myk@mozilla.org>
23 # Daniel Raichle <draichle@gmx.net>
24 # Dave Miller <justdave@syndicomm.com>
25 # Alexander J. Vincent <ajvincent@juno.com>
26 # Max Kanat-Alexander <mkanat@bugzilla.org>
27 # Greg Hendricks <ghendricks@novell.com>
28 # Frédéric Buclin <LpSolit@gmail.com>
29 # Marc Schumann <wurblzap@gmail.com>
30 # Byron Jones <bugzilla@glob.com.au>
32 ################################################################################
33 # Script Initialization
34 ################################################################################
36 # Make it harder for us to do dangerous things in Perl.
42 use Bugzilla::Constants;
45 use Bugzilla::FlagType;
50 use Bugzilla::Attachment;
51 use Bugzilla::Attachment::PatchReader;
53 use Bugzilla::Keyword;
57 #endif // WEBKIT_CHANGES
59 # For most scripts we don't make $cgi and $template global variables. But
60 # when preparing Bugzilla for mod_perl, this script used these
61 # variables in so many subroutines that it was easier to just
63 local our $cgi = Bugzilla->cgi;
64 local our $template = Bugzilla->template;
67 ################################################################################
69 ################################################################################
71 # All calls to this script should contain an "action" variable whose
72 # value determines what the user wants to do. The code below checks
73 # the value of that variable and runs the appropriate code. If none is
74 # supplied, we default to 'view'.
76 # Determine whether to use the action specified by the user or the default.
77 my $action = $cgi->param('action') || 'view';
79 # You must use the appropriate urlbase/sslbase param when doing anything
80 # but viewing an attachment.
81 if ($action ne 'view') {
82 my $urlbase = Bugzilla->params->{'urlbase'};
83 my $sslbase = Bugzilla->params->{'sslbase'};
84 my $path_regexp = $sslbase ? qr/^(\Q$urlbase\E|\Q$sslbase\E)/ : qr/^\Q$urlbase\E/;
85 if (use_attachbase() && $cgi->self_url !~ /$path_regexp/) {
86 $cgi->redirect_to_urlbase;
91 # Determine if PatchReader is installed
94 $vars->{'patchviewerinstalled'} = 1;
97 # When viewing an attachment, do not request credentials if we are on
98 # the alternate host. Let view() decide when to call Bugzilla->login.
99 if ($action eq "view")
103 elsif ($action eq "interdiff")
107 elsif ($action eq "diff")
111 elsif ($action eq "viewall")
115 elsif ($action eq "enter")
117 Bugzilla->login(LOGIN_REQUIRED);
120 elsif ($action eq "insert")
122 Bugzilla->login(LOGIN_REQUIRED);
125 elsif ($action eq "edit")
130 elsif ($action eq "review")
134 elsif ($action eq "reviewform")
138 elsif ($action eq "rietveldreview")
140 edit("rietveldreview");
142 #endif // WEBKIT_CHANGES
143 elsif ($action eq "update")
145 Bugzilla->login(LOGIN_REQUIRED);
149 elsif ($action eq "prettypatch")
153 #endif // WEBKIT_CHANGES
154 elsif ($action eq "delete") {
159 ThrowCodeError("unknown_action", { action => $action });
164 ################################################################################
165 # Data Validation / Security Authorization
166 ################################################################################
168 # Validates an attachment ID. Optionally takes a parameter of a form
169 # variable name that contains the ID to be validated. If not specified,
171 # If the second parameter is true, the attachment ID will be validated,
172 # however the current user's access to the attachment will not be checked.
173 # Will throw an error if 1) attachment ID is not a valid number,
174 # 2) attachment does not exist, or 3) user isn't allowed to access the
177 # Returns an attachment object.
180 my($param, $dont_validate_access) = @_;
183 # If we're not doing interdiffs, check if id wasn't specified and
184 # prompt them with a page that allows them to choose an attachment.
185 # Happens when calling plain attachment.cgi from the urlbar directly
186 if ($param eq 'id' && !$cgi->param('id')) {
187 print $cgi->header();
188 $template->process("attachment/choose.html.tmpl", $vars) ||
189 ThrowTemplateError($template->error());
193 my $attach_id = $cgi->param($param);
195 # Validate the specified attachment id. detaint kills $attach_id if
196 # non-natural, so use the original value from $cgi in our exception
198 detaint_natural($attach_id)
199 || ThrowUserError("invalid_attach_id", { attach_id => $cgi->param($param) });
201 # Make sure the attachment exists in the database.
202 my $attachment = Bugzilla::Attachment->get($attach_id)
203 || ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
205 return $attachment if ($dont_validate_access || check_can_access($attachment));
208 # Make sure the current user has access to the specified attachment.
209 sub check_can_access {
210 my $attachment = shift;
211 my $user = Bugzilla->user;
213 # Make sure the user is authorized to access this attachment's bug.
214 ValidateBugID($attachment->bug_id);
215 if ($attachment->isprivate && $user->id != $attachment->attacher->id && !$user->is_insider) {
216 ThrowUserError('auth_failure', {action => 'access',
217 object => 'attachment'});
222 # Determines if the attachment is public -- that is, if users who are
223 # not logged in have access to the attachment
224 sub attachmentIsPublic {
225 my $attachment = shift;
227 return 0 if Bugzilla->params->{'requirelogin'};
228 return 0 if $attachment->isprivate;
230 my $anon_user = new Bugzilla::User;
231 return $anon_user->can_see_bug($attachment->bug_id);
234 # Validates format of a diff/interdiff. Takes a list as an parameter, which
235 # defines the valid format values. Will throw an error if the format is not
236 # in the list. Returns either the user selected or default format.
239 # receives a list of legal formats; first item is a default
240 my $format = $cgi->param('format') || $_[0];
241 if ( lsearch(\@_, $format) == -1)
243 ThrowUserError("invalid_format", { format => $format, formats => \@_ });
249 # Validates context of a diff/interdiff. Will throw an error if the context
250 # is not number, "file" or "patch". Returns the validated, detainted context.
253 my $context = $cgi->param('context') || "patch";
254 if ($context ne "file" && $context ne "patch") {
255 detaint_natural($context)
256 || ThrowUserError("invalid_context", { context => $cgi->param('context') });
262 sub validateCanChangeBug
265 my $dbh = Bugzilla->dbh;
266 my ($productid) = $dbh->selectrow_array(
269 WHERE bug_id = ?", undef, $bugid);
271 Bugzilla->user->can_edit_product($productid)
272 || ThrowUserError("illegal_attachment_edit_bug",
273 { bug_id => $bugid });
276 ################################################################################
278 ################################################################################
280 # Display an attachment.
284 if (use_attachbase()) {
285 $attachment = validateID(undef, 1);
286 # Replace %bugid% by the ID of the bug the attachment belongs to, if present.
287 my $attachbase = Bugzilla->params->{'attachment_base'};
288 my $bug_id = $attachment->bug_id;
289 $attachbase =~ s/%bugid%/$bug_id/;
290 my $path = 'attachment.cgi?id=' . $attachment->id;
292 # Make sure the attachment is served from the correct server.
293 if ($cgi->self_url !~ /^\Q$attachbase\E/) {
294 # We couldn't call Bugzilla->login earlier as we first had to make sure
295 # we were not going to request credentials on the alternate host.
297 if (attachmentIsPublic($attachment)) {
298 # No need for a token; redirect to attachment base.
299 print $cgi->redirect(-location => $attachbase . $path);
302 # Make sure the user can view the attachment.
303 check_can_access($attachment);
304 # Create a token and redirect.
305 my $token = url_quote(issue_session_token($attachment->id));
306 print $cgi->redirect(-location => $attachbase . "$path&t=$token");
310 # No need to validate the token for public attachments. We cannot request
311 # credentials as we are on the alternate host.
312 if (!attachmentIsPublic($attachment)) {
313 my $token = $cgi->param('t');
314 my ($userid, undef, $token_attach_id) = Bugzilla::Token::GetTokenData($token);
316 && detaint_natural($token_attach_id)
317 && ($token_attach_id == $attachment->id))
320 print $cgi->redirect('-location' => correct_urlbase() . $path);
323 # Change current user without creating cookies.
324 Bugzilla->set_user(new Bugzilla::User($userid));
325 # Tokens are single use only, delete it.
326 delete_token($token);
330 # No alternate host is used. Request credentials if required.
332 $attachment = validateID();
335 # At this point, Bugzilla->login has been called if it had to.
336 my $contenttype = $attachment->contenttype;
337 my $filename = $attachment->filename;
339 # Bug 111522: allow overriding content-type manually in the posted form
341 if (defined $cgi->param('content_type'))
343 $cgi->param('contenttypemethod', 'manual');
344 $cgi->param('contenttypeentry', $cgi->param('content_type'));
345 Bugzilla::Attachment->validate_content_type(THROW_ERROR);
346 $contenttype = $cgi->param('content_type');
349 # Return the appropriate HTTP response headers.
350 $attachment->datasize || ThrowUserError("attachment_removed");
352 $filename =~ s/^.*[\/\\]//;
353 # escape quotes and backslashes in the filename, per RFCs 2045/822
354 $filename =~ s/\\/\\\\/g; # escape backslashes
355 $filename =~ s/"/\\"/g; # escape quotes
357 my $disposition = Bugzilla->params->{'allow_attachment_display'} ? 'inline' : 'attachment';
359 print $cgi->header(-type=>"$contenttype; name=\"$filename\"",
360 -content_disposition=> "$disposition; filename=\"$filename\"",
361 -content_length => $attachment->datasize);
363 print $attachment->data;
367 # Retrieve and validate parameters
368 my $old_attachment = validateID('oldid');
369 my $new_attachment = validateID('newid');
370 my $format = validateFormat('html', 'raw');
371 my $context = validateContext();
373 Bugzilla::Attachment::PatchReader::process_interdiff(
374 $old_attachment, $new_attachment, $format, $context);
380 # Retrieve and validate parameters
381 my $attachment = validateID();
382 my $format = validateFormat('html', 'raw');
383 my $context = validateContext();
385 # If it is not a patch, view normally.
386 if (!$attachment->ispatch) {
392 print $cgi->header(-type => 'text/html',
395 my $orig_path = $ENV{'PATH'};
396 $ENV{'PATH'} = "/opt/local/bin:" . $ENV{'PATH'};
397 open2(\*OUT, \*IN, "/usr/bin/ruby", "-I", "PrettyPatch", "PrettyPatch/prettify.rb", "--html-exceptions");
398 $ENV{'PATH'} = $orig_path;
399 print IN $attachment->data;
406 #endif // WEBKIT_CHANGES
409 # Retrieve and validate parameters
410 my $attachment = validateID();
411 my $format = validateFormat('html', 'raw');
412 my $context = validateContext();
414 # If it is not a patch, view normally.
415 if (!$attachment->ispatch) {
420 Bugzilla::Attachment::PatchReader::process_diff($attachment, $format, $context);
423 # Display all attachments for a given bug in a series of IFRAMEs within one
426 # Retrieve and validate parameters
427 my $bugid = $cgi->param('bugid');
428 ValidateBugID($bugid);
429 my $bug = new Bugzilla::Bug($bugid);
431 my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bugid);
433 # Define the variables and functions that will be passed to the UI template.
434 $vars->{'bug'} = $bug;
435 $vars->{'attachments'} = $attachments;
437 print $cgi->header();
439 # Generate and return the UI (HTML page) from the appropriate template.
440 $template->process("attachment/show-multiple.html.tmpl", $vars)
441 || ThrowTemplateError($template->error());
444 # Display a form for entering a new attachment.
446 # Retrieve and validate parameters
447 my $bugid = $cgi->param('bugid');
448 ValidateBugID($bugid);
449 validateCanChangeBug($bugid);
450 my $dbh = Bugzilla->dbh;
451 my $user = Bugzilla->user;
453 my $bug = new Bugzilla::Bug($bugid, $user->id);
454 # Retrieve the attachments the user can edit from the database and write
455 # them into an array of hashes where each hash represents one attachment.
457 if (!$user->in_group('editbugs', $bug->product_id)) {
458 $canEdit = "AND submitter_id = " . $user->id;
460 my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
461 WHERE bug_id = ? AND isobsolete = 0 $canEdit
462 ORDER BY attach_id", undef, $bugid);
464 # Define the variables and functions that will be passed to the UI template.
465 $vars->{'bug'} = $bug;
466 $vars->{'attachments'} = Bugzilla::Attachment->get_list($attach_ids);
468 my $flag_types = Bugzilla::FlagType::match({'target_type' => 'attachment',
469 'product_id' => $bug->product_id,
470 'component_id' => $bug->component_id});
471 $vars->{'flag_types'} = $flag_types;
472 $vars->{'any_flags_requesteeble'} = grep($_->is_requesteeble, @$flag_types);
473 $vars->{'token'} = issue_session_token('createattachment:');
475 print $cgi->header();
477 # Generate and return the UI (HTML page) from the appropriate template.
478 $template->process("attachment/create.html.tmpl", $vars)
479 || ThrowTemplateError($template->error());
482 # Insert a new attachment into the database.
484 my $dbh = Bugzilla->dbh;
485 my $user = Bugzilla->user;
487 $dbh->bz_start_transaction;
489 # Retrieve and validate parameters
490 my $bugid = $cgi->param('bugid');
491 ValidateBugID($bugid);
492 validateCanChangeBug($bugid);
493 my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
495 # Detect if the user already used the same form to submit an attachment
496 my $token = trim($cgi->param('token'));
498 my ($creator_id, $date, $old_attach_id) = Bugzilla::Token::GetTokenData($token);
500 && ($creator_id == $user->id)
501 && ($old_attach_id =~ "^createattachment:"))
503 # The token is invalid.
504 ThrowUserError('token_does_not_exist');
507 $old_attach_id =~ s/^createattachment://;
509 if ($old_attach_id) {
510 $vars->{'bugid'} = $bugid;
511 $vars->{'attachid'} = $old_attach_id;
512 print $cgi->header();
513 $template->process("attachment/cancel-create-dupe.html.tmpl", $vars)
514 || ThrowTemplateError($template->error());
519 my $bug = new Bugzilla::Bug($bugid);
521 Bugzilla::Attachment->insert_attachment_for_bug(THROW_ERROR, $bug, $user,
524 # Insert a comment about the new attachment into the database.
525 my $comment = "Created an attachment (id=" . $attachment->id . ")\n" .
526 $attachment->description . "\n";
527 $comment .= ("\n" . $cgi->param('comment')) if defined $cgi->param('comment');
529 $bug->add_comment($comment, { isprivate => $attachment->isprivate });
531 # Assign the bug to the user, if they are allowed to take it
533 if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) {
534 # When taking a bug, we have to follow the workflow.
535 my $bug_status = $cgi->param('bug_status') || '';
536 ($bug_status) = grep {$_->name eq $bug_status} @{$bug->status->can_change_to};
538 if ($bug_status && $bug_status->is_open
539 && ($bug_status->name ne 'UNCONFIRMED' || $bug->product_obj->votes_to_confirm))
541 $bug->set_status($bug_status->name);
542 $bug->clear_resolution();
544 # Make sure the person we are taking the bug from gets mail.
545 $owner = $bug->assigned_to->login;
546 $bug->set_assigned_to($user);
548 $bug->update($timestamp);
552 $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef,
553 ("createattachment:" . $attachment->id, $token));
556 $dbh->bz_commit_transaction;
558 # Define the variables and functions that will be passed to the UI template.
559 $vars->{'mailrecipients'} = { 'changer' => $user->login,
561 $vars->{'attachment'} = $attachment;
562 # We cannot reuse the $bug object as delta_ts has eventually been updated
563 # since the object was created.
564 $vars->{'bugs'} = [new Bugzilla::Bug($bugid)];
565 $vars->{'header_done'} = 1;
566 $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
567 $vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
569 print $cgi->header();
570 # Generate and return the UI (HTML page) from the appropriate template.
571 $template->process("attachment/created.html.tmpl", $vars)
572 || ThrowTemplateError($template->error());
575 # Displays a form for editing attachment properties.
576 # Any user is allowed to access this page, unless the attachment
577 # is private and the user does not belong to the insider group.
578 # Validations are done later when the user submits changes.
581 my ($template_name) = @_;
582 $template_name = $template_name || "edit";
583 #endif // WEBKIT_CHANGES
585 my $attachment = validateID();
586 my $dbh = Bugzilla->dbh;
588 # Retrieve a list of attachments for this bug as well as a summary of the bug
589 # to use in a navigation bar across the top of the screen.
591 Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
592 # We only want attachment IDs.
593 @$bugattachments = map { $_->id } @$bugattachments;
595 my ($bugsummary, $product_id, $component_id) =
596 $dbh->selectrow_array('SELECT short_desc, product_id, component_id
598 WHERE bug_id = ?', undef, $attachment->bug_id);
600 # Get a list of flag types that can be set for this attachment.
601 my $flag_types = Bugzilla::FlagType::match({ 'target_type' => 'attachment' ,
602 'product_id' => $product_id ,
603 'component_id' => $component_id });
604 foreach my $flag_type (@$flag_types) {
605 $flag_type->{'flags'} = Bugzilla::Flag->match({ 'type_id' => $flag_type->id,
606 'attach_id' => $attachment->id });
608 $vars->{'flag_types'} = $flag_types;
609 $vars->{'any_flags_requesteeble'} = grep($_->is_requesteeble, @$flag_types);
610 $vars->{'attachment'} = $attachment;
611 $vars->{'bugsummary'} = $bugsummary;
612 $vars->{'attachments'} = $bugattachments;
615 if ($attachment->ispatch) {
616 my $quotedpatch = $attachment->data;
617 $quotedpatch =~ s/^/> /mg;
618 $vars->{'quotedpatch'} = $quotedpatch;
620 #endif // WEBKIT_CHANGES
622 print $cgi->header();
624 # Generate and return the UI (HTML page) from the appropriate template.
625 $template->process("attachment/$template_name.html.tmpl", $vars) # WEBKIT_CHANGES
626 || ThrowTemplateError($template->error());
629 # Updates an attachment record. Users with "editbugs" privileges, (or the
630 # original attachment's submitter) can edit the attachment's description,
631 # content type, ispatch and isobsolete flags, and statuses, and they can
632 # also submit a comment that appears in the bug.
633 # Users cannot edit the content of the attachment itself.
635 my $user = Bugzilla->user;
636 my $dbh = Bugzilla->dbh;
638 # Retrieve and validate parameters
639 my $attachment = validateID();
640 my $bug = new Bugzilla::Bug($attachment->bug_id);
641 $attachment->validate_can_edit($bug->product_id);
642 validateCanChangeBug($bug->id);
643 Bugzilla::Attachment->validate_description(THROW_ERROR);
644 Bugzilla::Attachment->validate_is_patch(THROW_ERROR);
645 Bugzilla::Attachment->validate_content_type(THROW_ERROR) unless $cgi->param('ispatch');
646 $cgi->param('isobsolete', $cgi->param('isobsolete') ? 1 : 0);
647 $cgi->param('isprivate', $cgi->param('isprivate') ? 1 : 0);
649 # Now make sure the attachment has not been edited since we loaded the page.
650 if (defined $cgi->param('delta_ts')
651 && $cgi->param('delta_ts') ne $attachment->modification_time)
653 ($vars->{'operations'}) =
654 Bugzilla::Bug::GetBugActivity($bug->id, $attachment->id, $cgi->param('delta_ts'));
656 # The token contains the old modification_time. We need a new one.
657 $cgi->param('token', issue_hash_token([$attachment->id, $attachment->modification_time]));
659 # If the modification date changed but there is no entry in
660 # the activity table, this means someone commented only.
661 # In this case, there is no reason to midair.
662 if (scalar(@{$vars->{'operations'}})) {
663 $cgi->param('delta_ts', $attachment->modification_time);
664 $vars->{'attachment'} = $attachment;
666 print $cgi->header();
667 # Warn the user about the mid-air collision and ask them what to do.
668 $template->process("attachment/midair.html.tmpl", $vars)
669 || ThrowTemplateError($template->error());
674 # We couldn't do this check earlier as we first had to validate attachment ID
675 # and display the mid-air collision page if modification_time changed.
676 my $token = $cgi->param('token');
677 check_hash_token($token, [$attachment->id, $attachment->modification_time]);
679 # If the submitter of the attachment is not in the insidergroup,
680 # be sure that he cannot overwrite the private bit.
681 # This check must be done before calling Bugzilla::Flag*::validate(),
682 # because they will look at the private bit when checking permissions.
683 # XXX - This is a ugly hack. Ideally, we shouldn't have to look at the
684 # old private bit twice (first here, and then below again), but this is
685 # the less risky change.
686 unless ($user->is_insider) {
687 $cgi->param('isprivate', $attachment->isprivate);
690 # If the user submitted a comment while editing the attachment,
691 # add the comment to the bug. Do this after having validated isprivate!
692 if ($cgi->param('comment')) {
693 # Prepend a string to the comment to let users know that the comment came
694 # from the "edit attachment" screen.
695 my $comment = "(From update of attachment " . $attachment->id . ")\n" .
696 $cgi->param('comment');
698 $bug->add_comment($comment, { isprivate => $cgi->param('isprivate') });
701 # The order of these function calls is important, as Flag::validate
702 # assumes User::match_field has ensured that the values in the
703 # requestee fields are legitimate user email addresses.
704 Bugzilla::User::match_field($cgi, {
705 '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }
707 Bugzilla::Flag::validate($bug->id, $attachment->id);
709 # Start a transaction in preparation for updating the attachment.
710 $dbh->bz_start_transaction();
712 # Quote the description and content type for use in the SQL UPDATE statement.
713 my $description = $cgi->param('description');
714 my $contenttype = $cgi->param('contenttype');
715 my $filename = $cgi->param('filename');
716 # we can detaint this way thanks to placeholders
717 trick_taint($description);
718 trick_taint($contenttype);
719 trick_taint($filename);
721 # Figure out when the changes were made.
722 my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
724 # Update flags. We have to do this before committing changes
725 # to attachments so that we can delete pending requests if the user
726 # is obsoleting this attachment without deleting any requests
727 # the user submits at the same time.
728 Bugzilla::Flag->process($bug, $attachment, $timestamp, $vars);
730 # Update the attachment record in the database.
731 $dbh->do("UPDATE attachments
738 modification_time = ?
739 WHERE attach_id = ?",
740 undef, ($description, $contenttype, $filename,
741 $cgi->param('ispatch'), $cgi->param('isobsolete'),
742 $cgi->param('isprivate'), $timestamp, $attachment->id));
744 my $updated_attachment = Bugzilla::Attachment->get($attachment->id);
745 # Record changes in the activity table.
746 my $sth = $dbh->prepare('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
747 fieldid, removed, added)
748 VALUES (?, ?, ?, ?, ?, ?, ?)');
750 if ($attachment->description ne $updated_attachment->description) {
751 my $fieldid = get_field_id('attachments.description');
752 $sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
753 $attachment->description, $updated_attachment->description);
755 if ($attachment->contenttype ne $updated_attachment->contenttype) {
756 my $fieldid = get_field_id('attachments.mimetype');
757 $sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
758 $attachment->contenttype, $updated_attachment->contenttype);
760 if ($attachment->filename ne $updated_attachment->filename) {
761 my $fieldid = get_field_id('attachments.filename');
762 $sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
763 $attachment->filename, $updated_attachment->filename);
765 if ($attachment->ispatch != $updated_attachment->ispatch) {
766 my $fieldid = get_field_id('attachments.ispatch');
767 $sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
768 $attachment->ispatch, $updated_attachment->ispatch);
770 if ($attachment->isobsolete != $updated_attachment->isobsolete) {
771 my $fieldid = get_field_id('attachments.isobsolete');
772 $sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
773 $attachment->isobsolete, $updated_attachment->isobsolete);
775 if ($attachment->isprivate != $updated_attachment->isprivate) {
776 my $fieldid = get_field_id('attachments.isprivate');
777 $sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
778 $attachment->isprivate, $updated_attachment->isprivate);
781 # Commit the transaction now that we are finished updating the database.
782 $dbh->bz_commit_transaction();
784 # Commit the comment, if any.
787 # Define the variables and functions that will be passed to the UI template.
788 $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login };
789 $vars->{'attachment'} = $attachment;
790 # We cannot reuse the $bug object as delta_ts has eventually been updated
791 # since the object was created.
792 $vars->{'bugs'} = [new Bugzilla::Bug($bug->id)];
793 $vars->{'header_done'} = 1;
794 $vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
796 print $cgi->header();
798 # Generate and return the UI (HTML page) from the appropriate template.
799 $template->process("attachment/updated.html.tmpl", $vars)
800 || ThrowTemplateError($template->error());
803 # Only administrators can delete attachments.
804 sub delete_attachment {
805 my $user = Bugzilla->login(LOGIN_REQUIRED);
806 my $dbh = Bugzilla->dbh;
808 print $cgi->header();
810 $user->in_group('admin')
811 || ThrowUserError('auth_failure', {group => 'admin',
813 object => 'attachment'});
815 Bugzilla->params->{'allow_attachment_deletion'}
816 || ThrowUserError('attachment_deletion_disabled');
818 # Make sure the administrator is allowed to edit this attachment.
819 my $attachment = validateID();
820 validateCanChangeBug($attachment->bug_id);
822 $attachment->datasize || ThrowUserError('attachment_removed');
824 # We don't want to let a malicious URL accidentally delete an attachment.
825 my $token = trim($cgi->param('token'));
827 my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token);
829 && ($creator_id == $user->id)
830 && ($event eq 'attachment' . $attachment->id))
832 # The token is invalid.
833 ThrowUserError('token_does_not_exist');
836 my $bug = new Bugzilla::Bug($attachment->bug_id);
838 # The token is valid. Delete the content of the attachment.
840 $vars->{'attachment'} = $attachment;
841 $vars->{'date'} = $date;
842 $vars->{'reason'} = clean_text($cgi->param('reason') || '');
843 $vars->{'mailrecipients'} = { 'changer' => $user->login };
845 $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
846 || ThrowTemplateError($template->error());
848 # Paste the reason provided by the admin into a comment.
849 $bug->add_comment($msg);
851 # If the attachment is stored locally, remove it.
852 if (-e $attachment->_get_local_filename) {
853 unlink $attachment->_get_local_filename;
855 $attachment->remove_from_db();
857 # Now delete the token.
858 delete_token($token);
860 # Insert the comment.
863 # Required to display the bug the deleted attachment belongs to.
864 $vars->{'bugs'} = [$bug];
865 $vars->{'header_done'} = 1;
866 $vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
868 $template->process("attachment/updated.html.tmpl", $vars)
869 || ThrowTemplateError($template->error());
873 $token = issue_session_token('attachment' . $attachment->id);
875 $vars->{'a'} = $attachment;
876 $vars->{'token'} = $token;
878 $template->process("attachment/confirm-delete.html.tmpl", $vars)
879 || ThrowTemplateError($template->error());