]> git.donarmstrong.com Git - debbugs.git/commitdiff
Merge changes from the dla source tree
authorDon Armstrong <don@volo>
Wed, 7 Mar 2007 04:12:09 +0000 (20:12 -0800)
committerDon Armstrong <don@volo>
Wed, 7 Mar 2007 04:12:09 +0000 (20:12 -0800)
48 files changed:
Debbugs/Bugs.pm [new file with mode: 0644]
Debbugs/CGI.pm [new file with mode: 0644]
Debbugs/Common.pm
Debbugs/Config.pm
Debbugs/Estraier.pm [new file with mode: 0644]
Debbugs/Mail.pm
Debbugs/Packages.pm
Debbugs/SOAP/Status.pm [new file with mode: 0644]
Debbugs/SOAP/Usertag.pm [new file with mode: 0644]
Debbugs/Status.pm [new file with mode: 0644]
Debbugs/URI.pm [new file with mode: 0644]
Debbugs/User.pm
bin/add_bug_to_estraier [new file with mode: 0755]
bin/test_bts [new file with mode: 0755]
build [deleted file]
cgi/bugreport.cgi
cgi/common.pl
cgi/pkgindex.cgi
cgi/pkgreport.cgi
cgi/search.cgi [new file with mode: 0755]
cgi/soap.cgi [new file with mode: 0755]
cgi/version.cgi [new file with mode: 0755]
clean [deleted file]
debbugs-dump [deleted file]
debbugs-service [deleted file]
debian/changelog
debian/control
examples/apache.conf
export [deleted file]
future_directions [new file with mode: 0644]
html/server-control.html.in
scripts/config.debian
scripts/config.in
scripts/config.in.default
scripts/errorlib.in
scripts/gen-indices.in
scripts/process.in
scripts/processall.in
scripts/receive.in
scripts/service.in
scripts/text.in
t/03_packages.t [new file with mode: 0644]
t/03_versions.t
t/06_mail_handling.t [new file with mode: 0644]
t/07_bugreport.t [new file with mode: 0644]
t/08_pkgreport.t [new file with mode: 0644]
t/lib/DebbugsTest.pm [new file with mode: 0644]
t/sendmail_tester [new file with mode: 0755]

diff --git a/Debbugs/Bugs.pm b/Debbugs/Bugs.pm
new file mode 100644 (file)
index 0000000..e412b47
--- /dev/null
@@ -0,0 +1,477 @@
+
+package Debbugs::Bugs;
+
+=head1 NAME
+
+Debbugs::Bugs -- Bug selection routines for debbugs
+
+=head1 SYNOPSIS
+
+use Debbugs::Bugs qw(get_bugs);
+
+
+=head1 DESCRIPTION
+
+This module is a replacement for all of the various methods of
+selecting different types of bugs.
+
+It implements a single function, get_bugs, which defines the master
+interface for selecting bugs.
+
+It attempts to use subsidiary functions to actually do the selection,
+in the order specified in the configuration files. [Unless you're
+insane, they should be in order from fastest (and often most
+incomplete) to slowest (and most complete).]
+
+=head1 BUGS
+
+=head1 FUNCTIONS
+
+=cut
+
+use warnings;
+use strict;
+use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
+use base qw(Exporter);
+
+BEGIN{
+     $VERSION = 1.00;
+     $DEBUG = 0 unless defined $DEBUG;
+
+     @EXPORT = ();
+     %EXPORT_TAGS = ();
+     @EXPORT_OK = (qw(get_bugs));
+     $EXPORT_TAGS{all} = [@EXPORT_OK];
+}
+
+use Debbugs::Config qw(:config);
+use Params::Validate qw(validate_with :types);
+use IO::File;
+use Debbugs::Status qw(splitpackages);
+use Debbugs::Packages qw(getsrcpkgs);
+use Debbugs::Common qw(getparsedaddrs getmaintainers getmaintainers_reverse);
+use Fcntl qw(O_RDONLY);
+use MLDBM qw(DB_File Storable);
+
+=head2 get_bugs
+
+     get_bugs()
+
+=head3 Parameters
+
+The following parameters can either be a single scalar or a reference
+to an array. The parameters are ANDed together, and the elements of
+arrayrefs are a parameter are ORed. Future versions of this may allow
+for limited regular expressions, and/or more complex expressions.
+
+=over
+
+=item package -- name of the binary package
+
+=item src -- name of the source package
+
+=item maint -- address of the maintainer
+
+=item submitter -- address of the submitter
+
+=item severity -- severity of the bug
+
+=item status -- status of the bug
+
+=item tag -- bug tags
+
+=item owner -- owner of the bug
+
+=item dist -- distribution (I don't know about this one yet)
+
+=item bugs -- list of bugs to search within
+
+=item function -- see description below
+
+=back
+
+=head3 Special options
+
+The following options are special options used to modulate how the
+searches are performed.
+
+=over
+
+=item archive -- whether to search archived bugs or normal bugs;
+defaults to false.
+
+=item usertags -- set of usertags and the bugs they are applied to
+
+=back
+
+
+=head3 Subsidiary routines
+
+All subsidiary routines get passed exactly the same set of options as
+get_bugs. If for some reason they are unable to handle the options
+passed (for example, they don't have the right type of index for the
+type of selection) they should die as early as possible. [Using
+Params::Validate and/or die when files don't exist makes this fairly
+trivial.]
+
+This function will then immediately move on to the next subroutine,
+giving it the same arguments.
+
+=head3 function
+
+This option allows you to provide an arbitrary function which will be
+given the information in the index.db file. This will be super, super
+slow, so only do this if there's no other way to write the search.
+
+You'll be given a list (which you can turn into a hash) like the
+following:
+
+ (pkg => ['a','b'], # may be a scalar (most common)
+  bug => 1234,
+  status => 'pending',
+  submitter => 'boo@baz.com',
+  severity => 'serious',
+  tags => ['a','b','c'], # may be an empty arrayref
+ )
+
+The function should return 1 if the bug should be included; 0 if the
+bug should not.
+
+=cut
+
+sub get_bugs{
+     my %param = validate_with(params => \@_,
+                              spec   => {package   => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         src       => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         maint     => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         submitter => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         severity  => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         status    => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         tag       => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         owner     => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         dist      => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         function  => {type => CODEREF,
+                                                       optional => 1,
+                                                      },
+                                         bugs      => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         archive   => {type => BOOLEAN,
+                                                       default => 0,
+                                                      },
+                                         usertags  => {type => HASHREF,
+                                                       optional => 1,
+                                                      },
+                                        },
+                             );
+
+     # Normalize options
+     my %options = %param;
+     my @bugs;
+     # A configuration option will set an array that we'll use here instead.
+     for my $routine (qw(Debbugs::Bugs::get_bugs_by_idx Debbugs::Bugs::get_bugs_flatfile)) {
+         my ($package) = $routine =~ m/^(.+)\:\:/;
+         eval "use $package;";
+         if ($@) {
+              # We output errors here because using an invalid function
+              # in the configuration file isn't something that should
+              # be done.
+              warn "use $package failed with $@";
+              next;
+         }
+         @bugs = eval "${routine}(\%options)";
+         if ($@) {
+
+              # We don't output errors here, because failure here
+              # via die may be a perfectly normal thing.
+              print STDERR "$@" if $DEBUG;
+              next;
+         }
+         last;
+     }
+     # If no one succeeded, die
+     if ($@) {
+         die "$@";
+     }
+     return @bugs;
+}
+
+=head2 get_bugs_by_idx
+
+This routine uses the by-$index.idx indicies to try to speed up
+searches.
+
+
+=cut
+
+sub get_bugs_by_idx{
+     my %param = validate_with(params => \@_,
+                              spec   => {package   => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         submitter => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         severity  => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         tag       => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         archive   => {type => BOOLEAN,
+                                                       default => 0,
+                                                      },
+                                         src       => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         maint     => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                        },
+                             );
+     my %bugs = ();
+
+     # We handle src packages, maint and maintenc by mapping to the
+     # appropriate binary packages, then removing all packages which
+     # don't match all queries
+     my @packages = __handle_pkg_src_and_maint(map {exists $param{$_}?($_,$param{$_}):()}
+                                              qw(package src maint)
+                                             );
+     if (exists $param{package} or
+        exists $param{src} or
+        exists $param{maint}) {
+         delete @param{qw(maint src)};
+         $param{package} = [@packages];
+     }
+     my $keys = keys(%param) - 1;
+     die "Need at least 1 key to search by" unless $keys;
+     my $arc = $param{archive} ? '-arc':'';
+     my %idx;
+     for my $key (grep {$_ ne 'archive'} keys %param) {
+         my $index = $key;
+         $index = 'submitter-email' if $key eq 'submitter';
+         $index = "$config{spool_dir}/by-${index}${arc}.idx";
+         tie(%idx, MLDBM => $index, O_RDONLY)
+              or die "Unable to open $index: $!";
+         for my $search (__make_list($param{$key})) {
+              next unless defined $idx{$search};
+              for my $bug (keys %{$idx{$search}}) {
+                   # increment the number of searches that this bug matched
+                   $bugs{$bug}++;
+              }
+         }
+         untie %idx or die 'Unable to untie %idx';
+     }
+     # Throw out results that do not match all of the search specifications
+     return map {$keys <= $bugs{$_}?($_):()} keys %bugs;
+}
+
+
+=head2 get_bugs_flatfile
+
+This is the fallback search routine. It should be able to complete all
+searches. [Or at least, that's the idea.]
+
+=cut
+
+sub get_bugs_flatfile{
+     my %param = validate_with(params => \@_,
+                              spec   => {package   => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         src       => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         maint     => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         submitter => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         severity  => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         status    => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         tag       => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+# not yet supported
+#                                        owner     => {type => SCALAR|ARRAYREF,
+#                                                      optional => 1,
+#                                                     },
+#                                        dist      => {type => SCALAR|ARRAYREF,
+#                                                      optional => 1,
+#                                                     },
+                                         archive   => {type => BOOLEAN,
+                                                       default => 1,
+                                                      },
+                                         usertags  => {type => HASHREF,
+                                                       optional => 1,
+                                                      },
+                                         function  => {type => CODEREF,
+                                                       optional => 1,
+                                                      },
+                                        },
+                             );
+     my $flatfile;
+     if ($param{archive}) {
+         $flatfile = new IO::File "$debbugs::gSpoolDir/index.archive", 'r'
+              or die "Unable to open $debbugs::gSpoolDir/index.archive for reading: $!";
+     }
+     else {
+         $flatfile = new IO::File "$debbugs::gSpoolDir/index.db", 'r'
+              or die "Unable to open $debbugs::gSpoolDir/index.db for reading: $!";
+     }
+     my %usertag_bugs;
+     if (exists $param{tag} and exists $param{usertags}) {
+
+         # This complex slice makes a hash with the bugs which have the
+          # usertags passed in $param{tag} set.
+         @usertag_bugs{map {@{$_}}
+                            @{$param{usertags}}{__make_list($param{tag})}
+                       } = (1) x @{$param{usertags}}{__make_list($param{tag})}
+     }
+     # We handle src packages, maint and maintenc by mapping to the
+     # appropriate binary packages, then removing all packages which
+     # don't match all queries
+     my @packages = __handle_pkg_src_and_maint(map {exists $param{$_}?($_,$param{$_}):()}
+                                              qw(package src maint)
+                                             );
+     if (exists $param{package} or
+        exists $param{src} or
+        exists $param{maint}) {
+         delete @param{qw(maint src)};
+         $param{package} = [@packages];
+     }
+     my @bugs;
+     while (<$flatfile>) {
+         next unless m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/;
+         my ($pkg,$bug,$status,$submitter,$severity,$tags) = ($1,$2,$3,$4,$5,$6,$7);
+         next if exists $param{bug} and not grep {$bug == $_} __make_list($param{bugs});
+         if (exists $param{pkg}) {
+              my @packages = splitpackages($pkg);
+              next unless grep { my $pkg_list = $_;
+                                 grep {$pkg_list eq $_} __make_list($param{pkg})
+                            } @packages;
+         }
+         if (exists $param{src}) {
+              my @src_packages = map { getsrcpkgs($_)} __make_list($param{src});
+              my @packages = splitpackages($pkg);
+              next unless grep { my $pkg_list = $_;
+                                 grep {$pkg_list eq $_} @packages
+                            } @src_packages;
+         }
+         if (exists $param{submitter}) {
+              my @p_addrs = map {$_->address}
+                   map {lc(getparsedaddrs($_))}
+                        __make_list($param{submitter});
+              my @f_addrs = map {$_->address}
+                   getparsedaddrs($submitter||'');
+              next unless grep { my $f_addr = $_; 
+                                 grep {$f_addr eq $_} @p_addrs
+                            } @f_addrs;
+         }
+         next if exists $param{severity} and not grep {$severity eq $_} __make_list($param{severity});
+         next if exists $param{status} and not grep {$status eq $_} __make_list($param{status});
+         if (exists $param{tag}) {
+              my $bug_ok = 0;
+              # either a normal tag, or a usertag must be set
+              $bug_ok = 1 if exists $param{usertags} and $usertag_bugs{$bug};
+              my @bug_tags = split ' ', $tags;
+              $bug_ok = 1 if grep {my $bug_tag = $_;
+                                   grep {$bug_tag eq $_} __make_list($param{tag});
+                              } @bug_tags;
+              next unless $bug_ok;
+         }
+         # We do this last, because a function may be slow...
+         if (exists $param{function}) {
+              my @bug_tags = split ' ', $tags;
+              my @packages = splitpackages($pkg);
+              my $package = (@packages > 1)?\@packages:$packages[0];
+              next unless
+                   $param{function}->(pkg       => $package,
+                                      bug       => $bug,
+                                      status    => $status,
+                                      submitter => $submitter,
+                                      severity  => $severity,
+                                      tags      => \@bug_tags,
+                                     );
+         }
+         push @bugs, $bug;
+     }
+     return @bugs;
+}
+
+sub __handle_pkg_src_and_maint{
+     my %param = validate_with(params => \@_,
+                              spec   => {package   => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         src       => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                         maint     => {type => SCALAR|ARRAYREF,
+                                                       optional => 1,
+                                                      },
+                                        },
+                              allow_extra => 1,
+                             );
+
+     my @packages = __make_list($param{package});
+     my $package_keys = @packages?1:0;
+     my %packages;
+     @packages{@packages} = (1) x @packages;
+     if (exists $param{src}) {
+         # We only want to increment the number of keys if there is
+         # something to match
+         my $key_inc = 0;
+         for my $package (map { getsrcpkgs($_)} __make_list($param{src})) {
+              $packages{$package}++;
+              $key_inc=1;
+         }
+         $package_keys += $key_inc;
+     }
+     if (exists $param{maint}) {
+         my $key_inc = 0;
+         my $maint_rev = getmaintainers_reverse();
+         for my $package (map { exists $maint_rev->{$_}?@{$maint_rev->{$_}}:()}
+                          __make_list($param{maint})) {
+              $packages{$package}++;
+              $key_inc = 1;
+         }
+         $package_keys += $key_inc;
+     }
+     return grep {$packages{$_} >= $package_keys} keys %packages;
+}
+
+
+# This private subroutine takes a scalar and turns it into a list;
+# transforming arrayrefs into their contents along the way. It also
+# turns undef into the empty list.
+sub __make_list{
+     return map {defined $_?(ref($_) eq 'ARRAY'?@{$_}:$_):()} @_;
+}
+
+1;
+
+__END__
diff --git a/Debbugs/CGI.pm b/Debbugs/CGI.pm
new file mode 100644 (file)
index 0000000..0fd19af
--- /dev/null
@@ -0,0 +1,545 @@
+
+package Debbugs::CGI;
+
+=head1 NAME
+
+Debbugs::CGI -- General routines for the cgi scripts
+
+=head1 SYNOPSIS
+
+use Debbugs::CGI qw(:url :html);
+
+html_escape(bug_url($ref,mbox=>'yes',mboxstatus=>'yes'));
+
+=head1 DESCRIPTION
+
+This module is a replacement for parts of common.pl; subroutines in
+common.pl will be gradually phased out and replaced with equivalent
+(or better) functionality here.
+
+=head1 BUGS
+
+None known.
+
+=cut
+
+use warnings;
+use strict;
+use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
+use base qw(Exporter);
+use Debbugs::URI;
+use HTML::Entities;
+use Debbugs::Common qw(getparsedaddrs);
+use Params::Validate qw(validate_with :types);
+use Debbugs::Config qw(:config);
+use Debbugs::Status qw(splitpackages);
+use Mail::Address;
+use POSIX qw(ceil);
+use Storable qw(dclone);
+
+my %URL_PARAMS = ();
+
+
+BEGIN{
+     ($VERSION) = q$Revision: 1.3 $ =~ /^Revision:\s+([^\s+])/;
+     $DEBUG = 0 unless defined $DEBUG;
+
+     @EXPORT = ();
+     %EXPORT_TAGS = (url    => [qw(bug_url bug_links bug_linklist maybelink),
+                               qw(set_url_params pkg_url version_url),
+                               qw(submitterurl mainturl)
+                              ],
+                    html   => [qw(html_escape htmlize_bugs htmlize_packagelinks),
+                               qw(maybelink htmlize_addresslinks htmlize_maintlinks),
+                              ],
+                    util   => [qw(cgi_parameters quitcgi),
+                               qw(getpseudodesc)
+                              ],
+                    #status => [qw(getbugstatus)],
+                   );
+     @EXPORT_OK = ();
+     Exporter::export_ok_tags(qw(url html util));
+     $EXPORT_TAGS{all} = [@EXPORT_OK];
+}
+
+
+
+=head2 set_url_params
+
+     set_url_params($uri);
+
+
+Sets the url params which will be used to generate urls.
+
+=cut
+
+sub set_url_params{
+     if (@_ > 1) {
+         %URL_PARAMS = @_;
+     }
+     else {
+         my $url = Debbugs::URI->new($_[0]||'');
+         %URL_PARAMS = %{$url->query_form_hash};
+     }
+}
+
+
+=head2 bug_url
+
+     bug_url($ref,mbox=>'yes',mboxstat=>'yes');
+
+Constructs urls which point to a specific
+
+XXX use Params::Validate
+
+=cut
+
+sub bug_url{
+     my $ref = shift;
+     my %params;
+     if (@_ % 2) {
+         shift;
+         %params = (%URL_PARAMS,@_);
+     }
+     else {
+         %params = @_;
+     }
+     my $url = Debbugs::URI->new('bugreport.cgi?');
+     $url->query_form(bug=>$ref,%params);
+     return $url->as_string;
+}
+
+sub pkg_url{
+     my %params;
+     if (@_ % 2) {
+         shift;
+         %params = (%URL_PARAMS,@_);
+     }
+     else {
+         %params = @_;
+     }
+     my $url = Debbugs::URI->new('pkgreport.cgi?');
+     $url->query_form(%params);
+     return $url->as_string;
+}
+
+=head2 version_url
+
+     version_url($package,$found,$fixed)
+
+Creates a link to the version cgi script
+
+=cut
+
+sub version_url{
+     my ($package,$found,$fixed,$width,$height) = @_;
+     my $url = Debbugs::URI->new('version.cgi?');
+     $url->query_form(package => $package,
+                     found   => $found,
+                     fixed   => $fixed,
+                     (defined $width)?(width => $width):(),
+                     (defined $height)?(height => $height):()
+                    );
+     return $url->as_string;
+}
+
+=head2 html_escape
+
+     html_escape($string)
+
+Escapes html entities by calling HTML::Entities::encode_entities;
+
+=cut
+
+sub html_escape{
+     my ($string) = @_;
+
+     return HTML::Entities::encode_entities($string)
+}
+
+=head2 cgi_parameters
+
+     cgi_parameters
+
+Returns all of the cgi_parameters from a CGI script using CGI::Simple
+
+=cut
+
+sub cgi_parameters {
+     my %options = validate_with(params => \@_,
+                                spec   => {query   => {type => OBJECT,
+                                                       can  => 'param',
+                                                      },
+                                           single  => {type => ARRAYREF,
+                                                       default => [],
+                                                      },
+                                           default => {type => HASHREF,
+                                                       default => {},
+                                                      },
+                                          },
+                               );
+     my $q = $options{query};
+     my %single;
+     @single{@{$options{single}}} = (1) x @{$options{single}};
+     my %param;
+     for my $paramname ($q->param) {
+         if ($single{$paramname}) {
+              $param{$paramname} = $q->param($paramname);
+         }
+         else {
+              $param{$paramname} = [$q->param($paramname)];
+         }
+     }
+     for my $default (keys %{$options{default}}) {
+         if (not exists $param{$default}) {
+              # We'll clone the reference here to avoid surprises later.
+              $param{$default} = ref($options{default}{$default})?
+                   dclone($options{default}{$default}):$options{default}{$default};
+         }
+     }
+     return %param;
+}
+
+
+sub quitcgi {
+    my $msg = shift;
+    print "Content-Type: text/html\n\n";
+    print "<HTML><HEAD><TITLE>Error</TITLE></HEAD><BODY>\n";
+    print "An error occurred. Dammit.\n";
+    print "Error was: $msg.\n";
+    print "</BODY></HTML>\n";
+    exit 0;
+}
+
+
+my %common_bugusertags;
+
+
+
+=head HTML
+
+=head2 htmlize_bugs
+
+     htmlize_bugs({bug=>1,status=>\%status,extravars=>\%extra},{bug=>...}});
+
+Turns a list of bugs into an html snippit of the bugs.
+
+=cut
+#     htmlize_bugs(bugs=>[@bugs]);
+sub htmlize_bugs{
+     my @bugs = @_;
+     my @html;
+
+     for my $bug (@bugs) {
+         my $html = sprintf "<li><a href=\"%s\">#%d: %s</a>\n<br>",
+              bug_url($bug->{bug}), $bug->{bug}, html_escape($bug->{status}{subject});
+         $html .= htmlize_bugstatus($bug->{status}) . "\n";
+     }
+     return @html;
+}
+
+
+sub htmlize_bugstatus {
+     my %status = %{$_[0]};
+
+     my $result = "";
+
+     my $showseverity;
+     if  ($status{severity} eq $config{default_severity}) {
+         $showseverity = '';
+     } elsif (isstrongseverity($status{severity})) {
+         $showseverity = "Severity: <em class=\"severity\">$status{severity}</em>;\n";
+     } else {
+         $showseverity = "Severity: <em>$status{severity}</em>;\n";
+     }
+
+     $result .= htmlize_packagelinks($status{"package"}, 1);
+
+     my $showversions = '';
+     if (@{$status{found_versions}}) {
+         my @found = @{$status{found_versions}};
+         local $_;
+         s{/}{ } foreach @found;
+         $showversions .= join ', ', map html_escape($_), @found;
+     }
+     if (@{$status{fixed_versions}}) {
+         $showversions .= '; ' if length $showversions;
+         $showversions .= '<strong>fixed</strong>: ';
+         my @fixed = @{$status{fixed_versions}};
+         $showversions .= join ', ', map {s#/##; html_escape($_)} @fixed;
+     }
+     $result .= " ($showversions)" if length $showversions;
+     $result .= ";\n";
+
+     $result .= $showseverity;
+     $result .= htmlize_addresslinks("Reported by: ", \&submitterurl,
+                                $status{originator});
+     $result .= ";\nOwned by: " . html_escape($status{owner})
+         if length $status{owner};
+     $result .= ";\nTags: <strong>" 
+         . html_escape(join(", ", sort(split(/\s+/, $status{tags}))))
+              . "</strong>"
+                   if (length($status{tags}));
+
+     $result .= ";\nMerged with ".
+         bug_linklist(', ',
+                      'submitter',
+                      split(/ /,$status{mergedwith}))
+              if length $status{mergedwith};
+     $result .= ";\nBlocked by ".
+         bug_linklist(", ",
+                      'submitter',
+                      split(/ /,$status{blockedby}))
+              if length $status{blockedby};
+     $result .= ";\nBlocks ".
+         bug_linklist(", ",
+                      'submitter',
+                      split(/ /,$status{blocks})
+                     )
+              if length $status{blocks};
+
+     my $days = 0;
+     if (length($status{done})) {
+         $result .= "<br><strong>Done:</strong> " . html_escape($status{done});
+         $days = ceil($debbugs::gRemoveAge - -M buglog($status{id}));
+         if ($days >= 0) {
+              $result .= ";\n<strong>Will be archived" . ( $days == 0 ? " today" : $days == 1 ? " in $days day" : " in $days days" ) . "</strong>";
+         } else {
+              $result .= ";\n<strong>Archived</strong>";
+         }
+     }
+     else {
+         if (length($status{forwarded})) {
+              $result .= ";\n<strong>Forwarded</strong> to "
+                   . maybelink($status{forwarded});
+         }
+         my $daysold = int((time - $status{date}) / 86400);   # seconds to days
+         if ($daysold >= 7) {
+              my $font = "";
+              my $efont = "";
+              $font = "em" if ($daysold > 30);
+              $font = "strong" if ($daysold > 60);
+              $efont = "</$font>" if ($font);
+              $font = "<$font>" if ($font);
+
+              my $yearsold = int($daysold / 365);
+              $daysold -= $yearsold * 365;
+
+              $result .= ";\n $font";
+              my @age;
+              push @age, "1 year" if ($yearsold == 1);
+              push @age, "$yearsold years" if ($yearsold > 1);
+              push @age, "1 day" if ($daysold == 1);
+              push @age, "$daysold days" if ($daysold > 1);
+              $result .= join(" and ", @age);
+              $result .= " old$efont";
+        }
+    }
+
+    $result .= ".";
+
+    return $result;
+}
+
+=head2 htmlize_packagelinks
+
+     htmlize_packagelinks
+
+Given a scalar containing a list of packages separated by something
+that L<Debbugs::CGI/splitpackages> can separate, returns a
+formatted set of links to packages.
+
+=cut
+
+sub htmlize_packagelinks {
+    my ($pkgs,$strong) = @_;
+    return unless defined $pkgs and $pkgs ne '';
+    my @pkglist = splitpackages($pkgs);
+
+    $strong = 0;
+    my $openstrong  = $strong ? '<strong>' : '';
+    my $closestrong = $strong ? '</strong>' : '';
+
+    return 'Package' . (@pkglist > 1 ? 's' : '') . ': ' .
+           join(', ',
+                map {
+                    '<a class="submitter" href="' . pkg_url(pkg=>$_||'') . '">' .
+                    $openstrong . html_escape($_) . $closestrong . '</a>'
+                } @pkglist
+           );
+}
+
+
+=head2 maybelink
+
+     maybelink($in);
+     maybelink('http://foobarbaz,http://bleh',qr/[, ]+/);
+     maybelink('http://foobarbaz,http://bleh',qr/[, ]+/,', ');
+
+
+In the first form, links the link if it looks like a link. In the
+second form, first splits based on the regex, then reassembles the
+link, linking things that look like links. In the third form, rejoins
+the split links with commas and spaces.
+
+=cut
+
+sub maybelink {
+    my ($links,$regex,$join) = @_;
+    $join = ' ' if not defined $join;
+    my @return;
+    my @segments;
+    if (defined $regex) {
+        @segments = split $regex, $links;
+    }
+    else {
+        @segments = ($links);
+    }
+    for my $in (@segments) {
+        if ($in =~ /^[a-zA-Z0-9+.-]+:/) { # RFC 1738 scheme
+             push @return, qq{<a href="$in">} . html_escape($in) . '</a>';
+        } else {
+             push @return, html_escape($in);
+        }
+    }
+    return @return?join($join,@return):'';
+}
+
+
+=head2 htmlize_addresslinks
+
+     htmlize_addresslinks($prefixfunc,$urlfunc,$addresses,$class);
+
+
+Generate a comma-separated list of HTML links to each address given in
+$addresses, which should be a comma-separated list of RFC822
+addresses. $urlfunc should be a reference to a function like mainturl
+or submitterurl which returns the URL for each individual address.
+
+
+=cut
+
+sub htmlize_addresslinks {
+     my ($prefixfunc, $urlfunc, $addresses,$class) = @_;
+     $class = defined $class?qq(class="$class" ):'';
+     if (defined $addresses and $addresses ne '') {
+         my @addrs = getparsedaddrs($addresses);
+         my $prefix = (ref $prefixfunc) ?
+              $prefixfunc->(scalar @addrs):$prefixfunc;
+         return $prefix .
+              join(', ', map
+                   { sprintf qq(<a ${class}).
+                          'href="%s">%s</a>',
+                               $urlfunc->($_->address),
+                                    html_escape($_->format) ||
+                                         '(unknown)'
+                                    } @addrs
+                  );
+     }
+     else {
+         my $prefix = (ref $prefixfunc) ?
+              $prefixfunc->(1) : $prefixfunc;
+         return sprintf '%s<a '.$class.'href="%s">(unknown)</a>',
+              $prefix, $urlfunc->('');
+     }
+}
+
+sub emailfromrfc822{
+     my $addr = getparsedaddrs($_[0] || "");
+     $addr = defined $addr?$addr->address:'';
+     return $addr;
+}
+
+sub mainturl { pkg_url(maint => emailfromrfc822($_[0])); }
+sub submitterurl { pkg_url(submitter => emailfromrfc822($_[0])); }
+sub htmlize_maintlinks {
+    my ($prefixfunc, $maints) = @_;
+    return htmlize_addresslinks($prefixfunc, \&mainturl, $maints);
+}
+
+
+my $_maintainer;
+my $_maintainer_rev;
+
+my $_pseudodesc;
+sub getpseudodesc {
+    return $_pseudodesc if $_pseudodesc;
+    my %pseudodesc;
+
+    my $pseudo = new IO::File $config{pseudo_desc_file},'r'
+        or &quitcgi("Unable to open $config{pseudo_desc_file}: $!");
+    while(<$pseudo>) {
+       next unless m/^(\S+)\s+(\S.*\S)\s*$/;
+       $pseudodesc{lc $1} = $2;
+    }
+    close($pseudo);
+    $_pseudodesc = \%pseudodesc;
+    return $_pseudodesc;
+}
+
+
+=head2 bug_links
+
+     bug_links($one_bug);
+     bug_links($starting_bug,$stoping_bugs,);
+
+Creates a set of links to bugs, starting with bug number
+$starting_bug, and finishing with $stoping_bug; if only one bug is
+passed, makes a link to only a single bug.
+
+The content of the link is the bug number.
+
+XXX Use L<Params::Validate>; we want to be able to support query
+arguments here too.
+
+=cut
+
+sub bug_links{
+     my ($start,$stop,$query_arguments) = @_;
+     $stop = $stop || $start;
+     $query_arguments ||= '';
+     my @output;
+     for my $bug ($start..$stop) {
+         push @output,'<a href="'.bug_url($bug,'').qq(">$bug</a>);
+     }
+     return join(', ',@output);
+}
+
+=head2 bug_linklist
+
+     bug_linklist($separator,$class,@bugs)
+
+Creates a set of links to C<@bugs> separated by C<$separator> with
+link class C<$class>.
+
+XXX Use L<Params::Validate>; we want to be able to support query
+arguments here too; we should be able to combine bug_links and this
+function into one. [Hell, bug_url should be one function with this one
+too.]
+
+=cut
+
+
+sub bug_linklist{
+     my ($sep,$class,@bugs) = @_;
+     if (length $class) {
+         $class = qq(class="$class" );
+     }
+     return join($sep,map{qq(<a ${class}href=").
+                              bug_url($_).qq(">#$_</a>)
+                         } @bugs);
+}
+
+
+
+
+1;
+
+
+__END__
+
+
+
+
+
+
index b940edfef34ca73f06d79fb4fb5568b9d13d9466..91b0259d924d84982dd2219b1ad93595da12d826 100644 (file)
-package Debbugs::Common; 
 
+package Debbugs::Common;
+
+=head1 NAME
+
+Debbugs::Common -- Common routines for all of Debbugs
+
+=head1 SYNOPSIS
+
+use Debbugs::Common qw(:url :html);
+
+
+=head1 DESCRIPTION
+
+This module is a replacement for the general parts of errorlib.pl.
+subroutines in errorlib.pl will be gradually phased out and replaced
+with equivalent (or better) functionality here.
+
+=head1 FUNCTIONS
+
+=cut
+
+use warnings;
 use strict;
+use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
+use base qw(Exporter);
+
+BEGIN{
+     $VERSION = 1.00;
+     $DEBUG = 0 unless defined $DEBUG;
+
+     @EXPORT = ();
+     %EXPORT_TAGS = (util   => [qw(getbugcomponent getbuglocation getlocationpath get_hashname),
+                               qw(appendfile buglog getparsedaddrs getmaintainers),
+                               qw(getmaintainers_reverse)
+                              ],
+                    quit   => [qw(quit)],
+                    lock   => [qw(filelock unfilelock)],
+                   );
+     @EXPORT_OK = ();
+     Exporter::export_ok_tags(qw(lock quit util));
+     $EXPORT_TAGS{all} = [@EXPORT_OK];
+}
 
-BEGIN {
-       use Exporter   ();
-       use vars       qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+#use Debbugs::Config qw(:globals);
+use Debbugs::Config qw(:config);
+use IO::File;
+use Debbugs::MIME qw(decode_rfc1522);
+use Mail::Address;
 
-       # set the version for version checking
-       $VERSION     = 1.00;
+use Fcntl qw(:flock);
 
-       @ISA         = qw(Exporter);
-       @EXPORT      = qw(&fail &NameToPathHash &sani &quit);
-       %EXPORT_TAGS = (  );     # eg: TAG => [ qw!name1 name2! ],
+our $DEBUG_FH = \*STDERR if not defined $DEBUG_FH;
 
-       # your exported package globals go here,
-       # as well as any optionally exported functions
-       @EXPORT_OK   = qw();
+=head1 UTILITIES
+
+The following functions are exported by the C<:util> tag
+
+=head2 getbugcomponent
+
+     my $file = getbugcomponent($bug_number,$extension,$location)
+
+Returns the path to the bug file in location C<$location>, bug number
+C<$bugnumber> and extension C<$extension>
+
+=cut
+
+sub getbugcomponent {
+    my ($bugnum, $ext, $location) = @_;
+
+    if (not defined $location) {
+       $location = getbuglocation($bugnum, $ext);
+       # Default to non-archived bugs only for now; CGI scripts want
+       # archived bugs but most of the backend scripts don't. For now,
+       # anything that is prepared to accept archived bugs should call
+       # getbuglocation() directly first.
+       return undef if defined $location and
+                       ($location ne 'db' and $location ne 'db-h');
+    }
+    my $dir = getlocationpath($location);
+    return undef if not defined $dir;
+    if (defined $location and $location eq 'db') {
+       return "$dir/$bugnum.$ext";
+    } else {
+       my $hash = get_hashname($bugnum);
+       return "$dir/$hash/$bugnum.$ext";
+    }
 }
 
-use vars      @EXPORT_OK;
-use Debbugs::Config qw(%Globals);
-use FileHandle;
+=head2 getbuglocation
+
+     getbuglocation($bug_number,$extension)
+
+Returns the the location in which a particular bug exists; valid
+locations returned currently are archive, db-h, or db. If the bug does
+not exist, returns undef.
+
+=cut
+
+sub getbuglocation {
+    my ($bugnum, $ext) = @_;
+    my $archdir = get_hashname($bugnum);
+    return 'archive' if -r getlocationpath('archive')."/$archdir/$bugnum.$ext";
+    return 'db-h' if -r getlocationpath('db-h')."/$archdir/$bugnum.$ext";
+    return 'db' if -r getlocationpath('db')."/$bugnum.$ext";
+    return undef;
+}
+
+
+=head2 getlocationpath
+
+     getlocationpath($location)
+
+Returns the path to a specific location
+
+=cut
+
+sub getlocationpath {
+     my ($location) = @_;
+     if (defined $location and $location eq 'archive') {
+         return "$config{spool_dir}/archive";
+     } elsif (defined $location and $location eq 'db') {
+         return "$config{spool_dir}/db";
+     } else {
+         return "$config{spool_dir}/db-h";
+     }
+}
+
+
+=head2 get_hashname
+
+     get_hashname
+
+Returns the hash of the bug which is the location within the archive
+
+=cut
+
+sub get_hashname {
+    return "" if ( $_[ 0 ] < 0 );
+    return sprintf "%02d", $_[ 0 ] % 100;
+}
+
+=head2 buglog
+
+     buglog($bugnum);
+
+Returns the path to the logfile corresponding to the bug.
+
+=cut
+
+sub buglog {
+    my $bugnum = shift;
+    my $location = getbuglocation($bugnum, 'log');
+    return getbugcomponent($bugnum, 'log', $location) if ($location);
+    $location = getbuglocation($bugnum, 'log.gz');
+    return getbugcomponent($bugnum, 'log.gz', $location);
+}
+
+
+=head2 appendfile
+
+     appendfile($file,'data','to','append');
+
+Opens a file for appending and writes data to it.
+
+=cut
+
+sub appendfile {
+       my $file = shift;
+       if (!open(AP,">>$file")) {
+               print $DEBUG_FH "failed open log<\n" if $DEBUG;
+               print $DEBUG_FH "failed open log err $!<\n" if $DEBUG;
+               &quit("opening $file (appendfile): $!");
+       }
+       print(AP @_) || &quit("writing $file (appendfile): $!");
+       close(AP) || &quit("closing $file (appendfile): $!");
+}
+
+=head2 getparsedaddrs
+
+     my $address = getparsedaddrs($address);
+     my @address = getpasredaddrs($address);
+
+Returns the output from Mail::Address->parse, or the cached output if
+this address has been parsed before. In SCALAR context returns the
+first address parsed.
+
+=cut
+
+
+my %_parsedaddrs;
+sub getparsedaddrs {
+    my $addr = shift;
+    return () unless defined $addr;
+    return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0]
+        if exists $_parsedaddrs{$addr};
+    @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr);
+    return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0];
+}
+
+my $_maintainer;
+my $_maintainer_rev;
+sub getmaintainers {
+    return $_maintainer if $_maintainer;
+    my %maintainer;
+    my %maintainer_rev;
+    for my $file (@config{qw(maintainer_file maintainer_file_override)}) {
+        next unless defined $file;
+        my $maintfile = new IO::File $file,'r' or
+             &quitcgi("Unable to open $file: $!");
+        while(<$maintfile>) {
+             next unless m/^(\S+)\s+(\S.*\S)\s*$/;
+             ($a,$b)=($1,$2);
+             $a =~ y/A-Z/a-z/;
+             $maintainer{$a}= $b;
+             for my $maint (map {lc($_->address)} getparsedaddrs($b)) {
+                  push @{$maintainer_rev{$maint}},$a;
+             }
+        }
+        close($maintfile);
+    }
+    $_maintainer = \%maintainer;
+    $_maintainer_rev = \%maintainer_rev;
+    return $_maintainer;
+}
+sub getmaintainers_reverse{
+     return $_maintainer_rev if $_maintainer_rev;
+     getmaintainers();
+     return $_maintainer_rev;
+}
+
+
+=head1 LOCK
+
+These functions are exported with the :lock tag
+
+=head2 filelock
+
+     filelock
+
+FLOCKs the passed file. Use unfilelock to unlock it.
+
+=cut
+
+my @filelocks;
 my @cleanups;
-my $DEBUG = new FileHandle;
-
-sub fail
-{
-       print "$_[0]\n";
-       exit 1;
-}
-sub NameToPathHash
-{
-#   12345 -> 5/4/3/12345
-#   12 -> s/2/1/12
-    my $name = $_[0];
-    my $tmp = $name;
-    $name =~ /^.*?(.)(.)(.)$/ ;
-    if(!defined($1)) {
-       $name =~ /^(.*?)(.)(.)$/ ;
-       $tmp = "$1$2$3"."s";
+
+sub filelock {
+    # NB - NOT COMPATIBLE WITH `with-lock'
+    my ($lockfile) = @_;
+    my ($count,$errors) = @_;
+    $count= 10; $errors= '';
+    for (;;) {
+       my $fh = eval {
+            my $fh2 = IO::File->new($lockfile,'w')
+                 or die "Unable to open $lockfile for writing: $!";
+            flock($fh2,LOCK_EX|LOCK_NB)
+                 or die "Unable to lock $lockfile $!";
+            return $fh2;
+       };
+       if ($@) {
+            $errors .= $@;
+       }
+       if ($fh) {
+            push @filelocks, {fh => $fh, file => $lockfile};
+            last;
+       }
+        if (--$count <=0) {
+            $errors =~ s/\n+$//;
+            &quit("failed to get lock on $lockfile -- $errors");
+        }
+        sleep 10;
     }
-    $tmp =~ /^.*?(.)(.)(.)$/ ;
-    return "$3/$2/$1/$name";
+    push(@cleanups,\&unfilelock);
 }
 
-sub DEBUG
-{
-    print $DEBUG $_;
+
+=head2 unfilelock
+
+     unfilelock()
+
+Unlocks the file most recently locked.
+
+Note that it is not currently possible to unlock a specific file
+locked with filelock.
+
+=cut
+
+sub unfilelock {
+    if (@filelocks == 0) {
+        warn "unfilelock called with no active filelocks!\n";
+        return;
+    }
+    my %fl = %{pop(@filelocks)};
+    pop(@cleanups);
+    flock($fl{fh},LOCK_UN)
+        or warn "Unable to unlock lockfile $fl{file}: $!";
+    close($fl{fh})
+        or warn "Unable to close lockfile $fl{file}: $!";
+    unlink($fl{file})
+        or warn "Unable to unlink locfile $fl{file}: $!";
 }
-sub quit
-{
-    DEBUG("quitting >$_[0]<\n");
-    my $u;
+
+
+
+=head1 QUIT
+
+These functions are exported with the :quit tag.
+
+=head2 quit
+
+     quit()
+
+Exits the program by calling die after running some cleanups.
+
+This should be replaced with an END handler which runs the cleanups
+instead. (Or possibly a die handler, if the cleanups are important)
+
+=cut
+
+sub quit {
+    print $DEBUG_FH "quitting >$_[0]<\n" if $DEBUG;
+    my ($u);
     while ($u= $cleanups[$#cleanups]) { &$u; }
     die "*** $_[0]\n";
 }
-sub sani
-{
-    HTML::Entities::encode($a);
-}
+
+
+
+
 1;
-END { }       # module clean-up code here (global destructor)
+
+__END__
index 6cf66d83d1c8755cd2bd4a416dbc0c91dd8f323a..e8231f114d3bc57c15bf1ad2d16bb0c9a832f0ac 100644 (file)
-package Debbugs::Config;  # assumes Some/Module.pm
 
-use strict;
+package Debbugs::Config;
+
+=head1 NAME
+
+Debbugs::Config -- Configuration information for debbugs
+
+=head1 SYNOPSIS
+
+ use Debbugs::Config;
+
+# to get the compatiblity interface
+
+ use Debbugs::Config qw(:globals);
 
-BEGIN 
-{      use Exporter   ();
-       use vars       qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
-       
-    # set the version for version checking
-    $VERSION     = 1.00;
+=head1 DESCRIPTION
+
+This module provides configuration variables for all of debbugs.
+
+=head1 CONFIGURATION FILES
+
+The default configuration file location is /etc/debbugs/config; this
+configuration file location can be set by modifying the
+DEBBUGS_CONFIG_FILE env variable to point at a different location.
+
+=cut
+
+use warnings;
+use strict;
+use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT $USING_GLOBALS %config);
+use base qw(Exporter);
 
-    @ISA         = qw(Exporter);
-    @EXPORT      = qw(%Globals %GTags %Strong %Severity );
-    %EXPORT_TAGS = ( );     # eg: TAG => [ qw!name1 name2! ],
+BEGIN {
+     # set the version for version checking
+     $VERSION     = 1.00;
+     $DEBUG = 0 unless defined $DEBUG;
+     $USING_GLOBALS = 0;
 
-    # your exported package globals go here,
-    # as well as any optionally exported functions
-    @EXPORT_OK   = qw(%Globals %GTags %Severity %Strong &ParseConfigFile &ParseXMLConfigFile);
+     @EXPORT = ();
+     %EXPORT_TAGS = (globals => [qw($gEmailDomain $gListDomain $gWebHost $gWebHostBugDir),
+                                qw($gWebDomain $gHTMLSuffix $gCGIDomain $gMirrors),
+                                qw($gPackagePages $gSubscriptionDomain $gProject $gProjectTitle),
+                                qw($gMaintainer $gMaintainerWebpage $gMaintainerEmail $gUnknownMaintainerEmail),
+                                qw($gSubmitList $gMaintList $gQuietList $gForwardList),
+                                qw($gDoneList $gRequestList $gSubmitterList $gControlList),
+                                qw($gSummaryList $gMirrorList $gMailer $gBug),
+                                qw($gBugs $gRemoveAge $gSaveOldBugs $gDefaultSeverity),
+                                qw($gShowSeverities $gBounceFroms $gConfigDir $gSpoolDir),
+                                qw($gIncomingDir $gWebDir $gDocDir $gMaintainerFile),
+                                qw($gMaintainerFileOverride $gPseudoDescFile $gPackageSource),
+                                qw($gVersionPackagesDir $gVersionIndex $gBinarySourceMap $gSourceBinaryMap),
+                                qw($gSendmail $gLibPath),
+                                qw(%gSeverityDisplay @gTags @gSeverityList @gStrongSeverities),
+                                qw(%gSearchEstraier),
+                                qw(@gPostProcessall),
+                               ],
+                    text     => [qw($gBadEmailPrefix $gHTMLTail $gHTMLExpireNote),
+                                ],
+                    config   => [qw(%config)],
+                   );
+     @EXPORT_OK = ();
+     Exporter::export_ok_tags(qw(globals text config));
+     $EXPORT_TAGS{all} = [@EXPORT_OK];
 }
 
-use vars      @EXPORT_OK;
-use Debbugs::Common;
-use Debbugs::Email;
-
-# initialize package globals, first exported ones
-%Severity = ();
-%Strong = ();
-$Severity{ 'Text' } = ();
-%GTags = ();
-%Globals = (   "debug" => 0,
-               "verbose" => 0,
-               "quiet" => 0,
-               ##### domains
-               "email-domain" => "bugs.domain.com",
-               "list-domain" => "lists.domain.com",
-               "web-domain" => "web.domain.com",
-               "cgi-domain" => "cgi.domain.com",
-               ##### identification
-               "project-short" => "debbugs",
-               "project-long" => "Debbugs Test Project",
-               "owner-name" => "Fred Flintstone",
-               "owner-email" => "owner\@bugs.domain.com",
-               ##### directories
-               "work-dir" => "/var/lib/debbugs/spool",
-               "spool-dir" => "/var/lib/debbugs/spool/incoming",
-               "www-dir" => "/var/lib/debbugs/www",
-               "doc-dir" => "/var/lib/debbugs/www/txt",
-               ##### files
-               "maintainer-file" => "/etc/debbugs/Maintainers",
-               "pseudo-description" => "/etc/debbugs/pseudo-packages.description");
-
-my %ConfigMap = ( 
-               "Email Domain" => "email-domain",
-               "List Domain" => "list-domain",
-               "Web Domain" => "web-domain",
-               "CGI Domain" => "cgi-domain",
-               "Short Name" => "project-short",
-               "Long Name" => "project-long",
-               "Owner Name" => "owner-name",
-               "Owner Email" => "owner-email",
-               "Errors Email" => "errors-email",
-               "Owner Webpage" => "owner-webpage",
-               "Spool Dir" => "spool-dir",
-               "Work Dir" => "work-dir",
-               "Web Dir" => "www-dir",
-               "Doc Dir" => "doc-dir",
-               "Template Dir" => "template-dir",
-               "Not-Don-Con" => "not-don-con",
-               "Maintainer File" => "maintainer-file",
-               "Pseudo Description File" => "pseudo-description",
-               "Submit List" => "submit-list",
-               "Maint List" => "maint-list",
-               "Quiet List" => "quiet-list",
-               "Forwarded List" => "forwarded-list",
-               "Done List" => "done-list",
-               "Request List" => "request-list",
-               "Submitter List" => "submitter-list",
-               "Control List" => "control-list",
-               "Summary List" => "summary-list",
-               "Mirror List" => "mirror-list",
-               "Mailer" => "mailer",
-               "Singular Term" => "singluar",
-               "Plural Term" => "plural",
-               "Expire Age" => "expire-age",
-               "Save Expired Bugs" => "save-expired",
-               "Mirrors" => "mirrors",
-               "Default Severity" => "default-severity",
-               "Normal Severity" => "normal-severity",
-       );
-
-my %GTagsMap = ( 
-               "email-domain" => "EMAIL_DOMAIN",
-               "list-domain" => "LIST_DOMAIN",
-               "web-domain" => "WEB_DOMAIN",
-               "cgi-domain" => "CGI_DOMAIN",
-               "project-short" => "SHORT_NAME",
-               "project-long" => "LONG_NAME",
-               "owner-name" => "OWNER_NAME",
-               "owner-email" => "OWNER_EMAIL",
-               "submit-list" => "SUBMIT_LIST",
-               "quiet-list" => "QUIET_LIST",
-               "forwarded-list" => "FORWARDED_LIST",
-               "done-list" => "DONE_LIST",
-               "request-list" => "REQUEST_LIST",
-               "submitter-list" => "SUBMITTER_LIST",
-               "control-list" => "CONTROL_LIST",
-               "summary-list" => "SUMMARY_LIST",
-               "mirror-list" => "MIRROR_LIST",
-               "mirrors" => "MIRRORS"
-       );
-
-sub strip
-{   my $string = $_[0];
-    chop $string while $string =~ /\s$/; 
-    return $string;
+use File::Basename qw(dirname);
+use IO::File;
+use Safe;
+
+=head1 CONFIGURATION VARIABLES
+
+=head2 General Configuration
+
+=over
+
+=cut
+
+# read in the files;
+%config = ();
+# untaint $ENV{DEBBUGS_CONFIG_FILE} if it's owned by us
+# This enables us to test things that are -T.
+if (exists $ENV{DEBBUGS_CONFIG_FILE} and
+    ${[stat($ENV{DEBBUGS_CONFIG_FILE})]}[4] = $<) {
+     $ENV{DEBBUGS_CONFIG_FILE} =~ /(.+)/;
+     $ENV{DEBBUGS_CONFIG_FILE} = $1;
 }
+read_config(exists $ENV{DEBBUGS_CONFIG_FILE}?$ENV{DEBBUGS_CONFIG_FILE}:'/etc/debbugs/config');
 
-#############################################################################
-#  Read Config File and parse
-#############################################################################
-sub ParseConfigFile
-{   my $configfile = $_[0];
-    my @config;
-    my $votetitle = '';
-    my $ballottype = '';
-
-    #load config file
-    print "V: Loading Config File\n" if $Globals{ "verbose" };
-    open(CONFIG,$configfile) or &fail( "E: Unable to open `$configfile'" );
-    @config = <CONFIG>;
-    close CONFIG;
-
-    #parse config file
-    print "V: Parsing Config File\n" if $Globals{ "verbose" };
-    print "D3: Parse Config:\n@config\n" if $Globals{ 'debug' } > 2;
-    print "D1: Configuration\n" if $Globals{ 'debug' };
-
-    for( my $i=0; $i<=$#config; $i++)
-    {  $_ = $config[$i];
-       chop $_;
-       next unless length $_;
-       next if /^#/;
-
-       if ( /^([^:=]*)\s*[:=]\s*([^#]*)/i ) {
-           my $key = strip( $1 );
-           my $value = strip( $2 );
-           $value = "" if(!defined($value)); 
-           if ( $key =~ /Severity\s+#*(\d+)\s*(.*)/ ) {
-               my $options = $2;
-               my $severity = $1;
-               if( $options =~ /\btext\b/ ) {
-                   $Severity{ 'Text' }{ $severity } = $value;
-                   print "D2: (config) Severity $severity text = $value\n" if $Globals{ 'debug' } > 1;
-               } else {
-                   $Severity{ $1 } = $value;
-                   print "D2: (config) Severity $severity = $value" if $Globals{ 'debug' } > 1;
-                   if( $options =~ /\bdefault\b/ ) {
-                       $Globals{ "default-severity" } = $severity;
-                       print ", default" if $Globals{ 'debug' } > 1;
-                   }
-                   if( $options =~ /\bstrong\b/ ) {
-                       $Strong{ $severity } = 1;
-                       print ", strong" if $Globals{ 'debug' } > 1;
-                   }
-                   print "\n" if $Globals{ 'debug' } > 1;
-               }
-               next;
-           } else {
-               my $map = $ConfigMap{$key};
-               if(defined($map)) {
-                   $Globals{ $map } = $value;
-                   print "$key = '$value'" if $Globals{ 'debug' } > 1;
-                   my $gtag = $GTagsMap{ $map };
-                   if(defined($gtag)) {
-                       $GTags{ $gtag } = $value;
-                       print "GTag = '$gtag'" if $Globals{ 'debug' } > 1;
+=item email_domain $gEmailDomain
+
+The email domain of the bts
+
+=cut
+
+set_default(\%config,'email_domain','bugs.something');
+
+=item list_domain $gListDomain
+
+The list domain of the bts, defaults to the email domain
+
+=cut
+
+set_default(\%config,'list_domain',$config{email_domain});
+
+=item web_host $gWebHost
+
+The web host of the bts; defaults to the email domain
+
+=cut
+
+set_default(\%config,'web_host',$config{email_domain});
+
+=item web_host_bug_dir $gWebHostDir
+
+The directory of the web host on which bugs are kept, defaults to C<''>
+
+=cut
+
+set_default(\%config,'web_host_bug_dir','');
+
+=item web_domain $gWebDomain
+
+Full path of the web domain where bugs are kept, defaults to the
+concatenation of L</web_host> and L</web_host_bug_dir>
+
+=cut
+
+set_default(\%config,'web_domain',$config{web_host}.'/'.$config{web_host_bug_dir});
+
+=item html_suffix $gHTMLSuffix
+
+Suffix of html pages, defaults to .html
+
+=cut
+
+set_default(\%config,'html_suffix','.html');
+
+=item cgi_domain $gCGIDomain
+
+Full path of the web domain where cgi scripts are kept. Defaults to
+the concatentation of L</web_host> and cgi.
+
+=cut
+
+set_default(\%config,'cgi_domain',$config{web_domain}.($config{web_domain}=~m{/$}?'':'/').'cgi');
+
+=item mirrors @gMirrors
+
+List of mirrors [What these mirrors are used for, no one knows.]
+
+=cut
+
+
+set_default(\%config,'mirrors',[]);
+
+=item package_pages  $gPackagePages
+
+Domain where the package pages are kept; links should work in a
+package_pages/foopackage manner. Defaults to undef, which means that
+package links will not be made.
+
+=cut
+
+
+set_default(\%config,'package_pages',undef);
+
+=item subscription_domain $gSubscriptionDomain
+
+Domain where subscriptions to package lists happen
+
+=cut
+
+
+set_default(\%config,'subscription_domain',undef);
+
+=back
+
+=cut
+
+
+=head2 Project Identification
+
+=over
+
+=item project $gProject
+
+Name of the project
+
+Default: 'Something'
+
+=cut
+
+set_default(\%config,'project','Something');
+
+=item project_title $gProjectTitle
+
+Name of this install of Debbugs, defaults to "L</project> Debbugs Install"
+
+Default: "$config{project} Debbugs Install"
+
+=cut
+
+set_default(\%config,'project_title',"$config{project} Debbugs Install");
+
+=item maintainer $gMaintainer
+
+Name of the maintainer of this debbugs install
+
+Default: 'Local DebBugs Owner's
+
+=cut
+
+set_default(\%config,'maintainer','Local DebBugs Owner');
+
+=item maintainer_webpage $gMaintainerWebpage
+
+Webpage of the maintainer of this install of debbugs
+
+Default: "$config{web_domain}/~owner"
+
+=cut
+
+set_default(\%config,'maintainer_webpage',"$config{web_domain}/~owner");
+
+=item maintainer_email
+
+Email address of the maintainer of this Debbugs install
+
+Default: 'root@'.$config{email_domain}
+
+=cut
+
+set_default(\%config,'maintainer_email','root@'.$config{email_domain});
+
+=item unknown_maintainer_email
+
+Email address where packages with an unknown maintainer will be sent
+
+Default: $config{maintainer_email}
+
+=back
+
+=cut
+
+set_default(\%config,'unknown_maintainer_email',$config{maintainer_email});
+
+=head2 BTS Mailing Lists
+
+
+=over
+
+=item submit_list
+
+=item maint_list
+
+=item forward_list
+
+=item done_list
+
+=item request_list
+
+=item submitter_list
+
+=item control_list
+
+=item summary_list
+
+=item mirror_list
+
+=back
+
+=cut
+
+set_default(\%config,   'submit_list',   'bug-submit-list');
+set_default(\%config,    'maint_list',    'bug-maint-list');
+set_default(\%config,    'quiet_list',    'bug-quiet-list');
+set_default(\%config,  'forward_list',  'bug-forward-list');
+set_default(\%config,     'done_list',     'bug-done-list');
+set_default(\%config,  'request_list',  'bug-request-list');
+set_default(\%config,'submitter_list','bug-submitter-list');
+set_default(\%config,  'control_list',  'bug-control-list');
+set_default(\%config,  'summary_list',  'bug-summary-list');
+set_default(\%config,   'mirror_list',   'bug-mirror-list');
+
+=head2 Misc Options
+
+=over
+
+=cut
+
+set_default(\%config,'mailer','exim');
+set_default(\%config,'bug','bug');
+set_default(\%config,'bugs','bugs');
+
+=item remove_age
+
+Age at which bugs are archived/removed
+
+Default: 28
+
+=cut
+
+set_default(\%config,'remove_age',28);
+
+=item save_old_bugs
+
+Whether old bugs are saved or deleted
+
+Default: 1
+
+=cut
+
+set_default(\%config,'save_old_bugs',1);
+
+=item distributions
+
+List of valid distributions
+
+Default: qw(experimental unstable testing stable oldstable);
+
+=cut
+
+set_default(\%config,'distributions',[qw(experimental unstable testing stable oldstable)]);
+
+=item removal_distribution_tags
+
+Tags which specifiy distributions to check
+
+Default: @{$config{distributions}}
+
+=cut
+
+set_default(\%config,'removal_distribution_tags',
+           [@{$config{distributions}}]);
+
+=item removal_default_distribution_tags
+
+For removal/archival purposes, all bugs are assumed to have these tags
+set.
+
+Default: qw(unstable testing);
+
+=cut
+
+set_default(\%config,'removal_default_distribution_tags',
+           [qw(unstable testing)]
+          );
+
+set_default(\%config,'default_severity','normal');
+set_default(\%config,'show_severities','critical, grave, normal, minor, wishlist');
+set_default(\%config,'strong_severities',[qw(critical grave)]);
+set_default(\%config,'severity_list',[qw(critical grave normal wishlist)]);
+set_default(\%config,'severity_display',{critical => "Critical $config{bugs}",
+                                        grave    => "Grave $config{bugs}",
+                                        normal   => "Normal $config{bugs}",
+                                        wishlist => "Wishlist $config{bugs}",
+                                       });
+
+set_default(\%config,'tags',[qw(patch wontfix moreinfo unreproducible fixed),
+                            @{$config{distributions}}
+                           ]);
+
+set_default(\%config,'bounce_froms','^mailer|^da?emon|^post.*mast|^root|^wpuser|^mmdf|^smt.*|'.
+           '^mrgate|^vmmail|^mail.*system|^uucp|-maiser-|^mal\@|'.
+           '^mail.*agent|^tcpmail|^bitmail|^mailman');
+
+set_default(\%config,'config_dir',dirname(exists $ENV{DEBBUGS_CONFIG_FILE}?$ENV{DEBBUGS_CONFIG_FILE}:'/etc/debbugs/config'));
+set_default(\%config,'spool_dir','/var/lib/debbugs/spool');
+set_default(\%config,'incoming_dir','incoming');
+set_default(\%config,'web_dir','/var/lib/debbugs/www');
+set_default(\%config,'doc_dir','/var/lib/debbugs/www/txt');
+set_default(\%config,'lib_path','/usr/lib/debbugs');
+
+set_default(\%config,'maintainer_file',$config{config_dir}.'/Maintainers');
+set_default(\%config,'maintainer_file_override',$config{config_dir}.'/Maintainers.override');
+set_default(\%config,'pseudo_desc_file',$config{config_dir}.'/pseudo-packages.description');
+set_default(\%config,'package_source',$config{config_dir}.'/indices/sources');
+
+set_default(\%config,'version_packages_dir',$config{spool_dir}.'/../versions/pkg');
+
+set_default(\%config,'post_processall',[]);
+
+=item sendmail
+
+Sets the sendmail binary to execute; defaults to /usr/lib/sendmail
+
+=cut
+
+set_default(\%config,'sendmail',$config{sendmail},'/usr/lib/sendmail');
+
+=back
+
+
+=head2 Text Fields
+
+The following are the only text fields in general use in the scripts;
+a few additional text fields are defined in text.in, but are only used
+in db2html and a few other specialty scripts.
+
+Earlier versions of debbugs defined these values in /etc/debbugs/text,
+but now they are required to be in the configuration file. [Eventually
+the longer ones will move out into a fully fledged template system.]
+
+=cut
+
+=over
+
+=item bad_email_prefix
+
+This prefixes the text of all lines in a bad e-mail message ack.
+
+=cut
+
+set_default(\%config,'bad_email_prefix','');
+
+
+=item text_instructions
+
+This gives more information about bad e-mails to receive.in
+
+=cut
+
+set_default(\%config,'text_instructions',$config{bad_email_prefix});
+
+=item html_tail
+
+This shows up at the end of (most) html pages
+
+=cut
+
+set_default(\%config,'html_tail',<<END);
+ <ADDRESS>$config{maintainer} &lt;<A HREF=\"mailto:$config{maintainer_email}\">$config{maintainer_email}</A>&gt;.
+ Last modified:
+ <!--timestamp-->
+ SUBSTITUTE_DTIME
+ <!--timestamp-->
+ <P>
+ <A HREF=\"http://$config{web_domain}/\">Debian $config{bug} tracking system</A><BR>
+ Copyright (C) 1999 Darren O. Benham,
+ 1997,2003 nCipher Corporation Ltd,
+ 1994-97 Ian Jackson.
+ </ADDRESS>
+END
+
+
+=item html_expire_note
+
+This message explains what happens to archive/remove-able bugs
+
+=cut
+
+set_default(\%config,'html_expire_note',
+           "(Closed $config{bugs} are archived $config{remove_age} days after the last related message is received.)");
+
+=back
+
+=cut
+
+
+sub read_config{
+     my ($conf_file) = @_;
+     # first, figure out what type of file we're reading in.
+     my $fh = new IO::File $conf_file,'r'
+         or die "Unable to open configuration file $conf_file for reading: $!";
+     # A new version configuration file must have a comment as its first line
+     my $first_line = <$fh>;
+     my ($version) = defined $first_line?$first_line =~ /VERSION:\s*(\d+)/i:undef;
+     if (defined $version) {
+         if ($version == 1) {
+              # Do something here;
+              die "Version 1 configuration files not implemented yet";
+         }
+         else {
+              die "Version $version configuration files are not supported";
+         }
+     }
+     else {
+         # Ugh. Old configuration file
+         # What we do here is we create a new Safe compartment
+          # so fucked up crap in the config file doesn't sink us.
+         my $cpt = new Safe or die "Unable to create safe compartment";
+         # perldoc Opcode; for details
+         $cpt->permit('require',':filesys_read','entereval','caller','pack','unpack','dofile');
+         $cpt->reval(qq(require '$conf_file';));
+         die "Error in configuration file: $@" if $@;
+         # Now what we do is check out the contents of %EXPORT_TAGS to see exactly which variables
+         # we want to glob in from the configuration file
+         for my $variable (@{$EXPORT_TAGS{globals}}) {
+              my ($hash_name,$glob_name,$glob_type) = __convert_name($variable);
+              my $var_glob = $cpt->varglob($glob_name);
+              my $value; #= $cpt->reval("return $variable");
+              # print STDERR "$variable $value",qq(\n);
+              if (defined $var_glob) {{
+                   no strict 'refs';
+                   if ($glob_type eq '%') {
+                        $value = {%{*{$var_glob}}} if defined *{$var_glob}{HASH};
                    }
-                   print "\n" if $Globals{ 'debug' } > 1;
-                   next;
-               } else {
-                   print "$key\n";
-               }
-                   
-           }
-       }
-       print "Unknown line in config!($_)\n";
-       next;
-    }
-    return @config;
+                   elsif ($glob_type eq '@') {
+                        $value = [@{*{$var_glob}}] if defined *{$var_glob}{ARRAY};
+                   }
+                   else {
+                        $value = ${*{$var_glob}};
+                   }
+                   # We punt here, because we can't tell if the value was
+                    # defined intentionally, or if it was just left alone;
+                    # this tries to set sane defaults.
+                   set_default(\%config,$hash_name,$value) if defined $value;
+              }}
+         }
+     }
 }
 
-END { }       # module clean-up code here (global destructor)
+sub __convert_name{
+     my ($variable) = @_;
+     my $hash_name = $variable;
+     $hash_name =~ s/^([\$\%\@])g//;
+     my $glob_type = $1;
+     my $glob_name = 'g'.$hash_name;
+     $hash_name =~ s/(HTML|CGI)/ucfirst(lc($1))/ge;
+     $hash_name =~ s/^([A-Z]+)/lc($1)/e;
+     $hash_name =~ s/([A-Z]+)/'_'.lc($1)/ge;
+     return $hash_name unless wantarray;
+     return ($hash_name,$glob_name,$glob_type);
+}
+
+# set_default
+
+# sets the configuration hash to the default value if it's not set,
+# otherwise doesn't do anything
+# If $USING_GLOBALS, then sets an appropriate global.
+
+sub set_default{
+     my ($config,$option,$value) = @_;
+     my $varname;
+     if ($USING_GLOBALS) {
+         # fix up the variable name
+         $varname = 'g'.join('',map {ucfirst $_} split /_/, $option);
+         # Fix stupid HTML names
+         $varname =~ s/(Html|Cgi)/uc($1)/ge;
+     }
+     # update the configuration value
+     if (not $USING_GLOBALS and not exists $config->{$option}) {
+         $config->{$option} = $value;
+     }
+     elsif ($USING_GLOBALS) {{
+         no strict 'refs';
+         # Need to check if a value has already been set in a global
+         if (defined *{"Debbugs::Config::${varname}"}) {
+              $config->{$option} = *{"Debbugs::Config::${varname}"};
+         }
+         else {
+              $config->{$option} = $value;
+         }
+     }}
+     if ($USING_GLOBALS) {{
+         no strict 'refs';
+         *{"Debbugs::Config::${varname}"} = $config->{$option};
+     }}
+}
+
+
+### import magick
+
+# All we care about here is whether we've been called with the globals or text option;
+# if so, then we need to export some symbols back up.
+# In any event, we call exporter.
+
+sub import {
+     if (grep /^:(?:text|globals)$/, @_) {
+         $USING_GLOBALS=1;
+         for my $variable (map {@$_} @EXPORT_TAGS{map{(/^:(text|globals)$/?($1):())} @_}) {
+              my $tmp = $variable;
+              no strict 'refs';
+              # Yes, I don't care if these are only used once
+              no warnings 'once';
+              # No, it doesn't bother me that I'm assigning an undefined value to a typeglob
+              no warnings 'misc';
+              my ($hash_name,$glob_name,$glob_type) = __convert_name($variable);
+              $tmp =~ s/^[\%\$\@]//;
+              *{"Debbugs::Config::${tmp}"} = ref($config{$hash_name})?$config{$hash_name}:\$config{$hash_name};
+         }
+     }
+     Debbugs::Config->export_to_level(1,@_);
+}
+
+
+1;
diff --git a/Debbugs/Estraier.pm b/Debbugs/Estraier.pm
new file mode 100644 (file)
index 0000000..a079a20
--- /dev/null
@@ -0,0 +1,170 @@
+
+package Debbugs::Estraier;
+
+=head1 NAME
+
+Debbugs::Estraier -- Routines for interfacing bugs to HyperEstraier
+
+=head1 SYNOPSIS
+
+use Debbugs::Estraier;
+
+
+=head1 DESCRIPTION
+
+
+=head1 BUGS
+
+None known.
+
+=cut
+
+use warnings;
+use strict;
+use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
+use base qw(Exporter);
+use Debbugs::Log;
+#use Params::Validate;
+use Search::Estraier;
+use Date::Manip;
+use Debbugs::Common qw(getbuglocation getbugcomponent);
+use Debbugs::Status qw(readbug);
+
+BEGIN{
+     ($VERSION) = q$Revision: 1.3 $ =~ /^Revision:\s+([^\s+])/;
+     $DEBUG = 0 unless defined $DEBUG;
+
+     @EXPORT = ();
+     %EXPORT_TAGS = (add    => [qw(add_bug_log add_bug_message)],
+                   );
+     @EXPORT_OK = ();
+     Exporter::export_ok_tags(qw(add));
+     $EXPORT_TAGS{all} = [@EXPORT_OK];
+}
+
+
+sub add_bug_log{
+     my ($est,$bug_num) = @_;
+
+     # We want to read the entire bug log, pulling out individual
+     # messages, and shooting them through hyper estraier
+
+     my $location = getbuglocation($bug_num,'log');
+     my $bug_log = getbugcomponent($bug_num,'log',$location);
+     my $log_fh = new IO::File $bug_log, 'r' or
+         die "Unable to open bug log $bug_log for reading: $!";
+
+     my $log = Debbugs::Log->new($log_fh) or
+         die "Debbugs::Log was unable to be initialized";
+
+     my %seen_msg_ids;
+     my $msg_num=0;
+     my $status = {};
+     if (my $location = getbuglocation($bug_num,'summary')) {
+         $status = readbug($bug_num,$location);
+     }
+     while (my $record = $log->read_record()) {
+         $msg_num++;
+         next unless $record->{type} eq 'incoming-recv';
+         my ($msg_id) = $record->{text} =~ /^Message-Id:\s+<(.+)>/im;
+         next if defined $msg_id and exists $seen_msg_ids{$msg_id};
+         $seen_msg_ids{$msg_id} = 1 if defined $msg_id;
+         next if $msg_id =~ /handler\..+\.ack(?:info)?\@/;
+         add_bug_message($est,$record->{text},$bug_num,$msg_num,$status)
+     }
+     return $msg_num;
+}
+
+=head2 remove_old_message
+
+     remove_old_message($est,300000,50);
+
+Removes all messages which are no longer in the log
+
+=cut
+
+sub remove_old_messages{
+     my ($est,$bug_num,$max_message) = @_;
+     # remove records which are no longer present in the log (uri > $msg_num)
+     my $cond = new Search::Estraier::Condition;
+     $cond->add_attr('@uri STRBW '.$bug_num.'/');
+     $cond->set_max(50);
+     my $skip;
+     my $nres;
+     while ($nres = $est->search($cond,0) and $nres->doc_num > 0){
+         for my $rdoc (map {$nres->get_doc($_)} 0..($nres->doc_num-1)) {
+              my $uri = $rdoc->uri;
+              my ($this_message) = $uri =~ m{/(\d+)$};
+              next unless $this_message > $max_message;
+              $est->out_doc_by_uri($uri);
+         }
+         last unless $nres->doc_num >= $cond->max;
+         $cond->set_skip($cond->skip+$cond->max);
+     }
+
+}
+
+sub add_bug_message{
+     my ($est,$bug_message,$bug_num,
+        $msg_num,$status) = @_;
+
+     my $doc;
+     my $uri = "$bug_num/$msg_num";
+     $doc = $est->get_doc_by_uri($uri);
+     $doc = new Search::Estraier::Document if not defined $doc;
+     $doc->add_text($bug_message);
+
+     # * @id : the ID number determined automatically when the document is registered.
+     # * @uri : the location of a document which any document should have.
+     # * @digest : the message digest calculated automatically when the document is registered.
+     # * @cdate : the creation date.
+     # * @mdate : the last modification date.
+     # * @adate : the last access date.
+     # * @title : the title used as a headline in the search result.
+     # * @author : the author.
+     # * @type : the media type.
+     # * @lang : the language.
+     # * @genre : the genre.
+     # * @size : the size.
+     # * @weight : the scoring weight.
+     # * @misc : miscellaneous information.
+     my @attr = qw(status subject date submitter package tags severity);
+     # parse the date
+     my ($date) = $bug_message =~ /^Date:\s+(.+?)\s*$/mi;
+     $doc->add_attr('@cdate' => $date);
+     # parse the title
+     my ($subject) = $bug_message =~ /^Subject:\s+(.+?)\s*$/mi;
+     $doc->add_attr('@title' => $subject);
+     # parse the author
+     my ($author) = $bug_message =~ /^From:\s+(.+?)\s*$/mi;
+     $doc->add_attr('@author' => $author);
+     # create the uri
+     $doc->add_attr('@uri' => $uri);
+     foreach my $attr (@attr) {
+         $doc->add_attr($attr => $status->{$attr});
+     }
+     print STDERR "adding $uri\n" if $DEBUG;
+     # Try a bit harder if estraier is returning timeouts
+     my $attempt = 5;
+     while ($attempt > 0) {
+         $est->put_doc($doc) and last;
+         my $status = $est->status;
+         $attempt--;
+         print STDERR "Failed to add $uri\n".$status."\n";
+         last unless $status =~ /^5/;
+         sleep 20;
+     }
+
+}
+
+
+1;
+
+
+__END__
+
+
+
+
+
+
index 6d311f8af87d0654ee9734d8fc26fbe19551661e..d915c678e4e5caf53083fcd9b12afb9243ad7cd8 100644 (file)
@@ -41,6 +41,8 @@ use POSIX ":sys_wait_h";
 use Time::HiRes qw(usleep);
 use Mail::Address ();
 use Debbugs::MIME qw(encode_rfc1522);
+use Debbugs::Config qw(:config);
+use Params::Validate qw(:types validate_with);
 
 BEGIN{
      ($VERSION) = q$Revision: 1.1 $ =~ /^Revision:\s+([^\s+])/;
@@ -52,6 +54,9 @@ BEGIN{
 
 }
 
+# We set this here so it can be overridden for testing purposes
+our $SENDMAIL = $config{sendmail};
+
 =head2 get_addresses
 
      my @addresses = get_addresses('don@debian.org blars@debian.org
@@ -99,12 +104,26 @@ using warn.
 =cut
 
 sub send_mail_message{
-     die "send_mail_message requires an even number of arguments" if @_ % 2;
-     # It would be better to use Param::Validate instead...
-     my %param = @_;
-
-     die "send_mail_message requires a message" if not defined $param{message};
-
+     my %param = validate_with(params => \@_,
+                              spec  => {sendmail_arguments => {type => ARRAYREF,
+                                                               default => [qw(-odq -oem -oi)],
+                                                              },
+                                        parse_for_recipients => {type => BOOLEAN,
+                                                                 default => 0,
+                                                                },
+                                        encode_headers       => {type => BOOLEAN,
+                                                                 default => 1,
+                                                                },
+                                        message              => {type => SCALAR,
+                                                                },
+                                        envelope_from        => {type => SCALAR,
+                                                                 optional => 1,
+                                                                },
+                                        recipients           => {type => ARRAYREF|UNDEF,
+                                                                 optional => 1,
+                                                                },
+                                       },
+                             );
      my @sendmail_arguments = qw(-odq -oem -oi);
      push @sendmail_arguments, '-f', $param{envelope_from} if exists $param{envelope_from};
 
@@ -183,12 +202,12 @@ sub _send_message{
      my ($message,@sendmail_args) = @_;
 
      my ($wfh,$rfh);
-     my $pid = open3($wfh,$rfh,$rfh,'/usr/lib/sendmail',@sendmail_args)
-         or die "Unable to fork off /usr/lib/sendmail: $!";
+     my $pid = open3($wfh,$rfh,$rfh,$SENDMAIL,@sendmail_args)
+         or die "Unable to fork off $SENDMAIL: $!";
      local $SIG{PIPE} = 'IGNORE';
      eval {
-         print {$wfh} $message or die "Unable to write to /usr/lib/sendmail: $!";
-         close $wfh or die "/usr/lib/sendmail exited with $?";
+         print {$wfh} $message or die "Unable to write to $SENDMAIL: $!";
+         close $wfh or die "$SENDMAIL exited with $?";
      };
      if ($@) {
          local $\;
@@ -206,7 +225,7 @@ sub _send_message{
          usleep(50_000);
      }
      if ($loop >= 600) {
-         warn "Sendmail didn't exit within 30 seconds";
+         warn "$SENDMAIL didn't exit within 30 seconds";
      }
 }
 
index 4ae58a5d9078b569005dd9e7aa1bcf1b4492d2ad..5284e2c58f2aeecd459f32f75d96fd4a11f4efd2 100644 (file)
@@ -1,27 +1,32 @@
 package Debbugs::Packages;
 
+use warnings;
 use strict;
 
-# TODO: move config handling to a separate module
-my $config_path = '/etc/debbugs';
-require "$config_path/config";
-# Allow other modules to load config into their namespace.
-delete $INC{"$config_path/config"};
+use Debbugs::Config qw(:config :globals);
 
-use Exporter ();
-use vars qw($VERSION @ISA @EXPORT);
+use base qw(Exporter);
+use vars qw($VERSION @EXPORT_OK %EXPORT_TAGS @EXPORT);
 
 BEGIN {
     $VERSION = 1.00;
 
-    @ISA = qw(Exporter);
-    @EXPORT = qw(getpkgsrc getpkgcomponent getsrcpkgs
-                binarytosource sourcetobinary);
+     @EXPORT = ();
+     %EXPORT_TAGS = (versions => [qw(getversions)],
+                    mapping  => [qw(getpkgsrc getpkgcomponent getsrcpkgs),
+                                 qw(binarytosource sourcetobinary makesourceversions)
+                                ],
+                   );
+     @EXPORT_OK = ();
+     Exporter::export_ok_tags(qw(versions mapping));
+     $EXPORT_TAGS{all} = [@EXPORT_OK];
 }
 
 use Fcntl qw(O_RDONLY);
 use MLDBM qw(DB_File Storable);
+use Storable qw(dclone);
 
+$MLDBM::DumpMeth = 'portable';
 $MLDBM::RemoveTaint = 1;
 
 =head1 NAME
@@ -49,24 +54,28 @@ source package names.
 
 my $_pkgsrc;
 my $_pkgcomponent;
+my $_srcpkg;
 sub getpkgsrc {
     return $_pkgsrc if $_pkgsrc;
     return {} unless defined $Debbugs::Packages::gPackageSource;
     my %pkgsrc;
     my %pkgcomponent;
+    my %srcpkg;
 
     open(MM,"$Debbugs::Packages::gPackageSource")
-       or &quitcgi("open $Debbugs::Packages::gPackageSource: $!");
+       or die("open $Debbugs::Packages::gPackageSource: $!");
     while(<MM>) {
        next unless m/^(\S+)\s+(\S+)\s+(\S.*\S)\s*$/;
        my ($bin,$cmp,$src)=($1,$2,$3);
        $bin =~ y/A-Z/a-z/;
        $pkgsrc{$bin}= $src;
+       push @{$srcpkg{$src}}, $bin;
        $pkgcomponent{$bin}= $cmp;
     }
     close(MM);
     $_pkgsrc = \%pkgsrc;
     $_pkgcomponent = \%pkgcomponent;
+    $_srcpkg = \%srcpkg;
     return $_pkgsrc;
 }
 
@@ -92,13 +101,9 @@ Returns a list of the binary packages produced by a given source package.
 
 sub getsrcpkgs {
     my $src = shift;
-    return () if !$src;
-    my %pkgsrc = %{getpkgsrc()};
-    my @pkgs;
-    foreach ( keys %pkgsrc ) {
-       push @pkgs, $_ if $pkgsrc{$_} eq $src;
-    }
-    return @pkgs;
+    getpkgsrc() if not defined $_srcpkg;
+    return () if not defined $src or not exists $_srcpkg->{$src};
+    return @{$_srcpkg->{$src}};
 }
 
 =item binarytosource
@@ -117,26 +122,29 @@ sub binarytosource {
 
     # TODO: This gets hit a lot, especially from buggyversion() - probably
     # need an extra cache for speed here.
+    return () unless defined $gBinarySourceMap;
 
     if (tied %_binarytosource or
            tie %_binarytosource, 'MLDBM',
-               $Debbugs::Packages::gBinarySourceMap, O_RDONLY) {
+               $gBinarySourceMap, O_RDONLY) {
        # avoid autovivification
-       if (exists $_binarytosource{$binname} and
-               exists $_binarytosource{$binname}{$binver}) {
+       my $binary = $_binarytosource{$binname};
+       return () unless defined $binary;
+       my %binary = %{$binary};
+       if (exists $binary{$binver}) {
            if (defined $binarch) {
-               my $src = $_binarytosource{$binname}{$binver}{$binarch};
+               my $src = $binary{$binver}{$binarch};
                return () unless defined $src; # not on this arch
                # Copy the data to avoid tiedness problems.
-               return [@$src];
+               return dclone($src);
            } else {
                # Get (srcname, srcver) pairs for all architectures and
                # remove any duplicates. This involves some slightly tricky
                # multidimensional hashing; sorry. Fortunately there'll
                # usually only be one pair returned.
                my %uniq;
-               for my $ar (keys %{$_binarytosource{$binname}{$binver}}) {
-                   my $src = $_binarytosource{$binname}{$binver}{$ar};
+               for my $ar (keys %{$binary{$binver}}) {
+                   my $src = $binary{$binver}{$ar};
                    next unless defined $src;
                    $uniq{$src->[0]}{$src->[1]} = 1;
                }
@@ -171,13 +179,14 @@ sub sourcetobinary {
 
     if (tied %_sourcetobinary or
            tie %_sourcetobinary, 'MLDBM',
-               $Debbugs::Packages::gSourceBinaryMap, O_RDONLY) {
+               $gSourceBinaryMap, O_RDONLY) {
        # avoid autovivification
-       if (exists $_sourcetobinary{$srcname} and
-               exists $_sourcetobinary{$srcname}{$srcver}) {
-           my $bin = $_sourcetobinary{$srcname}{$srcver};
+       my $source = $_sourcetobinary{$srcname};
+       return () unless defined $source;
+       my %source = %{$source};
+       if (exists $source{$srcver}) {
+           my $bin = $source{$srcver};
            return () unless defined $bin;
-           # Copy the data to avoid tiedness problems.
            return @$bin;
        }
     }
@@ -188,6 +197,109 @@ sub sourcetobinary {
     return map [$_, $srcver], @srcpkgs;
 }
 
+=item getversions
+
+Returns versions of the package in a distribution at a specific
+architecture
+
+=cut
+
+my %_versions;
+sub getversions {
+    my ($pkg, $dist, $arch) = @_;
+    return () unless defined $gVersionIndex;
+    $dist = 'unstable' unless defined $dist;
+
+    unless (tied %_versions) {
+        tie %_versions, 'MLDBM', $gVersionIndex, O_RDONLY
+            or die "can't open versions index: $!";
+    }
+    my $version = $_versions{$pkg};
+    return () unless defined $version;
+    my %version = %{$version};
+
+    if (defined $arch and exists $version{$dist}{$arch}) {
+        my $ver = $version{$pkg}{$dist}{$arch};
+        return $ver if defined $ver;
+        return ();
+    } else {
+        my %uniq;
+        for my $ar (keys %{$version{$dist}}) {
+            $uniq{$version{$dist}{$ar}} = 1 unless $ar eq 'source';
+        }
+        if (%uniq) {
+            return keys %uniq;
+        } elsif (exists $version{$dist}{source}) {
+            # Maybe this is actually a source package with no corresponding
+            # binaries?
+            return $version{$dist}{source};
+        } else {
+            return ();
+        }
+    }
+}
+
+
+=item makesourceversions
+
+     @{$cgi_var{found}} = makesourceversions($cgi_var{package},undef,@{$cgi_var{found}});
+
+Canonicalize versions into source versions, which have an explicitly
+named source package. This is used to cope with source packages whose
+names have changed during their history, and with cases where source
+version numbers differ from binary version numbers.
+
+=cut
+
+my %_sourceversioncache = ();
+sub makesourceversions {
+    my $pkg = shift;
+    my $arch = shift;
+    my %sourceversions;
+
+    for my $version (@_) {
+        if ($version =~ m[/]) {
+            # Already a source version.
+            $sourceversions{$version} = 1;
+        } else {
+            my $cachearch = (defined $arch) ? $arch : '';
+            my $cachekey = "$pkg/$cachearch/$version";
+            if (exists($_sourceversioncache{$cachekey})) {
+                for my $v (@{$_sourceversioncache{$cachekey}}) {
+                   $sourceversions{$v} = 1;
+               }
+                next;
+            }
+
+            my @srcinfo = binarytosource($pkg, $version, $arch);
+            unless (@srcinfo) {
+                # We don't have explicit information about the
+                # binary-to-source mapping for this version (yet). Since
+                # this is a CGI script and our output is transient, we can
+                # get away with just looking in the unversioned map; if it's
+                # wrong (as it will be when binary and source package
+                # versions differ), too bad.
+                my $pkgsrc = getpkgsrc();
+                if (exists $pkgsrc->{$pkg}) {
+                    @srcinfo = ([$pkgsrc->{$pkg}, $version]);
+                } elsif (getsrcpkgs($pkg)) {
+                    # If we're looking at a source package that doesn't have
+                    # a binary of the same name, just try the same version.
+                    @srcinfo = ([$pkg, $version]);
+                } else {
+                    next;
+                }
+            }
+            $sourceversions{"$_->[0]/$_->[1]"} = 1 foreach @srcinfo;
+            $_sourceversioncache{$cachekey} = [ map { "$_->[0]/$_->[1]" } @srcinfo ];
+        }
+    }
+
+    return sort keys %sourceversions;
+}
+
+
+
 =back
 
 =cut
diff --git a/Debbugs/SOAP/Status.pm b/Debbugs/SOAP/Status.pm
new file mode 100644 (file)
index 0000000..b452eb0
--- /dev/null
@@ -0,0 +1,22 @@
+package Debbugs::SOAP::Status;
+
+# This is a hack that must be removed
+require '/home/don/projects/debbugs/source/cgi/common.pl';
+#use Debbugs::Status qw(getbugstatus);
+
+sub get_status {
+    my ($class, @bugs) = @_;
+    @bugs = map {ref($_)?@{$_}:$_} @bugs;
+
+    my %s;
+    foreach (@bugs) {
+       my $hash = getbugstatus($_);
+       if (scalar(%{$hash}) > 0) {
+           $s{$_} = $hash;
+       }
+    }
+    
+    return \%s;
+}
+
+1;
diff --git a/Debbugs/SOAP/Usertag.pm b/Debbugs/SOAP/Usertag.pm
new file mode 100644 (file)
index 0000000..2ee7cea
--- /dev/null
@@ -0,0 +1,18 @@
+package Debbugs::SOAP::Usertag;
+
+use Debbugs::User;
+
+sub get_usertag {
+    my ($class, $email, $tag) = @_;
+    my %ut = ();
+    Debbugs::User::read_usertags(\%ut, $email);
+    if (defined($tag) and $tag ne "") {
+       # Remove unwanted tags
+       foreach (keys %ut) {
+           delete $ut{$_} unless $_ eq $tag;
+       }
+    }
+    return \%ut;
+}
+
+1;
diff --git a/Debbugs/Status.pm b/Debbugs/Status.pm
new file mode 100644 (file)
index 0000000..3a7df96
--- /dev/null
@@ -0,0 +1,1010 @@
+
+package Debbugs::Status;
+
+=head1 NAME
+
+Debbugs::Status -- Routines for dealing with summary and status files
+
+=head1 SYNOPSIS
+
+use Debbugs::Status;
+
+
+=head1 DESCRIPTION
+
+This module is a replacement for the parts of errorlib.pl which write
+and read status and summary files.
+
+It also contains generic routines for returning information about the
+status of a particular bug
+
+=head1 FUNCTIONS
+
+=cut
+
+use warnings;
+use strict;
+use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
+use base qw(Exporter);
+
+use Params::Validate qw(validate_with :types);
+use Debbugs::Common qw(:util :lock :quit);
+use Debbugs::Config qw(:config);
+use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522);
+use Debbugs::Packages qw(makesourceversions getversions binarytosource);
+use Debbugs::Versions;
+use Debbugs::Versions::Dpkg;
+use POSIX qw(ceil);
+
+
+BEGIN{
+     $VERSION = 1.00;
+     $DEBUG = 0 unless defined $DEBUG;
+
+     @EXPORT = ();
+     %EXPORT_TAGS = (status => [qw(splitpackages get_bug_status buggy bug_archiveable),
+                               qw(isstrongseverity),
+                              ],
+                    read   => [qw(readbug read_bug lockreadbug)],
+                    write  => [qw(writebug makestatus unlockwritebug)],
+                    versions => [qw(addfoundversions addfixedversions),
+                                 qw(removefoundversions)
+                                ],
+                   );
+     @EXPORT_OK = ();
+     Exporter::export_ok_tags(qw(status read write versions));
+     $EXPORT_TAGS{all} = [@EXPORT_OK];
+}
+
+
+=head2 readbug
+
+     readbug($bug_num,$location)
+     readbug($bug_num)
+
+Reads a summary file from the archive given a bug number and a bug
+location. Valid locations are those understood by L</getbugcomponent>
+
+=cut
+
+
+my %fields = (originator     => 'submitter',
+              date           => 'date',
+              subject        => 'subject',
+              msgid          => 'message-id',
+              'package'      => 'package',
+              keywords       => 'tags',
+              done           => 'done',
+              forwarded      => 'forwarded-to',
+              mergedwith     => 'merged-with',
+              severity       => 'severity',
+              owner          => 'owner',
+              found_versions => 'found-in',
+             found_date     => 'found-date',
+              fixed_versions => 'fixed-in',
+             fixed_date     => 'fixed-date',
+              blocks         => 'blocks',
+              blockedby      => 'blocked-by',
+             );
+
+# Fields which need to be RFC1522-decoded in format versions earlier than 3.
+my @rfc1522_fields = qw(originator subject done forwarded owner);
+
+sub readbug {
+     return read_bug(bug => $_[0],
+                    (@_ > 1)?(location => $_[1]):()
+                   );
+}
+
+=head2 read_bug
+
+     read_bug(bug => $bug_num,
+              location => 'archive',
+             );
+     read_bug(summary => 'path/to/bugnum.summary');
+     read_bug($bug_num);
+
+A more complete function than readbug; it enables you to pass a full
+path to the summary file instead of the bug number and/or location.
+
+=head3 Options
+
+=over
+
+=item bug -- the bug number
+
+=item location -- optional location which is passed to getbugcomponent
+
+=item summary -- complete path to the .summary file which will be read
+
+=back
+
+One of C<bug> or C<summary> must be passed. This function will return
+undef on failure, and will die if improper arguments are passed.
+
+=cut
+
+sub read_bug{
+    if (@_ == 1) {
+        unshift @_, 'bug';
+    }
+    my %param = validate_with(params => \@_,
+                             spec   => {bug => {type => SCALAR,
+                                                optional => 1,
+                                                regex    => qr/^\d+/,
+                                               },
+                                        location => {type => SCALAR|UNDEF,
+                                                     optional => 1,
+                                                    },
+                                        summary  => {type => SCALAR,
+                                                     optional => 1,
+                                                    },
+                                       },
+                            );
+    die "One of bug or summary must be passed to read_bug"
+        if not exists $param{bug} and not exists $param{summary};
+    my $status;
+    if (not defined $param{summary}) {
+        my ($lref, $location) = @param{qw(bug location)};
+        if (not defined $location) {
+             $location = getbuglocation($lref,'summary');
+             return undef if not defined $location;
+        }
+        $status = getbugcomponent($lref, 'summary', $location);
+        return undef unless defined $status;
+    }
+    else {
+        $status = $param{summary};
+    }
+    my $status_fh = new IO::File $status, 'r' or
+        warn "Unable to open $status for reading: $!" and return undef;
+
+    my %data;
+    my @lines;
+    my $version = 2;
+    local $_;
+
+    while (<$status_fh>) {
+        chomp;
+        push @lines, $_;
+        $version = $1 if /^Format-Version: ([0-9]+)/i;
+    }
+
+    # Version 3 is the latest format version currently supported.
+    return undef if $version > 3;
+
+    my %namemap = reverse %fields;
+    for my $line (@lines) {
+        if ($line =~ /(\S+?): (.*)/) {
+            my ($name, $value) = (lc $1, $2);
+            $data{$namemap{$name}} = $value if exists $namemap{$name};
+        }
+    }
+    for my $field (keys %fields) {
+        $data{$field} = '' unless exists $data{$field};
+    }
+
+    $data{severity} = $config{default_severity} if $data{severity} eq '';
+    for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
+        $data{$field} = [split ' ', $data{$field}];
+    }
+    for my $field (qw(found fixed)) {
+        # create the found/fixed hashes which indicate when a
+        # particular version was marked found or marked fixed.
+        @{$data{$field}}{@{$data{"${field}_versions"}}} =
+             (('') x (@{$data{"${field}_date"}} - @{$data{"${field}_versions"}}),
+              @{$data{"${field}_date"}});
+    }
+
+    if ($version < 3) {
+       for my $field (@rfc1522_fields) {
+           $data{$field} = decode_rfc1522($data{$field});
+       }
+    }
+
+    return \%data;
+}
+
+=head2 lockreadbug
+
+     lockreadbug($bug_num,$location)
+
+Performs a filelock, then reads the bug; the bug is unlocked if the
+return is undefined, otherwise, you need to call unfilelock or
+unlockwritebug.
+
+See readbug above for information on what this returns
+
+=cut
+
+sub lockreadbug {
+    my ($lref, $location) = @_;
+    &filelock("lock/$lref");
+    my $data = read_bug(bug => $lref, location => $location);
+    &unfilelock unless defined $data;
+    return $data;
+}
+
+my @v1fieldorder = qw(originator date subject msgid package
+                      keywords done forwarded mergedwith severity);
+
+=head2 makestatus
+
+     my $content = makestatus($status,$version)
+     my $content = makestatus($status);
+
+Creates the content for a status file based on the $status hashref
+passed.
+
+Really only useful for writebug
+
+Currently defaults to version 2 (non-encoded rfc1522 names) but will
+eventually default to version 3. If you care, you should specify a
+version.
+
+=cut
+
+sub makestatus {
+    my ($data,$version) = @_;
+    $version = 2 unless defined $version;
+
+    my $contents = '';
+
+    my %newdata = %$data;
+    for my $field (qw(found fixed)) {
+        if (exists $newdata{$field}) {
+             $newdata{"${field}_date"} =
+                  [map {$newdata{$field}{$_}||''} keys %{$newdata{$field}}];
+        }
+    }
+
+    for my $field (qw(found_versions fixed_versions found_date fixed_date)) {
+        $newdata{$field} = join ' ', @{$newdata{$field}||[]};
+    }
+
+    if ($version < 3) {
+        for my $field (@rfc1522_fields) {
+            $newdata{$field} = encode_rfc1522($newdata{$field});
+        }
+    }
+
+    if ($version == 1) {
+        for my $field (@v1fieldorder) {
+            if (exists $newdata{$field} and defined $newdata{$field}) {
+                $contents .= "$newdata{$field}\n";
+            } else {
+                $contents .= "\n";
+            }
+        }
+    } elsif ($version == 2 or $version == 3) {
+        # Version 2 or 3. Add a file format version number for the sake of
+        # further extensibility in the future.
+        $contents .= "Format-Version: $version\n";
+        for my $field (keys %fields) {
+            if (exists $newdata{$field} and defined $newdata{$field}
+               and $newdata{$field} ne '') {
+                # Output field names in proper case, e.g. 'Merged-With'.
+                my $properfield = $fields{$field};
+                $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
+                $contents .= "$properfield: $newdata{$field}\n";
+            }
+        }
+    }
+
+    return $contents;
+}
+
+=head2 writebug
+
+     writebug($bug_num,$status,$location,$minversion,$disablebughook)
+
+Writes the bug status and summary files out.
+
+Skips writting out a status file if minversion is 2
+
+Does not call bughook if disablebughook is true.
+
+=cut
+
+sub writebug {
+    my ($ref, $data, $location, $minversion, $disablebughook) = @_;
+    my $change;
+
+    my %outputs = (1 => 'status', 2 => 'summary');
+    for my $version (keys %outputs) {
+        next if defined $minversion and $version < $minversion;
+        my $status = getbugcomponent($ref, $outputs{$version}, $location);
+        &quit("can't find location for $ref") unless defined $status;
+        open(S,"> $status.new") || &quit("opening $status.new: $!");
+        print(S makestatus($data, $version)) ||
+            &quit("writing $status.new: $!");
+        close(S) || &quit("closing $status.new: $!");
+        if (-e $status) {
+            $change = 'change';
+        } else {
+            $change = 'new';
+        }
+        rename("$status.new",$status) || &quit("installing new $status: $!");
+    }
+
+    # $disablebughook is a bit of a hack to let format migration scripts use
+    # this function rather than having to duplicate it themselves.
+    &bughook($change,$ref,$data) unless $disablebughook;
+}
+
+=head2 unlockwritebug
+
+     unlockwritebug($bug_num,$status,$location,$minversion,$disablebughook);
+
+Writes a bug, then calls unfilelock; see writebug for what these
+options mean.
+
+=cut
+
+sub unlockwritebug {
+    writebug(@_);
+    &unfilelock;
+}
+
+=head1 VERSIONS
+
+The following functions are exported with the :versions tag
+
+=head2 addfoundversions
+
+     addfoundversions($status,$package,$version,$isbinary);
+
+
+
+=cut
+
+
+sub addfoundversions {
+    my $data = shift;
+    my $package = shift;
+    my $version = shift;
+    my $isbinary = shift;
+    return unless defined $version;
+    undef $package if $package =~ m[(?:\s|/)];
+    my $source = $package;
+
+    if (defined $package and $isbinary) {
+        my @srcinfo = binarytosource($package, $version, undef);
+        if (@srcinfo) {
+            # We know the source package(s). Use a fully-qualified version.
+            addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
+            return;
+        }
+        # Otherwise, an unqualified version will have to do.
+       undef $source;
+    }
+
+    # Strip off various kinds of brain-damage.
+    $version =~ s/;.*//;
+    $version =~ s/ *\(.*\)//;
+    $version =~ s/ +[A-Za-z].*//;
+
+    foreach my $ver (split /[,\s]+/, $version) {
+        my $sver = defined($source) ? "$source/$ver" : '';
+        unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
+            push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
+        }
+        @{$data->{fixed_versions}} =
+            grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
+    }
+}
+
+=head2 removefoundversions
+
+     removefoundversions($data,$package,$versiontoremove)
+
+Removes found versions from $data
+
+If a version is fully qualified (contains /) only versions matching
+exactly are removed. Otherwise, all versions matching the version
+number are removed.
+
+Currently $package and $isbinary are entirely ignored, but accepted
+for backwards compatibilty.
+
+=cut
+
+sub removefoundversions {
+    my $data = shift;
+    my $package = shift;
+    my $version = shift;
+    my $isbinary = shift;
+    return unless defined $version;
+
+    foreach my $ver (split /[,\s]+/, $version) {
+        if ($ver =~ m{/}) {
+             # fully qualified version
+             @{$data->{found_versions}} =
+                  grep {$_ ne $ver}
+                       @{$data->{found_versions}};
+        }
+        else {
+             # non qualified version; delete all matchers
+             @{$data->{found_versions}} =
+                  grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
+                       @{$data->{found_versions}};
+        }
+    }
+}
+
+
+sub addfixedversions {
+    my $data = shift;
+    my $package = shift;
+    my $version = shift;
+    my $isbinary = shift;
+    return unless defined $version;
+    undef $package if $package =~ m[(?:\s|/)];
+    my $source = $package;
+
+    if (defined $package and $isbinary) {
+        my @srcinfo = binarytosource($package, $version, undef);
+        if (@srcinfo) {
+            # We know the source package(s). Use a fully-qualified version.
+            addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
+            return;
+        }
+        # Otherwise, an unqualified version will have to do.
+        undef $source;
+    }
+
+    # Strip off various kinds of brain-damage.
+    $version =~ s/;.*//;
+    $version =~ s/ *\(.*\)//;
+    $version =~ s/ +[A-Za-z].*//;
+
+    foreach my $ver (split /[,\s]+/, $version) {
+        my $sver = defined($source) ? "$source/$ver" : '';
+        unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
+            push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
+        }
+        @{$data->{found_versions}} =
+            grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
+    }
+}
+
+sub removefixedversions {
+    my $data = shift;
+    my $package = shift;
+    my $version = shift;
+    my $isbinary = shift;
+    return unless defined $version;
+    undef $package if $package =~ m[(?:\s|/)];
+    my $source = $package;
+
+    if (defined $package and $isbinary) {
+        my @srcinfo = binarytosource($package, $version, undef);
+        if (@srcinfo) {
+            # We know the source package(s). Use a fully-qualified version.
+            removefixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
+            return;
+        }
+        # Otherwise, an unqualified version will have to do.
+        undef $source;
+    }
+
+    foreach my $ver (split /[,\s]+/, $version) {
+        my $sver = defined($source) ? "$source/$ver" : '';
+        @{$data->{fixed_versions}} =
+            grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
+    }
+}
+
+
+
+=head2 splitpackages
+
+     splitpackages($pkgs)
+
+Split a package string from the status file into a list of package names.
+
+=cut
+
+sub splitpackages {
+    my $pkgs = shift;
+    return unless defined $pkgs;
+    return map lc, split /[ \t?,()]+/, $pkgs;
+}
+
+
+=head2 bug_archiveable
+
+     bug_archiveable(bug => $bug_num);
+
+Options
+
+=over
+
+=item bug -- bug number (required)
+
+=item status -- Status hashref returned by read_bug or get_bug_status (optional)
+
+=item version -- Debbugs::Version information (optional)
+
+=item days_until -- return days until the bug can be archived
+
+=back
+
+Returns 1 if the bug can be archived
+Returns 0 if the bug cannot be archived
+
+If days_until is true, returns the number of days until the bug can be
+archived, -1 if it cannot be archived. 0 means that the bug can be
+archived the next time the archiver runs.
+
+Returns undef on failure.
+
+=cut
+
+# This will eventually need to be fixed before we start using mod_perl
+my $version_cache = {};
+sub bug_archiveable{
+     my %param = validate_with(params => \@_,
+                              spec   => {bug => {type => SCALAR,
+                                                 regex => qr/^\d+$/,
+                                                },
+                                         status => {type => HASHREF,
+                                                    optional => 1,
+                                                   },
+                                         days_until => {type => BOOLEAN,
+                                                        default => 0,
+                                                       },
+                                        },
+                             );
+     # This is what we return if the bug cannot be archived.
+     my $cannot_archive = $param{days_until}?-1:0;
+     # read the status information
+     my $status = $param{status};
+     if (not exists $param{status} or not defined $status) {
+         $status = read_bug(bug=>$param{bug});
+         return undef if not defined $status;
+     }
+     # Bugs can be archived if they are
+     # 1. Closed
+     return $cannot_archive if not defined $status->{done} or not length $status->{done};
+     # If we just are checking if the bug can be archived, we'll not even bother
+     # checking the versioning information if the bug has been -done for less than 28 days.
+     if (not $param{days_until} and $config{remove_age} >
+        -M getbugcomponent($param{ref},'log')
+       ) {
+         return $cannot_archive;
+     }
+     # At this point, we have to get the versioning information for this bug.
+     # We examine the set of distribution tags. If a bug has no distribution
+     # tags set, we assume a default set, otherwise we use the tags the bug
+     # has set.
+
+     # There must be fixed_versions for us to look at the versioning
+     # information
+     if (@{$status->{fixed_versions}}) {
+         my %dist_tags;
+         @dist_tags{@{$config{removal_distribution_tags}}} =
+              (1) x @{$config{removal_distribution_tags}};
+         my %dists;
+         @dists{@{$config{removal_default_distribution_tags}}} = 
+              (1) x @{$config{removal_default_distribution_tags}};
+         for my $tag (split ' ', $status->{tags}) {
+              next unless $dist_tags{$tag};
+              $dists{$tag} = 1;
+         }
+         my %source_versions;
+         for my $dist (keys %dists){
+              my @versions;
+              @versions = getversions($status->{package},
+                                      $dist,
+                                      undef);
+              # TODO: This should probably be handled further out for efficiency and
+              # for more ease of distinguishing between pkg= and src= queries.
+              my @sourceversions = makesourceversions($status->{package},
+                                                      $dist,
+                                                      @versions);
+              @source_versions{@sourceversions} = (1) x @sourceversions;
+         }
+         if ('found' eq max_buggy(bug => $param{bug},
+                                  sourceversions => [keys %source_versions],
+                                  found          => $status->{found_versions},
+                                  fixed          => $status->{fixed_versions},
+                                  version_cache  => $version_cache,
+                                  package        => $status->{package},
+                                 )) {
+              return $cannot_archive;
+         }
+     }
+     # 6. at least 28 days have passed since the last action has occured or the bug was closed
+     # XXX We still need some more work here before we actually can archive;
+     # we really need to track when a bug was closed in a version.
+     my $age = ceil($config{remove_age} - -M getbugcomponent($param{bug},'log'));
+     if ($age > 0 ) {
+         return $param{days_until}?$age:0;
+     }
+     else {
+         return $param{days_until}?0:1;
+     }
+}
+
+
+=head2 get_bug_status
+
+     my $status = get_bug_status(bug => $nnn);
+
+     my $status = get_bug_status($bug_num)
+
+=head3 Options
+
+=over
+
+=item bug -- scalar bug number
+
+=item status -- optional hashref of bug status as returned by readbug
+(can be passed to avoid rereading the bug information)
+
+=item bug_index -- optional tied index of bug status infomration;
+currently not correctly implemented.
+
+=item version -- optional version to check package status at
+
+=item dist -- optional distribution to check package status at
+
+=item arch -- optional architecture to check package status at
+
+=item usertags -- optional hashref of usertags
+
+=item sourceversion -- optional arrayref of source/version; overrides
+dist, arch, and version. [The entries in this array must be in the
+"source/version" format.] Eventually this can be used to for caching.
+
+=back
+
+Note: Currently the version information is cached; this needs to be
+changed before using this function in long lived programs.
+
+=cut
+
+sub get_bug_status {
+     if (@_ == 1) {
+         unshift @_, 'bug';
+     }
+     my %param = validate_with(params => \@_,
+                              spec   => {bug       => {type => SCALAR,
+                                                       regex => qr/^\d+$/,
+                                                      },
+                                         status    => {type => HASHREF,
+                                                       optional => 1,
+                                                      },
+                                         bug_index => {type => OBJECT,
+                                                       optional => 1,
+                                                      },
+                                         version   => {type => SCALAR,
+                                                       optional => 1,
+                                                      },
+                                         dist       => {type => SCALAR,
+                                                        optional => 1,
+                                                       },
+                                         arch       => {type => SCALAR,
+                                                        optional => 1,
+                                                       },
+                                         usertags   => {type => HASHREF,
+                                                        optional => 1,
+                                                       },
+                                         sourceversions => {type => ARRAYREF,
+                                                            optional => 1,
+                                                           },
+                                        },
+                             );
+     my %status;
+
+     if (defined $param{bug_index} and
+        exists $param{bug_index}{$param{bug}}) {
+         %status = %{ $param{bug_index}{$param{bug}} };
+         $status{pending} = $status{ status };
+         $status{id} = $param{bug};
+         return \%status;
+     }
+     if (defined $param{status}) {
+         %status = %{$param{status}};
+     }
+     else {
+         my $location = getbuglocation($param{bug}, 'summary');
+         return {} if not length $location;
+         %status = %{ readbug( $param{bug}, $location ) };
+     }
+     $status{id} = $param{bug};
+
+     if (defined $param{usertags}{$param{bug}}) {
+         $status{keywords} = "" unless defined $status{keywords};
+         $status{keywords} .= " " unless $status{keywords} eq "";
+         $status{keywords} .= join(" ", @{$param{usertags}{$param{bug}}});
+     }
+     $status{tags} = $status{keywords};
+     my %tags = map { $_ => 1 } split ' ', $status{tags};
+
+     $status{"package"} =~ s/\s*$//;
+     $status{"package"} = 'unknown' if ($status{"package"} eq '');
+     $status{"severity"} = 'normal' if ($status{"severity"} eq '');
+
+     $status{"pending"} = 'pending';
+     $status{"pending"} = 'forwarded'      if (length($status{"forwarded"}));
+     $status{"pending"} = 'pending-fixed'    if ($tags{pending});
+     $status{"pending"} = 'fixed'          if ($tags{fixed});
+
+     my @sourceversions;
+     if (not exists $param{sourceversions}) {
+         my @versions;
+         if (defined $param{version}) {
+              @versions = ($param{version});
+         } elsif (defined $param{dist}) {
+              @versions = getversions($status{package}, $param{dist}, $param{arch});
+         }
+
+         # TODO: This should probably be handled further out for efficiency and
+         # for more ease of distinguishing between pkg= and src= queries.
+         @sourceversions = makesourceversions($status{package},
+                                              $param{arch},
+                                              @versions);
+     }
+     else {
+         @sourceversions = @{$param{sourceversions}};
+     }
+     if (@sourceversions) {
+         my $maxbuggy = max_buggy(bug => $param{bug},
+                                  sourceversions => \@sourceversions,
+                                  found => $status{found_versions},
+                                  fixed => $status{fixed_versions},
+                                  package => $status{package},
+                                  version_cache => $version_cache,
+                                 );
+         if ($maxbuggy eq 'absent') {
+              $status{pending} = 'absent';
+         }
+         elsif ($maxbuggy eq 'fixed' ) {
+              $status{pending} = 'done';
+         }
+     }
+     if (length($status{done}) and
+        (not @sourceversions or not @{$status{fixed_versions}})) {
+         $status{"pending"} = 'done';
+     }
+
+     return \%status;
+}
+
+
+=head2 max_buggy
+
+     max_buggy()
+
+=head3 Options
+
+=over
+
+=item bug -- scalar bug number
+
+=item sourceversion -- optional arrayref of source/version; overrides
+dist, arch, and version. [The entries in this array must be in the
+"source/version" format.] Eventually this can be used to for caching.
+
+=back
+
+Note: Currently the version information is cached; this needs to be
+changed before using this function in long lived programs.
+
+
+=cut
+sub max_buggy{
+     my %param = validate_with(params => \@_,
+                              spec   => {bug       => {type => SCALAR,
+                                                       regex => qr/^\d+$/,
+                                                      },
+                                         sourceversions => {type => ARRAYREF,
+                                                            default => [],
+                                                           },
+                                         found          => {type => ARRAYREF,
+                                                            default => [],
+                                                           },
+                                         fixed          => {type => ARRAYREF,
+                                                            default => [],
+                                                           },
+                                         package        => {type => SCALAR,
+                                                           },
+                                         version_cache  => {type => HASHREF,
+                                                            default => {},
+                                                           },
+                                        },
+                             );
+     # Resolve bugginess states (we might be looking at multiple
+     # architectures, say). Found wins, then fixed, then absent.
+     my $maxbuggy = 'absent';
+     for my $version (@{$param{sourceversions}}) {
+         my $buggy = buggy(bug => $param{bug},
+                           version => $version,
+                           found => $param{found},
+                           fixed => $param{fixed},
+                           version_cache => $param{version_cache},
+                           package => $param{package},
+                          );
+         if ($buggy eq 'found') {
+              return 'found';
+         } elsif ($buggy eq 'fixed') {
+              $maxbuggy = 'fixed';
+         }
+     }
+     return $maxbuggy;
+}
+
+
+=head2 buggy
+
+     buggy(bug => nnn,
+           found => \@found,
+           fixed => \@fixed,
+           package => 'foo',
+           version => '1.0',
+          );
+
+Returns the output of Debbugs::Versions::buggy for a particular
+package, version and found/fixed set. Automatically turns found, fixed
+and version into source/version strings.
+
+Caching can be had by using the version_cache, but no attempt to check
+to see if the on disk information is more recent than the cache is
+made. [This will need to be fixed for long-lived processes.]
+
+=cut
+
+sub buggy {
+     my %param = validate_with(params => \@_,
+                              spec   => {bug => {type => SCALAR,
+                                                 regex => qr/^\d+$/,
+                                                },
+                                         found => {type => ARRAYREF,
+                                                   default => [],
+                                                  },
+                                         fixed => {type => ARRAYREF,
+                                                   default => [],
+                                                  },
+                                         version_cache => {type => HASHREF,
+                                                           optional => 1,
+                                                          },
+                                         package => {type => SCALAR,
+                                                    },
+                                         version => {type => SCALAR,
+                                                    },
+                                        },
+                             );
+     my @found = @{$param{found}};
+     my @fixed = @{$param{fixed}};
+     if (grep {$_ !~ m{/}} (@{$param{found}}, @{$param{fixed}})) {
+         # We have non-source version versions
+         @found = makesourceversions($param{package},undef,
+                                     @found
+                                    );
+         @fixed = makesourceversions($param{package},undef,
+                                     @fixed
+                                    );
+     }
+     if ($param{version} !~ m{/}) {
+         $param{version} = makesourceversions($param{package},undef,
+                                              $param{version}
+                                             );
+     }
+     # Figure out which source packages we need
+     my %sources;
+     @sources{map {m{(.+)/}; $1} @found} = (1) x @found;
+     @sources{map {m{(.+)/}; $1} @fixed} = (1) x @fixed;
+     @sources{map {m{(.+)/}; $1} $param{version}} = 1;
+     my $version;
+     if (not defined $param{version_cache} or
+        not exists $param{version_cache}{join(',',sort keys %sources)}) {
+         $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
+         foreach my $source (keys %sources) {
+              my $srchash = substr $source, 0, 1;
+              my $version_fh = new IO::File "$config{version_packages_dir}/$srchash/$source", 'r' or
+                   warn "Unable to open $config{version_packages_dir}/$srchash/$source: $!" and next;
+              $version->load($version_fh);
+         }
+         if (defined $param{version_cache}) {
+              $param{version_cache}{join(',',sort keys %sources)} = $version;
+         }
+     }
+     else {
+         $version = $param{version_cache}{join(',',sort keys %sources)};
+     }
+     return $version->buggy($param{version},\@found,\@fixed);
+}
+
+sub isstrongseverity {
+    my $severity = shift;
+    $severity = $config{default_severity} if $severity eq '';
+    return grep { $_ eq $severity } @{$config{strong_severities}};
+}
+
+
+=head1 PRIVATE FUNCTIONS
+
+=cut
+
+sub update_realtime {
+       my ($file, $bug, $new) = @_;
+
+       # update realtime index.db
+
+       open(IDXDB, "<$file") or die "Couldn't open $file";
+       open(IDXNEW, ">$file.new");
+
+       my $line;
+       my @line;
+       while($line = <IDXDB>) {
+               @line = split /\s/, $line;
+               last if ($line[1] >= $bug);
+               print IDXNEW $line;
+               $line = "";
+       }
+
+       if ($new eq "NOCHANGE") {
+               print IDXNEW $line if ($line ne ""  and $line[1] == $bug);
+       } elsif ($new eq "REMOVE") {
+               0;
+       } else {
+               print IDXNEW $new;
+       }
+       if (defined $line and $line ne "" and  @line and $line[1] > $bug) {
+               print IDXNEW $line;
+               $line = "";
+       }
+
+       print IDXNEW while(<IDXDB>);
+
+       close(IDXNEW);
+       close(IDXDB);
+
+       rename("$file.new", $file);
+
+       return $line;
+}
+
+sub bughook_archive {
+       my $ref = shift;
+       &filelock("debbugs.trace.lock");
+       &appendfile("debbugs.trace","archive $ref\n");
+       my $line = update_realtime(
+               "$config{spool_dir}/index.db.realtime", 
+               $ref,
+               "REMOVE");
+       update_realtime("$config{spool_dir}/index.archive.realtime",
+               $ref, $line);
+       &unfilelock;
+}
+
+sub bughook {
+       my ( $type, $ref, $data ) = @_;
+       &filelock("debbugs.trace.lock");
+
+       &appendfile("debbugs.trace","$type $ref\n",makestatus($data, 1));
+
+       my $whendone = "open";
+       my $severity = $config{default_severity};
+       (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
+       $pkglist =~ s/^,+//;
+       $pkglist =~ s/,+$//;
+       $whendone = "forwarded" if defined $data->{forwarded} and length $data->{forwarded};
+       $whendone = "done" if defined $data->{done} and length $data->{done};
+       $severity = $data->{severity} if length $data->{severity};
+
+       my $k = sprintf "%s %d %d %s [%s] %s %s\n",
+                       $pkglist, $ref, $data->{date}, $whendone,
+                       $data->{originator}, $severity, $data->{keywords};
+
+       update_realtime("$config{spool_dir}/index.db.realtime", $ref, $k);
+
+       &unfilelock;
+}
+
+
+1;
+
+__END__
diff --git a/Debbugs/URI.pm b/Debbugs/URI.pm
new file mode 100644 (file)
index 0000000..307f11f
--- /dev/null
@@ -0,0 +1,94 @@
+package Debbugs::URI;
+
+=head1 NAME
+
+Debbugs::URI -- Derivative of URI which overrides the query_param
+ method to use ';' instead of '&' for separators.
+
+=head1 SYNOPSIS
+
+use Debbugs::URI;
+
+=head1 DESCRIPTION
+
+See L<URI> for more information.
+
+=head1 BUGS
+
+None known.
+
+=cut
+
+use warnings;
+use strict;
+use base qw(URI URI::_query);
+
+=head2 query_param
+
+     $uri->query_form( $key1 => $val1, $key2 => $val2, ... )
+
+Exactly like query_param in L<URI> except query elements are joined by
+; instead of &.
+
+=cut
+
+{
+
+     package URI::_query;
+
+     no warnings 'redefine';
+     # Handle ...?foo=bar&bar=foo type of query
+     sub URI::_query::query_form {
+         my $self = shift;
+         my $old = $self->query;
+         if (@_) {
+              # Try to set query string
+              my @new = @_;
+              if (@new == 1) {
+                   my $n = $new[0];
+                   if (ref($n) eq "ARRAY") {
+                        @new = @$n;
+                   }
+                   elsif (ref($n) eq "HASH") {
+                        @new = %$n;
+                   }
+              }
+              my @query;
+              while (my($key,$vals) = splice(@new, 0, 2)) {
+                   $key = '' unless defined $key;
+                   $key =~ s/([;\/?:@&=+,\$\[\]%])/$URI::Escape::escapes{$1}/g;
+                   $key =~ s/ /+/g;
+                   $vals = [ref($vals) eq "ARRAY" ? @$vals : $vals];
+                   for my $val (@$vals) {
+                        $val = '' unless defined $val;
+                        $val =~ s/([;\/?:@&=+,\$\[\]%])/$URI::Escape::escapes{$1}/g;
+                        $val =~ s/ /+/g;
+                        push(@query, "$key=$val");
+                   }
+              }
+              # We've changed & to a ; here.
+              $self->query(@query ? join(';', @query) : undef);
+         }
+         return if !defined($old) || !length($old) || !defined(wantarray);
+         return unless $old =~ /=/; # not a form
+         map { s/\+/ /g; uri_unescape($_) }
+              # We've also changed the split here to split on ; as well as &
+              map { /=/ ? split(/=/, $_, 2) : ($_ => '')} split(/[&;]/, $old);
+     }
+}
+
+
+
+
+
+
+1;
+
+
+__END__
+
+
+
+
+
+
index d55860b2cd409b4556576237912590facaa1db6b..7888f779a0f9795a2b7c29da37ec530c2db6f72a 100644 (file)
@@ -44,6 +44,8 @@ use Fcntl ':flock';
 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
 use base qw(Exporter);
 
+use Debbugs::Config qw(:globals);
+
 BEGIN {
     ($VERSION) = q$Revision: 1.4 $ =~ /^Revision:\s+([^\s+])/;
     $DEBUG = 0 unless defined $DEBUG;
@@ -53,8 +55,6 @@ BEGIN {
     $EXPORT_TAGS{all} = [@EXPORT_OK];
 }
 
-my $gSpoolPath = "/org/bugs.debian.org/spool";
-
 # Obsolete compatability functions
 
 sub read_usertags {
@@ -83,7 +83,7 @@ sub write_usertags {
 sub filefromemail {
     my $e = shift;
     my $l = length($e) % 7;
-    return "$gSpoolPath/user/$l/" . join("", 
+    return "$gSpoolDir/user/$l/" . join("", 
         map { m/^[0-9a-zA-Z_+.-]$/ ? $_ : sprintf("%%%02X", ord($_)) }
             split //, $e);
 }
diff --git a/bin/add_bug_to_estraier b/bin/add_bug_to_estraier
new file mode 100755 (executable)
index 0000000..f6d0cd5
--- /dev/null
@@ -0,0 +1,233 @@
+#!/usr/bin/perl
+# add_bug_to_estraier adds a log for a bug to the estaier db, and is
+# released under the terms of the GPL version 2, or any later version,
+# at your option. See the file README and COPYING for more
+# information.
+# Copyright 2006 by Don Armstrong <don@debian.org>.
+
+
+
+use warnings;
+use strict;
+
+
+use Getopt::Long;
+use Pod::Usage;
+
+=head1 NAME
+
+add_bug_to_estraier
+
+=head1 SYNOPSIS
+
+add_bug_to_estraier [options] < list_of_bugs_to_add
+
+ Options:
+  --debug, -d debugging level (Default 0)
+  --help, -h display this help
+  --man, -m display manual
+
+=head1 OPTIONS
+
+=over
+
+=item B<--url, -u>
+
+Url to the estraier node
+
+=item B<--user,-U>
+
+User to log onto the estraier node
+
+=item B<--pass,-P>
+
+Password to log onto the estraier node
+
+=item B<--spool,-s>
+
+Spool location; if not set defaults to /etc/debbugs/config
+
+=item B<--conf,-C>
+
+Configuration file; a set of key = value pairs separated by newlines;
+the long name of any option is the name that the configuration file
+takes
+
+=item B<--cron>
+
+Descend through the spool and add all of the bugs to estraier
+
+=item B<--timestamp>
+
+Use the timestamp file to only add new bugs; will lock the timestamp
+file to avoid racing with other invocations
+
+=item B<--debug, -d>
+
+Debug verbosity. (Default 0)
+
+=item B<--help, -h>
+
+Display brief useage information.
+
+=item B<--man, -m>
+
+Display this manual.
+
+=back
+
+
+=head1 EXAMPLES
+
+  test_bts --bug 7 --host donbugs.donarmstrong.com
+
+
+=cut
+
+
+use Debbugs::Config qw(:globals);
+use Debbugs::Mail qw(send_mail_message);
+use Debbugs::MIME qw(create_mime_message);
+
+use Search::Estraier;
+use Debbugs::Estraier qw(:add);
+use File::Find;
+use File::stat;
+
+use vars qw($DEBUG $VERBOSE);
+
+# XXX parse config file
+
+my %options = (debug           => 0,
+              help            => 0,
+              man             => 0,
+              url             => undef,
+              user            => undef,
+              passwd          => undef,
+              spool           => undef,
+              conf            => undef,
+              cron            => 0,
+              timestamp       => undef,
+             );
+
+GetOptions(\%options,'url|u=s','user|U=s','passwd|P=s',
+          'spool|s=s','conf|C=s','cron!','timestamp=s',
+          'debug|d+','help|h|?','man|m');
+
+my $ERRORS = '';
+
+if (not defined $options{conf}) {
+     $ERRORS .= "--url must be set\n" if not defined $options{url};
+     $ERRORS .= "--user must be set\n" if not defined $options{user};
+     $ERRORS .= "--passwd must be set\n" if not defined $options{passwd};
+}
+else {
+     # Read the conf file
+     my $conf_fh = new IO::File $options{conf},'r'
+         or die "Unable to open $options{conf} for reading";
+     while (<$conf_fh>) {
+         chomp;
+         next if /^\s*\#/;
+         my ($key,$value) = split /\s*[:=]\s*/,$_,2;
+         $options{$key} = $value if defined $key and not defined $options{$key};
+     }
+     $ERRORS .= "url must be set\n" if not defined $options{url};
+     $ERRORS .= "user must be set\n" if not defined $options{user};
+     $ERRORS .= "passwd must be set\n" if not defined $options{passwd};
+}
+$ERRORS .= "--spool must be set if --cron is used\n" if
+     not defined $options{spool} and $options{cron};
+pod2usage($ERRORS) if length $ERRORS;
+
+pod2usage() if $options{help};
+pod2usage({verbose=>2}) if $options{man};
+
+
+$DEBUG = $options{debug};
+
+$Debbugs::Estraier::DEBUG = $DEBUG;
+$VERBOSE = 0;
+
+my $node =  new Search::Estraier::Node (url    => $options{url},
+                                       user   => $options{user},
+                                       passwd => $options{passwd},
+                                      );
+$gSpoolDir = $options{spool} if defined $options{spool};
+
+if ($options{cron}) {
+     my %timestamps;
+     my $start_time = time;
+     my $unlink = 0;
+     my %seen_dirs;
+     check_pid($options{timestamp});
+     # read timestamp file
+     if (defined $options{timestamp}) {
+         my $timestamp_fh = new IO::File $options{timestamp},'r' or
+              die "Unable to open timestamp $options{timestamp}: $!";
+         while (<$timestamp_fh>) {
+              chomp;
+              my ($key,$value) = split /\s+/,$_,2;
+              $timestamps{$key} = $value;
+         }
+     }
+     for my $hash (map {sprintf '%02d',$_ } 0..99) {
+         find(sub {
+                   print STDERR "Examining $_\n" if $DEBUG > 1;
+                   return if not /^(\d+)\.log$/;
+                   my $bug_num = $1;
+                   my $stat = stat $_ or next;
+                   return unless -f _;
+                   return if exists $timestamps{$File::Find::dir} and
+                        ($timestamps{$File::Find::dir} > $stat->mtime);
+                   $seen_dirs{$File::Find::dir} = $start_time;
+                   print STDERR "Adding $bug_num\n" if $DEBUG;
+                   my $max_message = 0;
+                   eval{
+                        $max_message = add_bug_log($node,$bug_num);
+                   };
+                   if ($@) {
+                        print STDERR "Adding $bug_num failed with $@\n";
+                   }
+              },
+              map {(-d "$options{spool}/$_/$hash")?
+                        "$options{spool}/$_/$hash":()}
+              qw(db-h archive),
+             );
+         # write timestamp file
+         if (defined $options{timestamp}) {
+              %timestamps = (%timestamps,%seen_dirs);
+              my $timestamp_fh = new IO::File $options{timestamp},'w' or
+                   die "Unable to open timestamp $options{timestamp}: $!";
+              foreach my $key (keys %timestamps) {
+                   print {$timestamp_fh} $key,' ',
+                        $timestamps{$key}||'',qq(\n);
+              }
+         }
+     }
+     unlink("$options{timestamp}.pid");
+}
+else {
+     while (my $bug_num = <STDIN>) {
+         chomp $bug_num;
+         add_bug_log($node,$bug_num);
+     }
+}
+
+
+sub check_pid{
+     my ($timestamp) = @_;
+     if (-e "${timestamp}.pid") {
+         my $time_fh = new IO::File  "${timestamp}.pid", 'r' or die "Unable to read pidfile";
+         local $/;
+         my $pid = <$time_fh>;
+         if (kill(0,$pid)) {
+              print STDERR "Another cron is running" and exit 0;
+         }
+     }
+     my $time_fh = new IO::File  "${timestamp}.pid", 'w' or
+         die "Unable to read pidfile";
+     print {$time_fh} $$;
+}
+
+
+__END__
diff --git a/bin/test_bts b/bin/test_bts
new file mode 100755 (executable)
index 0000000..d9cf761
--- /dev/null
@@ -0,0 +1,178 @@
+#!/usr/bin/perl
+# test_bts tests a running BTS by sending mail to it, and is released
+# under the terms of the GPL version 2, or any later version, at your
+# option. See the file README and COPYING for more information.
+# Copyright 2006 by Don Armstrong <don@debian.org>.
+
+
+
+use warnings;
+use strict;
+
+
+use Getopt::Long;
+use Pod::Usage;
+
+=head1 NAME
+
+test_bts - Test a running bts install
+
+=head1 SYNOPSIS
+
+test_bts [options]
+
+ Options:
+  --bug, -b bug number to mail
+  --host, -h host to send mail to
+  --control, -c whether to send control messages (off by default)
+  --process, -p whether to send process messages (on by default)
+  --submit, -s whether a new bug is created (off by default)
+  --quiet, -q disable output (off by default)
+  --debug, -d debugging level (Default 0)
+  --help, -h display this help
+  --man, -m display manual
+
+=head1 OPTIONS
+
+=over
+
+=item B<--bug, -b>
+
+Bug number to mail
+
+=item B<--host, -H>
+
+The host running the bts
+
+=item B<--control, -c>
+
+Whether control messages are sent; defaults to false.
+
+=item B<--process, -p>
+
+Whether messages are sent to process (bugnum@host)
+
+=item B<--submit, -s>
+
+Whether a new bug is created by a message to submit; not enabled by default.
+
+=item B<--quiet,-q>
+
+Disable output
+
+=item B<--debug, -d>
+
+Debug verbosity. (Default 0)
+
+=item B<--help, -h>
+
+Display brief useage information.
+
+=item B<--man, -m>
+
+Display this manual.
+
+=back
+
+=head1 EXAMPLES
+
+  test_bts --bug 7 --host donbugs.donarmstrong.com
+
+
+=cut
+
+
+use Debbugs::Mail qw(send_mail_message);
+use Debbugs::MIME qw(create_mime_message);
+
+
+use vars qw($DEBUG $VERBOSE);
+
+# XXX parse config file
+
+my %options = (debug           => 0,
+              help            => 0,
+              man             => 0,
+              host            => undef,
+              bug             => undef,
+              quiet           => 0,
+              from            => undef,
+              process         => 1,
+              submit          => 0,
+              control         => 0,
+             );
+
+GetOptions(\%options,'host|H=s','bug|b=s','control|c!','submit|s!',
+          'process|p!','from|f=s','quiet|q+',
+          'debug|d+','help|h|?','man|m');
+
+my $ERRORS = '';
+
+$ERRORS .= "--from must be set\n" if not defined $options{from};
+$ERRORS .= "--host must be set\n" if not defined $options{host};
+$ERRORS .= "--bug must be set\n" if not defined $options{bug};
+pod2usage($ERRORS) if length $ERRORS;
+
+pod2usage() if $options{help};
+pod2usage({verbose=>2}) if $options{man};
+
+
+$DEBUG = $options{debug};
+
+$VERBOSE = 1 - $options{quiet};
+
+if ($options{process}) {
+     my @standard_headers = ([],
+                            ['X-Debbugs-No-Ack:','yes no ack'],
+                           );
+
+     my %process_messages = ('-maintonly' => \@standard_headers,
+                            '-quiet'     => \@standard_headers,
+                            '-forwarded' => \@standard_headers,
+                            '-done'      => \@standard_headers,
+                            '-submitter' => \@standard_headers,
+                            ''           => \@standard_headers,
+                           );
+     my $message_count = 0;
+     for my $addr (keys %process_messages) {
+         for my $header (@{$process_messages{$addr}}) {
+              $message_count++;
+              my $message =
+                   create_mime_message([To   => "$options{bug}$addr\@$options{host}",
+                                        From => $options{from},
+                                        Subject => "message $message_count to $addr from test_bts",
+                                        @{$header},
+                                       ],<<END
+This is a testing message from test_bts
+This message was sent: 
+To: $options{bug}$addr\@$options{host}
+From: $options{from}
+Subject: message $message_count to $options{bug}$addr\@$options{host} from test_bts
+
+with additional headers:
+@{$header}
+
+If you are seeing this, and have no idea what this means, please
+ignore this message. If you are sure that this message has been sent
+in error please send mail to $options{from} so they can stop sending
+stupid messages to you.
+
+If you are reading this message in a BTS, it's only a testing message.
+Please ignore it... it shouldn't have been sent to a public one, but
+accidents happen.
+END
+                                      );
+              send_mail_message(message   => $message,
+                                recipients => "$options{bug}$addr\@$options{host}",
+                               );
+         }
+     }
+}
+if ($options{control}) {
+     die "Not implemented";
+}
+if ($options{submit}) {
+     die "Not implemented";
+}
+
+__END__
diff --git a/build b/build
deleted file mode 100755 (executable)
index c8342b5..0000000
--- a/build
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/bin/sh
-
-set -e
-
-cwd="`pwd`"
-date="`date +'%d %h %Y'`"
-
-process () {
-       (cd config &&
-        rm -f trace &&
-        m4 -P -I "$config" -DDB_DATE="$date" \
-               common/init.m4 config.m4 common/main.m4 - common/final.m4
-       )
-}
-
-txtconvert () {
-       src=$1; dst=$2
-       echo "  generating $dst from $src ..."
-       rm -f html/txt/$dst.html html/txt/$dst.out
-       perl -ne 'print unless m/^Other pages:$/ .. /^\<hr\>/' \
-               html/$src.out >html/txt/$dst.html
-       HOME=/dev/null lynx -nolist -dump -cfg=/dev/null \
-               file://localhost/$cwd/html/txt/$dst.html >html/txt/$dst.out
-       rm html/txt/$dst.html
-}
-
-config=local
-if [ $# != 0 ]; then config="$1"; shift; fi
-if [ $# != 0 ]; then echo >&2 'usage: ./build [<config>]'; false; fi
-
-if [ ! -f config/$config/config.m4 ]; then echo >&2 "no such config: $config"; false; fi
-
-errs="`echo 'm4_undivert(1)' | process | sed -ne '/[^ \t]/ s/^/ /p'`"
-if [ "x$errs" != x ]
-then
-       echo >&2 'unexpected residues:'
-       echo "$errs"
-       false
-fi
-
-echo "macro substitutions ..."
-for f in `find -name '*.in'`
-do
-       h="`echo $f | sed -e 's/\.in$//'`"
-       process <"$f" >"$h.out"
-       mv config/trace "$h.trace"
-       if egrep 'DBC?U?_' /dev/null "$h.out"
-       then
-               echo >&2 'undefined macros'
-               false
-       fi
-       [ ! -x "$f" ] || chmod +x "$h.out"
-done
-
-echo "documentation conversion ..."
-txtconvert Reporting.html bug-reporting.txt
-txtconvert Access.html bug-log-access.txt
-txtconvert server-request.html bug-log-mailserver.txt
-txtconvert Developer.html bug-maint-info.txt
-txtconvert server-control.html bug-maint-mailcontrol.txt
-txtconvert server-refcard.html bug-mailserver-refcard.txt
-
-cgilibexist=`echo 'test -f DBC_CGILIB_PATH && echo true || echo false' | process`
-htaccesspath=`echo DBC_HTACCESS_PATH | process`
-
-rm -f install install.new
-process <<'END' >install.new
-#!/bin/sh
-       set -e
-       test -d DBC_BASE || mkdir DBC_BASE
-       bugsid () {
-               echo "installing $1 ..."
-               test -d "$2" || mkdir "$2"
-               cd "$1"
-               for f in *.out
-               do
-                       h="`echo $f | sed -e 's/\.out$//'`"
-                       rm -f "$2/$f"
-                       cp "./$f" "$2/"
-                       mv -f "$2/$f" "$2/$h"
-               done
-               cd "$3"
-       }
-       bugsid scripts DBC_SCRIPT_PATH ..
-       bugsid html DBC_HTML_PATH ..
-       bugsid html/txt DBC_DOCDIR_PATH ../..
-       bugsid cgi DBC_CGI_PATH ..
-END
-if [ "x$htaccesspath" != x ]; then
-       process <<END >>install.new
-               cat <<'END2' >$htaccesspath.new
-DBC_HTACCESS_CONTENTS
-END2
-               mv -f $htaccesspath.new $htaccesspath
-END
-fi
-
-if $cgilibexist
-then
-       cgiii='cgi-lib already exists in DBC_CGILIB_PATH'
-else
-       cgiii=' DBC_CGILIB_PATH'
-       process <<'END' >>install.new
-               echo "installing cgi-lib.pl ..."
-               cp cgi/cgi-lib.pl DBC_CGILIB_PATH.new
-               mv -f DBC_CGILIB_PATH.new DBC_CGILIB_PATH
-END
-fi
-process >>install.new <<'END'
-       echo "setting up bugs database ..."
-       DBC_SCRIPT_PATH/initialise
-       echo "done."
-       echo "You will have to intall the crontab (misc/crontab.out) yourself."
-       exit 0
-END
-chmod +x install.new
-mv -f install.new install
-
-cgi="`cd cgi && echo *.out | sed -e 's/\.out//g'`"
-
-process <<END
-built for $config date DB_DATE ...
-       will install unchanging files into:
-               DBC_SCRIPT_PATH/
-               DBC_HTML_PATH/
-               DBC_CGI_PATH/ ($cgi)
-       $cgiii
-       will store data in:
-               DB_HTMLDB_PATH/
-               DBC_SPOOL_PATH/
-       will expect CGI scripts to be available in:
-               DBC_CGI_URL/
-END
-
-echo "run ./install to install"
-exit 0
index bc669d5e0fb0cc8f44b797f8d244862adef1fb0d..01f6934067f45e93a22d99802959a0ecc57c83d6 100755 (executable)
@@ -1,7 +1,6 @@
 #!/usr/bin/perl -wT
 
-package debbugs;
-
+use warnings;
 use strict;
 use POSIX qw(strftime tzset);
 use MIME::Parser;
@@ -9,44 +8,65 @@ use MIME::Decoder;
 use IO::Scalar;
 use IO::File;
 
-#require '/usr/lib/debbugs/errorlib';
-require './common.pl';
-
-require '/etc/debbugs/config';
-require '/etc/debbugs/text';
-
-use vars(qw($gEmailDomain $gHTMLTail $gSpoolDir $gWebDomain));
+use Debbugs::Config qw(:globals :text);
 
 # for read_log_records
 use Debbugs::Log;
-use Debbugs::MIME qw(convert_to_utf8 decode_rfc1522);
+use Debbugs::MIME qw(convert_to_utf8 decode_rfc1522 create_mime_message);
+use Debbugs::CGI qw(:url :html :util);
+use Debbugs::Common qw(buglog getmaintainers);
+use Debbugs::Packages qw(getpkgsrc);
+use Debbugs::Status qw(splitpackages get_bug_status isstrongseverity);
 
 use Scalar::Util qw(looks_like_number);
-
-my %param = readparse();
+use CGI::Simple;
+my $q = new CGI::Simple;
+
+my %param = cgi_parameters(query => $q,
+                          single => [qw(bug msg att boring terse),
+                                     qw(reverse mbox mime trim),
+                                     qw(mboxstat mboxmaint archive),
+                                     qw(repeatmerged)
+                                    ],
+                          default => {msg       => '',
+                                      boring    => 'no',
+                                      terse     => 'no',
+                                      reverse   => 'no',
+                                      mbox      => 'no',
+                                      mime      => 'no',
+                                      mboxstat  => 'no',
+                                      mboxmaint => 'no',
+                                      archive   => 'no',
+                                      repeatmerged => 'yes',
+                                     },
+                         );
+# This is craptacular.
 
 my $tail_html;
 
-my $ref = $param{'bug'} || quitcgi("No bug number");
+my $ref = $param{bug} or quitcgi("No bug number");
 $ref =~ /(\d+)/ or quitcgi("Invalid bug number");
 $ref = $1;
 my $short = "#$ref";
-my $msg = $param{'msg'} || "";
+my $msg = $param{'msg'};
 my $att = $param{'att'};
-my $boring = ($param{'boring'} || 'no') eq 'yes'; 
-my $terse = ($param{'terse'} || 'no') eq 'yes';
-my $reverse = ($param{'reverse'} || 'no') eq 'yes';
-my $mbox = ($param{'mbox'} || 'no') eq 'yes'; 
-my $mime = ($param{'mime'} || 'yes') eq 'yes';
+my $boring = $param{'boring'} eq 'yes';
+my $terse = $param{'terse'} eq 'yes';
+my $reverse = $param{'reverse'} eq 'yes';
+my $mbox = $param{'mbox'} eq 'yes';
+my $mime = $param{'mime'} eq 'yes';
 
 my $trim_headers = ($param{trim} || ($msg?'no':'yes')) eq 'yes';
 
+my $mbox_status_message = $param{mboxstat} eq 'yes';
+my $mbox_maint = $param{mboxmaint} eq 'yes';
+$mbox = 1 if $mbox_status_message or $mbox_maint;
+
+
 # Not used by this script directly, but fetch these so that pkgurl() and
 # friends can propagate them correctly.
-my $archive = ($param{'archive'} || 'no') eq 'yes';
-my $repeatmerged = ($param{'repeatmerged'} || 'yes') eq 'yes';
-set_option('archive', $archive);
-set_option('repeatmerged', $repeatmerged);
+my $archive = $param{'archive'} eq 'yes';
+my $repeatmerged = $param{'repeatmerged'} eq 'yes';
 
 my $buglog = buglog($ref);
 
@@ -86,11 +106,11 @@ sub display_entity ($$$$\$\@) {
              foreach (qw(From To Cc Subject Date)) {
                   my $head_field = $head->get($_);
                   next unless defined $head_field and $head_field ne '';
-                  push @headers, qq(<b>$_:</b> ) . htmlsanit(decode_rfc1522($head_field));
+                  push @headers, qq(<b>$_:</b> ) . html_escape(decode_rfc1522($head_field));
              }
              $$this .= join(qq(), @headers) unless $terse;
         } else {
-             $$this .= htmlsanit(decode_rfc1522($entity->head->stringify));
+             $$this .= html_escape(decode_rfc1522($entity->head->stringify));
         }
         $$this .= "</pre>\n";
     }
@@ -98,11 +118,11 @@ sub display_entity ($$$$\$\@) {
     unless (($top and $type =~ m[^text(?:/plain)?(?:;|$)]) or
            ($type =~ m[^multipart/])) {
        push @$attachments, $entity;
-       my @dlargs = ($ref, "msg=$xmessage", "att=$#$attachments");
-       push @dlargs, "filename=$filename" if $filename ne '';
+       my @dlargs = ($ref, msg=>$xmessage, att=>$#$attachments);
+       push @dlargs, (filename=>$filename) if $filename ne '';
        my $printname = $filename;
        $printname = 'Message part ' . ($#$attachments + 1) if $filename eq '';
-       $$this .= '<pre class="mime">[<a href="' . bugurl(@dlargs) . qq{">$printname</a> } .
+       $$this .= '<pre class="mime">[<a href="' . bug_url(@dlargs) . qq{">$printname</a> } .
                  "($type, $disposition)]</pre>\n";
 
        if ($msg and defined($att) and $att eq $#$attachments) {
@@ -155,12 +175,12 @@ sub display_entity ($$$$\$\@) {
              my ($charset) = $content_type =~ m/charset\s*=\s*\"?([\w-]+)\"?/i;
              my $body = $entity->bodyhandle->as_string;
              $body = convert_to_utf8($body,$charset) if defined $charset;
-             $body = htmlsanit($body);
+             $body = html_escape($body);
              # Add links to URLs
              $body =~ s,((ftp|http|https)://[\S~-]+?/?)((\&gt\;)?[)]?[']?[:.\,]?(\s|$)),<a href=\"$1\">$1</a>$3,go;
              # Add links to bug closures
              $body =~ s[(closes:\s*(?:bug)?\#?\s?\d+(?:,?\s*(?:bug)?\#?\s?\d+)*)
-                       ][my $temp = $1; $temp =~ s{(\d+)}{qq(<a href=").bugurl($1).qq(">$1</a>)}ge; $temp;]gxie;
+                       ][my $temp = $1; $temp =~ s{(\d+)}{qq(<a href=").bug_url($1).qq(">$1</a>)}ge; $temp;]gxie;
              $$this .= qq(<pre class="message">$body</pre>\n);
         }
     }
@@ -175,24 +195,21 @@ my $showseverity;
 my $tpack;
 my $tmain;
 
-$ENV{"TZ"} = 'UTC';
-tzset();
-
-my $dtime = strftime "%a, %e %b %Y %T UTC", localtime;
-$tail_html = $debbugs::gHTMLTail;
+my $dtime = strftime "%a, %e %b %Y %T UTC", gmtime;
+$tail_html = $gHTMLTail;
 $tail_html =~ s/SUBSTITUTE_DTIME/$dtime/;
 
-my %status = %{getbugstatus($ref)};
+my %status = %{get_bug_status(bug=>$ref)};
 unless (%status) {
     print <<EOF;
 Content-Type: text/html; charset=utf-8
 
 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html>
-<head><title>$short - $debbugs::gProject $debbugs::gBug report logs</title></head>
+<head><title>$short - $gProject $gBug report logs</title></head>
 <body>
-<h1>$debbugs::gProject $debbugs::gBug report logs - $short</h1>
-<p>There is no record of $debbugs::gBug $short.
+<h1>$gProject $gBug report logs - $short</h1>
+<p>There is no record of $gBug $short.
 Try the <a href="http://$gWebDomain/">search page</a> instead.</p>
 $tail_html</body></html>
 EOF
@@ -212,29 +229,45 @@ if  ($status{severity} eq 'normal') {
        $showseverity = "Severity: $status{severity};\n";
 }
 
+if (@{$status{found_versions}} or @{$status{fixed_versions}}) {
+     $indexentry.= q(<div style="float:right"><a href=").
+         version_url($status{package},
+                     $status{found_versions},
+                     $status{fixed_versions},
+                    ).
+         q("><img src=").
+         version_url($status{package},
+                     $status{found_versions},
+                     $status{fixed_versions},
+                     2,
+                     2,
+                    ).qq{"></a></div>};
+}
+
+
 $indexentry .= "<div class=\"msgreceived\">\n";
-$indexentry .= htmlpackagelinks($status{package}, 0) . ";\n";
+$indexentry .= htmlize_packagelinks($status{package}, 0) . ";\n";
 
 foreach my $pkg (@tpacks) {
     my $tmaint = defined($maintainer{$pkg}) ? $maintainer{$pkg} : '(unknown)';
     my $tsrc = defined($pkgsrc{$pkg}) ? $pkgsrc{$pkg} : '(unknown)';
 
     $indexentry .=
-            htmlmaintlinks(sub { $_[0] == 1 ? "Maintainer for $pkg is\n"
+            htmlize_maintlinks(sub { $_[0] == 1 ? "Maintainer for $pkg is\n"
                                             : "Maintainers for $pkg are\n" },
                            $tmaint);
     $indexentry .= ";\nSource for $pkg is\n".
-            '<a href="'.srcurl($tsrc)."\">$tsrc</a>" if ($tsrc ne "(unknown)");
+            '<a href="'.pkg_url(src=>$tsrc)."\">$tsrc</a>" if ($tsrc ne "(unknown)");
     $indexentry .= ".\n";
 }
 
 $indexentry .= "<br>";
-$indexentry .= htmladdresslinks("Reported by: ", \&submitterurl,
+$indexentry .= htmlize_addresslinks("Reported by: ", \&submitterurl,
                                 $status{originator}) . ";\n";
 $indexentry .= sprintf "Date: %s.\n",
                (strftime "%a, %e %b %Y %T UTC", localtime($status{date}));
 
-$indexentry .= "<br>Owned by: " . htmlsanit($status{owner}) . ".\n"
+$indexentry .= "<br>Owned by: " . html_escape($status{owner}) . ".\n"
               if length $status{owner};
 
 $indexentry .= "</div>\n";
@@ -243,7 +276,7 @@ my @descstates;
 
 $indexentry .= "<h3>$showseverity";
 $indexentry .= sprintf "Tags: %s;\n", 
-               htmlsanit(join(", ", sort(split(/\s+/, $status{tags}))))
+               html_escape(join(", ", sort(split(/\s+/, $status{tags}))))
                        if length($status{tags});
 $indexentry .= "<br>" if (length($showseverity) or length($status{tags}));
 
@@ -252,7 +285,7 @@ if (@merged) {
        my $descmerged = 'Merged with ';
        my $mseparator = '';
        for my $m (@merged) {
-               $descmerged .= $mseparator."<a href=\"" . bugurl($m) . "\">#$m</a>";
+               $descmerged .= $mseparator."<a href=\"" . bug_url($m) . "\">#$m</a>";
                $mseparator= ",\n";
        }
        push @descstates, $descmerged;
@@ -261,21 +294,32 @@ if (@merged) {
 if (@{$status{found_versions}}) {
     my $foundtext = 'Found in ';
     $foundtext .= (@{$status{found_versions}} == 1) ? 'version ' : 'versions ';
-    $foundtext .= join ', ', map htmlsanit($_), @{$status{found_versions}};
+    $foundtext .= join ', ', map html_escape($_), @{$status{found_versions}};
     push @descstates, $foundtext;
 }
-
 if (@{$status{fixed_versions}}) {
     my $fixedtext = '<strong>Fixed</strong> in ';
     $fixedtext .= (@{$status{fixed_versions}} == 1) ? 'version ' : 'versions ';
-    $fixedtext .= join ', ', map htmlsanit($_), @{$status{fixed_versions}};
+    $fixedtext .= join ', ', map html_escape($_), @{$status{fixed_versions}};
     if (length($status{done})) {
-       $fixedtext .= ' by ' . htmlsanit(decode_rfc1522($status{done}));
+       $fixedtext .= ' by ' . html_escape(decode_rfc1522($status{done}));
     }
     push @descstates, $fixedtext;
-} elsif (length($status{done})) {
-    push @descstates, "<strong>Done:</strong> ".htmlsanit(decode_rfc1522($status{done}));
-} elsif (length($status{forwarded})) {
+}
+
+if (@{$status{found_versions}} or @{$status{fixed_versions}}) {
+     push @descstates, '<a href="'.
+         version_url($status{package},
+                     $status{found_versions},
+                     $status{fixed_versions},
+                    ).qq{">Version Graph</a>};
+}
+
+if (length($status{done})) {
+    push @descstates, "<strong>Done:</strong> ".html_escape(decode_rfc1522($status{done}));
+}
+
+if (length($status{forwarded})) {
     push @descstates, "<strong>Forwarded</strong> to ".maybelink($status{forwarded});
 }
 
@@ -283,18 +327,18 @@ if (@{$status{fixed_versions}}) {
 my @blockedby= split(/ /, $status{blockedby});
 if (@blockedby && $status{"pending"} ne 'fixed' && ! length($status{done})) {
     for my $b (@blockedby) {
-        my %s = %{getbugstatus($b)};
+        my %s = %{get_bug_status($b)};
         next if $s{"pending"} eq 'fixed' || length $s{done};
-        push @descstates, "Fix blocked by <a href=\"" . bugurl($b) . "\">#$b</a>: ".htmlsanit($s{subject});
+        push @descstates, "Fix blocked by <a href=\"" . bug_url($b) . "\">#$b</a>: ".html_escape($s{subject});
     }
 }
 
 my @blocks= split(/ /, $status{blocks});
 if (@blocks && $status{"pending"} ne 'fixed' && ! length($status{done})) {
     for my $b (@blocks) {
-        my %s = %{getbugstatus($b)};
+        my %s = %{get_bug_status($b)};
         next if $s{"pending"} eq 'fixed' || length $s{done};
-        push @descstates, "Blocking fix for <a href=\"" . bugurl($b) . "\">#$b</a>: ".htmlsanit($s{subject});
+        push @descstates, "Blocking fix for <a href=\"" . bug_url($b) . "\">#$b</a>: ".html_escape($s{subject});
     }
 }
 
@@ -323,7 +367,7 @@ eval{
      @records = read_log_records($buglogfh);
 };
 if ($@) {
-     quitcgi("Bad bug log for $debbugs::gBug $ref. Unable to read records: $@");
+     quitcgi("Bad bug log for $gBug $ref. Unable to read records: $@");
 }
 undef $buglogfh;
 
@@ -356,30 +400,6 @@ sub handle_email_message{
 
 }
 
-=head2 bug_links
-
-     bug_links($one_bug);
-     bug_links($starting_bug,$stoping_bugs,);
-
-Creates a set of links to bugs, starting with bug number
-$starting_bug, and finishing with $stoping_bug; if only one bug is
-passed, makes a link to only a single bug.
-
-The content of the link is the bug number.
-
-=cut
-
-sub bug_links{
-     my ($start,$stop,$query_arguments) = @_;
-     $stop = $stop || $start;
-     $query_arguments ||= '';
-     my @output;
-     for my $bug ($start..$stop) {
-         push @output,'<a href="'.bugurl($bug,'').qq(">$bug</a>);
-     }
-     return join(', ',@output);
-}
-
 =head2 handle_record
 
      push @log, handle_record($record,$ref,$msg_num);
@@ -406,14 +426,14 @@ sub handle_record{
          # Add links to merged bugs
          $output =~ s{(?<=Merged )([\d\s]+)(?=\.)}{join(' ',map {bug_links($_)} (split /\s+/, $1))}eo;
          # Add links to blocked bugs
-         $output =~ s{(?<=Blocking bugs)(?:(of )(\d+))?( (?:added|set to|removed):\s+)([\d\s\,]+)}
+         $output =~ s{(?<=Blocking bugs)(?:( of )(\d+))?( (?:added|set to|removed):\s+)([\d\s\,]+)}
                      {(defined $2?$1.bug_links($2):'').$3.
                            join(' ',map {bug_links($_)} (split /\,?\s+/, $4))}eo;
          # Add links to reassigned packages
          $output =~ s{(Bug reassigned from package \`)([^\']+)(' to \`)([^\']+)(')}
-         {$1.q(<a href=").pkgurl($2).qq(">$2</a>).$3.q(<a href=").pkgurl($4).qq(">$4</a>).$5}eo;
-         $output .= '<a href="' . bugurl($ref, 'msg='.($msg_number+1)) . '">Full text</a> and <a href="' .
-              bugurl($ref, 'msg='.($msg_number+1), 'mbox') . '">rfc822 format</a> available.';
+         {$1.q(<a href=").pkg_url(pkg=>$2).qq(">$2</a>).$3.q(<a href=").pkg_url(pkg=>$4).qq(">$4</a>).$5}eo;
+         $output .= '<a href="' . bug_url($ref, msg => ($msg_number+1)) . '">Full text</a> and <a href="' .
+              bug_url($ref, msg => ($msg_number+1), mbox => 'yes') . '">rfc822 format</a> available.';
 
          $output = qq(<div class="$class"><hr>\n<a name="$msg_number"></a>\n) . $output . "</div>\n";
      }
@@ -426,7 +446,7 @@ sub handle_record{
               $$seen_msg_ids{$msg_id} = 1;
          }
          $output .= qq(<hr><a name="$msg_number"></a>\n);
-         $output .= 'View this message in <a href="' . bugurl($ref, "msg=$msg_number", "mbox") . '">rfc822 format</a>';
+         $output .= 'View this message in <a href="' . bug_url($ref, "msg=$msg_number", "mbox") . '">rfc822 format</a>';
          $output .= handle_email_message($record->{text},
                                    ref        => $bug_number,
                                    msg_number => $msg_number,
@@ -445,8 +465,8 @@ sub handle_record{
          }
          # Incomming Mail Message
          my ($received,$hostname) = $record->{text} =~ m/Received: \(at (\S+)\) by (\S+)\;/;
-         $output .= qq|<hr><p class="msgreceived"><a name="$msg_number"><a name="msg$msg_number">Message received</a> at |.
-              htmlsanit("$received\@$hostname") . q| (<a href="| . bugurl($ref, "msg=$msg_number") . '">full text</a>'.q|, <a href="| . bugurl($ref, "msg=$msg_number") . ';mbox=yes">mbox</a>)'.":</p>\n";
+         $output .= qq|<hr><p class="msgreceived"><a name="$msg_number"></a><a name="msg$msg_number">Message received</a> at |.
+              html_escape("$received\@$hostname") . q| (<a href="| . bug_url($ref, msg=>$msg_number) . '">full text</a>'.q|, <a href="| . bug_url($ref, msg=>$msg_number,mbox=>'yes') .'">mbox</a>)'.":</p>\n";
          $output .= handle_email_message($record->{text},
                                    ref        => $bug_number,
                                    msg_number => $msg_number,
@@ -467,6 +487,7 @@ if (looks_like_number($msg) and ($msg-1) <= $#records) {
 }
 my @log;
 if ( $mbox ) {
+     my $date = strftime "%a %b %d %T %Y", localtime;
      if (@records > 1) {
          print qq(Content-Disposition: attachment; filename="bug_${ref}.mbox"\n);
          print "Content-Type: text/plain\n\n";
@@ -476,9 +497,49 @@ if ( $mbox ) {
          print qq(Content-Disposition: attachment; filename="bug_${ref}_message_${msg_num}.mbox"\n);
          print "Content-Type: message/rfc822\n\n";
      }
+     if ($mbox_status_message and @records > 1) {
+         my $status_message='';
+         my @status_fields = (retitle   => 'subject',
+                              package   => 'package',
+                              submitter => 'originator',
+                              severity  => 'severity',
+                              tag       => 'tags',
+                              owner     => 'owner',
+                              blocks    => 'blocks',
+                              forward   => 'forward',
+                             );
+         my ($key,$value);
+         while (($key,$value) = splice(@status_fields,0,2)) {
+              if (defined $status{$value} and length $status{$value}) {
+                   $status_message .= qq($key $ref $status{$value}\n);
+              }
+         }
+         print STDOUT qq(From unknown $date\n),
+              create_mime_message([From       => "$gBug#$ref <$ref\@$gEmailDomain>",
+                                   To         => "$gBug#$ref <$ref\@$gEmailDomain>",
+                                   Subject    => "Status: $status{subject}",
+                                   "Reply-To" => "$gBug#$ref <$ref\@$gEmailDomain>",
+                                  ],
+                                  <<END,);
+$status_message
+thanks
+
+
+END
+     }
+     my $message_number=0;
+     my %seen_message_ids;
      for my $record (@records) {
          next if $record->{type} !~ /^(?:recips|incoming-recv)$/;
-         next if not $boring and $record->{type} eq 'recips' and @records > 1;
+         my $wanted_type = $mbox_maint?'recips':'incoming-recv';
+         # we want to include control messages anyway
+         my $record_wanted_anyway = 0;
+         my ($msg_id) = $record->{text} =~ /^Message-Id:\s+<(.+)>/im;
+         next if exists $seen_message_ids{$msg_id};
+         $seen_message_ids{$msg_id} = 1;
+         next if $msg_id =~/handler\..+\.ack(?:info)?\@/;
+         $record_wanted_anyway = 1 if $record->{text} =~ /^Received: \(at control\)/;
+         next if not $boring and $record->{type} ne $wanted_type and not $record_wanted_anyway and @records > 1;
          my @lines = split( "\n", $record->{text}, -1 );
          if ( $lines[ 1 ] =~ m/^From / ) {
               my $tmp = $lines[ 0 ];
@@ -486,7 +547,6 @@ if ( $mbox ) {
               $lines[ 1 ] = $tmp;
          }
          if ( !( $lines[ 0 ] =~ m/^From / ) ) {
-              my $date = strftime "%a %b %d %T %Y", localtime;
               unshift @lines, "From unknown $date";
          }
          map { s/^(>*From )/>$1/ } @lines[ 1 .. $#lines ];
@@ -514,47 +574,47 @@ $log = join("\n",@log);
 
 print "Content-Type: text/html; charset=utf-8\n\n";
 
-my $title = htmlsanit($status{subject});
+my $title = html_escape($status{subject});
 
-my $dummy2 = $debbugs::gWebHostBugDir;
+my $dummy2 = $gWebHostBugDir;
 
 print "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n";
 print <<END;
 <HTML><HEAD>
-<TITLE>$short - $title - $debbugs::gProject $debbugs::gBug report logs</TITLE>
+<TITLE>$short - $title - $gProject $gBug report logs</TITLE>
 <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
-<link rel="stylesheet" href="$debbugs::gWebHostBugDir/css/bugs.css" type="text/css">
+<link rel="stylesheet" href="$gWebHostBugDir/css/bugs.css" type="text/css">
 <script type="text/javascript">
 <!--
-function toggle_infmessages(){
-       var styles = document.styleSheets;
-       var deleted = 0
-       for (var i = 0; i < styles.length; i++) {
-          for (var j = 0; j < styles[i].cssRules.length; j++) {
-            if (styles[i].cssRules[j].cssText == ".infmessage { display: none; }") {
-                 styles[i].deleteRule(j);
-                 deleted = 1;
-            }
-          }
-       }
-       if (!deleted) {
-            styles[0].insertRule(".infmessage { display: none; }",0);
-       }
+function toggle_infmessages()
+{
+        allDivs=document.getElementsByTagName("div");
+        for (var i = 0 ; i < allDivs.length ; i++ )
+        {
+                if (allDivs[i].className == "infmessage")
+                {
+                        allDivs[i].style.display=(allDivs[i].style.display == 'none') ? 'block' : 'none';
+                }
+        }
 }
 -->
 </script>
 </HEAD>
 <BODY>
 END
-print "<H1>" . "$debbugs::gProject $debbugs::gBug report logs - <A HREF=\"mailto:$ref\@$gEmailDomain\">$short</A>" .
+print "<H1>" . "$gProject $gBug report logs - <A HREF=\"mailto:$ref\@$gEmailDomain\">$short</A>" .
       "<BR>" . $title . "</H1>\n";
 
 print "$descriptivehead\n";
-print qq(<p><a href="mailto:$ref\@$debbugs::gEmailDomain">Reply</a> ),
-     qq(or <a href="mailto:$ref-subscribe\@$debbugs::gEmailDomain">subscribe</a> ),
+print qq(<p><a href="mailto:$ref\@$gEmailDomain">Reply</a> ),
+     qq(or <a href="mailto:$ref-subscribe\@$gEmailDomain">subscribe</a> ),
      qq(to this bug.</p>\n);
 print qq(<p><a href="javascript:toggle_infmessages();">Toggle useless messages</a></p>);
-printf "<div class=\"msgreceived\"><p>View this report as an <a href=\"%s\">mbox folder</a>.</p></div>\n", bugurl($ref, "mbox");
+printf qq(<div class="msgreceived"><p>View this report as an <a href="%s">mbox folder</a>, ).
+     qq(<a href="%s">status mbox</a>, <a href="%s">maintainer mbox</a></p></div>\n),
+     html_escape(bug_url($ref, mbox=>'yes')),
+     html_escape(bug_url($ref, mbox=>'yes',mboxstatus=>'yes')),
+     html_escape(bug_url($ref, mbox=>'yes',mboxmaint=>'yes'));
 print "$log";
 print "<HR>";
 print "<p class=\"msgreceived\">Send a report that <a href=\"/cgi-bin/bugspam.cgi?bug=$ref\">this bug log contains spam</a>.</p>\n<HR>\n";
index e497787ff1733ea9a288c51dd4d87457c4c7d156..5805907ce62eb18a055b644ac17ab968ff1c7014 100644 (file)
@@ -8,13 +8,17 @@ use POSIX qw/ceil/;
 
 use URI::Escape;
 
+use Debbugs::Config qw(:globals :text);
 $config_path = '/etc/debbugs';
 $lib_path = '/usr/lib/debbugs';
-require "$lib_path/errorlib";
+#require "$lib_path/errorlib";
 
-use Debbugs::Packages;
+use Debbugs::Packages qw(:versions :mapping);
 use Debbugs::Versions;
 use Debbugs::MIME qw(decode_rfc1522);
+use Debbugs::Common qw(:util);
+use Debbugs::Status qw(:status :read :versions);
+use Debbugs::CGI qw(:all);
 
 $MLDBM::RemoveTaint = 1;
 
@@ -83,7 +87,7 @@ my %field_match = (
 my @common_grouping = ( 'severity', 'pending' );
 my %common_grouping_order = (
     'pending' => [ qw( pending forwarded pending-fixed fixed done absent ) ],
-    'severity' => \@debbugs::gSeverityList,
+    'severity' => \@gSeverityList,
 );
 my %common_grouping_display = (
     'pending' => 'Status',
@@ -98,7 +102,7 @@ my %common_headers = (
        "forwarded"     => "forwarded to upstream software authors",
        "absent"        => "not applicable to this version",
     },
-    'severity' => \%debbugs::gSeverityDisplay,
+    'severity' => \%gSeverityDisplay,
 );
 
 my $common_version;
@@ -144,8 +148,8 @@ sub set_option {
        $use_bug_idx = $val;
        if ( $val ) {
            $common_headers{pending}{open} = $common_headers{pending}{pending};
-           my $bugidx = tie %bugidx, MLDBM => "$debbugs::gSpoolDir/realtime/bug.idx", O_RDONLY
-               or quitcgi( "$0: can't open $debbugs::gSpoolDir/realtime/bug.idx ($!)\n" );
+           my $bugidx = tie %bugidx, MLDBM => "$gSpoolDir/realtime/bug.idx", O_RDONLY
+               or quitcgi( "$0: can't open $gSpoolDir/realtime/bug.idx ($!)\n" );
            $bugidx->RemoveTaint(1);
        } else {
            untie %bugidx;
@@ -232,60 +236,11 @@ $debug = 1 if (defined $ret{"debug"} && $ret{"debug"} eq "aj");
     return %ret;
 }
 
-sub quitcgi {
-    my $msg = shift;
-    print "Content-Type: text/html\n\n";
-    print "<HTML><HEAD><TITLE>Error</TITLE></HEAD><BODY>\n";
-    print "An error occurred. Dammit.\n";
-    print "Error was: $msg.\n";
-    print "</BODY></HTML>\n";
-    exit 0;
-}
-
-#sub abort {
-#    my $msg = shift;
-#    my $Archive = $common_archive ? "archive" : "";
-#    print header . start_html("Sorry");
-#    print "Sorry bug #$msg doesn't seem to be in the $Archive database.\n";
-#    print end_html;
-#    exit 0;
-#}
-
-# Split a package string from the status file into a list of package names.
-sub splitpackages {
-    my $pkgs = shift;
-    return unless defined $pkgs;
-    return map lc, split /[ \t?,()]+/, $pkgs;
-}
-
-my %_parsedaddrs;
-sub getparsedaddrs {
-    my $addr = shift;
-    return () unless defined $addr;
-    return @{$_parsedaddrs{$addr}} if exists $_parsedaddrs{$addr};
-    @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr);
-    return @{$_parsedaddrs{$addr}};
-}
-
 # Generate a comma-separated list of HTML links to each package given in
 # $pkgs. $pkgs may be empty, in which case an empty string is returned, or
 # it may be a comma-separated list of package names.
 sub htmlpackagelinks {
-    my $pkgs = shift;
-    return unless defined $pkgs and $pkgs ne '';
-    my $strong = shift;
-    my @pkglist = splitpackages($pkgs);
-
-    my $openstrong  = $strong ? '<strong>' : '';
-    my $closestrong = $strong ? '</strong>' : '';
-
-    return 'Package' . (@pkglist > 1 ? 's' : '') . ': ' .
-           join(', ',
-                map {
-                    '<a href="' . pkgurl($_) . '">' .
-                    $openstrong . htmlsanit($_) . $closestrong . '</a>'
-                } @pkglist
-           );
+     return htmlize_packagelinks(@_);
 }
 
 # Generate a comma-separated list of HTML links to each address given in
@@ -293,20 +248,7 @@ sub htmlpackagelinks {
 # $urlfunc should be a reference to a function like mainturl or submitterurl
 # which returns the URL for each individual address.
 sub htmladdresslinks {
-    my ($prefixfunc, $urlfunc, $addresses) = @_;
-    if (defined $addresses and $addresses ne '') {
-        my @addrs = getparsedaddrs($addresses);
-        my $prefix = (ref $prefixfunc) ? $prefixfunc->(scalar @addrs)
-                                       : $prefixfunc;
-        return $prefix .
-               join ', ', map { sprintf '<a href="%s">%s</a>',
-                                        $urlfunc->($_->address),
-                                        htmlsanit($_->format) || '(unknown)'
-                              } @addrs;
-    } else {
-        my $prefix = (ref $prefixfunc) ? $prefixfunc->(1) : $prefixfunc;
-        return sprintf '%s<a href="%s">(unknown)</a>', $prefix, $urlfunc->('');
-    }
+     htmlize_addresslinks(@_);
 }
 
 # Generate a comma-separated list of HTML links to each maintainer given in
@@ -366,7 +308,6 @@ sub htmlindexentrystatus {
                 . htmlsanit(join(", ", sort(split(/\s+/, $status{tags}))))
                 . "</strong>"
                        if (length($status{tags}));
-
     my @merged= split(/ /,$status{mergedwith});
     my $mseparator= ";\nmerged with ";
     for my $m (@merged) {
@@ -376,7 +317,7 @@ sub htmlindexentrystatus {
 
     if (length($status{done})) {
         $result .= ";\n<strong>Done:</strong> " . htmlsanit($status{done});
-        $days = ceil($debbugs::gRemoveAge - -M buglog($status{id}));
+        $days = ceil($gRemoveAge - -M buglog($status{id}));
         if ($days >= 0) {
             $result .= ";\n<strong>Will be archived:</strong>" . ( $days == 0 ? " today" : $days == 1 ? " in $days day" : " in $days days" );
         } else {
@@ -429,11 +370,9 @@ sub urlargs {
     return $args;
 }
 
-sub submitterurl { pkg_etc_url(emailfromrfc822($_[0] || ""), "submitter"); }
-sub mainturl { pkg_etc_url(emailfromrfc822($_[0] || ""), "maint"); }
-sub pkgurl { pkg_etc_url($_[0] || "", "pkg"); }
-sub srcurl { pkg_etc_url($_[0] || "", "src"); }
-sub tagurl { pkg_etc_url($_[0] || "", "tag"); }
+sub pkgurl { pkg_url(pkg => $_[0] || ""); }
+sub srcurl { pkg_url(src => $_[0] || ""); }
+sub tagurl { pkg_url(tag => $_[0] || ""); }
 
 sub pkg_etc_url {
     my $ref = shift;
@@ -467,15 +406,6 @@ sub htmlsanit {
     return $in;
 }
 
-sub maybelink {
-    my $in = shift;
-    if ($in =~ /^[a-zA-Z0-9+.-]+:/) { # RFC 1738 scheme
-       return qq{<a href="$in">} . htmlsanit($in) . '</a>';
-    } else {
-       return htmlsanit($in);
-    }
-}
-
 sub bugurl {
     my $ref = shift;
     my $params = "bug=$ref";
@@ -650,7 +580,7 @@ sub htmlizebugs {
     }
 
     $result = $header . $result if ( $common{show_list_header} );
-    $result .= $debbugs::gHTMLExpireNote if $debbugs::gRemoveAge and $anydone;
+    $result .= $gHTMLExpireNote if $gRemoveAge and $anydone;
     $result .= "<hr>" . $footer if ( $common{show_list_footer} );
     return $result;
 }
@@ -658,11 +588,11 @@ sub htmlizebugs {
 sub countbugs {
     my $bugfunc = shift;
     if ($common_archive) {
-        open I, "<$debbugs::gSpoolDir/index.archive"
-            or &quitcgi("$debbugs::gSpoolDir/index.archive: $!");
+        open I, "<$gSpoolDir/index.archive"
+            or &quitcgi("$gSpoolDir/index.archive: $!");
     } else {
-        open I, "<$debbugs::gSpoolDir/index.db"
-            or &quitcgi("$debbugs::gSpoolDir/index.db: $!");
+        open I, "<$gSpoolDir/index.db"
+            or &quitcgi("$gSpoolDir/index.db: $!");
     }
 
     my %count = ();
@@ -689,9 +619,9 @@ sub getbugs {
     if (!defined $opt) {
         # leave $fastidx undefined;
     } elsif (!$common_archive) {
-        $fastidx = "$debbugs::gSpoolDir/by-$opt.idx";
+        $fastidx = "$gSpoolDir/by-$opt.idx";
     } else {
-        $fastidx = "$debbugs::gSpoolDir/by-$opt-arc.idx";
+        $fastidx = "$gSpoolDir/by-$opt-arc.idx";
     }
 
     if (defined $fastidx && -e $fastidx) {
@@ -709,11 +639,11 @@ print STDERR "optimized\n" if ($debug);
 print STDERR "done optimized\n" if ($debug);
     } else {
         if ( $common_archive ) {
-            open I, "<$debbugs::gSpoolDir/index.archive" 
-                or &quitcgi("$debbugs::gSpoolDir/index.archive: $!");
+            open I, "<$gSpoolDir/index.archive" 
+                or &quitcgi("$gSpoolDir/index.archive: $!");
         } else {
-            open I, "<$debbugs::gSpoolDir/index.db" 
-                or &quitcgi("$debbugs::gSpoolDir/index.db: $!");
+            open I, "<$gSpoolDir/index.db" 
+                or &quitcgi("$gSpoolDir/index.db: $!");
         }
         while(<I>) {
             if (m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/) {
@@ -755,240 +685,16 @@ sub maintencoded {
     return $encoded;
 }
 
-my $_maintainer;
-sub getmaintainers {
-    return $_maintainer if $_maintainer;
-    my %maintainer;
-
-    open(MM,"$debbugs::gMaintainerFile") or &quitcgi("open $debbugs::gMaintainerFile: $!");
-    while(<MM>) {
-       next unless m/^(\S+)\s+(\S.*\S)\s*$/;
-       ($a,$b)=($1,$2);
-       $a =~ y/A-Z/a-z/;
-       $maintainer{$a}= $b;
-    }
-    close(MM);
-    if (defined $debbugs::gMaintainerFileOverride) {
-       open(MM,"$debbugs::gMaintainerFileOverride") or &quitcgi("open $debbugs::gMaintainerFileOverride: $!");
-       while(<MM>) {
-           next unless m/^(\S+)\s+(\S.*\S)\s*$/;
-           ($a,$b)=($1,$2);
-           $a =~ y/A-Z/a-z/;
-           $maintainer{$a}= $b;
-       }
-       close(MM);
-    }
-    $_maintainer = \%maintainer;
-    return $_maintainer;
-}
-
-my $_pseudodesc;
-sub getpseudodesc {
-    return $_pseudodesc if $_pseudodesc;
-    my %pseudodesc;
-
-    open(PSEUDO, "< $debbugs::gPseudoDescFile") or &quitcgi("open $debbugs::gPseudoDescFile: $!");
-    while(<PSEUDO>) {
-       next unless m/^(\S+)\s+(\S.*\S)\s*$/;
-       $pseudodesc{lc $1} = $2;
-    }
-    close(PSEUDO);
-    $_pseudodesc = \%pseudodesc;
-    return $_pseudodesc;
-}
 
 sub getbugstatus {
-    my $bugnum = shift;
-
-    my %status;
-
-    if ( $use_bug_idx eq 1 && exists( $bugidx{ $bugnum } ) ) {
-       %status = %{ $bugidx{ $bugnum } };
-       $status{ pending } = $status{ status };
-       $status{ id } = $bugnum;
-       return \%status;
-    }
-
-    my $location = getbuglocation( $bugnum, 'summary' );
-    return {} if ( !$location );
-    %status = %{ readbug( $bugnum, $location ) };
-    $status{ id } = $bugnum;
-
-
-    if (defined $common_bugusertags{$bugnum}) {
-        $status{keywords} = "" unless defined $status{keywords};
-        $status{keywords} .= " " unless $status{keywords} eq "";
-        $status{keywords} .= join(" ", @{$common_bugusertags{$bugnum}});
-    }
-    $status{tags} = $status{keywords};
-    my %tags = map { $_ => 1 } split ' ', $status{tags};
-
-    $status{"package"} =~ s/\s*$//;
-    $status{"package"} = 'unknown' if ($status{"package"} eq '');
-    $status{"severity"} = 'normal' if ($status{"severity"} eq '');
-
-    $status{"pending"} = 'pending';
-    $status{"pending"} = 'forwarded'       if (length($status{"forwarded"}));
-    $status{"pending"} = 'pending-fixed'    if ($tags{pending});
-    $status{"pending"} = 'fixed'           if ($tags{fixed});
-
-    my @versions;
-    if (defined $common_version) {
-        @versions = ($common_version);
-    } elsif (defined $common_dist) {
-        @versions = getversions($status{package}, $common_dist, $common_arch);
-    }
-
-    # TODO: This should probably be handled further out for efficiency and
-    # for more ease of distinguishing between pkg= and src= queries.
-    my @sourceversions = makesourceversions($status{package}, $common_arch,
-                                            @versions);
-
-    if (@sourceversions) {
-        # Resolve bugginess states (we might be looking at multiple
-        # architectures, say). Found wins, then fixed, then absent.
-        my $maxbuggy = 'absent';
-        for my $version (@sourceversions) {
-            my $buggy = buggyversion($bugnum, $version, \%status);
-            if ($buggy eq 'found') {
-                $maxbuggy = 'found';
-                last;
-            } elsif ($buggy eq 'fixed' and $maxbuggy ne 'found') {
-                $maxbuggy = 'fixed';
-            }
-        }
-        if ($maxbuggy eq 'absent') {
-            $status{"pending"} = 'absent';
-        } elsif ($maxbuggy eq 'fixed') {
-            $status{"pending"} = 'done';
-        }
-    }
-    
-    if (length($status{done}) and
-            (not @sourceversions or not @{$status{fixed_versions}})) {
-        $status{"pending"} = 'done';
-    }
-
-    return \%status;
-}
-
-sub buglog {
-    my $bugnum = shift;
-    my $location = getbuglocation($bugnum, 'log');
-    return getbugcomponent($bugnum, 'log', $location) if ($location);
-    $location = getbuglocation($bugnum, 'log.gz');
-    return getbugcomponent($bugnum, 'log.gz', $location);
-}
-
-# Canonicalize versions into source versions, which have an explicitly
-# named source package. This is used to cope with source packages whose
-# names have changed during their history, and with cases where source
-# version numbers differ from binary version numbers.
-my %_sourceversioncache = ();
-sub makesourceversions {
-    my $pkg = shift;
-    my $arch = shift;
-    my %sourceversions;
-
-    for my $version (@_) {
-        if ($version =~ m[/]) {
-            # Already a source version.
-            $sourceversions{$version} = 1;
-        } else {
-            my $cachearch = (defined $arch) ? $arch : '';
-            my $cachekey = "$pkg/$cachearch/$version";
-            if (exists($_sourceversioncache{$cachekey})) {
-                for my $v (@{$_sourceversioncache{$cachekey}}) {
-                   $sourceversions{$v} = 1;
-               }
-                next;
-            }
-
-            my @srcinfo = binarytosource($pkg, $version, $arch);
-            unless (@srcinfo) {
-                # We don't have explicit information about the
-                # binary-to-source mapping for this version (yet). Since
-                # this is a CGI script and our output is transient, we can
-                # get away with just looking in the unversioned map; if it's
-                # wrong (as it will be when binary and source package
-                # versions differ), too bad.
-                my $pkgsrc = getpkgsrc();
-                if (exists $pkgsrc->{$pkg}) {
-                    @srcinfo = ([$pkgsrc->{$pkg}, $version]);
-                } elsif (getsrcpkgs($pkg)) {
-                    # If we're looking at a source package that doesn't have
-                    # a binary of the same name, just try the same version.
-                    @srcinfo = ([$pkg, $version]);
-                } else {
-                    next;
-                }
-            }
-            $sourceversions{"$_->[0]/$_->[1]"} = 1 foreach @srcinfo;
-            $_sourceversioncache{$cachekey} = [ map { "$_->[0]/$_->[1]" } @srcinfo ];
-        }
-    }
-
-    return sort keys %sourceversions;
-}
-
-my %_versionobj;
-sub buggyversion {
-    my ($bug, $ver, $status) = @_;
-    return '' unless defined $debbugs::gVersionPackagesDir;
-    my $src = getpkgsrc()->{$status->{package}};
-    $src = $status->{package} unless defined $src;
-
-    my $tree;
-    if (exists $_versionobj{$src}) {
-        $tree = $_versionobj{$src};
-    } else {
-        $tree = Debbugs::Versions->new(\&DpkgVer::vercmp);
-        my $srchash = substr $src, 0, 1;
-        if (open VERFILE, "< $debbugs::gVersionPackagesDir/$srchash/$src") {
-            $tree->load(\*VERFILE);
-            close VERFILE;
-        }
-        $_versionobj{$src} = $tree;
-    }
-
-    my @found = makesourceversions($status->{package}, undef,
-                                   @{$status->{found_versions}});
-    my @fixed = makesourceversions($status->{package}, undef,
-                                   @{$status->{fixed_versions}});
-
-    return $tree->buggy($ver, \@found, \@fixed);
-}
-
-my %_versions;
-sub getversions {
-    my ($pkg, $dist, $arch) = @_;
-    return () unless defined $debbugs::gVersionIndex;
-    $dist = 'unstable' unless defined $dist;
-
-    unless (tied %_versions) {
-        tie %_versions, 'MLDBM', $debbugs::gVersionIndex, O_RDONLY
-            or die "can't open versions index: $!";
-    }
-
-    if (defined $arch and exists $_versions{$pkg}{$dist}{$arch}) {
-        my $ver = $_versions{$pkg}{$dist}{$arch};
-        return $ver if defined $ver;
-        return ();
-    } else {
-        my %uniq;
-        for my $ar (keys %{$_versions{$pkg}{$dist}}) {
-            $uniq{$_versions{$pkg}{$dist}{$ar}} = 1 unless $ar eq 'source';
-        }
-        if (%uniq) {
-            return keys %uniq;
-        } elsif (exists $_versions{$pkg}{$dist}{source}) {
-            # Maybe this is actually a source package with no corresponding
-            # binaries?
-            return $_versions{$pkg}{$dist}{source};
-        } else {
-            return ();
-        }
-    }
+    my ($bug) = @_;
+    return get_bug_status(bug => $bug,
+                         $use_bug_idx?(bug_index => \%bugidx):(),
+                         usertags => \%common_bugusertags,
+                         (defined $common_dist)?(dist => $common_dist):(),
+                         (defined $common_version)?(version => $common_version):(),
+                         (defined $common_arch)?(arch => $common_arch):(),
+                        );
 }
 
 sub getversiondesc {
index 60860473840b47ca20364f75edab9f910979a6eb..c667aacab0e2d11905e2a9cd81b45f28347479ff 100755 (executable)
@@ -1,55 +1,64 @@
 #!/usr/bin/perl -wT
 
-package debbugs;
-
+use warnings;
 use strict;
-use POSIX qw(strftime tzset nice);
+use POSIX qw(strftime nice);
 
-#require '/usr/lib/debbugs/errorlib';
+use Debbugs::Config;
+use CGI::Simple;
+use Debbugs::CGI qw(cgi_parameters);
 require './common.pl';
 
-require '/etc/debbugs/config';
-require '/etc/debbugs/text';
-
 nice(5);
 
-my %param = readparse();
+my $q = new CGI::Simple;
+my %param = cgi_parameters(query   => $q,
+                          single  => [qw(indexon repeatmerged archive sortby),
+                                      qw(skip max_results first),
+                                     ],
+                          default => {indexon      => 'pkg',
+                                      repeatmerged => 'yes',
+                                      archive      => 'no',
+                                      sortby       => 'alpha',
+                                      skip         => 0,
+                                      max_results  => 100,
+                                     },
+                         );
+
+if (defined $param{first}) {
+     # rip out all non-words from first
+     $param{first} =~ s/\W//g;
+}
+if (defined $param{next}) {
+     $param{skip}+=$param{max_results};
+}
+elsif (defined $param{prev}) {
+     $param{skip}-=$param{max_results};
+     $param{skip} = 0 if $param{skip} < 0;
+}
 
-my $indexon = $param{'indexon'} || 'pkg';
-if ($indexon !~ m/^(pkg|src|maint|submitter|tag)$/) {
+my $indexon = $param{indexon};
+if ($param{indexon} !~ m/^(pkg|src|maint|submitter|tag)$/) {
     quitcgi("You have to choose something to index on");
 }
 
-my $repeatmerged = ($param{'repeatmerged'} || "yes") eq "yes";
-my $archive = ($param{'archive'} || "no") eq "yes";
-my $sortby = $param{'sortby'} || 'alpha';
+my $repeatmerged = $param{repeatmerged} eq 'yes';
+my $archive = $param{archive} eq "yes";
+my $sortby = $param{sortby};
 if ($sortby !~ m/^(alpha|count)$/) {
     quitcgi("Don't know how to sort like that");
 }
 
-#my $include = $param{'include'} || "";
-#my $exclude = $param{'exclude'} || "";
-
 my $Archived = $archive ? " Archived" : "";
 
 my %maintainers = %{&getmaintainers()};
 my %strings = ();
 
-$ENV{"TZ"} = 'UTC';
-tzset();
-
-my $dtime = strftime "%a, %e %b %Y %T UTC", localtime;
-my $tail_html = $debbugs::gHTMLTail;
-$tail_html = $debbugs::gHTMLTail;
+my $dtime = strftime "%a, %e %b %Y %T UTC", gmtime;
+my $tail_html = '';#$gHTMLTail;
+$tail_html = '';#$gHTMLTail;
 $tail_html =~ s/SUBSTITUTE_DTIME/$dtime/;
 
-set_option("repeatmerged", $repeatmerged);
-set_option("archive", $archive);
-#set_option("include", { map {($_,1)} (split /[\s,]+/, $include) })
-#      if ($include);
-#set_option("exclude", { map {($_,1)} (split /[\s,]+/, $exclude) })
-#      if ($exclude);
-
 my %count;
 my $tag;
 my $note;
@@ -58,6 +67,16 @@ my %sortkey = ();
 if ($indexon eq "pkg") {
   $tag = "package";
   %count = countbugs(sub {my %d=@_; return splitpackages($d{"pkg"})});
+  if (defined $param{first}) {
+       %count = map {
+           if (/^\Q$param{first}\E/) {
+                ($_,$count{$_});
+           }
+           else {
+                ();
+           } 
+       } keys %count;
+  }
   $note = "<p>Note that with multi-binary packages there may be other\n";
   $note .= "reports filed under the different binary package names.</p>\n";
   foreach my $pkg (keys %count) {
@@ -72,6 +91,16 @@ if ($indexon eq "pkg") {
 } elsif ($indexon eq "src") {
   $tag = "source package";
   my $pkgsrc = getpkgsrc();
+  if (defined $param{first}) {
+       %count = map {
+           if (/^\Q$param{first}\E/) {
+                ($_,$count{$_});
+           }
+           else {
+                ();
+           } 
+       } keys %count;
+  }
   %count = countbugs(sub {my %d=@_;
                           return map {
                             $pkgsrc->{$_} || $_
@@ -100,6 +129,16 @@ if ($indexon eq "pkg") {
                             map { $_->address } @me;
                           } splitpackages($d{"pkg"});
                          });
+  if (defined $param{first}) {
+       %count = map {
+           if (/^\Q$param{first}\E/) {
+                ($_,$count{$_});
+           }
+           else {
+                ();
+           } 
+       } keys %count;
+  }
   $note = "<p>Note that maintainers may use different Maintainer fields for\n";
   $note .= "different packages, so there may be other reports filed under\n";
   $note .= "different addresses.</p>\n";
@@ -118,6 +157,16 @@ if ($indexon eq "pkg") {
                           }
                           map { $_->address } @se;
                          });
+  if (defined $param{first}) {
+       %count = map {
+           if (/^\Q$param{first}\E/) {
+                ($_,$count{$_});
+           }
+           else {
+                ();
+           } 
+       } keys %count;
+  }
   foreach my $sub (keys %count) {
     $sortkey{$sub} = lc $fullname{$sub};
     $htmldescrip{$sub} = sprintf('<a href="%s">%s</a>',
@@ -130,6 +179,16 @@ if ($indexon eq "pkg") {
 } elsif ($indexon eq "tag") {
   $tag = "tag";
   %count = countbugs(sub {my %d=@_; return split ' ', $d{tags}; });
+  if (defined $param{first}) {
+       %count = map {
+           if (/^\Q$param{first}\E/) {
+                ($_,$count{$_});
+           }
+           else {
+                ();
+           } 
+       } keys %count;
+  }
   $note = "";
   foreach my $keyword (keys %count) {
     $sortkey{$keyword} = lc $keyword;
@@ -146,7 +205,13 @@ if ($sortby eq "count") {
 } else { # sortby alpha
   @orderedentries = sort { $sortkey{$a} cmp $sortkey{$b} } keys %count;
 }
+my $skip = $param{skip};
+my $max_results = $param{max_results};
 foreach my $x (@orderedentries) {
+     if (not defined $param{first}) {
+         $skip-- and next if $skip > 0;
+         last if --$max_results < 0;
+     }
   $result .= "<li>" . $htmldescrip{$x} . " has $count{$x} " .
             ($count{$x} == 1 ? "bug" : "bugs") . "</li>\n";
 }
@@ -164,6 +229,28 @@ print "<H1>" . "$debbugs::gProject$Archived $debbugs::gBug report logs by $tag"
       "</H1>\n";
 
 print $note;
+print <<END;
+<form>
+<input type="hidden" name="skip" value="$param{skip}">
+<input type="hidden" name="max_results" value="$param{max_results}">
+<input type="hidden" name="indexon" value="$param{indexon}">
+<input type="hidden" name="repeatmerged" value="$param{repeatmerged}">
+<input type="hidden" name="archive" value="$param{archive}">
+<input type="hidden" name="sortby" value="$param{sortby}">
+END
+if (defined $param{first}) {
+     print qq(<input type="hidden" name="first" value="$param{first}">\n);
+}
+else {
+     print q(<p>);
+     if ($param{skip} > 0) {
+         print q(<input type="submit" name="prev" value="Prev">);
+     }
+     if (keys %count > ($param{skip} + $param{max_results})) {
+         print q(<input type="submit" name="next" value="Next">);
+     }
+     print qq(</p>\n);
+}
 print $result;
 
 print "<hr>\n";
index 3a96210957183adfccc9d57ba5684d70300c0cf3..1f2294cf8449b5f00854811687fccba6cb60cc8b 100755 (executable)
@@ -3,15 +3,15 @@
 package debbugs;
 
 use strict;
-use POSIX qw(strftime tzset nice);
+use POSIX qw(strftime nice);
 
-#require '/usr/lib/debbugs/errorlib';
 require './common.pl';
 
-require '/etc/debbugs/config';
-require '/etc/debbugs/text';
-
+use Debbugs::Config qw(:globals :text);
 use Debbugs::User;
+use Debbugs::CGI qw(version_url);
+use Debbugs::Common qw(getparsedaddrs);
+use Debbugs::Bugs qw(get_bugs);
 
 use vars qw($gPackagePages $gWebDomain %gSeverityDisplay @gSeverityList);
 
@@ -90,10 +90,10 @@ my %cats = (
     } ],
     "severity" => [ {
         "nam" => "Severity",
-        "pri" => [map { "severity=$_" } @debbugs::gSeverityList],
-        "ttl" => [map { $debbugs::gSeverityDisplay{$_} } @debbugs::gSeverityList],
+        "pri" => [map { "severity=$_" } @gSeverityList],
+        "ttl" => [map { $gSeverityDisplay{$_} } @gSeverityList],
         "def" => "Unknown Severity",
-        "ord" => [0,1,2,3,4,5,6,7],
+        "ord" => [0..@gSeverityList],
     } ],
     "classification" => [ {
         "nam" => "Classification",
@@ -181,10 +181,7 @@ my $this = "";
 my %indexentry;
 my %strings = ();
 
-$ENV{"TZ"} = 'UTC';
-tzset();
-
-my $dtime = strftime "%a, %e %b %Y %T UTC", localtime;
+my $dtime = strftime "%a, %e %b %Y %T UTC", gmtime;
 my $tail_html = $debbugs::gHTMLTail;
 $tail_html = $debbugs::gHTMLTail;
 $tail_html =~ s/SUBSTITUTE_DTIME/$dtime/;
@@ -246,12 +243,7 @@ if (defined $pkg) {
     $title .= " ($verdesc)" if defined $verdesc;
   }
   my @pkgs = split /,/, $pkg;
-  @bugs = @{getbugs(sub {my %d=@_;
-                         foreach my $try (splitpackages($d{"pkg"})) {
-                           return 1 if grep($try eq $_, @pkgs);
-                         }
-                         return 0;
-                        }, 'package', @pkgs)};
+  @bugs = get_bugs(package=>\@pkgs);
 } elsif (defined $src) {
   add_user("$src\@packages.debian.org");
   $title = "source $src";
@@ -263,47 +255,12 @@ if (defined $pkg) {
     my $verdesc = getversiondesc($src);
     $title .= " ($verdesc)" if defined $verdesc;
   }
-  my @pkgs = ();
-  my @srcs = split /,/, $src;
-  foreach my $try (@srcs) {
-    push @pkgs, getsrcpkgs($try);
-    push @pkgs, $try if ( !grep(/^\Q$try\E$/, @pkgs) );
-  }
-  @bugs = @{getbugs(sub {my %d=@_;
-                         foreach my $try (splitpackages($d{"pkg"})) {
-                           return 1 if grep($try eq $_, @pkgs);
-                         }
-                         return 0;
-                        }, 'package', @pkgs)};
+  @bugs = get_bugs(src=>[split /,/, $src]);
 } elsif (defined $maint) {
-  my %maintainers = %{getmaintainers()};
   add_user($maint);
   $title = "maintainer $maint";
   $title .= " in $dist" if defined $dist;
-  if ($maint eq "") {
-    @bugs = @{getbugs(sub {my %d=@_;
-                           foreach my $try (splitpackages($d{"pkg"})) {
-                             return 1 if !getparsedaddrs($maintainers{$try});
-                           }
-                           return 0;
-                          })};
-  } else {
-    my @maints = split /,/, $maint;
-    my @pkgs = ();
-    foreach my $try (@maints) {
-      foreach my $p (keys %maintainers) {
-        my @me = getparsedaddrs($maintainers{$p});
-        push @pkgs, $p if grep { $_->address eq $try } @me;
-      }
-    }
-    @bugs = @{getbugs(sub {my %d=@_;
-                           foreach my $try (splitpackages($d{"pkg"})) {
-                             my @me = getparsedaddrs($maintainers{$try});
-                             return 1 if grep { $_->address eq $maint } @me;
-                           }
-                           return 0;
-                          }, 'package', @pkgs)};
-  }
+  @bugs = get_bugs(maint=>[split /,/,$maint]);
 } elsif (defined $maintenc) {
   my %maintainers = %{getmaintainers()};
   $title = "encoded maintainer $maintenc";
@@ -322,12 +279,7 @@ if (defined $pkg) {
   $title = "submitter $submitter";
   $title .= " in $dist" if defined $dist;
   my @submitters = split /,/, $submitter;
-  @bugs = @{getbugs(sub {my %d=@_;
-                         my @se = getparsedaddrs($d{"submitter"} || "");
-                         foreach my $try (@submitters) {
-                           return 1 if grep { $_->address eq $try } @se;
-                         }
-                        }, 'submitter-email', @submitters)};
+  @bugs = get_bugs(submitter => \@submitters);
 } elsif (defined($severity) && defined($status)) {
   $title = "$status $severity bugs";
   $title .= " in $dist" if defined $dist;
@@ -376,12 +328,12 @@ print "Content-Type: text/html; charset=utf-8\n\n";
 
 print "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n";
 print "<HTML><HEAD>\n" . 
-    "<TITLE>$debbugs::gProject$Archived $debbugs::gBug report logs: $title</TITLE>\n" .
-    '<link rel="stylesheet" href="/css/bugs.css" type="text/css">' .
+    "<TITLE>$gProject$Archived $gBug report logs: $title</TITLE>\n" .
+    qq(<link rel="stylesheet" href="$gWebHostBugDir/css/bugs.css" type="text/css">) .
     "</HEAD>\n" .
     '<BODY onload="pagemain();">' .
     "\n";
-print "<H1>" . "$debbugs::gProject$Archived $debbugs::gBug report logs: $title" .
+print "<H1>" . "$gProject$Archived $gBug report logs: $title" .
       "</H1>\n";
 
 my $showresult = 1;
@@ -426,12 +378,12 @@ if (defined $pkg || defined $src) {
         if ($pkg and defined($pseudodesc) and exists($pseudodesc->{$pkg})) {
             push @references, "to the <a href=\"http://${debbugs::gWebDomain}/pseudo-packages${debbugs::gHTMLSuffix}\">list of other pseudo-packages</a>";
         } else {
-            if ($pkg and defined $debbugs::gPackagePages) {
+            if ($pkg and defined $gPackagePages) {
                 push @references, sprintf "to the <a href=\"%s\">%s package page</a>", urlsanit("http://${debbugs::gPackagePages}/$pkg"), htmlsanit("$pkg");
             }
-            if (defined $debbugs::gSubscriptionDomain) {
+            if (defined $gSubscriptionDomain) {
                 my $ptslink = $pkg ? $srcforpkg : $src;
-                push @references, "to the <a href=\"http://$debbugs::gSubscriptionDomain/$ptslink\">Package Tracking System</a>";
+                push @references, "to the <a href=\"http://$gSubscriptionDomain/$ptslink\">Package Tracking System</a>";
             }
             # Only output this if the source listing is non-trivial.
             if ($pkg and $srcforpkg and (@pkgs or $pkg ne $srcforpkg)) {
@@ -466,12 +418,13 @@ if (defined $pkg || defined $src) {
 
 set_option("archive", !$archive);
 printf "<p>See the <a href=\"%s\">%s reports</a></p>",
-     urlsanit('pkgreport.cgi?'.join(';',
-                                   (map {$_ eq 'archived'?():("$_=$param{$_}")
-                                    } keys %param
-                                   ),
-                                   ('archived='.($archive?"no":"yes"))
-                                  )
+     urlsanit(pkg_url((
+                      map {
+                           $_ eq 'archive'?():($_,$param{$_})
+                      } keys %param
+                     ),
+                     ('archive',($archive?"no":"yes"))
+                    )
             ), ($archive ? "active" : "archived");
 set_option("archive", $archive);
 
@@ -511,10 +464,10 @@ if (defined $pkg) {
 }
 print "<tr><td>&nbsp;</td></tr>\n";
 
-my $includetags = htmlsanit(join(" ", grep { !m/^subj:/i } split /[\s,]+/, $include));
-my $excludetags = htmlsanit(join(" ", grep { !m/^subj:/i } split /[\s,]+/, $exclude));
-my $includesubj = htmlsanit(join(" ", map { s/^subj://i; $_ } grep { m/^subj:/i } split /[\s,]+/, $include));
-my $excludesubj = htmlsanit(join(" ", map { s/^subj://i; $_ } grep { m/^subj:/i } split /[\s,]+/, $exclude));
+my $includetags = htmlsanit(join(" ", grep { !m/^subj:/i } map {split /[\s,]+/} ref($include)?@{$include}:$include));
+my $excludetags = htmlsanit(join(" ", grep { !m/^subj:/i } map {split /[\s,]+/} ref($exclude)?@{$exclude}:$exclude));
+my $includesubj = htmlsanit(join(" ", map { s/^subj://i; $_ } grep { m/^subj:/i } map {split /[\s,]+/} ref($include)?@{$include}:$include));
+my $excludesubj = htmlsanit(join(" ", map { s/^subj://i; $_ } grep { m/^subj:/i } map {split /[\s,]+/} ref($exclude)?@{$exclude}:$exclude));
 my $vismindays = ($mindays == 0 ? "" : $mindays);
 my $vismaxdays = ($maxdays == -1 ? "" : $maxdays);
 
@@ -617,7 +570,11 @@ sub pkg_htmlindexentrystatus {
         my @fixed = @{$status{fixed_versions}};
         $showversions .= join ', ', map {s{/}{ }; htmlsanit($_)} @fixed;
     }
-    $result .= " ($showversions)" if length $showversions;
+    $result .= ' (<a href="'.
+        version_url($status{package},
+                    $status{found_versions},
+                    $status{fixed_versions},
+                   ).qq{">$showversions</a>)} if length $showversions;
     $result .= ";\n";
 
     $result .= $showseverity;
@@ -637,16 +594,15 @@ sub pkg_htmlindexentrystatus {
     $result .= buglinklist(";\nBlocks ", ", ",
         split(/ /,$status{blocks}));
 
-    my $days = 0;
     if (length($status{done})) {
         $result .= "<br><strong>Done:</strong> " . htmlsanit($status{done});
-# Disabled until archiving actually works again
-#        $days = ceil($debbugs::gRemoveAge - -M buglog($status{id}));
-#         if ($days >= 0) {
-#             $result .= ";\n<strong>Will be archived" . ( $days == 0 ? " today" : $days == 1 ? " in $days day" : " in $days days" ) . "</strong>";
-#         } else {
-#             $result .= ";\n<strong>Archived</strong>";
-        }
+        my $days = bug_archiveable(bug => $status{id},
+                                  status => \%status,
+                                  days_until => 1,
+                                 );
+        if ($days >= 0) {
+            $result .= ";\n<strong>Will be archived" . ( $days == 0 ? " today" : $days == 1 ? " in $days day" : " in $days days" ) . "</strong>";
+        }
     }
 
     unless (length($status{done})) {
@@ -654,7 +610,7 @@ sub pkg_htmlindexentrystatus {
             $result .= ";\n<strong>Forwarded</strong> to "
                        . join(', ',
                              map {maybelink($_)}
-                             split /,\s*/,$status{forwarded}
+                             split /[,\s]+/,$status{forwarded}
                             );
         }
         my $daysold = int((time - $status{date}) / 86400);   # seconds to days
@@ -695,7 +651,7 @@ sub pkg_htmlizebugs {
     my $header = '';
     my $footer = "<h2 class=\"outstanding\">Summary</h2>\n";
 
-    my @dummy = ($debbugs::gRemoveAge); #, @debbugs::gSeverityList, @debbugs::gSeverityDisplay);  #, $debbugs::gHTMLExpireNote);
+    my @dummy = ($gRemoveAge); #, @gSeverityList, @gSeverityDisplay);  #, $gHTMLExpireNote);
 
     if (@bugs == 0) {
         return "<HR><H2>No reports found!</H2></HR>\n";
@@ -821,20 +777,7 @@ sub pkg_htmlpackagelinks {
 }
 
 sub pkg_htmladdresslinks {
-    my ($prefixfunc, $urlfunc, $addresses) = @_;
-    if (defined $addresses and $addresses ne '') {
-        my @addrs = getparsedaddrs($addresses);
-        my $prefix = (ref $prefixfunc) ? $prefixfunc->(scalar @addrs)
-                                       : $prefixfunc;
-        return $prefix .
-               join ', ', map { sprintf '<a class="submitter" href="%s">%s</a>',
-                                        $urlfunc->($_->address),
-                                        htmlsanit($_->format) || '(unknown)'
-                              } @addrs;
-    } else {
-        my $prefix = (ref $prefixfunc) ? $prefixfunc->(1) : $prefixfunc;
-        return sprintf '%s<a class="submitter" href="%s">(unknown)</a>', $prefix, $urlfunc->('');
-    }
+     htmlize_addresslinks(@_,'submitter');
 }
 
 sub pkg_javascript {
@@ -957,12 +900,10 @@ sub pkg_htmlselectarch {
 }
 
 sub myurl {
-     return urlsanit('pkgreport.cgi?'.
-                    join(';',
-                         (map {("$_=$param{$_}")
-                                           } keys %param
-                         )
-                        )
+     return urlsanit(pkg_url(map {exists $param{$_}?($_,$param{$_}):()}
+                            qw(archive repeatmerged mindays maxdays),
+                            qw(version dist arch pkg src tag maint submitter)
+                           )
                    );
 }
 
@@ -1018,13 +959,8 @@ sub get_bug_order_index {
 
 sub buglinklist {
     my ($prefix, $infix, @els) = @_;
-    my $sep = $prefix;
-    my $r = "";
-    for my $e (@els) {
-        $r .= $sep."<A class=\"submitter\" href=\"" . bugurl($e) . "\">#$e</A>";
-        $sep = $infix;
-    }
-    return $r;
+    return '' if not @els;
+    return $prefix . bug_linklist($infix,'submitter',@els);
 }
 
 
diff --git a/cgi/search.cgi b/cgi/search.cgi
new file mode 100755 (executable)
index 0000000..5f6eadc
--- /dev/null
@@ -0,0 +1,322 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+# Hack to work on merkel where suexec is in place
+BEGIN{
+     if ($ENV{HTTP_HOST} eq 'merkel.debian.org') {
+         unshift @INC, qw(/home/don/perl/usr/share/perl5 /home/don/perl/usr/lib/perl5 /home/don/source);
+         $ENV{DEBBUGS_CONFIG_FILE}="/home/don/config_internal";
+     }
+}
+
+
+use CGI::Simple;
+
+use CGI::Alert 'don@donarmstrong.com';
+
+use Search::Estraier;
+use Debbugs::Config qw(:config);
+use Debbugs::Estraier;
+use Debbugs::CGI qw(htmlize_packagelinks html_escape cgi_parameters);
+use HTML::Entities qw(encode_entities);
+
+my $q = new CGI::Simple;
+
+#my %var_defaults = (attr => 1,);
+
+my %cgi_var = cgi_parameters(query => $q,
+                            single => [qw(phrase max_results order_field order_operator),
+                                       qw(skip prev next),
+                                      ],
+                            default => {phrase      => '',
+                                        max_results => 10,
+                                        skip        => 0,
+                                       },
+                           );
+
+$cgi_var{attribute} = parse_attribute(\%cgi_var) || [];
+
+my @results;
+
+if (defined $cgi_var{next}) {
+     $cgi_var{search} = 1;
+     $cgi_var{skip} += $cgi_var{max_results};
+}
+elsif (defined $cgi_var{prev}) {
+     $cgi_var{search} = 1;
+     $cgi_var{skip} -= $cgi_var{max_results};
+     $cgi_var{skip} = 0 if $cgi_var{skip} < 0;
+}
+
+my $nres;
+if (defined $cgi_var{search} and length $cgi_var{phrase}) {
+     # connect to a node if we need to
+     my $node =  new Search::Estraier::Node (url    => $config{search_estraier}{url},
+                                            user   => $config{search_estraier}{user},
+                                            passwd => $config{search_estraier}{pass},
+                                            croak_on_error => 1,
+                                           ) or die "Unable to connect to the node";
+     my $cond = new Search::Estraier::Condition;
+     $cond->set_phrase($cgi_var{phrase});
+     if (defined $cgi_var{order_field} and length $cgi_var{order_field} and
+        defined $cgi_var{order_operator} and length $cgi_var{order_operator}) {
+         $cond->set_order($cgi_var{order_field}.' '.$cgi_var{order_operator});
+     }
+     foreach my $attribute (@{$cgi_var{attribute}}) {
+         if (defined $$attribute{field} and defined $$attribute{value} and
+             defined $$attribute{operator} and length $$attribute{value}) {
+              $cond->add_attr(join(' ',map {$$attribute{$_}} qw(field operator value)));
+         }
+     }
+     $cond->set_skip($cgi_var{skip}) if defined $cgi_var{skip} and $cgi_var{skip} =~ /(\d+)/;
+     $cond->set_max($cgi_var{max_results}) if defined $cgi_var{max_results} and $cgi_var{max_results} =~ /^\d+$/;
+     print STDERR "skip: ".$cond->skip()."\n";
+     print STDERR $node->cond_to_query($cond),qq(\n);
+     $nres = $node->search($cond,0) or
+         die "Unable to search for condition";
+
+}
+elsif (defined $cgi_var{add_attribute} and length $cgi_var{add_attribute}) {
+     push @{$cgi_var{attribute}}, {value => ''};
+}
+elsif (grep /^delete_attribute_\d+$/, keys %cgi_var) {
+     foreach my $delete_key (sort {$b <=> $a} map {/^delete_attribute_(\d+)$/?($1):()} keys %cgi_var) {
+         splice @{$cgi_var{attribute}},$delete_key,1;
+     }
+}
+
+my $url = 'http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=';
+
+print <<END;
+Content-Type: text/html
+
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<HTML><HEAD><TITLE>BTS Search</TITLE>
+<link rel="stylesheet" href="http://bugs.debian.org/css/bugs.css" type="text/css">
+</HEAD>
+<BODY>
+<FORM>
+<table class="forms">
+<tr><td>
+<p>Phrase: <input type="text" name="phrase" value="$cgi_var{phrase}" size="80" id="phrase" title="Input some words for full-text search" tabindex="1" accesskey="a" />
+<input type="submit" name="search" value="search" title="Perform the search" tabindex="8" accesskey="f" />
+<input type="hidden" name="skip" value="$cgi_var{skip}"></p>
+END
+
+# phrase
+# attributes
+# NUMEQ : is equal to the number or date
+# NUMNE : is not equal to the number or date
+# NUMGT : is greater than the number or date
+# NUMGE : is greater than or equal to the number or date
+# NUMLT : is less than the number or date
+# NUMLE : is less than or equal to the number or date
+# NUMBT : is between the two numbers or dates
+my @num_operators = (NUMEQ => 'equal to',
+                    NUMNE => 'not equal to',
+                    NUMGT => 'greater than',
+                    NUMGE => 'greater than or equal to',
+                    NUMLT => 'less than',
+                    NUMLE => 'less than or equal to',
+                    NUMBT => 'between',
+                   );
+
+# STREQ : is equal to the string
+# STRNE : is not equal to the string
+# STRINC : includes the string
+# STRBW : begins with the string
+# STREW : ends with the string
+# STRAND : includes all tokens in the string
+# STROR : includes at least one token in the string
+# STROREQ : is equal to at least one token in the string
+# STRRX : matches regular expressions of the string
+my @str_operators = (STREQ   => 'equal to',
+                    STRNE   => 'not equal to',
+                    STRINC  => 'includes',
+                    STRBW   => 'begins with',
+                    STREW   => 'ends with',
+                    STRAND  => 'includes all tokens',
+                    STROR   => 'includes at least one token',
+                    STROREQ => 'is equal to at least one token',
+                    STRRX   => 'matches regular expression',
+                   );
+
+my @attributes_order = ('@cdate','@title','@author',
+                       qw(status subject date submitter package tags severity),
+                      );
+my %attributes = ('@cdate'  => {name => 'Date',
+                               type      => 'num',
+                              },
+                 '@title'  => {name => 'Message subject',
+                               type      => 'str',
+                              },
+                 '@author' => {name => 'Author',
+                               type      => 'str',
+                              },
+                 status    => {name => 'Status',
+                               type      => 'str',
+                              },
+                 subject   => {name => 'Bug Title',
+                               type      => 'num',
+                              },
+                 date      => {name => 'Submission date',
+                               type      => 'num',
+                              },
+                 submitter => {name => 'Bug Submitter',
+                               type      => 'str',
+                              },
+                 package   => {name => 'Package',
+                               type      => 'str',
+                              },
+                 tags      => {name => 'Tags',
+                               type      => 'str',
+                              },
+                 severity  => {name => 'Severity',
+                               type      => 'str',
+                              },
+                );
+my $attr_num = 0;
+print qq(<p>Attributes:</p>\n);
+for my $attribute (@{$cgi_var{attribute}}) {
+     print qq(<select name="attribute_field">\n);
+     foreach my $attr (keys %attributes) {
+         my $selected = (defined $$attribute{field} and $$attribute{field} eq $attr) ? ' selected' : '';
+         print qq(<option value="$attr"$selected>$attributes{$attr}{name}</option>\n);
+     }
+     print qq(</select>\n);
+     print qq(<select name="attribute_operator">\n);
+     my $operator;
+     my $name;
+     my @tmp_array = (@num_operators,@str_operators);
+     while (($operator,$name) = splice(@tmp_array,0,2)) {
+         my $type = $operator =~ /^NUM/ ? 'Number' : 'String';
+         my $selected = (defined $$attribute{operator} and $$attribute{operator} eq $operator) ? 'selected' : '';
+         print qq(<option value="$operator"$selected>$name ($type)</option>\n);
+     }
+     print qq(</select>\n);
+     $$attribute{value}='' if not defined $$attribute{value};
+     print qq(<input type="text" name="attribute_value" value="$$attribute{value}"><input type="submit" name="delete_attribute_$attr_num" value="Delete"><br/>\n);
+     $attr_num++;
+
+}
+print qq(<input type="submit" name="add_attribute" value="Add Attribute"><br/>);
+
+# order
+
+# STRA : ascending by string
+# STRD : descending by string
+# NUMA : ascending by number or date
+# NUMD : descending by number or date
+
+my @order_operators = (STRA => 'ascending (string)',
+                      STRD => 'descending (string)',
+                      NUMA => 'ascending (number or date)',
+                      NUMD => 'descending (number or date)',
+                     );
+
+print qq(<p>Order by: <select name="order_field">\n);
+print qq(<option value="">Default</option>);
+foreach my $attr (keys %attributes) {
+     my $selected = (defined $cgi_var{order_field} and $cgi_var{order_field} eq $attr) ? ' selected' : '';
+     print qq(<option value="$attr"$selected>$attributes{$attr}{name}</option>\n);
+}
+print qq(</select>\n);
+print qq(<select name="order_operator">\n);
+my $operator;
+my $name;
+my @tmp_array = (@order_operators);
+while (($operator,$name) = splice(@tmp_array,0,2)) {
+     my $selected = (defined $cgi_var{order_field} and $cgi_var{order_operator} eq $operator) ? ' selected' : '';
+     print qq(<option value="$operator"$selected>$name</option>\n);
+}
+print qq(</select></p>\n);
+
+# max results
+
+print qq(<p>Max results: <select name="max_results">\n);
+for my $max_results (qw(10 25 50 100 150 200)) {
+     my $selected = (defined $cgi_var{max_results} and $cgi_var{max_results} eq $max_results) ? ' selected' : '';
+     print qq(<option value="$max_results"$selected>$max_results</option>\n);
+}
+print qq(</select></p>\n);
+
+print qq(</tr></table>\n);
+
+
+
+if (defined $nres) {
+     print "<h2> Results</h2>\n";
+     my $hits = $nres->hits();
+     print "<p>Hits: ".$hits;
+     if (($cgi_var{skip} > 0)) {
+         print q(<input type="submit" name="prev" value="Prev">);
+     }
+     if ($hits > ($cgi_var{skip}+$cgi_var{max_results})) {
+         print q(<input type="submit" name="next" value="Next">);
+     }
+     print "</p>\n";
+     print qq(<ul class="msgreceived">\n);
+     for my $rdoc (map {$nres->get_doc($_)} 0.. ($nres->doc_num-1)) {
+         my ($bugnum,$msgnum) = split m#/#,$rdoc->attr('@uri');
+         my %attr = map {($_,$rdoc->attr($_))} $rdoc->attr_names;
+         # initialize any missing variables
+         for my $var ('@title','@author','@cdate','package','severity') {
+              $attr{$var} = '' if not defined $attr{$var};
+         }
+         my $showseverity;
+         $showseverity = "Severity: <em>$attr{severity}</em>;\n";
+         print <<END;
+<li><a href="$url${bugnum}#${msgnum}">#${bugnum}: $attr{'@title'}</a> @{[htmlize_packagelinks($attr{package})]}<br/>
+$showseverity<br/>
+Sent by: @{[encode_entities($attr{'@author'})]} at $attr{'@cdate'}<br/>
+END
+         # Deal with the snippet
+         # make the things that match bits of the phrase bold, the rest normal.
+         my $snippet_mod = html_escape($attr{snippet});
+         $snippet_mod =~ s/\n\n/&nbsp;&nbsp;. . .&nbsp;&nbsp;/g;
+         for my $phrase_bits (split /\s+/,$cgi_var{phrase}) {
+              $snippet_mod =~ s{\n(\Q$phrase_bits\E)(?:\s+\Q$phrase_bits\E\n)}{'<b>'.$1.'</b>'}gei;
+         }
+         print "<p>$snippet_mod</p>\n";
+     }
+     print "</ul>\n<p>";
+     if (($cgi_var{skip} > 0)) {
+         print q(<input type="submit" name="prev" value="Prev">);
+     }
+     if ($hits > ($cgi_var{skip}+$cgi_var{max_results})) {
+         print q(<input type="submit" name="next" value="Next">);
+     }
+     print "</p>\n";
+
+}
+
+print "</form>\n";
+
+# This CGI should make an abstract method of displaying information
+# about specific bugs and their messages; the information should be
+# fairly similar to the way that pkgreport.cgi works, with the
+# addition of snippit information and links to ajavapureapi/overview-summary.html specific message
+# within the bug.
+
+# For now we'll brute force the display, but methods to display a bug
+# or a particular bug message should be made common between the two
+# setups
+
+
+sub parse_attribute {
+     my ($cgi_var) = @_;
+
+     my @attributes = ();
+     if (ref $$cgi_var{attribute_operator}) {
+         for my $elem (0 ... $#{$$cgi_var{attribute_operator}}) {
+              push @attributes,{map {($_,$$cgi_var{"attribute_$_"}[$elem]);} qw(value field operator)};
+         }
+     }
+     elsif (defined $$cgi_var{attribute_operator}) {
+         push @attributes,{map {($_,$$cgi_var{"attribute_$_"});} qw(value field operator)};
+     }
+     return \@attributes;
+}
diff --git a/cgi/soap.cgi b/cgi/soap.cgi
new file mode 100755 (executable)
index 0000000..6599109
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/perl -wT
+
+package debbugs;
+
+use SOAP::Transport::HTTP;
+
+use Debbugs::SOAP::Usertag;
+use Debbugs::SOAP::Status;
+
+SOAP::Transport::HTTP::CGI
+    -> dispatch_to('Debbugs::SOAP::Usertag', 'Debbugs::SOAP::Status')
+    -> handle;
+
diff --git a/cgi/version.cgi b/cgi/version.cgi
new file mode 100755 (executable)
index 0000000..105111a
--- /dev/null
@@ -0,0 +1,190 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+# Hack to work on merkel where suexec is in place
+BEGIN{
+     if ($ENV{HTTP_HOST} eq 'merkel.debian.org') {
+         unshift @INC, qw(/home/don/perl/usr/share/perl5 /home/don/perl/usr/lib/perl5 /home/don/source);
+         $ENV{DEBBUGS_CONFIG_FILE}="/home/don/config_internal";
+     }
+}
+
+
+use CGI::Simple;
+
+use CGI::Alert 'don@donarmstrong.com';
+
+use Debbugs::Config qw(:config);
+use Debbugs::CGI qw(htmlize_packagelinks html_escape cgi_parameters);
+use Debbugs::Versions;
+use Debbugs::Versions::Dpkg;
+use Debbugs::Packages qw(getversions makesourceversions);
+use HTML::Entities qw(encode_entities);
+use File::Temp qw(tempdir);
+use IO::File;
+use IO::Handle;
+
+
+my %img_types = (svg => 'image/svg+xml',
+                png => 'image/png',
+               );
+
+my $q = new CGI::Simple;
+
+my %cgi_var = cgi_parameters(query   => $q,
+                            single  => [qw(package format ignore_boring width height)],
+                            default => {package       => 'xterm',
+                                        found         => [],
+                                        fixed         => [],
+                                        ignore_boring => 1,
+                                        format        => 'png',
+                                        width         => undef,
+                                        height        => undef,
+                                       },
+                           );
+
+# we want to first load the appropriate file,
+# then figure out which versions are there in which architectures,
+my %versions;
+my %version_to_dist;
+for my $dist (@{$config{distributions}}) {
+     $versions{$dist} = [getversions($cgi_var{package},$dist)];
+     # make version_to_dist
+     foreach my $version (@{$versions{$dist}}){
+         push @{$version_to_dist{$version}}, $dist;
+     }
+}
+
+if (defined $cgi_var{width}) {
+     $cgi_var{width} =~ /(\d+)/;
+     $cgi_var{width} = $1;
+}
+if (defined $cgi_var{height}) {
+     $cgi_var{height} =~ /(\d+)/;
+     $cgi_var{height} = $1;
+}
+
+if (defined $cgi_var{format}) {
+     $cgi_var{format} =~ /(png|svg|jpg|gif)/;
+     $cgi_var{format} = $1 || 'png';
+}
+else {
+     $cgi_var{format} = 'png';
+}
+
+# then figure out which are affected.
+# turn found and fixed into full versions
+@{$cgi_var{found}} = makesourceversions($cgi_var{package},undef,@{$cgi_var{found}});
+@{$cgi_var{fixed}} = makesourceversions($cgi_var{package},undef,@{$cgi_var{fixed}});
+my @interesting_versions = makesourceversions($cgi_var{package},undef,keys %version_to_dist);
+
+# We need to be able to rip out leaves which the versions that do not affect the current versions of unstable/testing
+my %sources;
+@sources{map {m{(.+)/}; $1} @{$cgi_var{found}}} = (1) x @{$cgi_var{found}};
+@sources{map {m{(.+)/}; $1} @{$cgi_var{fixed}}} = (1) x @{$cgi_var{fixed}};
+@sources{map {m{(.+)/}; $1} @interesting_versions} = (1) x @interesting_versions;
+my $version = Debbugs::Versions->new(\&Debbugs::Versions::Dpkg::vercmp);
+foreach my $source (keys %sources) {
+     my $srchash = substr $source, 0, 1;
+     my $version_fh = new IO::File "$config{version_packages_dir}/$srchash/$source", 'r';
+     $version->load($version_fh);
+}
+# Here, we need to generate a short version to full version map
+my %version_map;
+foreach my $key (keys %{$version->{parent}}) {
+     my ($short_version) = $key =~ m{/(.+)$};
+     next unless length $short_version;
+     # we let the first short version have presidence.
+     $version_map{$short_version} = $key if not exists $version_map{$short_version};
+}
+# Turn all short versions into long versions
+for my $found_fixed (qw(found fixed)) {
+     $cgi_var{$found_fixed} =
+         [
+          map {
+               if ($_ !~ m{/}) { # short version
+                    ($version_map{$_});
+               }
+               else { # long version
+                    ($_);
+               }
+          } @{$cgi_var{$found_fixed}}
+         ];
+}
+my %all_states = $version->allstates($cgi_var{found},$cgi_var{fixed});
+
+my $dot = "digraph G {\n";
+if (defined $cgi_var{width} and defined $cgi_var{height}) {
+     $dot .= qq(size="$cgi_var{width},$cgi_var{height}";\n);
+}
+my %state = (found  => ['fillcolor="salmon"',
+                       'style="filled"',
+                       'shape="diamond"',
+                      ],
+            absent => ['fillcolor="grey"',
+                       'style="filled"',
+                      ],
+            fixed  => ['fillcolor="chartreuse"',
+                       'style="filled"',
+                       'shape="rect"',
+                      ],
+           );
+# TODO: Allow collapsing versions which are at the same state and not
+# in a suite.
+foreach my $key (keys %all_states) {
+     my ($short_version) = $key =~ m{/(.+)$};
+     next if $cgi_var{ignore_boring} and (not defined $all_states{$key}
+                                         or $all_states{$key} eq 'absent');
+     next if $cgi_var{ignore_boring} and not version_relevant($version,$key,\@interesting_versions);
+     my @attributes = @{$state{$all_states{$key}}};
+     if (length $short_version and exists $version_to_dist{$short_version}) {
+         push @attributes, 'label="'.$key.'\n'."(".join(', ',@{$version_to_dist{$short_version}}).")\"";
+     }
+     my $node_attributes = qq("$key" [).join(',',@attributes).qq(]\n);
+     $dot .= $node_attributes;
+}
+foreach my $key (keys %{$version->{parent}}) {
+     next if not defined $version->{parent}{$key};
+     next if $cgi_var{ignore_boring} and $all_states{$key} eq 'absent';
+     next if $cgi_var{ignore_boring} and (not defined $all_states{$version->{parent}{$key}}
+                                         or $all_states{$version->{parent}{$key}} eq 'absent');
+     # Ignore branches which are not ancestors of a currently distributed version
+     next if $cgi_var{ignore_boring} and not version_relevant($version,$key,\@interesting_versions);
+     $dot .= qq("$key").'->'.qq("$version->{parent}{$key}" [dir="back"])."\n" if defined $version->{parent}{$key};
+}
+$dot .= "}\n";
+
+my $temp_dir = tempdir(CLEANUP => 1);
+
+if (not defined $cgi_var{dot}) {
+     my $dot_fh = new IO::File "$temp_dir/temp.dot",'w' or
+         die "Unable to open $temp_dir/temp.dot for writing: $!";
+     print {$dot_fh} $dot or die "Unable to print output to the dot file: $!";
+     close $dot_fh or die "Unable to close the dot file: $!";
+     system('dot','-T'.$cgi_var{format},"$temp_dir/temp.dot",'-o',"$temp_dir/temp.$cgi_var{format}") == 0
+         or print "Content-Type: text\n\nDot failed." and die "Dot failed: $?";
+     my $img_fh = new IO::File "$temp_dir/temp.$cgi_var{format}", 'r' or
+         die "Unable to open $temp_dir/temp.$cgi_var{format} for reading: $!";
+     print "Content-Type: $img_types{$cgi_var{format}}\n\n";
+     print <$img_fh>;
+     close $img_fh;
+}
+else {
+     print "Content-Type: text\n\n";
+     print $dot;
+}
+
+
+my %_version_relevant_cache;
+sub version_relevant {
+     my ($version,$test_version,$relevant_versions) = @_;
+     for my $dist_version (@{$relevant_versions}) {
+         print STDERR "Testing $dist_version against $test_version\n";
+         return 1 if $version->isancestor($test_version,$dist_version);
+     }
+     return 0;
+}
+
+
diff --git a/clean b/clean
deleted file mode 100755 (executable)
index 745d1e9..0000000
--- a/clean
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-find \( -name '*.out' -o -name '*~' -o -name '#*#' \) -print | xargs -r rm -f --
-rm -f install install.new
diff --git a/debbugs-dump b/debbugs-dump
deleted file mode 100755 (executable)
index 7c2b29b..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-#!/usr/bin/perl -w
-
-push(@INC,'.');
-use strict;
-use Debbugs::Config qw(%Globals &ParseConfigFile);
-#use Debvote::Email qw(&InitEmailTags &LoadEmail &ProcessTags %gtags);
-use Debbugs::DBase;
-use Getopt::Long;
-
-#############################################################################
-#  Customization Variables
-#############################################################################
-
-#############################################################################
-#  Gloabal Variable Declaration
-#############################################################################
-my $VERSION = '3.01';                          #External Version number
-my $BANNER = "DebBugs v$VERSION";              #Version Banner - text form
-my $FILE = 'debbugs-dump';                     #File name
-my $config = '';
-my @config = undef;
-
-#############################################################################
-#  Commandline parsing
-#############################################################################
-# Hash used to process commandline options
-my $verbose = 0;
-my $quiet = 0;
-my $debug = 0;
-my %opthash = (# ------------------ actions
-    "config|c=s" => \$config,
-    "help|h" => \&syntax,
-    "version|V" => \&banner,
-    "verbose|v!" => \$verbose,
-    "quiet|q!" => \$quiet,
-    "debug|d+" => \$debug,     # Count the -d flags
-    );
-Getopt::Long::config('bundling', 'no_getopt_compat', 'no_auto_abbrev');
-GetOptions(%opthash) or &syntax( 1 );
-if ( $debug > 1 )
-{      print "D2: Commandline:\n";
-       print "\tconfig = $config\n" unless $config eq '';
-       print "\tverbos\n" if $verbose;
-       print "\tquiet\n" if $quiet;
-       print "\tdebug  = $debug\n";
-}
-$Globals{ 'debug' } = $debug;
-$Globals{ 'quiet' } = $quiet;
-$Globals{ 'verbose' } = $verbose;
-
-#############################################################################
-#  Read Config File and parse
-#############################################################################
-$config = "./debbugs.cfg" if( $config eq '' );
-print "D1: config file=$config\n" if $Globals{ 'debug' };
-@config = ParseConfigFile( $config );
-
-## Put Real Code Here
-
-my @bugs = Debbugs::DBase::GetBugList(["db", "archive"]);
-print "Active Bugs:";
-foreach (@bugs) {
-    print " $_";
-}
-print "\n";
-foreach (@bugs) {
-    Debbugs::DBase::OpenRecord( $_ );
-    Debbugs::DBase::OpenLogfile( $_ );
-    Debbugs::DBase::ReadLogfile( $_ );
-    Debbugs::DBase::CloseLogfile();
-    Debbugs::DBase::ReadRecord( $_ );
-    foreach my $key ( keys( %Record ) )
-    {
-       print "Key= $key   Value = " . $Record{ $key } . "\n";
-    }
-    Debbugs::DBase::CloseRecord();
-}
-
-#############################################################################
-#  Ack Back
-#############################################################################
-
-sub syntax {
-  print "$BANNER\n";
-  print <<"EOT-EOT-EOT";
-Syntax: $FILE [options]
-    -c, --config CFGFILE      read CFGFILE for configuration (default=./debvote.cfg)
-    -h, --help                display this help text
-    -v, --verbose             verbose messages
-    -q, --quiet               cancels verbose in a config file
-    -V, --version             display Debvote version and exit
-    -d, --debug               turn debug messages ON (multiple -d for more verbose)
-EOT-EOT-EOT
-
-  exit $_[0];
-}
diff --git a/debbugs-service b/debbugs-service
deleted file mode 100755 (executable)
index ab5089b..0000000
+++ /dev/null
@@ -1,889 +0,0 @@
-#!/usr/bin/perl -w
-# Usage: service <code>.nn
-# Temps:  incoming/P<code>.nn
-
-
-use strict;
-use Debbugs::Config;
-use Debbugs::Email;
-use Debbugs::DBase;
-use Debbugs::Common;
-use Getopt::Long;
-use MIME::Parser;
-
-#############################################################################
-#  Gloabal Variable Declaration
-#############################################################################
-my $VERSION = '3.01';                        #External Version number
-my $BANNER = "DebBugs v$VERSION";            #Version Banner - text form
-my $FILE = 'debbugs-service';                #File name
-my $config = '';
-my @config = undef;
-
-my $inputfilename;                           #file specified on commandline
-my @inputfile;
-my @imputlog;
-my $control;                                 #call to control or request
-
-my @body;                                  #list of commands
-my $replyto;                               #address of to send reply to
-my $transcript = '';                       #building of return message
-my %LTags;                                 #Tags Local to this email
-my @message;                               #holds copy of msg to apply tags
-
-#############################################################################
-#  Commandline parsing
-#############################################################################
-# Hash used to process commandline options
-my $verbose = 0;
-my $quiet = 0;
-my $debug = 0;
-my %opthash = (# ------------------ actions
-    "config|c=s" => \$config,
-    "help|h" => \&syntax,
-    "version|V" => \&banner,
-    "verbose|v!" => \$verbose,
-    "quiet|q!" => \$quiet,
-    "debug|d+" => \$debug,     # Count the -d flags
-    );
-Getopt::Long::config('bundling', 'no_getopt_compat', 'no_auto_abbrev');
-GetOptions(%opthash) or &syntax( 1 );
-if ( $debug > 1 )
-{      print "D2: Commandline:\n";
-       print "\tconfig = $config\n" unless $config eq '';
-       print "\tverbos\n" if $verbose;
-       print "\tquiet\n" if $quiet;
-       print "\tdebug  = $debug\n";
-}
-$Globals{ 'debug' } = $debug;
-$Globals{ 'quiet' } = $quiet;
-$Globals{ 'verbose' } = $verbose;
-
-#############################################################################
-#  Read Config File and parse
-#############################################################################
-$config = "./debbugs.cfg" if( $config eq '' );
-print "D1: config file=$config\n" if $Globals{ 'debug' };
-@config = Debbugs::Config::ParseConfigFile( $config );
-
-#############################################################################
-#  Load in template emails
-#############################################################################
-@notify_done_email = Debbugs::Email::LoadEmail( $Globals{ 'template-dir' }.'/'.$Globals{ 'not-don-con' } );
-
-#############################################################################
-#  Find file name and load input file
-#############################################################################
-$_=shift;
-m/^[RC]\.\d+$/ || &fail("bad argument");
-$control= m/C/;
-$inputfilename = $_;
-if (!rename( $Globals{ 'spool-dir' }."G$inputfilename", $Globals{ 'spool-dir' }."P$inputfilename")) 
-{      $_=$!.'';  
-       m/no such file or directory/i && exit 0;
-       &fail("renaming to lock: $!");
-}
-
-############################################################################
-#  Set up MIME Message class
-############################################################################
-my $parser = new MIME::Parser;
-$parser->output_dir("$ENV{HOME}/mimemail");
-$parser->output_prefix("part");
-$parser->output_to_core(100000);
-my $inputmail = $parser->parse_in("P$inputfilename") or die "couldn't parse MIME file";
-#for use when stdin in stead of file is used
-#my $inputmail = $parser->read(\*STDIN) or die "couldn't parse MIME stream";
-
-############################################################################
-#  Extract first part (if mime type) for processing.  All else assumed junk
-############################################################################
-if ( $inputmail->is_multipart )
-{   my $parts = $inputmail->parts( 0 );
-    while( $parts->is_multipart ) { $parts = $parts->parts( 0 ); }
-    @body = $parts->bodyhandle->as_lines;
-}
-else { @body = $inputmail->bodyhandle->as_lines; }
-
-
-$inputmail->head->count('From') || &fail( "no From header" );
-
-############################################################################
-#  Determine Reply To address
-############################################################################
-my $header = $input->mail->head;
-$replyto= $header->count( "Reply-to" ) ? $header->get( "Reply-to" ) : $header->get( "From" );
-
-############################################################################
-#  Add Email info to Local Tags (LTags)
-############################################################################
-$LTags{ 'REPLY_TO' ) = $replyto;
-$LTags{ 'CC_TO' ) = $header->get( 'CC' ) if $header->count( 'CC' );
-$LTags{ 'MESSAGE_ID' } = $header->get( 'Message-id' ) if $header->count( 'Message-id' );
-$LTags{ 'MESSAGE_BODY' } = join( '\n', @body );
-$LTags( 'MESSAGE_DATA' } = "control";
-$LTags{ 'MESSAGE_DATE' } = $header->get( 'Date' ) if $header->count( 'Date');
-if ( $header->count( 'Subject' ) )
-{   $LTags{ 'MESSAGE_SUBJECT' } = $header->get( 'Subject' ); }
-else { &transcript( <<END ); }
-Your email does not include a Subject line in the header.  This is a 
-violation of the specifications and may cause your email to be rejected at
-some later date.
-
-END
-
-############################################################################
-#  Start processing of commands 
-############################################################################
-if ( $control ) { &transcript("Processing commands for control message:\n\n"); }
-else { &transcript("Processing commands for request message:\n\n"); }
-
-####################################### HERE ###############################
-$state= 'idle';
-$lowstate= 'idle';
-$mergelowstate= 'idle';
-$midix=0;    
-$extras="";
-
-for ( my $procline=0; $procline<=$#body; $procline++) 
-{   
-    #test state
-    $state eq 'idle' || print "$state ?\n";
-    $lowstate eq 'idle' || print "$lowstate ?\n";
-    $mergelowstate eq 'idle' || print "$mergelowstate ?\n";
-
-    #get line
-    $_= $msg[$procline]; 
-    s/\s+$//;  #strip ending white space, including newlines
-
-    #cleanup line
-    next unless m/\S/;     #skip blank lines
-    next if m/^\s*\#/;     #skip comment-only lines
-    &transcript("> $_\n");
-
-
-    $action= '';
-    if (m/^stop\s/i || m/^quit\s/i || m/^--/ || m/^thank\s/i) 
-    {   &transcript("Stopping processing here.\n\n");
-        last;
-    } elsif (m/^debug\s+(\d+)$/i && $1 >= 0 && $1 <= 1000) 
-    {  $debug= $1+0;
-        &transcript("Debug level $debug.\n\n");
-    } elsif (m/^(send|get)\s+\#?(\d{2,})$/i) 
-    {  $ref= $2+0; $reffile= $ref; $reffile =~ s,^..,$&/$&,;
-        &sendlynxdoc( "db/$reffile.html", "logs for $gBug#$ref" );
-    } elsif (m/^send-detail\s+\#?(\d+)$/i) 
-    {  $ref= $1+0; $reffile= $ref; $reffile =~ s,^..,$&/$&,;
-        &sendlynxdoc("db/$reffile-b.html","additional logs for $gBug#$ref");
-    } elsif (m/^index(\s+full)?$/i) {
-        &sendlynxdoc("db/ix/full.html",'full index');
-    } elsif (m/^index-summary\s+by-package$/i) {
-        &sendlynxdoc("db/ix/psummary.html",'summary index sorted by package/title');
-    } elsif (m/^index-summary(\s+by-number)?$/i) {
-        &sendlynxdoc("db/ix/summary.html",'summary index sorted by number/date');
-    } elsif (m/^index(\s+|-)pack(age)?s?$/i) {
-        &sendlynxdoc("db/ix/packages.html",'index of packages');
-    } elsif (m/^index(\s+|-)maints?$/i) {
-        &sendlynxdoc("db/ix/maintainers.html",'index of maintainers');
-    } elsif (m/^index(\s+|-)maint\s+(\S.*\S)$/i) {
-        $substrg= $2; $matches=0;
-        opendir(DBD,"$gWebDir/db/ma") || die $!;
-        while (defined($_=readdir(DBD))) {
-            next unless m/^l/ && m/\.html$/;
-            &transcript("F|$_\n") if $dl>1;
-            $filename= $_; s/^l//; s/\.html$//;
-            &transcript("P|$_\n") if $dl>2;
-            while (s/-(..)([^_])/-$1_-$2/) { }
-            &transcript("P|$_\n") if $dl>2;
-            s/^(.{0,2})_/$1-20_/g; while (s/([^-]..)_/$1-20_/) { };
-            &transcript("P|$_\n") if $dl>2;
-            s/^,(.*),(.*),([^,]+)$/$1-40_$2-20_-28_$3-29_/;
-            &transcript("P|$_\n") if $dl>2;
-            s/^([^,]+),(.*),(.*),$/$1-20_-3c_$2-40_$3-3e_/;
-            &transcript("P|$_\n") if $dl>2;
-            s/\./-2e_/g;
-            &transcript("P|$_\n") if $dl>2;
-            $out='';
-            while (m/-(..)_/) { $out.= $`.sprintf("%c",hex($1)); $_=$'; }
-            $out.=$_;
-            &transcript("M|$out\n") if $dl>1;
-            next unless index(lc $out, lc $substrg)>=0;
-            &transcript("S|$filename\n") if $dl>0;
-            &transcript("S|$out\n") if $dl>0;
-            $matches++;
-            &sendlynxdocraw("db/ma/$filename","$gBug list for maintainer \`$out'");
-        }
-        if ($matches) {
-            &transcript("$gBug list(s) for $matches maintainer(s) sent.\n\n");
-        } else {
-            &transcript("No maintainers found containing \`$substrg'.\n".
-                        "Use \`index-maint' to get list of maintainers.\n\n");
-        }
-        $ok++;
-    } elsif (m/^index(\s+|-)pack(age)?s?\s+(\S.*\S)$/i) {
-        $substrg= $+; $matches=0;
-        opendir(DBD,"$gWebDir/db/pa") || die $!;
-        while (defined($_=readdir(DBD))) {
-            next unless m/^l/ && m/\.html$/;
-            &transcript("F|$_\n") if $dl>1;
-            $filename= $_; s/^l//; s/\.html$//;
-            next unless index(lc $_, lc $substrg)>=0;
-            &transcript("S|$filename\n") if $dl>0;
-            &transcript("S|$out\n") if $dl>0;
-            $matches++;
-            &sendlynxdocraw("db/pa/$filename","$gBug list for package \`$_'");
-        }
-        if ($matches) {
-            &transcript("$gBug list(s) for $matches package(s) sent.\n\n");
-        } else {
-            &transcript("No packages found containing \`$substrg'.\n".
-                        "Use \`index-packages' to get list of packages.\n\n");
-        }
-        $ok++;
-    } elsif (m/^send-unmatched(\s+this|\s+-?0)?$/i) {
-        &sendlynxdoc("db/ju/unmatched-1.html","junk (this week)");
-    } elsif (m/^send-unmatched\s+(last|-1)$/i) {
-        &sendlynxdoc("db/ju/unmatched-2.html","junk (last week)");
-    } elsif (m/^send-unmatched\s+(old|-2)$/i) {
-        &sendlynxdoc("db/ju/unmatched-3.html","junk (two weeks ago)");
-    } elsif (m/^getinfo\s+(\S+)$/i) {
-        $file= $1;
-        if ($file =~ m/^\./ || $file !~ m/^[-.0-9a-z]+$/ || $file =~ m/\.gz$/) {
-            &transcript("Filename $file is badly formatted.\n\n");
-        } elsif (open(P,"$gDocDir/$file")) {
-            $ok++;
-            &transcript("Info file $file appears below.\n\n");
-            $extras.= "\n---------- Info file $file follows:\n\n";
-            while(<P>) { $extras.= $_; }
-            close(P);
-        } else {
-            &transcript("Info file $file does not exist.\n\n");
-         }
-    } elsif (m/^help$/i) {
-        &sendhelp;
-        &transcript("\n");
-        $ok++;
-    } elsif (m/^refcard$/i) {
-        &sendtxthelp("bug-mailserver-refcard.txt","mailservers' reference card");
-    } elsif (m/^subscribe/i) {
-        &transcript(<<END);
-There is no $gProject $gBug mailing list.  If you wish to review bug reports
-please do so via http://$gWebUrl/ or ask this mailserver
-to send them to you.
-soon: MAILINGLISTS_TEXT
-END
-    } elsif (m/^unsubscribe/i) {
-        &transcript(<<END);
-soon: UNSUBSCRIBE_TEXT
-soon: MAILINGLISTS_TEXT
-END
-    } elsif (!$control) {
-        &transcript(<<END);
-Unknown command or malformed arguments to command.
-(Use control\@$gEmailDomain to manipulate reports.)
-
-END
-        if (++$unknowns >= 3) {
-            &transcript("Too many unknown commands, stopping here.\n\n");
-            last;
-        }
-    } elsif (m/^close\s+\#?(\d+)$/i) {
-       $ok++;
-       $ref= $1;
-       if ( &setbug ) {
-           if(length($s_done)) {
-               &transcript("$gBug is already closed, cannot re-close.\n\n");
-                &nochangebug;
-            } else {
-                $action= "$gBug closed, ack sent to submitter - they'd better know why !";
-                do {
-                   CLOSE BUG RECORD
-                    &addmaintainers($s_package);
-                   if ( length( $gDoneList ) > 0 && length( $gListDomain ) > 0 ) 
-                   { &addccaddress("$gDoneList\@$gListDomain"); }
-                    $s_done= $replyto;
-                   @message = @notify_done_email;
-                   &Debbugs::Email::ProcessTags( \@message, \@BTags, "BTAG" );
-                   &Debbugs::Email::ProcessTags( \@message, \@LTags, "LTAG" );
-                    &sendmailmessage( join( "\n", @message), $s_originator );
-                   Save the bug record
-                } while (&getnextbug);
-            }
-        }
-    } elsif (m/^reassign\s+\#?(\d+)\s+(\S.*\S)$/i) {
-        $ok++;
-        $ref= $1; $newpackage= $2;
-       $newpackage =~ y/A-Z/a-z/;
-        if (&setbug) {
-            if (length($s_package)) {
-                $action= "$gBug reassigned from package \`$s_package'".
-                         " to \`$newpackage'.";
-            } else {
-                $action= "$gBug assigned to package \`$newpackage'.";
-            }
-            do {
-                &addmaintainers($s_package);
-                &addmaintainers($newpackage);
-                $s_package= $newpackage;
-            } while (&getnextbug);
-        }
-    } elsif (m/^reopen\s+\#?(\d+)$/i ? ($noriginator='', 1) :
-             m/^reopen\s+\#?(\d+)\s+\=$/i ? ($noriginator='', 1) :
-             m/^reopen\s+\#?(\d+)\s+\!$/i ? ($noriginator=$replyto, 1) :
-             m/^reopen\s+\#?(\d+)\s+(\S.*\S)$/i ? ($noriginator=$2, 1) : 0) {
-        $ok++;
-        $ref= $1;
-        if (&setbug) {
-            if (!length($s_done)) {
-                &transcript("$gByg is already open, cannot reopen.\n\n");
-                &nochangebug;
-            } else {
-                $action=
-                    $noriginator eq '' ? "$gBug reopened, originator not changed." :
-                        "$gBug reopened, originator set to $noriginator.";
-                do {
-                    &addmaintainers($s_package);
-                    $s_originator= $noriginator eq '' ?  $s_originator : $noriginator;
-                    $s_done= '';
-                } while (&getnextbug);
-            }
-        }
-    } elsif (m/^forwarded\s+\#?(\d+)\s+(\S.*\S)$/i) {
-        $ok++;
-        $ref= $1; $whereto= $2;
-        if (&setbug) {
-            if (length($s_forwarded)) {
-    $action= "Forwarded-to-address changed from $s_forwarded to $whereto.";
-            } else {
-    $action= "Noted your statement that $gBug has been forwarded to $whereto.";
-            }
-            if (length($s_done)) {
-                $extramessage= "(By the way, this $gBug is currently marked as done.)\n";
-            }
-            do {
-                &addmaintainers($s_package);
-                               if (length($gFowardList)>0 && length($gListDomain)>0 )
-                { &addccaddress("$gFowardList\@$gListDomain"); }
-                $s_forwarded= $whereto;
-            } while (&getnextbug);
-        }
-    } elsif (m/^notforwarded\s+\#?(\d+)$/i) {
-        $ok++;
-        $ref= $1;
-        if (&setbug) {
-            if (!length($s_forwarded)) {
-                &transcript("$gBug is not marked as having been forwarded.\n\n");
-                &nochangebug;
-            } else {
-    $action= "Removed annotation that $gBug had been forwarded to $s_forwarded.";
-                do {
-                    &addmaintainers($s_package);
-                    $s_forwarded= '';
-                } while (&getnextbug);
-            }
-        }
-    } elsif (m/^severity\s+\#?(\d+)\s+([-0-9a-z]+)$/i ||
-       m/^priority\s+\#?(\d+)\s+([-0-9a-z]+)$/i) {
-        $ok++;
-        $ref= $1;
-        $newseverity= $2;
-        if (!grep($_ eq $newseverity, @severities, "$gDefaultSeverity")) {
-            &transcript("Severity level \`$newseverity' is not known.\n".
-                       "Recognised are: ".join(' ',@showseverities).".\n\n");
-        } elsif (&setbug) {
-            $printseverity= $s_severity;
-            $printseverity= "$gDefaultSeverity" if $printseverity eq '';
-           $action= "Severity set to \`$newseverity'.";
-           do {
-                &addmaintainers($s_package);
-                $s_severity= $newseverity;
-            } while (&getnextbug);
-        }
-    } elsif (m/^retitle\s+\#?(\d+)\s+(\S.*\S)\s*$/i) {
-        $ok++;
-        $ref= $1; $newtitle= $2;
-        if (&getbug) {
-            &foundbug;
-            &addmaintainers($s_package);
-            $s_subject= $newtitle;
-            $action= "Changed $gBug title.";
-            &savebug;
-            &transcript("$action\n");
-            if (length($s_done)) {
-                &transcript("(By the way, that $gBug is currently marked as done.)\n");
-            }
-            &transcript("\n");
-        } else {
-            &notfoundbug;
-        }
-    } elsif (m/^unmerge\s+\#?(\d+)$/i) {
-       $ok++;
-       $ref= $1;
-       if (&setbug) {
-           if (!length($s_mergedwith)) {
-               &transcript("$gBug is not marked as being merged with any others.\n\n");
-               &nochangebug;
-           } else {
-                $mergelowstate eq 'locked' || die "$mergelowstate ?";
-               $action= "Disconnected #$ref from all other report(s).";
-               @newmergelist= split(/ /,$s_mergedwith);
-                $discref= $ref;
-                do {
-                    &addmaintainers($s_package);
-                   $s_mergedwith= ($ref == $discref) ? ''
-                        : join(' ',grep($_ ne $ref,@newmergelist));
-                } while (&getnextbug);
-           }
-       }
-    } elsif (m/^merge\s+(\d+(\s+\d+)+)\s*$/i) {
-       $ok++;
-        @tomerge= sort { $a <=> $b } split(/\s+/,$1);
-        @newmergelist= ();
-        &getmerge;
-        while (defined($ref= shift(@tomerge))) {
-            &transcript("D| checking merge $ref\n") if $dl;
-           $ref+= 0;
-           next if grep($_ eq $ref,@newmergelist);
-           if (!&getbug) { &notfoundbug; @newmergelist=(); last }
-            &foundbug;
-            &transcript("D| adding $ref ($s_mergewith)\n") if $dl;
-           $mismatch= '';
-           &checkmatch('package','m_package',$s_package);
-           &checkmatch('forwarded addr','m_forwarded',$s_forwarded);
-           &checkmatch('severity','m_severity',$s_severity);
-           &checkmatch('done mark','m_done',length($s_done) ? 'done' : 'open');
-           if (length($mismatch)) {
-               &transcript("Mismatch - only $Bugs in same state can be merged:\n".
-                            $mismatch."\n");
-               &cancelbug; @newmergelist=(); last;
-           }
-            push(@newmergelist,$ref);
-            push(@tomerge,split(/ /,$s_mergedwith));
-           &cancelbug;
-       }
-       if (@newmergelist) {
-            @newmergelist= sort { $a <=> $b } @newmergelist;
-            $action= "Merged @newmergelist.";
-           for $ref (@newmergelist) {
-               &getbug || die "huh ?  $gBug $ref disappeared during merge";
-                &addmaintainers($s_package);
-               $s_mergedwith= join(' ',grep($_ ne $ref,@newmergelist));
-               &savebug;
-           }
-           &transcript("$action\n\n");
-       }
-        &endmerge;
-    } else {
-        &transcript("Unknown command or malformed arguments to command.\n\n");
-        if (++$unknowns >= 5) {
-            &transcript("Too many unknown commands, stopping here.\n\n");
-            last;
-        }
-    }
-}
-if ($procline>$#msg) {
-    &transcript(">\nEnd of message, stopping processing here.\n\n");
-}
-if (!$ok) {
-    &transcript("No commands successfully parsed; sending the help text(s).\n");
-    &sendhelp;
-    &transcript("\n");
-}
-
-&transcript("MC\n") if $dl>1;
-@maintccs= ();
-for $maint (keys %maintccreasons) {
-&transcript("MM|$maint|\n") if $dl>1;
-    next if $maint eq $replyto;
-    $reasonstring= '';
-    $reasonsref= $maintccreasons{$maint};
-&transcript("MY|$maint|\n") if $dl>2;
-    for $p (sort keys %$reasonsref) {
-&transcript("MP|$p|\n") if $dl>2;
-        $reasonstring.= ', ' if length($reasonstring);
-        $reasonstring.= $p.' ' if length($p);
-        $reasonstring.= join(' ',map("#$_",sort keys %{$$reasonsref{$p}}));
-    }
-    push(@maintccs,"$maint ($reasonstring)");
-    push(@maintccaddrs,"$maint");
-}
-if (@maintccs) {
-    &transcript("MC|@maintccs|\n") if $dl>2;
-    $maintccs= "Cc: ".join(",\n    ",@maintccs)."\n";
-} else { $maintccs = ""; }
-
-$reply= <<END;
-From: $gMaintainerEmail ($gProject $gBug Tracking System)
-To: $replyto
-${maintccs}Subject: Processed: $header{'subject'}
-In-Reply-To: $header{'message-id'}
-References: $header{'message-id'}
-Message-ID: <handler.s.$nn.transcript\@$gEmailDomain>
-
-${transcript}Please contact me if you need assistance.
-
-$gMaintainer
-(administrator, $gProject $gBugs database)
-$extras
-END
-
-$repliedshow= join(', ',$replyto,@maintccaddrs);
-&filelock("lock/-1");
-open(AP,">>db/-1.log") || &quit("open db/-1.log: $!");
-print(AP
-      "\2\n$repliedshow\n\5\n$reply\n\3\n".
-      "\6\n".
-      "<strong>Request received</strong> from <code>".
-      &sani($header{'from'})."</code>\n".
-      "to <code>".&sani($controlrequestaddr)."</code>\n".
-      "\3\n".
-      "\7\n",@log,"\n\3\n") || &quit("writing db/-1.log: $!");
-close(AP) || &quit("open db/-1.log: $!");
-&unfilelock;
-utime(time,time,"db");
-
-&sendmailmessage($reply,$replyto,@maintccaddrs);
-
-unlink("incoming/P$nn") || &quit("unlinking incoming/P$nn: $!");
-
-sub get_addresses {
-    return
-       map { $_->address() }
-       map { Mail::Address->parse($_) } @_;
-}
-
-sub sendmailmessage {
-    local ($message,@recips) = @_;
-    print "mailing to >@recips<\n" if $debug;
-    $c= open(D,"|-");
-    defined($c) || &quit("mailing forking for sendmail: $!");
-    if (!$c) { # ie, we are the child process
-        exec '/usr/lib/sendmail','-f'."$gMaintainerEmail",'-odi','-oem','-oi',get_addresses(@recips);
-        die $!;
-    }
-    print(D $message) || &quit("writing to sendmail process: $!");
-    $!=0; close(D); $? && &quit("sendmail gave exit status $? ($!)");
-    $midix++;
-}
-
-sub sendhelp {
-        &sendtxthelpraw("bug-log-mailserver.txt","instructions for request\@$gEmailDomain");
-        &sendtxthelpraw("bug-maint-mailcontrol.txt","instructions for control\@$gEmailDomain")
-            if $control;
-}
-
-#sub unimplemented {
-#    &transcript("Sorry, command $_[0] not yet implemented.\n\n");
-#}
-
-sub checkmatch {
-    local ($string,$mvarname,$svarvalue) = @_;
-    local ($mvarvalue);
-    if (@newmergelist) {
-        eval "\$mvarvalue= \$$mvarname";
-        &transcript("D| checkmatch \`$string' /$mvarname/$mvarvalue/$svarvalue/\n")
-            if $dl;
-        $mismatch .=
-            "Values for \`$string' don't match:\n".
-            " #$newmergelist[0] has \`$mvarvalue';\n".
-            " #$ref has \`$svarvalue'\n"
-            if $mvarvalue ne $svarvalue;
-    } else {
-        &transcript("D| setupmatch \`$string' /$mvarname/$svarvalue/\n")
-            if $dl;
-        eval "\$$mvarname= \$svarvalue";
-    }
-}
-
-# High-level bug manipulation calls
-# Do announcements themselves
-#
-# Possible calling sequences:
-#    setbug (returns 0)
-#    
-#    setbug (returns 1)
-#    &transcript(something)
-#    nochangebug
-#
-#    setbug (returns 1)
-#    $action= (something)
-#    do {
-#      (modify s_* variables)
-#    } while (getnextbug);
-
-sub nochangebug {
-    &dlen("nochangebug");
-    $state eq 'single' || $state eq 'multiple' || die "$state ?";
-    &cancelbug;
-    &endmerge if $manybugs;
-    $state= 'idle';
-    &dlex("nochangebug");
-}
-
-sub setbug {
-    &dlen("setbug $ref");
-    $state eq 'idle' || die "$state ?";
-    if (!&getbug) {
-        &notfoundbug;
-        &dlex("setbug => 0s");
-        return 0;
-    }
-    @thisbugmergelist= split(/ /,$s_mergedwith);
-    if (!@thisbugmergelist) {
-        &foundbug;
-        $manybugs= 0;
-        $state= 'single';
-        $sref=$ref;
-        &dlex("setbug => 1s");
-        return 1;
-    }
-    &cancelbug;
-    &getmerge;
-    $manybugs= 1;
-    if (!&getbug) {
-        &notfoundbug;
-        &endmerge;
-        &dlex("setbug => 0mc");
-        return 0;
-    }
-    &foundbug;
-    $state= 'multiple'; $sref=$ref;
-    &dlex("setbug => 1m");
-    return 1;
-}
-
-sub getnextbug {
-    &dlen("getnextbug");
-    $state eq 'single' || $state eq 'multiple' || die "$state ?";
-    &savebug;
-    if (!$manybugs || !@thisbugmergelist) {
-        length($action) || die;
-        &transcript("$action\n$extramessage\n");
-        &endmerge if $manybugs;
-        $state= 'idle';
-        &dlex("getnextbug => 0");
-        return 0;
-    }
-    $ref= shift(@thisbugmergelist);
-    &getbug || die "bug $ref disappeared";
-    &foundbug;
-    &dlex("getnextbug => 1");
-    return 1;
-}
-
-# Low-level bug-manipulation calls
-# Do no announcements
-#
-#    getbug (returns 0)
-#
-#    getbug (returns 1)
-#    cancelbug
-#
-#    getmerge
-#    $action= (something)
-#    getbug (returns 1)
-#    savebug/cancelbug
-#    getbug (returns 1)
-#    savebug/cancelbug
-#    [getbug (returns 0)]
-#    &transcript("$action\n\n")
-#    endmerge
-
-sub notfoundbug { &transcript("$gBug number $ref not found.\n\n"); }
-sub foundbug { &transcript("$gBug#$ref: $s_subject\n"); }
-
-sub getmerge {
-    &dlen("getmerge");
-    $mergelowstate eq 'idle' || die "$mergelowstate ?";
-    &filelock('lock/merge');
-    $mergelowstate='locked';
-    &dlex("getmerge");
-}
-
-sub endmerge {
-    &dlen("endmerge");
-    $mergelowstate eq 'locked' || die "$mergelowstate ?";
-    &unfilelock;
-    $mergelowstate='idle';
-    &dlex("endmerge");
-}
-
-sub getbug {
-    &dlen("getbug $ref");
-    $lowstate eq 'idle' || die "$state ?";
-    if (&lockreadbug($ref)) {
-        $sref= $ref;
-        $lowstate= "open";
-        &dlex("getbug => 1");
-        $extramessage='';
-        return 1;
-    }
-    $lowstate= 'idle';
-    &dlex("getbug => 0");
-    return 0;
-}
-
-sub cancelbug {
-    &dlen("cancelbug");
-    $lowstate eq 'open' || die "$state ?";
-    &unfilelock;
-    $lowstate= 'idle';
-    &dlex("cancelbug");
-}
-
-sub savebug {
-    &dlen("savebug $ref");
-    $lowstate eq 'open' || die "$lowstate ?";
-    length($action) || die;
-    $ref == $sref || die "read $sref but saving $ref ?";
-    open(L,">>db/$ref.log") || &quit("opening db/$ref.log: $!");
-    print(L
-          "\6\n".
-          "<strong>".&sani($action)."</strong>\n".
-          "Request was from <code>".&sani($header{'from'})."</code>\n".
-          "to <code>".&sani($controlrequestaddr)."</code>. \n".
-          "\3\n".
-          "\7\n",@log,"\n\3\n") || &quit("writing db/$ref.log: $!");
-    close(L) || &quit("closing db/$ref.log: $!");
-    open(S,">db/$ref.status.new") || &quit("opening db/$ref.status.new: $!");
-    print(S
-          "$s_originator\n".
-          "$s_date\n".
-          "$s_subject\n".
-          "$s_msgid\n".
-          "$s_package\n".
-          "$s_keywords\n".
-          "$s_done\n".
-          "$s_forwarded\n".
-          "$s_mergedwith\n".
-         "$s_severity\n") || &quit("writing db/$ref.status.new: $!");
-    close(S) || &quit("closing db/$ref.status.new: $!");
-    rename("db/$ref.status.new","db/$ref.status") ||
-        &quit("installing new db/$ref.status: $!");
-    &unfilelock;
-    $lowstate= "idle";
-    &dlex("savebug");
-}
-
-sub dlen {
-    return if !$dl;
-    &transcript("C> @_ ($state $lowstate $mergelowstate)\n");
-}
-
-sub dlex {
-    return if !$dl;
-    &transcript("R> @_ ($state $lowstate $mergelowstate)\n");
-}
-
-sub transcript {
-    print $_[0] if $debug;
-    $transcript.= $_[0];
-}
-
-sub sendlynxdoc {
-    &sendlynxdocraw;
-    &transcript("\n");
-    $ok++;
-}
-
-sub sendtxthelp {
-    &sendtxthelpraw;
-    &transcript("\n");
-    $ok++;
-}
-
-sub sendtxthelpraw {
-    local ($relpath,$description) = @_;
-    $doc='';
-    open(D,"$gDocDir/$relpath") || &quit("open doc file $relpath: $!");
-    while(<D>) { $doc.=$_; }
-    close(D);
-    &transcript("Sending $description in separate message.\n");
-    &sendmailmessage(<<END.$doc,$replyto);
-From: $gMaintainerEmail ($gProject $gBug Tracking System)
-To: $replyto
-Subject: $gProject $gBug help: $description
-References: $header{'message-id'}
-In-Reply-To: $header{'message-id'}
-Message-ID: <handler.s.$nn.help.$midix\@$gEmailDomain>
-
-END
-    $ok++;
-}
-
-sub sendlynxdocraw {
-    local ($relpath,$description) = @_;
-    $doc='';
-    open(L,"lynx -nolist -dump $wwwbase/$relpath 2>&1 |") || &quit("fork for lynx: $!");
-    while(<L>) { $doc.=$_; }
-    $!=0; close(L);
-    if ($? == 255 && $doc =~ m/^\n*lynx: Can\'t access start file/) {
-        &transcript("Information ($description) is not available -\n".
-                    "perhaps the $gBug does not exist or is not on the WWW yet.\n");
-         $ok++;
-    } elsif ($?) {
-        &transcript("Error getting $description (code $? $!):\n$doc\n");
-    } else {
-        &transcript("Sending $description.\n");
-        &sendmailmessage(<<END.$doc,$replyto);
-From: $gMaintainerEmail ($gProject $gBug Tracking System)
-To: $replyto
-Subject: $gProject $gBugs information: $description
-References: $header{'message-id'}
-In-Reply-To: $header{'message-id'}
-Message-ID: <handler.s.$nn.info.$midix\@$gEmailDomain>
-
-END
-         $ok++;
-    }
-}
-
-sub addccaddress {
-    my ($cca) = @_;
-    $maintccreasons{$cca}{''}{$ref}= 1;
-}
-
-sub addmaintainers 
-{      # Data structure is:
-    #   maintainer email address &c -> assoc of packages -> assoc of bug#'s
-    my ($p, $addmaint, $pshow);
-    &ensuremaintainersloaded;
-    $anymaintfound=0; $anymaintnotfound=0;
-    for $p (split(m/[ \t?,()]+/,$_[0])) 
-       {       $p =~ y/A-Z/a-z/;
-        $pshow= ($p =~ m/[-+.a-z0-9]+/ ? $& : '');
-        if (defined($maintainerof{$p})) 
-               {       $addmaint= $maintainerof{$p};
-                       &transcript("MR|$addmaint|$p|$ref|\n") if $dl>2;
-            $maintccreasons{$addmaint}{$p}{$ref}= 1;
-                       print "maintainer add >$p|$addmaint<\n" if $debug;
-        } else { print "maintainer none >$p<\n" if $debug; }
-    }
-}
-
-sub ensuremaintainersloaded {
-    my ($a,$b);
-    return if $maintainersloaded++;
-    open(MAINT,"$gMaintainerFile") || die &quit("maintainers open: $!");
-    while (<MAINT>) {
-        m/^(\S+)\s+(\S.*\S)\n$/ || &quit("maintainers bogus \`$_'");
-        $a= $1; $b= $2; $a =~ y/A-Z/a-z/;
-        $maintainerof{$1}= $2;
-    }
-    close(MAINT);
-}
-
-sub syntax {
-  print "$BANNER\n";
-  print <<"EOT-EOT-EOT";
-Syntax: $FILE [options]
-    -c, --config CFGFILE      read CFGFILE for configuration (default=./debvote.cfg)
-    -h, --help                display this help text
-    -v, --verbose             verbose messages
-    -q, --quiet               cancels verbose in a config file
-    -V, --version             display Debvote version and exit
-    -d, --debug               turn debug messages ON (multiple -d for more verbose)
-EOT-EOT-EOT
-
-  exit $_[0];
-}
index df88ac098fd6fc84650acc7f6f8a21cdafd129ed..aaab5631a8786f82259249c4fe02fde9b571b2cd 100644 (file)
@@ -124,6 +124,7 @@ debbugs (2.4.2) UNRELEASED; urgency=low
     - Explain how to close bugs in the ack message (closes: #37605)
     - Make the moreinfo ack more general (closes: #70810)
     - Use RFC compliant dates in headers (closes: #362935)
+    - Add SOAP support (closes: #377520) Thanks to Raphael Hertzog.
     - Split forwarded on commas for linking (closes: #367813)
     - Don't display duplicate bugs (closes: #348116)
     - Display links to archived bugs for all searches (closes: #53710)
@@ -142,6 +143,14 @@ debbugs (2.4.2) UNRELEASED; urgency=low
     - Dissallow forwarded being set to a $gEmailDomain address
       (closes: #397486)
     - Fix broken sorting by usertags by forcing numeric (closes: #395027)
+    - Add support for hiding useless messages; thanks to Sune Vuorela.
+      (closes: #406020)
+    - Fix arrayrefs leaking into the myurl function (closes: #397344)
+    - List bugs being blocked (closes: #356680)
+    - Fix multiple submitters for a single bug in the index
+      (closes: #402362)
+    - Marking a bug as fixed now overrides a found at that exact version
+      (closes: #395865)
 
   
  -- Colin Watson <cjwatson@debian.org>  Fri, 20 Jun 2003 18:57:25 +0100
index c681a8e661239481c49c45c71be7b70248dc1fb5..20d9c01778e2d872b4832af1254a1cedb02aa5b5 100644 (file)
@@ -1,16 +1,16 @@
 Source: debbugs
 Section: misc
-Priority: optional
+Priority: extra
 Maintainer: Debbugs developers <debian-debbugs@lists.debian.org>
 Uploaders: Josip Rodin <joy-packages@debian.org>, Colin Watson <cjwatson@debian.org>
 Standards-Version: 3.2.1
-Build-Depends-Indep: debhelper
+Build-Depends-Indep: debhelper, libparams-validate-perl, libmailtools-perl, libmime-perl, libio-stringy-perl, libmldbm-perl, liburi-perl, libsoap-lite-perl, libcgi-simple-perl, libhttp-server-simple-perl, libtest-www-mechanize-perl
 
 Package: debbugs
 Architecture: all
-Depends: perl5 | perl, exim4 | mail-transport-agent, libmailtools-perl, ed, libmime-perl, libio-stringy-perl, libmldbm-perl, liburi-perl
-Recommends: httpd, links | lynx
-Suggests: spamassassin (>= 3.0)
+Depends: perl5 | perl, exim4 | mail-transport-agent, libmailtools-perl, ed, libmime-perl, libio-stringy-perl, libmldbm-perl, liburi-perl, libsoap-lite-perl, libcgi-simple-perl, libparams-validate-perl
+Recommends: apache | httpd, links | lynx
+Suggests: spamassassin (>= 3.0), libcgi-alert-perl
 Description: The bug tracking system based on the active Debian BTS
  Debian has a bug tracking system which files details of bugs reported by
  users and developers. Each bug is given a number, and is kept on file until
index 00ec1c8df439624be489ef76e44a48670393bd3e..e322c0fbdca8ce8e162c62f0fbae8c345aaeaa5d 100644 (file)
     RewriteRule ^/$ http://www.debian.org/Bugs/
     RewriteRule ^/(robots\.txt|release-critical|apt-listbugs\.html)$ - [L]
     # The following two redirect to up-to-date pages
-    RewriteRule ^/[[:space:]]*#?([[:digit:]][[:digit:]][[:digit:]]+)$ /cgi-bin/bugreport.cgi?bug=$1 [L,R]
+    RewriteRule ^/[[:space:]]*#?([[:digit:]][[:digit:]][[:digit:]]+)([;&].+)?$ /cgi-bin/bugreport.cgi?bug=$1$2 [L,R,NE]
     RewriteRule ^/([^/+]*)([+])([^/]*)$ "/$1%%{%}2B$3" [N]
     RewriteRule ^/[Ff][Rr][Oo][Mm]:([^/]+\@.+)$ /cgi-bin/pkgreport.cgi?submitter=$1 [L,R,NE]
     # Commented out, 'cuz aj says it will crash master. (old master)
     # RewriteRule ^/[Ss][Ee][Vv][Ee][Rr][Ii][Tt][Yy]:([^/]+\@.+)$ /cgi-bin/pkgreport.cgi?severity=$1 [L,R]
     RewriteRule ^/([^/]+\@.+)$ /cgi-bin/pkgreport.cgi?maint=$1 [L,R,NE]
-    RewriteRule ^/mbox:([[:digit:]][[:digit:]][[:digit:]]+)$ /cgi-bin/bugreport.cgi?mbox=yes&bug=$1 [L,R,NE]
+    RewriteRule ^/mbox:([[:digit:]][[:digit:]][[:digit:]]+)([;&].+)?$ /cgi-bin/bugreport.cgi?mbox=yes&bug=$1$2 [L,R,NE]
     RewriteRule ^/src:([^/]+)$ /cgi-bin/pkgreport.cgi?src=$1 [L,R,NE]
     RewriteRule ^/severity:([^/]+)$ /cgi-bin/pkgreport.cgi?severity=$1 [L,R,NE]
     RewriteRule ^/tag:([^/]+)$ /cgi-bin/pkgreport.cgi?tag=$1 [L,R,NE]
diff --git a/export b/export
deleted file mode 100755 (executable)
index 298770a..0000000
--- a/export
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/sh
-
-set -e
-if [ $# != 1 ]; then echo >&2 'need version'; exit 1; fi
-version="$1"; shift
-tag="`echo \"release-$version\" | sed -e 's/\./-/g'`"
-cvs -Q tag -F "$tag"
-
-cd ..
-rm -rf bugs-export-temp$$ "debbugs-$version"
-rm -f "debbugs-$version.tar" "debbugs-$version.tar.gz"
-mkdir bugs-export-temp$$
-cd bugs-export-temp$$
-
-cvs -Q co -r "$tag" bugsdb
-mv bugsdb "../debbugs-$version"
-cd ..
-rm -rf bugs-export-temp$$
-
-tar    --exclude CVS \
-       --exclude ncipher \
-       --exclude '*.out' \
-       --exclude '*.trace' \
-       --exclude '*.new' \
-       --exclude '*~' \
-       --exclude 'trace' \
-       -cf "debbugs-$version.tar" "debbugs-$version"
-gzip -9 "debbugs-$version.tar"
-rm -rf "debbugs-$version"
-echo "../debbugs-$version.tar.gz created."
diff --git a/future_directions b/future_directions
new file mode 100644 (file)
index 0000000..adfa9ff
--- /dev/null
@@ -0,0 +1,47 @@
+Here are outlined some of the future directions for the debbugs
+codebase and things that should be done.
+
+PACKAGE CLEANUP
+
+* Stop doing the .in -> foo translation
+* Use ExtUtils::Makemaker instead of a custom makefile
+* More testing of modules so it's not so easy to break things
+
+
+GENERAL ORGANIZATIONAL CLEANUP
+
+* Modularization
+  * use of strict and warnings everywhere
+  * Split out errorlib and common.pl; stop requiring stuff
+  * Use CGI::Simple, URI, and Params::Validate instead of our own
+    versions
+
+* Configuration Module
+  * Allow for multiple debbugs configurations; easy overriding of values
+  * Possibly implemented using Config::General (maybe Config::Simple?)
+    with hack to allow for backwards compatibility
+    - The backwards compatibility hack is now fully implemented, a
+      decision on a configuration system just needs to be made.
+
+* Separation of Output forms from the code
+  * Use Text::Template to do this; it's simple, we don't lose
+    flexibility if we need it
+
+* Bring back all of the helper scripts into the bzr repository and
+  document the setup of Debian's bts
+
+FEATURES
+
+* Full text searching -- using Hyper Estraier (in progress on merkel)
+  * See Debbugs::Estraier
+
+* CGI Options enable/disable forms
+
+* Better display of bugs in bug lists
+  - Display subsidiary bugs: merged bugs, block bugs, etc.
+
+* Archiving [display of applicable bugs even if they are archived]
+
+BUG FIXING
+
+* ... 
\ No newline at end of file
index 8120307f8df4747e70bbd3f3bab401c07b73881f..a9835fcbb38892a1cf95b35190efd49f38885e68 100644 (file)
@@ -91,6 +91,12 @@ mailservers is available via the WWW, in
   the $gBug is cleared. This is identical to the behaviour of
   <code>reopen</code>.
 
+  <p>This command will only cause a bug to be marked as not done if no
+    version is specified, or if the <var>version</var> being marked found
+    is equal to the <var>version</var> which was last marked fixed. (If
+    you are certain that you want the bug marked as not done,
+    use <code>reopen</code> in conjunction with <code>found</code>.</p>
+
   <p>This command was introduced in preference to <code>reopen</code>
   because it was difficult to add a <var>version</var> to that command's
   syntax without suffering ambiguity.
@@ -319,8 +325,8 @@ mailservers is available via the WWW, in
 <dt><code>owner</code> <var>bugnumber</var> <var>address</var> | <code>!</code>
 
   <dd>Sets <var>address</var> to be the "owner" of #<var>bugnumber</var>.
-  The owner of a $gBug claims responsibility for fixing it and will receive
-  all mail regarding it.  This is useful to share out work in cases where a
+  The owner of a $gBug claims responsibility for fixing it.
+  This is useful to share out work in cases where a
   package has a team of maintainers.
 
   <p>If you wish to become the owner of the $gBug yourself, you can use the
index 7f5f4fe8b8fe2378c5eb63b2d233a70676659e7d..193e7b528d65e3c4d9ca1a59bb8344c6b6b19946 100644 (file)
@@ -4,6 +4,7 @@
 # Domains
 $gEmailDomain = "bugs.debian.org";
 $gListDomain = "lists.debian.org";
+$gWebHostBugDir = "";
 $gWebDomain = "www.debian.org/Bugs";
 $gHTMLSuffix = "";
 $gPackagePages = "packages.debian.org";
index 830342b04bd262e0a32e85ae07c104040bea9403..4767f6a577b5a23b2b9f7890af4394aa273f7f26 100644 (file)
@@ -1,3 +1,4 @@
+# -*- mode: cperl -*-
 # This is the template debbugs configuration file.
 # You *must* edit it in order for debbugs to work.
 # $Id: config.in,v 1.23 2005/07/17 19:07:01 cjwatson Exp $
@@ -72,4 +73,11 @@ $gMaintainerFileOverride = "$gConfigDir/Maintainers.override";
 $gPseudoDescFile = "$gConfigDir/pseudo-packages.description";
 $gPackageSource = "$gConfigDir/indices/sources";
 
+
+# Estraier Configuration
+%gSearchEstraier = (url  => 'http://localhost:1978/node/bts1',
+                   user => 'user',
+                   pass => 'pass',
+                  );
+
 1;
index 10dfae6988dca66054282afe6bc229ccd2596022..d8c8ea10e568ffe9a073743d0f920b15c2450a31 100644 (file)
@@ -2,6 +2,7 @@
 #domains
 $gEmailDomain = "bugs.top.domain";             #bugs.debian.org
 $gListDomain = "lists.top.domain";             #lists.debian.org
+$gWebHostBugDir = "";
 $gWebDomain = "www.top.domain";                        #www.debian.org/Bugs
 $gCGIDomain = "cgi.top.domain";                        #cgi.debian.org
 
index fb32ba133e52387747708528159ffe634374e50b..a93c79bf3542e376203c79f1664de98dbe839130 100755 (executable)
@@ -1,17 +1,11 @@
 # -*- perl -*-
-# $Id: errorlib.in,v 1.52 2005/10/06 03:46:13 ajt Exp $
 
 use Mail::Address;
 use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522);
-use Debbugs::Packages;
-
-sub F_SETLK { 6; } sub F_WRLCK{ 1; }
-$flockstruct= 'sslll'; # And there ought to be something for this too.
-
-sub get_hashname {
-    return "" if ( $_[ 0 ] < 0 );
-    return sprintf "%02d", $_[ 0 ] % 100;
-}
+use Debbugs::Packages qw(:all);
+use Debbugs::Common qw(:all);
+use Debbugs::Status qw(:all);
+use Carp;
 
 sub unlockreadbugmerge {
     local ($rv) = @_;
@@ -30,480 +24,13 @@ sub lockreadbugmerge {
     return ( 2, $data );
 }
 
-sub getbuglocation {
-    my ( $bugnum, $ext ) = @_;
-    my $archdir = sprintf "%02d", $bugnum % 100;
-    return 'archive' if ( -r "$gSpoolDir/archive/$archdir/$bugnum.$ext" );
-    return 'db-h' if ( -r "$gSpoolDir/db-h/$archdir/$bugnum.$ext" );
-    return 'db' if ( -r "$gSpoolDir/db/$bugnum.$ext" );
-    return undef;
-}
-
-sub getlocationpath {
-    my ($location) = @_;
-    if ($location eq 'archive') {
-        return "$gSpoolDir/archive";
-    } elsif ($location eq 'db') {
-        return "$gSpoolDir/db";
-    } else {
-        return "$gSpoolDir/db-h";
-    }
-}
-
-sub getbugcomponent {
-    my ($bugnum, $ext, $location) = @_;
-
-    unless (defined $location) {
-       $location = getbuglocation($bugnum, $ext);
-       # Default to non-archived bugs only for now; CGI scripts want
-       # archived bugs but most of the backend scripts don't. For now,
-       # anything that is prepared to accept archived bugs should call
-       # getbuglocation() directly first.
-       return undef if defined $location and
-                       ($location ne 'db' and $location ne 'db-h');
-    }
-    my $dir = getlocationpath($location);
-    return undef unless $dir;
-    if ($location eq 'db') {
-       return "$dir/$bugnum.$ext";
-    } else {
-       my $hash = get_hashname($bugnum);
-       return "$dir/$hash/$bugnum.$ext";
-    }
-}
-
-my @v1fieldorder = qw(originator date subject msgid package
-                      keywords done forwarded mergedwith severity);
-
-my %fields = (originator => 'submitter',
-              date => 'date',
-              subject => 'subject',
-              msgid => 'message-id',
-              'package' => 'package',
-              keywords => 'tags',
-              done => 'done',
-              forwarded => 'forwarded-to',
-              mergedwith => 'merged-with',
-              severity => 'severity',
-              owner => 'owner',
-              found_versions => 'found-in',
-              fixed_versions => 'fixed-in',
-              blocks => 'blocks',
-              blockedby => 'blocked-by',
-             );
-
-# Fields which need to be RFC1522-decoded in format versions earlier than 3.
-my @rfc1522_fields = qw(originator subject done forwarded owner);
-
-sub readbug {
-    my ($lref, $location) = @_;
-    my $status = getbugcomponent($lref, 'summary', $location);
-    return undef unless defined $status;
-    if (!open(S,$status)) { return undef; }
-
-    my %data;
-    my @lines;
-    my $version = 2;
-    local $_;
-
-    while (<S>) {
-        chomp;
-        push @lines, $_;
-        $version = $1 if /^Format-Version: ([0-9]+)/i;
-    }
-
-    # Version 3 is the latest format version currently supported.
-    return undef if $version > 3;
-
-    my %namemap = reverse %fields;
-    for my $line (@lines) {
-        if ($line =~ /(\S+?): (.*)/) {
-            my ($name, $value) = (lc $1, $2);
-            $data{$namemap{$name}} = $value if exists $namemap{$name};
-        }
-    }
-    for my $field (keys %fields) {
-        $data{$field} = '' unless exists $data{$field};
-    }
-
-    close(S);
-
-    $data{severity} = $gDefaultSeverity if $data{severity} eq '';
-    $data{found_versions} = [split ' ', $data{found_versions}];
-    $data{fixed_versions} = [split ' ', $data{fixed_versions}];
-
-    if ($version < 3) {
-       for my $field (@rfc1522_fields) {
-           $data{$field} = decode_rfc1522($data{$field});
-       }
-    }
-
-    return \%data;
-}
-
-sub lockreadbug {
-    local ($lref, $location) = @_;
-    &filelock("lock/$lref");
-    my $data = readbug($lref, $location);
-    &unfilelock unless defined $data;
-    return $data;
-}
-
-sub makestatus {
-    my $data = shift;
-    my $version = shift;
-    $version = 2 unless defined $version;
-
-    local $data->{found_versions} = join ' ', @{$data->{found_versions}};
-    local $data->{fixed_versions} = join ' ', @{$data->{fixed_versions}};
-
-    my $contents = '';
-
-    my %newdata = %$data;
-    if ($version < 3) {
-        for my $field (@rfc1522_fields) {
-            $newdata{$field} = encode_rfc1522($newdata{$field});
-        }
-    }
-
-    if ($version == 1) {
-        for my $field (@v1fieldorder) {
-            if (exists $newdata{$field}) {
-                $contents .= "$newdata{$field}\n";
-            } else {
-                $contents .= "\n";
-            }
-        }
-    } elsif ($version == 2 or $version == 3) {
-        # Version 2 or 3. Add a file format version number for the sake of
-        # further extensibility in the future.
-        $contents .= "Format-Version: $version\n";
-        for my $field (keys %fields) {
-            if (exists $newdata{$field} and $newdata{$field} ne '') {
-                # Output field names in proper case, e.g. 'Merged-With'.
-                my $properfield = $fields{$field};
-                $properfield =~ s/(?:^|(?<=-))([a-z])/\u$1/g;
-                $contents .= "$properfield: $newdata{$field}\n";
-            }
-        }
-    }
-
-    return $contents;
-}
-
-sub writebug {
-    my ($ref, $data, $location, $minversion, $disablebughook) = @_;
-    my $change;
-
-    my %outputs = (1 => 'status', 2 => 'summary');
-    for my $version (keys %outputs) {
-        next if defined $minversion and $version < $minversion;
-        my $status = getbugcomponent($ref, $outputs{$version}, $location);
-        &quit("can't find location for $ref") unless defined $status;
-        open(S,"> $status.new") || &quit("opening $status.new: $!");
-        print(S makestatus($data, $version)) ||
-            &quit("writing $status.new: $!");
-        close(S) || &quit("closing $status.new: $!");
-        if (-e $status) {
-            $change = 'change';
-        } else {
-            $change = 'new';
-        }
-        rename("$status.new",$status) || &quit("installing new $status: $!");
-    }
-
-    # $disablebughook is a bit of a hack to let format migration scripts use
-    # this function rather than having to duplicate it themselves.
-    &bughook($change,$ref,$data) unless $disablebughook;
-}
-
-sub unlockwritebug {
-    writebug(@_);
-    &unfilelock;
-}
-
-sub filelock {
-    # NB - NOT COMPATIBLE WITH `with-lock'
-    local ($lockfile,$flockpushno,$evalstring,$count,$errors,@s1,@s2) = @_;
-    $flockpushno= $#filelocks+1;
-    $count= 10; $errors= '';
-    for (;;) {
-        $evalstring= "
-            open(FLOCK${flockpushno},\"> \$lockfile\") || die \"open: \$!\";
-            \$flockwant= pack(\$flockstruct,&F_WRLCK,0,0,1,0);".
-                ($] >= 5.000 ? "
-            fcntl(FLOCK$flockpushno,&F_SETLK,\$flockwant) || die \"setlk: \$!\";" : "
-            \$z= syscall(&SYS_fcntl,fileno(FLOCK$flockpushno),&F_SETLK,\$flockwant) < 0
-                 && die \"syscall fcntl setlk: \$!\";") ."
-            (\@s1= lstat(\$lockfile)) || die \"lstat: \$!\";
-            (\@s2= stat(FLOCK$flockpushno)) || die \"fstat: \$!\";
-            join(',',\@s1) eq join(',',\@s2) || die \"file switched\";
-            1;
-        ";
-        last if eval $evalstring;
-        $errors .= $@;
-        eval "close(FLOCK$flockpushno);";
-        if (--$count <=0) {
-            $errors =~ s/\n+$//;
-            &quit("failed to get lock on file $lockfile: $errors // $evalstring");
-        }
-        sleep 10;
-    }
-    push(@cleanups,'unfilelock');
-    push(@filelocks,$lockfile);
-}
-
-sub unfilelock {
-    if (@filelocks == 0) {
-        warn "unfilelock called with no active filelocks!\n";
-        return;
-    }
-    local ($lockfile) = pop(@filelocks);
-    pop(@cleanups);
-    eval 'close(FLOCK'.($#filelocks+1).');' || warn "failed to close lock file $lockfile: $!";
-    unlink($lockfile) || warn "failed to remove lock file $lockfile: $!";
-}
-
-sub addfoundversions {
-    my $data = shift;
-    my $package = shift;
-    my $version = shift;
-    my $isbinary = shift;
-    return unless defined $version;
-    undef $package if $package =~ m[(?:\s|/)];
-    my $source = $package;
-
-    if (defined $package and $isbinary) {
-        my @srcinfo = binarytosource($package, $version, undef);
-        if (@srcinfo) {
-            # We know the source package(s). Use a fully-qualified version.
-            addfoundversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
-            return;
-        }
-        # Otherwise, an unqualified version will have to do.
-        undef $source;
-    }
-
-    # Strip off various kinds of brain-damage.
-    $version =~ s/;.*//;
-    $version =~ s/ *\(.*\)//;
-    $version =~ s/ +[A-Za-z].*//;
-
-    foreach my $ver (split /[,\s]+/, $version) {
-        my $sver = defined($source) ? "$source/$ver" : '';
-        unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{found_versions}}) {
-            push @{$data->{found_versions}}, defined($source) ? $sver : $ver;
-        }
-        @{$data->{fixed_versions}} =
-            grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
-    }
-}
-
-=head2 removefoundversions
-
-     removefoundversions($data,$package,$versiontoremove)
-
-Removes found versions from $data
-
-If a version is fully qualified (contains /) only versions matching
-exactly are removed. Otherwise, all versions matching the version
-number are removed.
-
-Currently $package and $isbinary are entirely ignored, but accepted
-for backwards compatibilty.
-
-=cut
-
-sub removefoundversions {
-    my $data = shift;
-    my $package = shift;
-    my $version = shift;
-    my $isbinary = shift;
-    return unless defined $version;
-
-    foreach my $ver (split /[,\s]+/, $version) {
-        if ($ver =~ m{/}) {
-             # fully qualified version
-             @{$data->{found_versions}} =
-                  grep {$_ ne $ver}
-                       @{$data->{found_versions}};
-        }
-        else {
-             # non qualified version; delete all matchers
-             @{$data->{found_versions}} =
-                  grep {$_ !~ m[(?:^|/)\Q$ver\E$]}
-                       @{$data->{found_versions}};
-        }
-    }
-}
-
-sub addfixedversions {
-    my $data = shift;
-    my $package = shift;
-    my $version = shift;
-    my $isbinary = shift;
-    return unless defined $version;
-    undef $package if $package =~ m[(?:\s|/)];
-    my $source = $package;
-
-    if (defined $package and $isbinary) {
-        my @srcinfo = binarytosource($package, $version, undef);
-        if (@srcinfo) {
-            # We know the source package(s). Use a fully-qualified version.
-            addfixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
-            return;
-        }
-        # Otherwise, an unqualified version will have to do.
-        undef $source;
-    }
-
-    # Strip off various kinds of brain-damage.
-    $version =~ s/;.*//;
-    $version =~ s/ *\(.*\)//;
-    $version =~ s/ +[A-Za-z].*//;
-
-    foreach my $ver (split /[,\s]+/, $version) {
-        my $sver = defined($source) ? "$source/$ver" : '';
-        unless (grep { $_ eq $ver or $_ eq $sver } @{$data->{fixed_versions}}) {
-            push @{$data->{fixed_versions}}, defined($source) ? $sver : $ver;
-        }
-        @{$data->{found_versions}} =
-            grep { $_ ne $ver and $_ ne $sver } @{$data->{found_versions}};
-    }
-}
-
-sub removefixedversions {
-    my $data = shift;
-    my $package = shift;
-    my $version = shift;
-    my $isbinary = shift;
-    return unless defined $version;
-    undef $package if $package =~ m[(?:\s|/)];
-    my $source = $package;
-
-    if (defined $package and $isbinary) {
-        my @srcinfo = binarytosource($package, $version, undef);
-        if (@srcinfo) {
-            # We know the source package(s). Use a fully-qualified version.
-            removefixedversions($data, $_->[0], $_->[1], '') foreach @srcinfo;
-            return;
-        }
-        # Otherwise, an unqualified version will have to do.
-        undef $source;
-    }
-
-    foreach my $ver (split /[,\s]+/, $version) {
-        my $sver = defined($source) ? "$source/$ver" : '';
-        @{$data->{fixed_versions}} =
-            grep { $_ ne $ver and $_ ne $sver } @{$data->{fixed_versions}};
-    }
-}
-
-sub quit {
-    print DEBUG "quitting >$_[0]<\n";
-    local ($u);
-    while ($u= $cleanups[$#cleanups]) { &$u; }
-    die "*** $_[0]\n";
-}
-
 %saniarray= ('<','lt', '>','gt', '&','amp', '"','quot');
 
 sub sani {
-    local ($in) = @_;
-    local ($out);
-    while ($in =~ m/[<>&"]/) {
-        $out.= $`. '&'. $saniarray{$&}. ';';
-        $in=$';
-    }
-    $out.= $in;
-    $out;
-}
-
-sub update_realtime {
-       my ($file, $bug, $new) = @_;
-
-       # update realtime index.db
-
-       open(IDXDB, "<$file") or die "Couldn't open $file";
-       open(IDXNEW, ">$file.new");
-
-       my $line;
-       my @line;
-       while($line = <IDXDB>) {
-               @line = split /\s/, $line;
-               last if ($line[1] >= $bug);
-               print IDXNEW $line;
-               $line = "";
-       }
-
-       if ($new eq "NOCHANGE") {
-               print IDXNEW $line if ($line ne "" && $line[1] == $ref);
-       } elsif ($new eq "REMOVE") {
-               0;
-       } else {
-               print IDXNEW $new;
-       }
-       if ($line ne "" && $line[1] > $bug) {
-               print IDXNEW $line;
-               $line = "";
-       }
-
-       print IDXNEW while(<IDXDB>);
-
-       close(IDXNEW);
-       close(IDXDB);
-
-       rename("$file.new", $file);
-
-       return $line;
-}
-
-sub bughook_archive {
-       my $ref = shift;
-       &filelock("debbugs.trace.lock");
-       &appendfile("debbugs.trace","archive $ref\n");
-       my $line = update_realtime(
-               "$gSpoolDir/index.db.realtime", 
-               $ref,
-               "REMOVE");
-       update_realtime("$gSpoolDir/index.archive.realtime",
-               $ref, $line);
-       &unfilelock;
-}      
-
-sub bughook {
-       my ( $type, $ref, $data ) = @_;
-       &filelock("debbugs.trace.lock");
-
-       &appendfile("debbugs.trace","$type $ref\n",makestatus($data, 1));
-
-       my $whendone = "open";
-       my $severity = $gDefaultSeverity;
-       (my $pkglist = $data->{package}) =~ s/[,\s]+/,/g;
-       $pkglist =~ s/^,+//;
-       $pkglist =~ s/,+$//;
-       $whendone = "forwarded" if length $data->{forwarded};
-       $whendone = "done" if length $data->{done};
-       $severity = $data->{severity} if length $data->{severity};
-
-       my $k = sprintf "%s %d %d %s [%s] %s %s\n",
-                       $pkglist, $ref, $data->{date}, $whendone,
-                       $data->{originator}, $severity, $data->{keywords};
-
-       update_realtime("$gSpoolDir/index.db.realtime", $ref, $k);
-
-       &unfilelock;
-}
-
-sub appendfile {
-       my $file = shift;
-       if (!open(AP,">>$file")) {
-               print DEBUG "failed open log<\n";
-               print DEBUG "failed open log err $!<\n";
-               &quit("opening $file (appendfile): $!");
-       }
-       print(AP @_) || &quit("writing $file (appendfile): $!");
-       close(AP) || &quit("closing $file (appendfile): $!");
+    my ($in) = @_;
+    carp "You should be using HTML::Entities instead.";
+    $in =~ s/([<>&"])/$saniarray{$1}/g;
+    return $in;
 }
 
 sub getmailbody {
@@ -541,13 +68,6 @@ sub escapelog {
        return \@log;
 }
 
-sub isstrongseverity {
-    my $severity = shift;
-    $severity = $gDefaultSeverity if $severity eq '';
-    return grep { $_ eq $severity } @gStrongSeverities;
-}
-
-
 @severities= grep { not exists $gObsoleteSeverities{$_} } @gSeverityList;
 @showseverities= @severities;
 grep ($_= $_ eq '' ? $gDefaultSeverity : $_, @showseverities);
index b701ea59b89853e25272b162126b830e04047f30..14d7b1c131e92a8f229be595ef2bebc7db561bef 100755 (executable)
@@ -4,8 +4,6 @@
 # Copyright (c) 2005/08/03 Anthony Towns
 # GPL v2
 
-#use strict;
-
 use DB_File;
 use MLDBM qw(DB_FILE Storable);
 use Fcntl qw/O_RDWR O_CREAT O_TRUNC/;
@@ -77,16 +75,14 @@ GetOptions(\%options,'quick!','index_path|index-path=s','debug|d+','help|h|?','m
 pod2usage(1) if $options{help};
 pod2usage(-verbose=>2) if $options{man};
 
-{ no warnings;
-  no strict;
-require '/etc/debbugs/config';
-require '/org/bugs.debian.org/scripts/errorlib';
-}
+use Debbugs::Config qw(:config);
+use Debbugs::Common qw(getparsedaddrs getbugcomponent);
+use Debbugs::Status qw(readbug);
 
-chdir('/org/bugs.debian.org/spool') or die "chdir spool: $!\n";
+chdir($config{spool_dir}) or die "chdir $config{spool_dir} failed: $!";
 
 my $verbose = $options{debug};
-my $indexdest = $options{index_path} || "/org/bugs.debian.org/spool";
+my $indexdest = $options{index_path} || $config{spool_dir};
 
 my $initialdir = "db-h";
 my $suffix = "";
@@ -97,7 +93,7 @@ if (defined $ARGV[0] and $ARGV[0] eq "archive") {
 }
 
 # NB: The reverse index is special; it's used to clean up during updates to bugs
-my @indexes = ('package', 'tag', 'severity', 'submitter-email','reverse');
+my @indexes = ('package', 'tag', 'severity','owner','submitter-email','reverse');
 my $indexes;
 my %slow_index = ();
 my %fast_index = ();
@@ -186,8 +182,9 @@ while (my $dir = shift @dirs) {
                addbugtoindex("package", $bug, split /[\s,]+/, $fdata->{"package"});
                addbugtoindex("tag", $bug, split /[\s,]+/, $fdata->{"keywords"});
                addbugtoindex('submitter-email', $bug,
-                             emailfromrfc822($fdata->{"originator"}));
+                             map {$_->address} getparsedaddrs($fdata->{originator}));
                addbugtoindex("severity", $bug, $fdata->{"severity"});
+               addbugtoindex("owner", $bug, $fdata->{"owner"});
        }
 }
 
@@ -208,3 +205,4 @@ for my $i (@indexes) {
        # We do this, because old versions of touch don't support -d '@epoch'
        system('touch','-d',"1/1/1970 UTC + ${start_time}secs","$indexdest/by-$i$suffix.idx");
 }
+
index bdeec7f72609d990e10b20ebc5023ee95eb99819..efdfe470e7a41651ef65bf4e1ff64ed8de55794a 100755 (executable)
@@ -4,9 +4,7 @@
 # Usage: process nn
 # Temps:  incoming/Pnn
 
-use POSIX qw(strftime tzset);
-$ENV{"TZ"} = 'UTC';
-tzset();
+use POSIX qw(strftime);
 
 use MIME::Parser;
 use Debbugs::MIME qw(decode_rfc1522 create_mime_message);
@@ -14,14 +12,16 @@ use Debbugs::Mail qw(send_mail_message encode_headers);
 use Debbugs::Packages qw(getpkgsrc);
 use Debbugs::User qw(read_usertags write_usertags);
 
-my $config_path = '/etc/debbugs';
-my $lib_path = '/usr/lib/debbugs';
+use HTML::Entities qw(encode_entities);
 
 # TODO DLA; needs config reworking and errorlib reworking
 # use warnings;
 # use strict;
 
-require "$config_path/config";
+use Debbugs::Status qw(:versions);
+use Debbugs::Config qw(:globals);
+my $lib_path = $gLibPath;
+
 require "$lib_path/errorlib";
 $ENV{'PATH'} = $lib_path.':'.$ENV{'PATH'};
 
@@ -152,7 +152,7 @@ for my $phline (@bodylines)
     print DEBUG ">$fn|$fv|\n";
     $fn = lc $fn;
     # Don't lc owner or forwarded
-    $fv = lc $fv unless $fh =~ /^(?:owner|forwarded|usertags)$/;
+    $fv = lc $fv unless $fh =~ /^(?:owner|forwarded|usertags|version|source-version)$/;
     $pheader{$fn} = $fv;
     print DEBUG ">$fn~$fv<\n";
 }
@@ -775,7 +775,7 @@ END
     &htmllog($newref ? "Report" : "Information", "forwarded",
              join(', ',"$gSubmitList\@$gListDomain",@resentccs),
              "<code>$gBug#$ref</code>".
-             (length($data->{package})? "; Package <code>".&sani($data->{package})."</code>" : '').
+             (length($data->{package})? "; Package <code>".encode_entities($data->{package})."</code>" : '').
              ".");
     &sendmessage(<<END,["$gSubmitList\@$gListDomain",@resentccs],[@bccs]);
 Subject: $gBug#$ref: $newsubject
@@ -798,13 +798,13 @@ END
         &htmllog($newref ? "Report" : "Information", "forwarded",
                  $resentccval,
                  "<code>$gBug#$ref</code>".
-                 (length($data->{package}) ? "; Package <code>".&sani($data->{package})."</code>" : '').
+                 (length($data->{package}) ? "; Package <code>".encode_entities($data->{package})."</code>" : '').
                  ".");
     } else {
         &htmllog($newref ? "Report" : "Information", "stored",
                  "",
                  "<code>$gBug#$ref</code>".
-                 (length($data->{package}) ? "; Package <code>".&sani($data->{package})."</code>" : '').
+                 (length($data->{package}) ? "; Package <code>".encode_entities($data->{package})."</code>" : '').
                  ".");
     }
     &sendmessage(<<END,[@resentccs],[@bccs]);
@@ -822,10 +822,10 @@ ${source_pr_header}$fwd
 END
 }
 
-$htmlbreak= length($brokenness) ? "<p>\n".&sani($brokenness)."\n<p>\n" : '';
+$htmlbreak= length($brokenness) ? "<p>\n".encode_entities($brokenness)."\n<p>\n" : '';
 $htmlbreak =~ s/\n\n/\n<P>\n\n/g;
 if (length($resentccval)) {
-    $htmlbreak = "  Copy sent to <code>".&sani($resentccval)."</code>.".
+    $htmlbreak = "  Copy sent to <code>".encode_entities($resentccval)."</code>.".
         $htmlbreak;
 }
 unless (exists $header{'x-debbugs-no-ack'}) {
@@ -1090,7 +1090,7 @@ sub htmllog {
     print(AP
           "\6\n".
           "<strong>$whatobj $whatverb</strong>".
-          ($where eq '' ? "" : " to <code>".&sani($where)."</code>").
+          ($where eq '' ? "" : " to <code>".encode_entities($where)."</code>").
           ":<br>\n". $desc.
           "\n\3\n") || &quit("writing db-h/$hash/$ref.log (lh): $!");
     close(AP) || &quit("closing db-h/$hash/$ref.log (lh): $!");
index 1d7496d1dcc03218f13289199be0751a7f3637be..2606b26e26bcf06afaefc44904157a8d7bd4550a 100755 (executable)
@@ -8,25 +8,28 @@
 # Creates: incoming/E.nn
 # Stop:    stop
 
-$config_path = '/etc/debbugs';
-$lib_path = '/usr/lib/debbugs';
+use warnings;
+use strict;
 
-require "$config_path/config";
-require "$lib_path/errorlib";
-$ENV{'PATH'} = $lib_path.':'.$ENV{'PATH'};
+
+use Debbugs::Config qw(:globals);
+use Debbugs::Common qw(:lock);
+
+my $lib_path = $gLibPath;
 
 use File::Path;
 
 chdir( $gSpoolDir ) || die "chdir spool: $!\n";
-push( @INC, $lib_path );
 
 #open(DEBUG,">&4");
 
 umask(002);
 
 $|=1;
-undef %fudged;
+my %fudged;
+my @ids;
 
+my $ndone = 0;
 &filelock('incoming-cleaner');
 for (;;) {
     if (-f 'stop') {
@@ -40,8 +43,8 @@ for (;;) {
         @ids= sort(@ids);
     }
     stat("$gMaintainerFile") || die "stat $gMaintainerFile: $!\n";
-    $nf= @ids;
-    $id= shift(@ids);
+    my $nf= @ids;
+    my $id= shift(@ids);
     unless (rename("incoming/I$id","incoming/G$id")) {
         if ($fudged{$id}) {
             die "$id already fudged once! $!\n";
@@ -49,19 +52,20 @@ for (;;) {
         $fudged{$id}= 1;
         next;
     }
+    my $c;
     if ($id =~ m/^[RC]/) {
-        print(STDOUT "[$nf] $id service ...") || die $!;
+        print(STDOUT "[$nf] $id service ...") || die $!;
         defined($c=fork) || die $!;
-        if (!$c) { exec("$lib_path/service",$id); die $!; }
+        if (!$c) { exec("$lib_path/service",$id); die "unable to execute $lib_path/service: $!"; }
     } elsif ($id =~ m/^[BMQFDUL]/) {
         print(STDOUT "[$nf] $id process ...") || die $!;
         defined($c=fork) || die $!;
-        if (!$c) { exec("$lib_path/process",$id); die $!; }
+        if (!$c) { exec("$lib_path/process",$id); die "unable to execute $lib_path/process: $!"; }
     } else {
         die "bad name $id";
     }
-    $cc=waitpid($c,0); $cc == $c || die "$cc $c $!";
-    $status=$?;
+    my $cc=waitpid($c,0); $cc == $c || die "$cc $c $!";
+    my $status=$?;
     if ($status) {
         print(STDERR "$id: process failed ($status $!) - now in [PG]$id\n") || die $!;
     }
index 815b6c80c649acdbfc7550a5deacc761d0e48c41..eb101a4029fe5cf681c1c5f2b6fe60ceb8c62c61 100755 (executable)
@@ -5,11 +5,9 @@
 #set umask in order to have group-writable incoming/*
 #umask(002);
 
-#load configuration file
-$config_path = '/etc/debbugs';
-#$lib_path = '/usr/lib/debbugs';
+use Debbugs::Config qw(:globals :text);
+my $lib_path = $gLibPath;
 
-require "$config_path/config";
 $ENV{'PATH'} = '/usr/lib/debbugs:'.$ENV{'PATH'};
 
 #set source of mail delivery
@@ -28,7 +26,6 @@ if ( $gMailer eq 'exim' )
        s/\>//;
        s/\<//;
 }
-require("/etc/debbugs/text");
 
 #remove everything from @ to end of line
 s/\@.*$//;
index 4d0b4645451074c64484a1ee0e6af84e149ba5b1..80d8cb1d839f970ea0e04b99533dee1bf7b6485d 100755 (executable)
@@ -9,11 +9,10 @@ use MIME::Parser;
 use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522);
 use Debbugs::Mail qw(send_mail_message);
 use Debbugs::User;
+use HTML::Entities qw(encode_entities);
 
-$config_path = '/etc/debbugs';
-$lib_path = '/usr/lib/debbugs';
-
-require "$config_path/config";
+use Debbugs::Config qw(:globals);
+$lib_path = $gLibPath;
 require "$lib_path/errorlib";
 $ENV{'PATH'} = $lib_path.':'.$ENV{'PATH'};
 
@@ -487,7 +486,7 @@ END
        $bug_affected{$ref}=1;
         if (&setbug) {
             if (@{$data->{fixed_versions}}) {
-                &transcript("'reopen' is deprecated when a bug has been closed with a version;\nuse 'found' or 'submitter' as appropriate instead.\n");
+                &transcript("'reopen' may be inappropriate when a bug has been closed with a version;\nyou may need to use 'found' to remove fixed versions.\n");
             }
             if (!length($data->{done})) {
                 &transcript("$gBug is already open, cannot reopen.\n\n");
@@ -752,6 +751,7 @@ END
                     $ref = $clonebugs{$ref};
                }
                if (&getbug) {
+                   &foundbug;
                    push @okayblockers, $ref;
 
                    # add to the list all bugs that are merged with $b,
@@ -1196,8 +1196,8 @@ print(AP
       "\2\n$repliedshow\n\5\n$reply\n\3\n".
       "\6\n".
       "<strong>Request received</strong> from <code>".
-      &sani($header{'from'})."</code>\n".
-      "to <code>".&sani($controlrequestaddr)."</code>\n".
+      encode_entities($header{'from'})."</code>\n".
+      "to <code>".encode_entities($controlrequestaddr)."</code>\n".
       "\3\n".
       "\7\n",@{escapelog(@log)},"\n\3\n") || &quit("writing db-h/-1.log: $!");
 close(AP) || &quit("open db-h/-1.log: $!");
@@ -1429,9 +1429,9 @@ sub savebug {
     open(L,">>db-h/$hash/$ref.log") || &quit("opening db-h/$hash/$ref.log: $!");
     print(L
           "\6\n".
-          "<strong>".&sani($action)."</strong>\n".
-          "Request was from <code>".&sani($header{'from'})."</code>\n".
-          "to <code>".&sani($controlrequestaddr)."</code>. \n".
+          "<strong>".encode_entities($action)."</strong>\n".
+          "Request was from <code>".encode_entities($header{'from'})."</code>\n".
+          "to <code>".encode_entities($controlrequestaddr)."</code>. \n".
           "\3\n".
           "\7\n",@{escapelog(@log)},"\n\3\n") || &quit("writing db-h/$hash/$ref.log: $!");
     close(L) || &quit("closing db-h/$hash/$ref.log: $!");
index a1df91940f7154fca8bcc8303061fdd0bc4fb7da..415aba05cd7ae42e6027b1fc7622000d3f762759 100644 (file)
@@ -1,4 +1,6 @@
-# $Id: text.in,v 1.23 2003/09/18 11:20:54 joy Exp $
+# -*- mode: cperl -*-
+
+use Debbugs::Config qw(:globals);
 
 ############################################################################
 #  Here is a blurb to point people to ftp archive of directions.  It is
diff --git a/t/03_packages.t b/t/03_packages.t
new file mode 100644 (file)
index 0000000..24a81df
--- /dev/null
@@ -0,0 +1,12 @@
+# -*- mode: cperl;-*-
+
+use Test::More tests => 1;
+
+use warnings;
+use strict;
+
+use utf8;
+use Encode;
+
+use_ok('Debbugs::Packages');
+
index ace98ce7709c61560a63dc032e96edec07f1692c..1a9af80905b557bfdaf4496ba77d93e117c1c5f7 100644 (file)
@@ -20,7 +20,7 @@ my %data = (package => q(foo),
           );
 
 
-require_ok('scripts/errorlib.in');
+use_ok('Debbugs::Status',qw(:versions));
 # check removefoundversions
 my $data = dclone(\%data);
 removefoundversions($data,$data->{package},'1.00');
diff --git a/t/06_mail_handling.t b/t/06_mail_handling.t
new file mode 100644 (file)
index 0000000..c5be847
--- /dev/null
@@ -0,0 +1,186 @@
+# -*- mode: cperl;-*-
+# $Id: 05_mail.t,v 1.1 2005/08/17 21:46:17 don Exp $
+
+use Test::More tests => 31;
+
+use warnings;
+use strict;
+
+# Here, we're going to shoot messages through a set of things that can
+# happen.
+
+# First, we're going to send mesages to receive.
+# To do so, we'll first send a message to submit,
+# then send messages to the newly created bugnumber.
+
+use IO::File;
+use File::Temp qw(tempdir);
+use Cwd qw(getcwd);
+use Debbugs::MIME qw(create_mime_message);
+use File::Basename qw(dirname basename);
+# The test functions are placed here to make things easier
+use lib qw(t/lib);
+use DebbugsTest qw(:all);
+use Data::Dumper;
+
+# HTTP::Server:::Simple defines a SIG{CHLD} handler that breaks system; undef it here.
+$SIG{CHLD} = sub {};
+my %config;
+eval {
+     %config = create_debbugs_configuration(debug => exists $ENV{DEBUG}?$ENV{DEBUG}:0);
+};
+if ($@) {
+     BAIL_OUT($@);
+}
+
+my $sendmail_dir = $config{sendmail_dir};
+my $spool_dir = $config{spool_dir};
+my $config_dir = $config{config_dir};
+
+END{
+     if ($ENV{DEBUG}) {
+         diag("spool_dir:   $spool_dir\n");
+         diag("config_dir:   $config_dir\n");
+         diag("sendmail_dir: $sendmail_dir\n");
+     }
+}
+
+# We're going to use create mime message to create these messages, and
+# then just send them to receive.
+
+send_message(to=>'submit@bugs.something',
+            headers => [To   => 'submit@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => 'Submiting a bug',
+                       ],
+            body => <<EOF) or fail('Unable to send message');
+Package: foo
+Severity: normal
+
+This is a silly bug
+EOF
+
+# now we check to see that we have a bug, and nextnumber has been incremented
+ok(-e "$spool_dir/db-h/01/1.log",'log file created');
+ok(-e "$spool_dir/db-h/01/1.summary",'sumary file created');
+ok(-e "$spool_dir/db-h/01/1.status",'status file created');
+ok(-e "$spool_dir/db-h/01/1.report",'report file created');
+
+# next, we check to see that (at least) the proper messages have been
+# sent out. 1) ack to submitter 2) mail to maintainer
+
+# This keeps track of the previous size of the sendmail directory
+my $SD_SIZE_PREV = 0;
+my $SD_SIZE_NOW = dirsize($sendmail_dir);
+ok($SD_SIZE_NOW-$SD_SIZE_PREV >= 2,'submit messages appear to have been sent out properly');
+$SD_SIZE_PREV=$SD_SIZE_NOW;
+
+# now send a message to the bug
+
+send_message(to => '1@bugs.something',
+            headers => [To   => '1@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => 'Sending a message to a bug',
+                       ],
+            body => <<EOF) or fail('sending message to 1@bugs.someting failed');
+Package: foo
+Severity: normal
+
+This is a silly bug
+EOF
+
+$SD_SIZE_NOW = dirsize($sendmail_dir);
+ok($SD_SIZE_NOW-$SD_SIZE_PREV >= 2,'1@bugs.something messages appear to have been sent out properly');
+$SD_SIZE_PREV=$SD_SIZE_NOW;
+
+# just check to see that control doesn't explode
+send_message(to => 'control@bugs.something',
+            headers => [To   => 'control@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => 'Munging a bug',
+                       ],
+            body => <<EOF) or fail 'message to control@bugs.something failed';
+severity 1 wishlist
+retitle 1 new title
+thanks
+EOF
+
+$SD_SIZE_NOW = dirsize($sendmail_dir);
+ok($SD_SIZE_NOW-$SD_SIZE_PREV >= 1,'control@bugs.something messages appear to have been sent out properly');
+$SD_SIZE_PREV=$SD_SIZE_NOW;
+# now we need to check to make sure the control message was processed without errors
+ok(system('sh','-c','find '.$sendmail_dir.q( -type f | xargs grep -q "Subject: Processed: Munging a bug")) == 0,
+   'control@bugs.something message was parsed without errors');
+# now we need to check to make sure that the control message actually did anything
+# This is an eval because $ENV{DEBBUGS_CONFIG_FILE} isn't set at BEGIN{} time
+eval "use Debbugs::Status qw(read_bug);";
+my $status = read_bug(bug=>1);
+ok($status->{subject} eq 'new title','bug 1 retitled');
+ok($status->{severity} eq 'wishlist','bug 1 wishlisted');
+
+# now we're going to go through and methododically test all of the control commands.
+my @control_commands =
+     (severity_wishlist => {command => 'severity',
+                           value   => 'wishlist',
+                           status_key => 'severity',
+                           status_value => 'wishlist',
+                          },
+      'found_1.0'        => {command => 'found',
+                            value   => '1.0',
+                            status_key => 'found_versions',
+                            status_value => ['1.0'],
+                           },
+      'notfound_1.0'     => {command => 'notfound',
+                            value   => '1.0',
+                            status_key => 'found_versions',
+                            status_value => [],
+                           },
+      submitter_foo      => {command => 'submitter',
+                            value   => 'foo@bar.com',
+                            status_key => 'originator',
+                            status_value => 'foo@bar.com',
+                           },
+
+      forwarded_foo      => {command => 'forwarded',
+                            value   => 'foo@bar.com',
+                            status_key => 'forwarded',
+                            status_value => 'foo@bar.com',
+                           },
+      owner_foo          => {command => 'owner',
+                            value   => 'foo@bar.com',
+                            status_key => 'owner',
+                            status_value => 'foo@bar.com',
+                           },
+      noowner      => {command => 'noowner',
+                      value   => '',
+                      status_key => 'owner',
+                      status_value => '',
+                     },
+
+     );
+
+while (my ($command,$control_command) = splice(@control_commands,0,2)) {
+     # just check to see that control doesn't explode
+     $control_command->{value} = " $control_command->{value}" if length $control_command->{value}
+         and $control_command->{value} !~ /^\s/;
+     send_message(to => 'control@bugs.something',
+                 headers => [To   => 'control@bugs.something',
+                             From => 'foo@bugs.something',
+                             Subject => "Munging a bug with $command",
+                            ],
+                 body => <<EOF) or fail 'message to control@bugs.something failed';
+$control_command->{command} 1$control_command->{value}
+thanks
+EOF
+                                 ;
+     $SD_SIZE_NOW = dirsize($sendmail_dir);
+     ok($SD_SIZE_NOW-$SD_SIZE_PREV >= 1,'control@bugs.something messages appear to have been sent out properly');
+     $SD_SIZE_PREV=$SD_SIZE_NOW;
+     # now we need to check to make sure the control message was processed without errors
+     ok(system('sh','-c','find '.$sendmail_dir.q( -type f | xargs grep -q "Subject: Processed: Munging a bug with $command")) == 0,
+       'control@bugs.something'. "$command message was parsed without errors");
+     # now we need to check to make sure that the control message actually did anything
+     my $status = read_bug(bug=>1);
+     is_deeply($status->{$control_command->{status_key}},$control_command->{status_value},"bug 1 $command")
+         or fail(Dumper($status));
+}
diff --git a/t/07_bugreport.t b/t/07_bugreport.t
new file mode 100644 (file)
index 0000000..dedd445
--- /dev/null
@@ -0,0 +1,80 @@
+# -*- mode: cperl;-*-
+
+
+use Test::More tests => 3;
+
+use warnings;
+use strict;
+
+# Here, we're going to shoot messages through a set of things that can
+# happen.
+
+# First, we're going to send mesages to receive.
+# To do so, we'll first send a message to submit,
+# then send messages to the newly created bugnumber.
+
+use IO::File;
+use File::Temp qw(tempdir);
+use Cwd qw(getcwd);
+use Debbugs::MIME qw(create_mime_message);
+use File::Basename qw(dirname basename);
+use Test::WWW::Mechanize;
+# The test functions are placed here to make things easier
+use lib qw(t/lib);
+use DebbugsTest qw(:all);
+
+my %config;
+eval {
+     %config = create_debbugs_configuration(debug => exists $ENV{DEBUG}?$ENV{DEBUG}:0);
+};
+if ($@) {
+     BAIL_OUT($@);
+}
+
+# Output some debugging information if there's an error
+END{
+     if ($ENV{DEBUG}) {
+         foreach my $key (keys %config) {
+              diag("$key: $config{$key}\n");
+         }
+     }
+}
+
+# create a bug
+send_message(to=>'submit@bugs.something',
+            headers => [To   => 'submit@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => 'Submitting a bug',
+                       ],
+            body => <<EOF) or fail('Unable to send message');
+Package: foo
+Severity: normal
+
+This is a silly bug
+EOF
+
+
+# test bugreport.cgi
+
+# start up an HTTP::Server::Simple
+my $bugreport_cgi_handler = sub {
+     # I do not understand why this is necessary.
+     $ENV{DEBBUGS_CONFIG_FILE} = "$config{config_dir}/debbugs_config";
+     my $content = qx(perl -I. -T cgi/bugreport.cgi);
+     $content =~ s/^\s*Content-Type:[^\n]+\n*//si;
+     print $content;
+};
+
+my $port = 11342;
+
+ok(DebbugsTest::HTTPServer::fork_and_create_webserver($bugreport_cgi_handler,$port),
+   'forked HTTP::Server::Simple successfully');
+
+my $mech = Test::WWW::Mechanize->new();
+
+$mech->get_ok('http://localhost:'.$port.'/?bug=1',
+             'Page received ok');
+ok($mech->content() =~ qr/\<title\>\#1\s+\-\s+Submitting a bug/i,
+   'Title of bug is submitting a bug');
+
+# Other tests for bugs in the page should be added here eventually
diff --git a/t/08_pkgreport.t b/t/08_pkgreport.t
new file mode 100644 (file)
index 0000000..df4861d
--- /dev/null
@@ -0,0 +1,86 @@
+# -*- mode: cperl;-*-
+
+
+use Test::More tests => 3;
+
+use warnings;
+use strict;
+
+# Here, we're going to shoot messages through a set of things that can
+# happen.
+
+# First, we're going to send mesages to receive.
+# To do so, we'll first send a message to submit,
+# then send messages to the newly created bugnumber.
+
+use IO::File;
+use File::Temp qw(tempdir);
+use Cwd qw(getcwd);
+use Debbugs::MIME qw(create_mime_message);
+use File::Basename qw(dirname basename);
+use Test::WWW::Mechanize;
+# The test functions are placed here to make things easier
+use lib qw(t/lib);
+use DebbugsTest qw(:all);
+
+my %config;
+eval {
+     %config = create_debbugs_configuration(debug => exists $ENV{DEBUG}?$ENV{DEBUG}:0);
+};
+if ($@) {
+     BAIL_OUT($@);
+}
+
+# Output some debugging information if there's an error
+END{
+     if ($ENV{DEBUG}) {
+         foreach my $key (keys %config) {
+              diag("$key: $config{$key}\n");
+         }
+     }
+}
+
+# create a bug
+send_message(to=>'submit@bugs.something',
+            headers => [To   => 'submit@bugs.something',
+                        From => 'foo@bugs.something',
+                        Subject => 'Submitting a bug',
+                       ],
+            body => <<EOF) or fail('Unable to send message');
+Package: foo
+Severity: normal
+
+This is a silly bug
+EOF
+
+
+# test bugreport.cgi
+
+# start up an HTTP::Server::Simple
+my $pkgreport_cgi_handler = sub {
+     # I do not understand why this is necessary.
+     $ENV{DEBBUGS_CONFIG_FILE} = "$config{config_dir}/debbugs_config";
+     # We cd here because pkgreport uses require ./common.pl
+     my $content = qx(cd cgi; perl -I.. -T pkgreport.cgi);
+     # Strip off the Content-Type: stuff
+     $content =~ s/^\s*Content-Type:[^\n]+\n*//si;
+     print $content;
+};
+
+my $port = 11342;
+
+ok(DebbugsTest::HTTPServer::fork_and_create_webserver($pkgreport_cgi_handler,$port),
+   'forked HTTP::Server::Simple successfully');
+
+
+my $mech = Test::WWW::Mechanize->new(autocheck => 1);
+
+$mech->get_ok('http://localhost:'.$port.'/?pkg=foo');
+
+# I'd like to use $mech->title_ok(), but I'm not sure why it doesn't
+# work.
+ok($mech->content()=~ qr/package foo/i,
+   'Package title seems ok',
+  );
+
+# Test more stuff here
diff --git a/t/lib/DebbugsTest.pm b/t/lib/DebbugsTest.pm
new file mode 100644 (file)
index 0000000..a2e0537
--- /dev/null
@@ -0,0 +1,235 @@
+
+package DebbugsTest;
+
+=head1 NAME
+
+DebbugsTest
+
+=head1 SYNOPSIS
+
+use DebbugsTest
+
+
+=head1 DESCRIPTION
+
+This module contains various testing routines used to test debbugs in
+a "pseudo install"
+
+=head1 FUNCTIONS
+
+=cut
+
+use warnings;
+use strict;
+use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
+use base qw(Exporter);
+
+use IO::File;
+use File::Temp qw(tempdir);
+use Cwd qw(getcwd);
+use Debbugs::MIME qw(create_mime_message);
+use File::Basename qw(dirname basename);
+use IPC::Open3;
+use IO::Handle;
+
+use Params::Validate qw(validate_with :types);
+
+BEGIN{
+     $VERSION = 1.00;
+     $DEBUG = 0 unless defined $DEBUG;
+
+     @EXPORT = ();
+     %EXPORT_TAGS = (configuration => [qw(dirsize create_debbugs_configuration send_message)],
+                   );
+     @EXPORT_OK = ();
+     Exporter::export_ok_tags(qw(configuration));
+     $EXPORT_TAGS{all} = [@EXPORT_OK];
+}
+
+# First, we're going to send mesages to receive.
+# To do so, we'll first send a message to submit,
+# then send messages to the newly created bugnumber.
+
+
+
+sub create_debbugs_configuration {
+     my %param = validate_with(params => \@_,
+                              spec   => {debug => {type => BOOLEAN,
+                                                   default => 0,
+                                                  },
+                                         cleanup => {type => BOOLEAN,
+                                                     optional => 1,
+                                                    },
+                                        },
+                             );
+     $param{cleanup} = $param{debug}?0:1 if not exists $param{cleanup};
+     my $sendmail_dir = tempdir(CLEANUP => $param{cleanup});
+     my $spool_dir = tempdir(CLEANUP => $param{cleanup});
+     my $config_dir = tempdir(CLEANUP => $param{cleanup});
+
+
+     $ENV{DEBBUGS_CONFIG_FILE}  ="$config_dir/debbugs_config";
+     $ENV{PERL5LIB} = getcwd();
+     $ENV{SENDMAIL_TESTDIR} = $sendmail_dir;
+     my $sendmail_tester = getcwd().'/t/sendmail_tester';
+     unless (-x $sendmail_tester) {
+         die q(t/sendmail_tester doesn't exist or isn't executable. You may be in the wrong directory.);
+     }
+     my %files_to_create = ("$config_dir/debbugs_config" => <<END,
+\$gSendmail='$sendmail_tester';
+\$gSpoolDir='$spool_dir';
+\$gLibPath='@{[getcwd()]}/scripts';
+1;
+END
+                           "$spool_dir/nextnumber" => qq(1\n),
+                           "$config_dir/Maintainers" => qq(foo Blah Bleargh <bar\@baz.com>\n),
+                           "$config_dir/Maintainers.override" => qq(),
+                           "$config_dir/indices/sources" => <<END,
+foo main foo
+END
+                           "$config_dir/pseudo-packages.description" => '',
+                          );
+     while (my ($file,$contents) = each %files_to_create) {
+         system('mkdir','-p',dirname($file));
+         my $fh = IO::File->new($file,'w') or
+              die "Unable to create $file: $!";
+         print {$fh} $contents or die "Unable to write $contents to $file: $!";
+         close $fh or die "Unable to close $file: $!";
+     }
+
+     system('touch',"$spool_dir/index.db.realtime");
+     system('ln','-s','index.db.realtime',
+           "$spool_dir/index.db");
+     system('touch',"$spool_dir/index.archive.realtime");
+     system('ln','-s','index.archive.realtime',
+           "$spool_dir/index.archive");
+
+     # create the spool files and sub directories
+     map {system('mkdir','-p',"$spool_dir/$_"); }
+         map {('db-h/'.$_,'archive/'.$_)}
+              map { sprintf "%02d",$_ % 100} 0..99;
+     system('mkdir','-p',"$spool_dir/incoming");
+     system('mkdir','-p',"$spool_dir/lock");
+
+     return (spool_dir => $spool_dir,
+            sendmail_dir => $sendmail_dir,
+            config_dir => $config_dir,
+           );
+}
+
+sub dirsize{
+     my ($dir) = @_;
+     opendir(DIR,$dir);
+     my @content = grep {!/^\.\.?$/} readdir(DIR);
+     closedir(DIR);
+     return scalar @content;
+}
+
+
+# We're going to use create mime message to create these messages, and
+# then just send them to receive.
+# First, check that submit@ works
+
+sub send_message{
+     my %param = validate_with(params => \@_,
+                              spec   => {to => {type => SCALAR,
+                                                default => 'submit@bugs.something',
+                                               },
+                                         headers => {type => ARRAYREF,
+                                                    },
+                                         body    => {type => SCALAR,
+                                                    },
+                                         run_processall =>{type => BOOLEAN,
+                                                           default => 1,
+                                                          },
+                                        }
+                             );
+     $ENV{LOCAL_PART} = $param{to};
+     my ($rfd,$wfd);
+     my $output='';
+     local $SIG{PIPE} = 'IGNORE';
+     local $SIG{CHLD} = sub {};
+     my $pid = open3($wfd,$rfd,$rfd,'scripts/receive.in')
+         or die "Unable to start receive.in: $!";
+     print {$wfd} create_mime_message($param{headers},
+                                        $param{body}) or die "Unable to to print to receive.in";
+     close($wfd) or die "Unable to close receive.in";
+     my $err = $? >> 8;
+     my $childpid = waitpid($pid,0);
+     if ($childpid != -1) {
+         $err = $? >> 8;
+         print STDERR "receive.in pid: $pid doesn't match childpid: $childpid\n" if $childpid != $pid;
+     }
+     if ($err != 0 ) {
+         my $rfh =  IO::Handle->new_from_fd($rfd,'r') or die "Unable to create filehandle: $!";
+         $rfh->blocking(0);
+         my $rv;
+         while ($rv = $rfh->sysread($output,1000,length($output))) {}
+         if (not defined $rv) {
+              print STDERR "Reading from STDOUT/STDERR would have blocked.";
+         }
+         print STDERR $output,qq(\n);
+         die "receive.in failed with exit status $err";
+     }
+     # now we should run processall to see if the message gets processed
+     if ($param{run_processall}) {
+         system('scripts/processall.in') == 0 or die "processall.in failed";
+     }
+}
+
+{
+     package DebbugsTest::HTTPServer;
+     use base qw(HTTP::Server::Simple::CGI);
+
+     our $child_pid = undef;
+     our $webserver = undef;
+     our $server_handler = undef;
+
+     END {
+         if (defined $child_pid) {
+              # stop the child
+              kill(15,$child_pid);
+              waitpid(-1,0);
+         }
+     }
+
+     sub fork_and_create_webserver {
+         my ($handler,$port) = @_;
+         $port ||= 8080;
+         if (defined $child_pid) {
+              die "We appear to have already forked once";
+         }
+         $server_handler = $handler;
+         my $pid = fork;
+         return 0 if not defined $pid;
+         if ($pid) {
+              $child_pid = $pid;
+              # Wait here for a second to let the child start up
+              sleep 1;
+              return $pid;
+         }
+         else {
+              $webserver = DebbugsTest::HTTPServer->new($port);
+              $webserver->run;
+         }
+
+     }
+
+     sub handle_request {
+         if (defined $server_handler) {
+              $server_handler->(@_);
+         }
+         else {
+              warn "No handler defined\n";
+              print "No handler defined\n";
+         }
+     }
+}
+
+
+1;
+
+__END__
+
+
+
diff --git a/t/sendmail_tester b/t/sendmail_tester
new file mode 100755 (executable)
index 0000000..ace1463
--- /dev/null
@@ -0,0 +1,21 @@
+#!/usr/bin/perl
+
+# All this script does is write whatever is piped to it to a unique
+# filename, with the first line containing the arguments sent.
+
+use IO::File;
+
+# create a unique filename
+if (not -d $ENV{SENDMAIL_TESTDIR}) {
+     system('mkdir','-p',$ENV{SENDMAIL_TESTDIR});
+}
+
+my $fn = "$ENV{SENDMAIL_TESTDIR}/".time.$$;
+
+my $fh = IO::File->new($fn ,'w') or die "Unable to open file $fn for writing: $!";
+
+print {$fh} "$0 called with: ", join(' ',map {"'$_'"} @ARGV) or die "Unable to write to file $fn: $!";
+print {$fh} "\n\n";
+print {$fh} <STDIN> or die "Unable to write to file $fn: $!";
+
+close $fh or die "Unable to close file $fn: $!";