]> git.donarmstrong.com Git - debbugs.git/blob - cgi/bugreport.cgi
use Debbugs::Bug in pkgreport
[debbugs.git] / cgi / bugreport.cgi
1 #!/usr/bin/perl
2
3 use warnings;
4 use strict;
5
6 # Sanitize environent for taint
7 BEGIN{
8     delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
9 }
10
11
12 use POSIX qw(strftime);
13 use MIME::Parser;
14 use MIME::Decoder;
15 use IO::Scalar;
16 use IO::File;
17
18 # if we're running out of git, we want to use the git base directory as the
19 # first INC directory. If you're not running out of git, don't do that.
20 use File::Basename qw(dirname);
21 use Cwd qw(abs_path);
22 our $debbugs_dir;
23 BEGIN {
24     $debbugs_dir =
25         abs_path(dirname(abs_path(__FILE__)) . '/../');
26     # clear the taint; we'll assume that the absolute path to __FILE__ is the
27     # right path if there's a .git directory there
28     ($debbugs_dir) = $debbugs_dir =~ /([[:print:]]+)/;
29     if (defined $debbugs_dir and
30         -d $debbugs_dir . '/.git/') {
31     } else {
32         undef $debbugs_dir;
33     }
34     # if the first directory in @INC is not an absolute directory, assume that
35     # someone has overridden us via -I.
36     if ($INC[0] !~ /^\//) {
37     }
38 }
39 use if defined $debbugs_dir, lib => $debbugs_dir;
40
41 use Debbugs::Config qw(:globals :text :config);
42
43 # for read_log_records
44 use Debbugs::Log qw(:read);
45 use Debbugs::Log::Spam;
46 use Debbugs::CGI qw(:url :html :util :cache :usertags);
47 use Debbugs::CGI::Bugreport qw(:all);
48 use Debbugs::Common qw(buglog getmaintainers make_list bug_status package_maintainer);
49 use Debbugs::Packages qw(binary_to_source);
50 use Debbugs::DB;
51 use Debbugs::Status qw(splitpackages split_status_fields get_bug_status isstrongseverity);
52 use Debbugs::Bug;
53
54 use Scalar::Util qw(looks_like_number);
55
56 use Debbugs::Text qw(:templates);
57 use URI::Escape qw(uri_escape_utf8);
58 use List::AllUtils qw(max);
59
60 my $s;
61 if (defined $config{database}) {
62     $s = Debbugs::DB->connect($config{database}) or
63         die "Unable to connect to database";
64 }
65
66 use CGI::Simple;
67 my $q = new CGI::Simple;
68 # STDOUT should be using the utf8 io layer
69 binmode(STDOUT,':raw:encoding(UTF-8)');
70
71 my %param = cgi_parameters(query => $q,
72                            single => [qw(bug msg att boring terse),
73                                       qw(reverse mbox mime trim),
74                                       qw(mboxstat mboxmaint archive),
75                                       qw(repeatmerged avatars),
76                                      ],
77                            default => {# msg       => '',
78                                        boring    => 'no',
79                                        terse     => 'no',
80                                        reverse   => 'no',
81                                        mbox      => 'no',
82                                        mime      => 'no',
83                                        mboxstat  => 'no',
84                                        mboxmaint => 'no',
85                                        archive   => 'no',
86                                        repeatmerged => 'yes',
87                                        avatars   => 'yes',
88                                       },
89                           );
90 # This is craptacular.
91
92 my $ref = $param{bug} or quitcgi("No bug number", '400 Bad Request');
93 $ref =~ /(\d+)/ or quitcgi("Invalid bug number", '400 Bad Request');
94 $ref = $1;
95 my $short = "#$ref";
96 my ($msg) = $param{msg} =~ /^(\d+)$/ if exists $param{msg};
97 my ($att) = $param{att} =~ /^(\d+)$/ if exists $param{att};
98 my $boring = $param{'boring'} eq 'yes';
99 my $terse = $param{'terse'} eq 'yes';
100 my $reverse = $param{'reverse'} eq 'yes';
101 my $mbox = $param{'mbox'} eq 'yes';
102 my $mime = $param{'mime'} eq 'yes';
103 my $avatars = $param{avatars} eq 'yes';
104
105 my $trim_headers = ($param{trim} || ((defined $msg and $msg)?'no':'yes')) eq 'yes';
106
107 my $mbox_status_message = $param{mboxstat} eq 'yes';
108 my $mbox_maint = $param{mboxmaint} eq 'yes';
109 $mbox = 1 if $mbox_status_message or $mbox_maint;
110
111 # Not used by this script directly, but fetch these so that pkgurl() and
112 # friends can propagate them correctly.
113 my $archive = $param{'archive'} eq 'yes';
114 my $repeatmerged = $param{'repeatmerged'} eq 'yes';
115
116 my %bugusertags;
117 my %ut;
118 my %seen_users;
119
120 my $buglog = buglog($ref);
121 my $bug_status = bug_status($ref);
122 if (not defined $buglog or not defined $bug_status) {
123     no_such_bug($q,$ref);
124 }
125
126 sub no_such_bug {
127     my ($q,$ref) = @_;
128     print $q->header(-status => 404,
129                      -content_type => "text/html",
130                      -charset => 'utf-8',
131                      -cache_control => 'public, max-age=600',
132                     );
133     print fill_in_template(template=>'cgi/no_such_bug',
134                            variables => {modify_time => strftime('%a, %e %b %Y %T UTC', gmtime),
135                                          bug_num     => $ref,
136                                         },
137                           );
138     exit 0;
139 }
140
141 ## calculate etag for this bugreport.cgi call
142 my $etag;
143 ## identify the files that we need to look at; if someone just wants the mbox,
144 ## they don't need to see anything but the buglog; otherwise, track what is
145 ## necessary for the usertags and things to calculate status.
146
147 my @dependent_files = ($buglog);
148 my $need_status = 0;
149 if (not (($mbox and not $mbox_status_message) or
150          (defined $att and defined $msg))) {
151     $need_status = 1;
152     push @dependent_files,
153         $bug_status,
154         defined $config{version_index} ? $config{version_index}:(),
155         defined $config{binary_source_map} ? $config{binary_source_map}:();
156 }
157
158 ## Identify the users required
159 for my $user (map {split /[\s*,\s*]+/} make_list($param{users}||[])) {
160     next unless length($user);
161     push @dependent_files,Debbugs::User::usertag_file_from_email($user);
162 }
163 if (defined $param{usertag}) {
164     for my $usertag (make_list($param{usertag})) {
165         my ($user, $tag) = split /:/, $usertag, 2;
166         push @dependent_files,Debbugs::User::usertag_file_from_email($user);
167     }
168 }
169 $etag =
170     etag_does_not_match(cgi => $q,
171                         additional_data => [grep {defined $_ ? $_ :()}
172                                             values %param
173                                            ],
174                         files => [@dependent_files,
175                                  ],
176                        );
177 if (not $etag) {
178     print $q->header(-status => 304,
179                      -cache_control => 'public, max-age=600',
180                      -etag => $etag,
181                      -charset => 'utf-8',
182                      -content_type => 'text/html',
183                     );
184     print "304: Not modified\n";
185     exit 0;
186 }
187
188 ## if they're just asking for the head, stop here.
189 if ($q->request_method() eq 'HEAD' and not defined($att) and not $mbox) {
190     print $q->header(-status => 200,
191                      -cache_control => 'public, max-age=600',
192                      -etag => $etag,
193                      -charset => 'utf-8',
194                      -content_type => 'text/html',
195                     );
196      exit 0;
197 }
198
199 for my $user (map {split /[\s*,\s*]+/} make_list($param{users}||[])) {
200     next unless length($user);
201     add_user($user,\%ut,\%bugusertags,\%seen_users);
202 }
203
204 if (defined $param{usertag}) {
205      for my $usertag (make_list($param{usertag})) {
206           my %select_ut = ();
207           my ($u, $t) = split /:/, $usertag, 2;
208           Debbugs::User::read_usertags(\%select_ut, $u);
209           unless (defined $t && $t ne "") {
210                $t = join(",", keys(%select_ut));
211           }
212           add_user($u,\%ut,\%bugusertags,\%seen_users);
213           push @{$param{tag}}, split /,/, $t;
214      }
215 }
216
217 my $bug = Debbugs::Bug->new(bug => $ref,
218                             @schema_arg,
219                            );
220
221 my %status;
222 if ($need_status) {
223     %status = %{split_status_fields(get_bug_status(bug=>$ref,
224                                                    bugusertags => \%bugusertags,
225                                                    @schema_arg,
226                                                   ))}
227 }
228
229 my @records;
230 eval{
231     @records = $bug->log_records();
232 };
233 if ($@) {
234      quitcgi("Bad bug log for $gBug $ref. Unable to read records: $@");
235 }
236
237 my $log='';
238 my $msg_num = 0;
239 my $skip_next = 0;
240 if (defined($msg) and ($msg-1) <= $#records) {
241      @records = ($records[$msg-1]);
242      $msg_num = $msg - 1;
243 }
244 my @log;
245 if ( $mbox ) {
246      binmode(STDOUT,":raw");
247      my $date = strftime "%a %b %d %T %Y", localtime;
248      if (@records > 1) {
249          print $q->header(-type => "application/mbox",
250                           -cache_control => 'public, max-age=600',
251                           -etag => $etag,
252                           content_disposition => qq(attachment; filename="bug_${ref}.mbox"),
253                          );
254      }
255      else {
256           $msg_num++;
257           print $q->header(-type => "message/rfc822",
258                            -cache_control => 'public, max-age=86400',
259                            -etag => $etag,
260                            content_disposition => qq(attachment; filename="bug_${ref}_message_${msg_num}.mbox"),
261                           );
262      }
263      if ($mbox_status_message and @records > 1) {
264           my $status_message='';
265           my @status_fields = (retitle   => 'subject',
266                                package   => 'package',
267                                submitter => 'originator',
268                                severity  => 'severity',
269                                tag       => 'tags',
270                                owner     => 'owner',
271                                blocks    => 'blocks',
272                                forward   => 'forward',
273                               );
274           my ($key,$value);
275           while (($key,$value) = splice(@status_fields,0,2)) {
276                if (defined $status{$value} and length $status{$value}) {
277                     $status_message .= qq($key $ref $status{$value}\n);
278                }
279           }
280           print STDOUT qq(From unknown $date\n),
281                create_mime_message([From       => "$gBug#$ref <$ref\@$gEmailDomain>",
282                                     To         => "$gBug#$ref <$ref\@$gEmailDomain>",
283                                     Subject    => "Status: $status{subject}",
284                                     "Reply-To" => "$gBug#$ref <$ref\@$gEmailDomain>",
285                                    ],
286                                    <<END,);
287 $status_message
288 thanks
289
290
291 END
292      }
293      my $message_number=0;
294      my %seen_message_ids;
295      for my $record (@records) {
296           next if $record->{type} !~ /^(?:recips|incoming-recv)$/;
297           my $wanted_type = $mbox_maint?'recips':'incoming-recv';
298           # we want to include control messages anyway
299           my $record_wanted_anyway = 0;
300           my ($msg_id) = record_regex($record,qr/^Message-Id:\s+<(.+)>/im);
301           next if defined $msg_id and exists $seen_message_ids{$msg_id};
302           next if defined $msg_id and $msg_id =~/handler\..+\.ack(?:info|done)?\@/;
303           $record_wanted_anyway = 1 if record_regex($record,qr/^Received: \(at control\)/);
304           next if not $boring and not $record->{type} eq $wanted_type and not $record_wanted_anyway and @records > 1;
305           $seen_message_ids{$msg_id} = 1 if defined $msg_id;
306           # skip spam messages if we're outputting more than one message
307           next if @records > 1 and $bug->is_spam($msg_id);
308       my @lines;
309       if ($record->{inner_file}) {
310           push @lines, scalar $record->{fh}->getline;
311           push @lines, scalar $record->{fh}->getline;
312           chomp $lines[0];
313           chomp $lines[1];
314       } else {
315           @lines = split( "\n", $record->{text}, -1 );
316       }
317           if ( $lines[ 1 ] =~ m/^From / ) {
318           @lines = reverse @lines;
319           }
320           if ( !( $lines[ 0 ] =~ m/^From / ) ) {
321                unshift @lines, "From unknown $date";
322        }
323       print $lines[0]."\n";
324           print map { s/^(>*From )/>$1/; $_."\n" } @lines[ 1 .. $#lines ];
325       if ($record->{inner_file}) {
326           my $fh = $record->{fh};
327           print $_ while (<$fh>);
328       }
329      }
330      exit 0;
331 }
332
333 else {
334      if (defined $att and defined $msg and @records) {
335          binmode(STDOUT,":raw");
336          $msg_num++;
337          ## allow this to be cached for a week
338          print "Status: 200 OK\n";
339          print "Cache-Control: public, max-age=604800\n";
340          print "Etag: $etag\n";
341           print handle_email_message($records[0],
342                                      ref => $ref,
343                                      msg_num => $msg_num,
344                                      att => $att,
345                                      msg => $msg,
346                                      trim_headers => $trim_headers,
347                                     );
348           exit 0;
349      }
350      my %seen_msg_ids;
351      for my $record (@records) {
352           $msg_num++;
353           if ($skip_next) {
354                $skip_next = 0;
355                next;
356           }
357           $skip_next = 1 if $record->{type} eq 'html' and not $boring;
358           push @log, handle_record($record,$ref,$msg_num,
359                                    \%seen_msg_ids,
360                                    trim_headers => $trim_headers,
361                                    avatars => $avatars,
362                                    terse => $terse,
363                                    # if we're only looking at one record, allow
364                                    # spam to be output
365                                    spam  => (@records > 1)?$bug:undef,
366                                   );
367      }
368 }
369
370 @log = reverse @log if $reverse;
371 $log = join("\n",@log);
372
373
374 # All of the below should be turned into a template
375
376 my $indexentry;
377 my $showseverity;
378
379 unless (%status) {
380     no_such_bug($q,$ref);
381 }
382
383 my @packages = make_list($status{package});
384
385
386 my %packages_affects;
387 for my $p_a (qw(package affects)) {
388     foreach my $pkg (make_list($status{$p_a})) {
389         if ($pkg =~ /^src\:/) {
390             my ($srcpkg) = $pkg =~ /^src:(.*)/;
391             $packages_affects{$p_a}{$pkg} =
392                {maintainer => exists($maintainer{$srcpkg}) ? $maintainer{$srcpkg} : '(unknown)',
393                 source     => $srcpkg,
394                 package    => $pkg,
395                 is_source  => 1,
396                };
397         }
398         else {
399             $packages_affects{$p_a}{$pkg} =
400                {maintainer => exists($maintainer{$pkg}) ? $maintainer{$pkg} : '(unknown)',
401                 exists($pkgsrc{$pkg}) ? (source => $pkgsrc{$pkg}) : (),
402                 package    => $pkg,
403                };
404         }
405     }
406 }
407
408 # fixup various bits of the status
409 $status{tags_array} = [sort(make_list($status{tags}))];
410 $status{date_text} = strftime('%a, %e %b %Y %T UTC', gmtime($status{date}));
411 $status{mergedwith_array} = [make_list($status{mergedwith})];
412
413
414 my $version_graph = '';
415 if (@{$status{found_versions}} or @{$status{fixed_versions}}) {
416      $version_graph = q(<a href=").
417           html_escape(version_url(package => $status{package},
418                                   found => $status{found_versions},
419                                   fixed => $status{fixed_versions},
420                                  )
421                      ).
422           q("><img alt="version graph" src=").
423           html_escape(version_url(package => $status{package},
424                                   found => $status{found_versions},
425                                   fixed => $status{fixed_versions},
426                                   width => 2,
427                                   height => 2,
428                                  )
429                      ).
430           qq{"></a>};
431 }
432
433
434
435 my @blockedby= make_list($status{blockedby});
436 $status{blockedby_array} = [];
437 if (@blockedby && $status{"pending"} ne 'fixed' && ! length($status{done})) {
438     for my $b (@blockedby) {
439         my %s = %{get_bug_status($b)};
440         next if (defined $s{pending} and
441                  $s{"pending"} eq 'fixed') or
442                      length $s{done};
443         push @{$status{blockedby_array}},{bug_num => $b, subject => $s{subject}, status => \%s};
444    }
445 }
446
447 my @blocks= make_list($status{blocks});
448 $status{blocks_array} = [];
449 if (@blocks && $status{"pending"} ne 'fixed' && ! length($status{done})) {
450     for my $b (@blocks) {
451         my %s = %{get_bug_status($b)};
452         next if $s{"pending"} eq 'fixed' || length $s{done};
453         push @{$status{blocks_array}}, {bug_num => $b, subject => $s{subject}, status => \%s};
454     }
455 }
456
457 if ($buglog !~ m#^\Q$gSpoolDir/db#) {
458      $status{archived} = 1;
459 }
460
461 my $descriptivehead = $indexentry;
462
463 print $q->header(-type => "text/html",
464                  -charset => 'utf-8',
465                  -cache_control => 'public, max-age=300',
466                  -etag => $etag,
467                 );
468
469 print fill_in_template(template => 'cgi/bugreport',
470                        variables => {bug => $bug,
471                                      status => \%status,
472                                      package => $packages_affects{'package'},
473                                      affects => $packages_affects{'affects'},
474                                      log           => $log,
475                                      bug_num       => $ref,
476                                      version_graph => $version_graph,
477                                      msg           => $msg,
478                                      isstrongseverity => \&Debbugs::Status::isstrongseverity,
479                                      html_escape   => \&Debbugs::CGI::html_escape,
480                                      uri_escape    => \&URI::Escape::uri_escape_utf8,
481                                      looks_like_number => \&Scalar::Util::looks_like_number,
482                                      make_list        => \&Debbugs::Common::make_list,
483                                     },
484                        hole_var  => {'&package_links' => \&Debbugs::CGI::package_links,
485                                      '&bug_links'     => \&Debbugs::CGI::bug_links,
486                                      '&version_url'   => \&Debbugs::CGI::version_url,
487                                      '&strftime'      => \&POSIX::strftime,
488                                      '&maybelink'     => \&Debbugs::CGI::maybelink,
489                                     },
490                       );
491
492 __END__
493
494 # Local Variables:
495 # indent-tabs-mode: nil
496 # cperl-indent-level: 4
497 # End: