Upgrade bugs.webkit.org to Bugzilla 4.2
[WebKit-https.git] / Websites / bugs.webkit.org / attachment.cgi
1 #!/usr/bin/env perl -wT
2 # -*- Mode: perl; indent-tabs-mode: nil -*-
3 #
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/
8 #
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.
13 #
14 # The Original Code is the Bugzilla Bug Tracking System.
15 #
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
19 # Rights Reserved.
20 #
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>
31
32 ################################################################################
33 # Script Initialization
34 ################################################################################
35
36 # Make it harder for us to do dangerous things in Perl.
37 use strict;
38
39 use lib qw(. lib);
40
41 use Bugzilla;
42 use Bugzilla::BugMail;
43 use Bugzilla::Constants;
44 use Bugzilla::Error;
45 use Bugzilla::Flag; 
46 use Bugzilla::FlagType; 
47 use Bugzilla::User;
48 use Bugzilla::Util;
49 use Bugzilla::Bug;
50 use Bugzilla::Field;
51 use Bugzilla::Attachment;
52 use Bugzilla::Attachment::PatchReader;
53 use Bugzilla::Token;
54 use Bugzilla::Keyword;
55
56 use Encode qw(encode find_encoding);
57 #if WEBKIT_CHANGES
58 use Apache2::SubProcess ();
59 use Apache2::RequestUtil ();
60 #endif // WEBKIT_CHANGES
61
62 # For most scripts we don't make $cgi and $template global variables. But
63 # when preparing Bugzilla for mod_perl, this script used these
64 # variables in so many subroutines that it was easier to just
65 # make them globals.
66 local our $cgi = Bugzilla->cgi;
67 local our $template = Bugzilla->template;
68 local our $vars = {};
69
70 ################################################################################
71 # Main Body Execution
72 ################################################################################
73
74 # All calls to this script should contain an "action" variable whose
75 # value determines what the user wants to do.  The code below checks
76 # the value of that variable and runs the appropriate code. If none is
77 # supplied, we default to 'view'.
78
79 # Determine whether to use the action specified by the user or the default.
80 my $action = $cgi->param('action') || 'view';
81 my $format = $cgi->param('format') || '';
82
83 # You must use the appropriate urlbase/sslbase param when doing anything
84 # but viewing an attachment, or a raw diff.
85 if ($action ne 'view'
86     && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw'))
87 {
88     do_ssl_redirect_if_required();
89     if ($cgi->url_is_attachment_base) {
90         $cgi->redirect_to_urlbase;
91     }
92     Bugzilla->login();
93 }
94
95 # When viewing an attachment, do not request credentials if we are on
96 # the alternate host. Let view() decide when to call Bugzilla->login.
97 if ($action eq "view")
98 {
99     view();
100 }
101 elsif ($action eq "interdiff")
102 {
103     interdiff();
104 }
105 elsif ($action eq "diff")
106 {
107     diff();
108 }
109 elsif ($action eq "viewall") 
110
111     viewall(); 
112 }
113 elsif ($action eq "enter") 
114
115     Bugzilla->login(LOGIN_REQUIRED);
116     enter(); 
117 }
118 elsif ($action eq "insert")
119 {
120     Bugzilla->login(LOGIN_REQUIRED);
121     insert();
122 }
123 elsif ($action eq "edit") 
124
125     edit(); 
126 }
127 #if WEBKIT_CHANGES
128 elsif ($action eq "review")
129 {
130     prettyPatch();
131 }
132 elsif ($action eq "reviewform")
133 {
134     edit("reviewform");
135 }
136 elsif ($action eq "rietveldreview")
137 {
138     edit("rietveldreview");
139 }
140 #endif // WEBKIT_CHANGES
141 elsif ($action eq "update") 
142
143     Bugzilla->login(LOGIN_REQUIRED);
144     update();
145 }
146 #if WEBKIT_CHANGES
147 elsif ($action eq "prettypatch")
148 {
149     prettyPatch();
150 }
151 #endif // WEBKIT_CHANGES
152 elsif ($action eq "delete") {
153     delete_attachment();
154 }
155 else 
156
157   ThrowUserError('unknown_action', {action => $action});
158 }
159
160 exit;
161
162 ################################################################################
163 # Data Validation / Security Authorization
164 ################################################################################
165
166 # Validates an attachment ID. Optionally takes a parameter of a form
167 # variable name that contains the ID to be validated. If not specified,
168 # uses 'id'.
169 # If the second parameter is true, the attachment ID will be validated,
170 # however the current user's access to the attachment will not be checked.
171 # Will throw an error if 1) attachment ID is not a valid number,
172 # 2) attachment does not exist, or 3) user isn't allowed to access the
173 # attachment.
174 #
175 # Returns an attachment object.
176
177 sub validateID {
178     my($param, $dont_validate_access) = @_;
179     $param ||= 'id';
180
181     # If we're not doing interdiffs, check if id wasn't specified and
182     # prompt them with a page that allows them to choose an attachment.
183     # Happens when calling plain attachment.cgi from the urlbar directly
184     if ($param eq 'id' && !$cgi->param('id')) {
185         print $cgi->header();
186         $template->process("attachment/choose.html.tmpl", $vars) ||
187             ThrowTemplateError($template->error());
188         exit;
189     }
190     
191     my $attach_id = $cgi->param($param);
192
193     # Validate the specified attachment id. detaint kills $attach_id if
194     # non-natural, so use the original value from $cgi in our exception
195     # message here.
196     detaint_natural($attach_id)
197         || ThrowUserError("invalid_attach_id",
198                           { attach_id => scalar $cgi->param($param) });
199   
200     # Make sure the attachment exists in the database.
201     my $attachment = new Bugzilla::Attachment($attach_id)
202         || ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
203
204     return $attachment if ($dont_validate_access || check_can_access($attachment));
205 }
206
207 # Make sure the current user has access to the specified attachment.
208 sub check_can_access {
209     my $attachment = shift;
210     my $user = Bugzilla->user;
211
212     # Make sure the user is authorized to access this attachment's bug.
213     Bugzilla::Bug->check($attachment->bug_id);
214     if ($attachment->isprivate && $user->id != $attachment->attacher->id 
215         && !$user->is_insider) 
216     {
217         ThrowUserError('auth_failure', {action => 'access',
218                                         object => 'attachment',
219                                         attach_id => $attachment->id});
220     }
221     return 1;
222 }
223
224 # Determines if the attachment is public -- that is, if users who are
225 # not logged in have access to the attachment
226 sub attachmentIsPublic {
227     my $attachment = shift;
228
229     return 0 if Bugzilla->params->{'requirelogin'};
230     return 0 if $attachment->isprivate;
231
232     my $anon_user = new Bugzilla::User;
233     return $anon_user->can_see_bug($attachment->bug_id);
234 }
235
236 # Validates format of a diff/interdiff. Takes a list as an parameter, which
237 # defines the valid format values. Will throw an error if the format is not
238 # in the list. Returns either the user selected or default format.
239 sub validateFormat {
240   # receives a list of legal formats; first item is a default
241   my $format = $cgi->param('format') || $_[0];
242   if (not grep($_ eq $format, @_)) {
243      ThrowUserError("invalid_format", { format  => $format, formats => \@_ });
244   }
245
246   return $format;
247 }
248
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.
251 sub validateContext
252 {
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') });
257   }
258
259   return $context;
260 }
261
262 # Gets the attachment object(s) generated by validateID, while ensuring
263 # attachbase and token authentication is used when required.
264 sub get_attachment {
265     my @field_names = @_ ? @_ : qw(id);
266
267     my %attachments;
268
269     if (use_attachbase()) {
270         # Load each attachment, and ensure they are all from the same bug
271         my $bug_id = 0;
272         foreach my $field_name (@field_names) {
273             my $attachment = validateID($field_name, 1);
274             if (!$bug_id) {
275                 $bug_id = $attachment->bug_id;
276             } elsif ($attachment->bug_id != $bug_id) {
277                 ThrowUserError('attachment_bug_id_mismatch');
278             }
279             $attachments{$field_name} = $attachment;
280         }
281         my @args = map { $_ . '=' . $attachments{$_}->id } @field_names;
282         my $cgi_params = $cgi->canonicalise_query(@field_names, 't',
283             'Bugzilla_login', 'Bugzilla_password');
284         push(@args, $cgi_params) if $cgi_params;
285         my $path = 'attachment.cgi?' . join('&', @args);
286
287         # Make sure the attachment is served from the correct server.
288         if ($cgi->url_is_attachment_base($bug_id)) {
289             # No need to validate the token for public attachments. We cannot request
290             # credentials as we are on the alternate host.
291             if (!all_attachments_are_public(\%attachments)) {
292                 my $token = $cgi->param('t');
293                 my ($userid, undef, $token_data) = Bugzilla::Token::GetTokenData($token);
294                 my %token_data = unpack_token_data($token_data);
295                 my $valid_token = 1;
296                 foreach my $field_name (@field_names) {
297                     my $token_id = $token_data{$field_name};
298                     if (!$token_id
299                         || !detaint_natural($token_id)
300                         || $attachments{$field_name}->id != $token_id)
301                     {
302                         $valid_token = 0;
303                         last;
304                     }
305                 }
306                 unless ($userid && $valid_token) {
307                     # Not a valid token.
308                     print $cgi->redirect('-location' => correct_urlbase() . $path);
309                     exit;
310                 }
311                 # Change current user without creating cookies.
312                 Bugzilla->set_user(new Bugzilla::User($userid));
313                 # Tokens are single use only, delete it.
314                 delete_token($token);
315             }
316         }
317         elsif ($cgi->url_is_attachment_base) {
318             # If we come here, this means that each bug has its own host
319             # for attachments, and that we are trying to view one attachment
320             # using another bug's host. That's not desired.
321             $cgi->redirect_to_urlbase;
322         }
323         else {
324             # We couldn't call Bugzilla->login earlier as we first had to
325             # make sure we were not going to request credentials on the
326             # alternate host.
327             Bugzilla->login();
328             my $attachbase = Bugzilla->params->{'attachment_base'};
329             # Replace %bugid% by the ID of the bug the attachment 
330             # belongs to, if present.
331             $attachbase =~ s/\%bugid\%/$bug_id/;
332             if (all_attachments_are_public(\%attachments)) {
333                 # No need for a token; redirect to attachment base.
334                 print $cgi->redirect(-location => $attachbase . $path);
335                 exit;
336             } else {
337                 # Make sure the user can view the attachment.
338                 foreach my $field_name (@field_names) {
339                     check_can_access($attachments{$field_name});
340                 }
341                 # Create a token and redirect.
342                 my $token = url_quote(issue_session_token(pack_token_data(\%attachments)));
343                 print $cgi->redirect(-location => $attachbase . "$path&t=$token");
344                 exit;
345             }
346         }
347     } else {
348         do_ssl_redirect_if_required();
349         # No alternate host is used. Request credentials if required.
350         Bugzilla->login();
351         foreach my $field_name (@field_names) {
352             $attachments{$field_name} = validateID($field_name);
353         }
354     }
355
356     return wantarray
357         ? map { $attachments{$_} } @field_names
358         : $attachments{$field_names[0]};
359 }
360
361 sub all_attachments_are_public {
362     my $attachments = shift;
363     foreach my $field_name (keys %$attachments) {
364         if (!attachmentIsPublic($attachments->{$field_name})) {
365             return 0;
366         }
367     }
368     return 1;
369 }
370
371 sub pack_token_data {
372     my $attachments = shift;
373     return join(' ', map { $_ . '=' . $attachments->{$_}->id } keys %$attachments);
374 }
375
376 sub unpack_token_data {
377     my @token_data = split(/ /, shift || '');
378     my %data;
379     foreach my $token (@token_data) {
380         my ($field_name, $attach_id) = split('=', $token);
381         $data{$field_name} = $attach_id;
382     }
383     return %data;
384 }
385
386 ################################################################################
387 # Functions
388 ################################################################################
389
390 # Display an attachment.
391 sub view {
392     my $attachment = get_attachment();
393
394     # At this point, Bugzilla->login has been called if it had to.
395     my $contenttype = $attachment->contenttype;
396     my $filename = $attachment->filename;
397
398     # Bug 111522: allow overriding content-type manually in the posted form
399     # params.
400     if (defined $cgi->param('content_type')) {
401         $contenttype = $attachment->_check_content_type($cgi->param('content_type'));
402     }
403
404     # Return the appropriate HTTP response headers.
405     $attachment->datasize || ThrowUserError("attachment_removed");
406
407     $filename =~ s/^.*[\/\\]//;
408     # escape quotes and backslashes in the filename, per RFCs 2045/822
409     $filename =~ s/\\/\\\\/g; # escape backslashes
410     $filename =~ s/"/\\"/g; # escape quotes
411
412     # Avoid line wrapping done by Encode, which we don't need for HTTP
413     # headers. See discussion in bug 328628 for details.
414     local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 10000;
415     $filename = encode('MIME-Q', $filename);
416
417     my $disposition = Bugzilla->params->{'allow_attachment_display'} ? 'inline' : 'attachment';
418
419     # Don't send a charset header with attachments--they might not be UTF-8.
420     # However, we do allow people to explicitly specify a charset if they
421     # want.
422     if ($contenttype !~ /\bcharset=/i) {
423         # In order to prevent Apache from adding a charset, we have to send a
424         # charset that's a single space.
425         $cgi->charset(' ');
426         if (Bugzilla->feature('detect_charset') && $contenttype =~ /^text\//) {
427             my $encoding = detect_encoding($attachment->data);
428             if ($encoding) {
429                 $cgi->charset(find_encoding($encoding)->mime_name);
430             }
431         }
432     }
433     print $cgi->header(-type=>"$contenttype; name=\"$filename\"",
434                        -content_disposition=> "$disposition; filename=\"$filename\"",
435                        -content_length => $attachment->datasize,
436                        -x_content_type_options => "nosniff");
437     disable_utf8();
438     print $attachment->data;
439 }
440
441 sub interdiff {
442     # Retrieve and validate parameters
443     my $format = validateFormat('html', 'raw');
444     my($old_attachment, $new_attachment);
445     if ($format eq 'raw') {
446         ($old_attachment, $new_attachment) = get_attachment('oldid', 'newid');
447     } else {
448         $old_attachment = validateID('oldid');
449         $new_attachment = validateID('newid');
450     }
451     my $context = validateContext();
452
453     Bugzilla::Attachment::PatchReader::process_interdiff(
454         $old_attachment, $new_attachment, $format, $context);
455 }
456
457 #if WEBKIT_CHANGES
458 sub prettyPatch
459 {
460     # Retrieve and validate parameters
461     my $attachment = validateID();
462     my $format = validateFormat('html', 'raw');
463     my $context = validateContext();
464
465     # If it is not a patch, view normally.
466     if (!$attachment->ispatch) {
467       view();
468       return;
469     }
470
471     use vars qw($cgi);
472     print $cgi->header(-type => 'text/html',
473                        -expires => '+3M');
474
475     my $orig_path = $ENV{'PATH'};
476     $ENV{'PATH'} = "/usr/bin:" . $ENV{'PATH'};
477     my @prettyargs = ("-I", "/var/www/html/PrettyPatch", "/var/www/html/PrettyPatch/prettify.rb", "--html-exceptions");
478     my $r = Apache2::RequestUtil->request;
479     my ($in, $out, $err) = $r->spawn_proc_prog("/usr/bin/ruby", \@prettyargs);
480     $ENV{'PATH'} = $orig_path;
481     print $in $attachment->data;
482     close($in);
483     while (<$out>) {
484         print;
485     }
486     close($out);
487     close($err);
488 }
489 #endif // WEBKIT_CHANGES
490
491 sub diff {
492     # Retrieve and validate parameters
493     my $format = validateFormat('html', 'raw');
494     my $attachment = $format eq 'raw' ? get_attachment() : validateID();
495     my $context = validateContext();
496
497     # If it is not a patch, view normally.
498     if (!$attachment->ispatch) {
499         view();
500         return;
501     }
502
503     Bugzilla::Attachment::PatchReader::process_diff($attachment, $format, $context);
504 }
505
506 # Display all attachments for a given bug in a series of IFRAMEs within one
507 # HTML page.
508 sub viewall {
509     # Retrieve and validate parameters
510     my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
511     my $bugid = $bug->id;
512
513     my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bugid);
514     # Ignore deleted attachments.
515     @$attachments = grep { $_->datasize } @$attachments;
516
517     if ($cgi->param('hide_obsolete')) {
518         @$attachments = grep { !$_->isobsolete } @$attachments;
519         $vars->{'hide_obsolete'} = 1;
520     }
521
522     # Define the variables and functions that will be passed to the UI template.
523     $vars->{'bug'} = $bug;
524     $vars->{'attachments'} = $attachments;
525
526     print $cgi->header();
527
528     # Generate and return the UI (HTML page) from the appropriate template.
529     $template->process("attachment/show-multiple.html.tmpl", $vars)
530       || ThrowTemplateError($template->error());
531 }
532
533 # Display a form for entering a new attachment.
534 sub enter {
535   # Retrieve and validate parameters
536   my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
537   my $bugid = $bug->id;
538   Bugzilla::Attachment->_check_bug($bug);
539   my $dbh = Bugzilla->dbh;
540   my $user = Bugzilla->user;
541
542   # Retrieve the attachments the user can edit from the database and write
543   # them into an array of hashes where each hash represents one attachment.
544   my $canEdit = "";
545   if (!$user->in_group('editbugs', $bug->product_id)) {
546       $canEdit = "AND submitter_id = " . $user->id;
547   }
548   my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
549                                              WHERE bug_id = ? AND isobsolete = 0 $canEdit
550                                              ORDER BY attach_id", undef, $bugid);
551
552   # Define the variables and functions that will be passed to the UI template.
553   $vars->{'bug'} = $bug;
554   $vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids);
555
556   my $flag_types = Bugzilla::FlagType::match({'target_type'  => 'attachment',
557                                               'product_id'   => $bug->product_id,
558                                               'component_id' => $bug->component_id});
559   $vars->{'flag_types'} = $flag_types;
560   $vars->{'any_flags_requesteeble'} =
561     grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
562   $vars->{'token'} = issue_session_token('create_attachment');
563
564   print $cgi->header();
565
566   # Generate and return the UI (HTML page) from the appropriate template.
567   $template->process("attachment/create.html.tmpl", $vars)
568     || ThrowTemplateError($template->error());
569 }
570
571 # Insert a new attachment into the database.
572 sub insert {
573     my $dbh = Bugzilla->dbh;
574     my $user = Bugzilla->user;
575
576     $dbh->bz_start_transaction;
577
578     # Retrieve and validate parameters
579     my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
580     my $bugid = $bug->id;
581     my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
582
583     # Detect if the user already used the same form to submit an attachment
584     my $token = trim($cgi->param('token'));
585     check_token_data($token, 'create_attachment', 'index.cgi');
586
587     # Check attachments the user tries to mark as obsolete.
588     my @obsolete_attachments;
589     if ($cgi->param('obsolete')) {
590         my @obsolete = $cgi->param('obsolete');
591         @obsolete_attachments = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete);
592     }
593
594     # Must be called before create() as it may alter $cgi->param('ispatch').
595     my $content_type = Bugzilla::Attachment::get_content_type();
596
597     # Get the filehandle of the attachment.
598     my $data_fh = $cgi->upload('data');
599
600     my $attachment = Bugzilla::Attachment->create(
601         {bug           => $bug,
602          creation_ts   => $timestamp,
603          data          => scalar $cgi->param('attach_text') || $data_fh,
604          description   => scalar $cgi->param('description'),
605          filename      => $cgi->param('attach_text') ? "file_$bugid.txt" : scalar $cgi->upload('data'),
606          ispatch       => scalar $cgi->param('ispatch'),
607          isprivate     => scalar $cgi->param('isprivate'),
608          mimetype      => $content_type,
609          });
610
611     # Delete the token used to create this attachment.
612     delete_token($token);
613
614     foreach my $obsolete_attachment (@obsolete_attachments) {
615         $obsolete_attachment->set_is_obsolete(1);
616         $obsolete_attachment->update($timestamp);
617     }
618
619     my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
620                                   $bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR);
621     $attachment->set_flags($flags, $new_flags);
622     $attachment->update($timestamp);
623
624     # Insert a comment about the new attachment into the database.
625     my $comment = $cgi->param('comment');
626     $comment = '' unless defined $comment;
627     $bug->add_comment($comment, { isprivate => $attachment->isprivate,
628                                   type => CMT_ATTACHMENT_CREATED,
629                                   extra_data => $attachment->id });
630
631   # Assign the bug to the user, if they are allowed to take it
632   my $owner = "";
633   if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) {
634       # When taking a bug, we have to follow the workflow.
635       my $bug_status = $cgi->param('bug_status') || '';
636       ($bug_status) = grep {$_->name eq $bug_status} @{$bug->status->can_change_to};
637
638       if ($bug_status && $bug_status->is_open
639           && ($bug_status->name ne 'UNCONFIRMED' 
640               || $bug->product_obj->allows_unconfirmed))
641       {
642           $bug->set_bug_status($bug_status->name);
643           $bug->clear_resolution();
644       }
645       # Make sure the person we are taking the bug from gets mail.
646       $owner = $bug->assigned_to->login;
647       $bug->set_assigned_to($user);
648   }
649   $bug->update($timestamp);
650
651   $dbh->bz_commit_transaction;
652
653   # Define the variables and functions that will be passed to the UI template.
654   $vars->{'attachment'} = $attachment;
655   # We cannot reuse the $bug object as delta_ts has eventually been updated
656   # since the object was created.
657   $vars->{'bugs'} = [new Bugzilla::Bug($bugid)];
658   $vars->{'header_done'} = 1;
659   $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
660
661   my $recipients =  { 'changer' => $user, 'owner' => $owner };
662   $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients);
663
664   print $cgi->header();
665   # Generate and return the UI (HTML page) from the appropriate template.
666   $template->process("attachment/created.html.tmpl", $vars)
667     || ThrowTemplateError($template->error());
668 }
669
670 # Displays a form for editing attachment properties.
671 # Any user is allowed to access this page, unless the attachment
672 # is private and the user does not belong to the insider group.
673 # Validations are done later when the user submits changes.
674 sub edit {
675 #if WEBKIT_CHANGES
676   my ($template_name) = @_;
677   $template_name = $template_name || "edit";
678 #endif // WEBKIT_CHANGES
679
680   my $attachment = validateID();
681
682   my $bugattachments =
683       Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
684   # We only want attachment IDs.
685   @$bugattachments = map { $_->id } @$bugattachments;
686
687   my $any_flags_requesteeble =
688     grep { $_->is_requestable && $_->is_requesteeble } @{$attachment->flag_types};
689   # Useful in case a flagtype is no longer requestable but a requestee
690   # has been set before we turned off that bit.
691   $any_flags_requesteeble ||= grep { $_->requestee_id } @{$attachment->flags};
692   $vars->{'any_flags_requesteeble'} = $any_flags_requesteeble;
693   $vars->{'attachment'} = $attachment;
694   $vars->{'attachments'} = $bugattachments;
695
696 #if WEBKIT_CHANGES
697   if ($attachment->ispatch) {
698       my $quotedpatch = $attachment->data;
699       $quotedpatch =~ s/^/> /mg;
700       $vars->{'quotedpatch'} = $quotedpatch;
701   }
702 #endif // WEBKIT_CHANGES
703
704   print $cgi->header();
705
706   # Generate and return the UI (HTML page) from the appropriate template.
707   $template->process("attachment/$template_name.html.tmpl", $vars) # WEBKIT_CHANGES
708     || ThrowTemplateError($template->error());
709 }
710
711 # Updates an attachment record. Only users with "editbugs" privileges,
712 # (or the original attachment's submitter) can edit the attachment.
713 # Users cannot edit the content of the attachment itself.
714 sub update {
715     my $user = Bugzilla->user;
716     my $dbh = Bugzilla->dbh;
717
718     # Start a transaction in preparation for updating the attachment.
719     $dbh->bz_start_transaction();
720
721     # Retrieve and validate parameters
722     my $attachment = validateID();
723     my $bug = $attachment->bug;
724     $attachment->_check_bug;
725     my $can_edit = $attachment->validate_can_edit($bug->product_id);
726
727     if ($can_edit) {
728         $attachment->set_description(scalar $cgi->param('description'));
729         $attachment->set_is_patch(scalar $cgi->param('ispatch'));
730         $attachment->set_content_type(scalar $cgi->param('contenttypeentry'));
731         $attachment->set_is_obsolete(scalar $cgi->param('isobsolete'));
732         $attachment->set_is_private(scalar $cgi->param('isprivate'));
733         $attachment->set_filename(scalar $cgi->param('filename'));
734
735         # Now make sure the attachment has not been edited since we loaded the page.
736         if (defined $cgi->param('delta_ts')
737             && $cgi->param('delta_ts') ne $attachment->modification_time)
738         {
739             ($vars->{'operations'}) =
740                 Bugzilla::Bug::GetBugActivity($bug->id, $attachment->id, $cgi->param('delta_ts'));
741
742             # The token contains the old modification_time. We need a new one.
743             $cgi->param('token', issue_hash_token([$attachment->id, $attachment->modification_time]));
744
745             # If the modification date changed but there is no entry in
746             # the activity table, this means someone commented only.
747             # In this case, there is no reason to midair.
748             if (scalar(@{$vars->{'operations'}})) {
749                 $cgi->param('delta_ts', $attachment->modification_time);
750                 $vars->{'attachment'} = $attachment;
751
752                 print $cgi->header();
753                 # Warn the user about the mid-air collision and ask them what to do.
754                 $template->process("attachment/midair.html.tmpl", $vars)
755                   || ThrowTemplateError($template->error());
756                 exit;
757             }
758         }
759     }
760
761     # We couldn't do this check earlier as we first had to validate attachment ID
762     # and display the mid-air collision page if modification_time changed.
763     my $token = $cgi->param('token');
764     check_hash_token($token, [$attachment->id, $attachment->modification_time]);
765
766     # If the user submitted a comment while editing the attachment,
767     # add the comment to the bug. Do this after having validated isprivate!
768     my $comment = $cgi->param('comment');
769     if (defined $comment && trim($comment) ne '') {
770         $bug->add_comment($comment, { isprivate => $attachment->isprivate,
771                                       type => CMT_ATTACHMENT_UPDATED,
772                                       extra_data => $attachment->id });
773     }
774
775     if ($can_edit) {
776         my ($flags, $new_flags) =
777           Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
778         $attachment->set_flags($flags, $new_flags);
779     }
780
781     # Figure out when the changes were made.
782     my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
783
784     if ($can_edit) {
785         my $changes = $attachment->update($timestamp);
786         # If there are changes, we updated delta_ts in the DB. We have to
787         # reflect this change in the bug object.
788         $bug->{delta_ts} = $timestamp if scalar(keys %$changes);
789     }
790
791     # Commit the comment, if any.
792     $bug->update($timestamp);
793
794     # Commit the transaction now that we are finished updating the database.
795     $dbh->bz_commit_transaction();
796
797     # Define the variables and functions that will be passed to the UI template.
798     $vars->{'attachment'} = $attachment;
799     $vars->{'bugs'} = [$bug];
800     $vars->{'header_done'} = 1;
801     $vars->{'sent_bugmail'} = 
802         Bugzilla::BugMail::Send($bug->id, { 'changer' => $user });
803
804     print $cgi->header();
805
806     # Generate and return the UI (HTML page) from the appropriate template.
807     $template->process("attachment/updated.html.tmpl", $vars)
808       || ThrowTemplateError($template->error());
809 }
810
811 # Only administrators can delete attachments.
812 sub delete_attachment {
813     my $user = Bugzilla->login(LOGIN_REQUIRED);
814     my $dbh = Bugzilla->dbh;
815
816     print $cgi->header();
817
818     $user->in_group('admin')
819       || ThrowUserError('auth_failure', {group  => 'admin',
820                                          action => 'delete',
821                                          object => 'attachment'});
822
823     Bugzilla->params->{'allow_attachment_deletion'}
824       || ThrowUserError('attachment_deletion_disabled');
825
826     # Make sure the administrator is allowed to edit this attachment.
827     my $attachment = validateID();
828     Bugzilla::Attachment->_check_bug($attachment->bug);
829
830     $attachment->datasize || ThrowUserError('attachment_removed');
831
832     # We don't want to let a malicious URL accidentally delete an attachment.
833     my $token = trim($cgi->param('token'));
834     if ($token) {
835         my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token);
836         unless ($creator_id
837                   && ($creator_id == $user->id)
838                   && ($event eq 'delete_attachment' . $attachment->id))
839         {
840             # The token is invalid.
841             ThrowUserError('token_does_not_exist');
842         }
843
844         my $bug = new Bugzilla::Bug($attachment->bug_id);
845
846         # The token is valid. Delete the content of the attachment.
847         my $msg;
848         $vars->{'attachment'} = $attachment;
849         $vars->{'date'} = $date;
850         $vars->{'reason'} = clean_text($cgi->param('reason') || '');
851
852         $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
853           || ThrowTemplateError($template->error());
854
855         # Paste the reason provided by the admin into a comment.
856         $bug->add_comment($msg);
857
858         # If the attachment is stored locally, remove it.
859         if (-e $attachment->_get_local_filename) {
860             unlink $attachment->_get_local_filename;
861         }
862         $attachment->remove_from_db();
863
864         # Now delete the token.
865         delete_token($token);
866
867         # Insert the comment.
868         $bug->update();
869
870         # Required to display the bug the deleted attachment belongs to.
871         $vars->{'bugs'} = [$bug];
872         $vars->{'header_done'} = 1;
873
874         $vars->{'sent_bugmail'} =
875             Bugzilla::BugMail::Send($bug->id, { 'changer' => $user });
876
877         $template->process("attachment/updated.html.tmpl", $vars)
878           || ThrowTemplateError($template->error());
879     }
880     else {
881         # Create a token.
882         $token = issue_session_token('delete_attachment' . $attachment->id);
883
884         $vars->{'a'} = $attachment;
885         $vars->{'token'} = $token;
886
887         $template->process("attachment/confirm-delete.html.tmpl", $vars)
888           || ThrowTemplateError($template->error());
889     }
890 }