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