summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CGI.pl3
-rwxr-xr-xbuglist.cgi1466
-rwxr-xr-xchecksetup.pl16
-rw-r--r--css/buglist.css38
-rw-r--r--globals.pl120
-rw-r--r--skins/standard/buglist.css38
-rw-r--r--template/default/buglist/buglist-rdf.rdf.tmpl52
-rw-r--r--template/default/buglist/buglist-simple.html.tmpl44
-rw-r--r--template/default/buglist/buglist.html.tmpl160
-rw-r--r--template/default/buglist/change-form.tmpl339
-rw-r--r--template/default/buglist/server-push.html.tmpl35
-rw-r--r--template/default/buglist/table.tmpl142
-rwxr-xr-xtemplate/default/global/header9
-rw-r--r--template/default/global/message.html.tmpl4
14 files changed, 1601 insertions, 865 deletions
diff --git a/CGI.pl b/CGI.pl
index 0882a967c..76c53627d 100644
--- a/CGI.pl
+++ b/CGI.pl
@@ -1201,7 +1201,8 @@ sub PutFooter {
sub DisplayError {
my ($message, $title) = (@_);
$title ||= "Error";
-
+ $message ||= "An unknown error occurred.";
+
print "Content-type: text/html\n\n";
PutHeader($title);
diff --git a/buglist.cgi b/buglist.cgi
index ce67f648e..9238212a7 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -22,77 +22,165 @@
# Dan Mosedale <dmose@mozilla.org>
# Stephan Niemz <st.n@gmx.net>
# Andreas Franke <afranke@mathweb.org>
+# Myk Melez <myk@mozilla.org>
+################################################################################
+# Script Initialization
+################################################################################
+
+# Make it harder for us to do dangerous things in Perl.
use diagnostics;
use strict;
use lib qw(.);
+use vars qw( $template $vars );
+
+# Include the Bugzilla CGI and general utility library.
require "CGI.pl";
-use Date::Parse;
# Shut up misguided -w warnings about "used only once". "use vars" just
# doesn't work for me.
-
sub sillyness {
my $zz;
$zz = $::db_name;
- $zz = $::defaultqueryname;
- $zz = $::unconfirmedstate;
- $zz = $::userid;
$zz = @::components;
$zz = @::default_column_list;
+ $zz = $::defaultqueryname;
+ $zz = @::dontchange;
$zz = @::legal_keywords;
$zz = @::legal_platform;
$zz = @::legal_priority;
$zz = @::legal_product;
- $zz = @::settable_resolution;
$zz = @::legal_severity;
- $zz = @::versions;
+ $zz = @::settable_resolution;
$zz = @::target_milestone;
- $zz = %::proddesc;
+ $zz = $::unconfirmedstate;
+ $zz = $::userid;
+ $zz = @::versions;
};
-my $serverpush = 0;
-
ConnectToDatabase();
-#print "Content-type: text/plain\n\n"; # Handy for debugging.
-#$::FORM{'debug'} = 1;
+################################################################################
+# Data and Security Validation
+################################################################################
+# Determine the format in which the user would like to receive the output.
+# Uses the default format if the user did not specify an output format;
+# otherwise validates the user's choice against the list of available formats.
+my $format = ValidateOutputFormat($::FORM{'format'});
-if (grep(/^cmd-/, keys(%::FORM))) {
- my $url = "query.cgi?$::buffer#chart";
- print qq{Refresh: 0; URL=$url
-Content-type: text/html
+# Whether or not the user wants to change multiple bugs.
+my $dotweak = $::FORM{'tweak'} ? 1 : 0;
-Adding field to query page...
-<P>
-<A HREF="$url">Click here if page doesn't redisplay automatically.</A>
-};
- exit();
+# Use server push to display a "Please wait..." message for the user while
+# executing their query if their browser supports it and they are viewing
+# the bug list as HTML and they have not disabled it by adding &serverpush=0
+# to the URL.
+#
+# Server push is a Netscape 3+ hack incompatible with MSIE, Lynx, and others.
+# Even Communicator 4.51 has bugs with it, especially during page reload.
+# http://www.browsercaps.org used as source of compatible browsers.
+#
+my $serverpush =
+ exists $ENV{'HTTP_USER_AGENT'}
+ && $ENV{'HTTP_USER_AGENT'} =~ /Mozilla.[3-9]/
+ && $ENV{'HTTP_USER_AGENT'} !~ /[Cc]ompatible/
+ && $format->{'extension'} eq "html"
+ && !defined($::FORM{'serverpush'})
+ || $::FORM{'serverpush'};
+
+my $order = $::FORM{'order'} || "";
+
+# If the user is retrieving the last bug list they looked at, hack the buffer
+# storing the query string so that it looks like a query retrieving those bugs.
+if ($::FORM{'regetlastlist'}) {
+ if (!$::COOKIE{'BUGLIST'}) {
+ DisplayError(qq|Sorry, I seem to have lost the cookie that recorded
+ the results of your last query. You will have to start
+ over at the <a href="query.cgi">query page</a>.|);
+ exit;
+ }
+ $::FORM{'bug_id'} = join(",", split(/:/, $::COOKIE{'BUGLIST'}));
+ $order = "reuse last sort" unless $order;
+ $::buffer = "bug_id=$::FORM{'bug_id'}&order=" . url_quote($order);
}
+if ($::buffer =~ /&cmd-/) {
+ my $url = "query.cgi?$::buffer#chart";
+ print "Refresh: 0; URL=$url\n";
+ print "Content-Type: text/html\n\n";
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $vars->{'title'} = "Adding field to query page...";
+ $vars->{'url'} = $url;
+ $vars->{'link'} = "Click here if the page does not redisplay automatically.";
+ $template->process("global/message.html.tmpl", $vars)
+ || DisplayError("Template process failed: " . $template->error());
+ exit;
+}
+# Generate a reasonable filename for the user agent to suggest to the user
+# when the user saves the bug list. Uses the name of the remembered query
+# if available. We have to do this now, even though we return HTTP headers
+# at the end, because the fact that there is a remembered query gets
+# forgotten in the process of retrieving it.
+my @time = localtime(time());
+my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3];
+my $filename = "bugs-$date.$format->{extension}";
+$::FORM{'cmdtype'} ||= "";
+if ($::FORM{'cmdtype'} eq 'runnamed') {
+ $filename = "$::FORM{'namedcmd'}-$date.$format->{extension}";
+ # Remove white-space from the filename so the user cannot tamper
+ # with the HTTP headers.
+ $filename =~ s/\s//;
+}
-if (!defined $::FORM{'cmdtype'}) {
- # This can happen if there's an old bookmark to a query...
- $::FORM{'cmdtype'} = 'doit';
+if ($dotweak) {
+ confirm_login();
+ if (!UserInGroup("editbugs")) {
+ DisplayError("Sorry, you do not have sufficient privileges to edit
+ multiple bugs.");
+ exit;
+ }
+ GetVersionTable();
+}
+else {
+ quietly_check_login();
}
+################################################################################
+# Utilities
+################################################################################
+
sub SqlifyDate {
- my ($str) = (@_);
- if (!defined $str) {
- $str = "";
- }
+ my ($str) = @_;
+ $str = "" if !defined $str;
my $date = str2time($str);
- if (!defined $date) {
- PuntTryAgain("The string '<tt>".html_quote($str)."</tt>' is not a legal date.");
+ if (!defined($date)) {
+ my $htmlstr = html_quote($str);
+ DisplayError("The string <tt>$htmlstr</tt> is not a legal date.");
+ exit;
}
- return time2str("%Y/%m/%d %H:%M:%S", $date);
+ return time2str("%Y-%m-%d %H:%M:%S", $date);
}
+my @weekday= qw( Sun Mon Tue Wed Thu Fri Sat );
+sub DiffDate {
+ my ($datestr) = @_;
+ my $date = str2time($datestr);
+ my $age = time() - $date;
+ my ($s,$m,$h,$d,$mo,$y,$wd)= localtime $date;
+ if( $age < 18*60*60 ) {
+ $date = sprintf "%02d:%02d:%02d", $h,$m,$s;
+ } elsif( $age < 6*24*60*60 ) {
+ $date = sprintf "%s %02d:%02d", $weekday[$wd],$h,$m;
+ } else {
+ $date = sprintf "%04d-%02d-%02d", 1900+$y,$mo+1,$d;
+ }
+ return $date;
+}
sub GetByWordList {
my ($field, $strs) = (@_);
@@ -129,28 +217,75 @@ sub GetByWordListSubstr {
return \@list;
}
+sub LookupNamedQuery {
+ my ($name) = @_;
+ confirm_login();
+ my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"});
+ my $qname = SqlQuote($name);
+ SendSQL("SELECT query FROM namedqueries WHERE userid = $userid AND name = $qname");
+ my $result = FetchOneColumn();
+ if (!$result) {
+ my $qname = html_quote($name);
+ DisplayError("The query named <em>$qname</em> seems to no longer exist.");
+ exit;
+ }
+ return $result;
+}
+
+sub GetQuip {
+ return if !Param('usequip');
-sub Error {
- my ($str) = (@_);
- if (!$serverpush) {
- print "Content-type: text/html\n\n";
+ my $quip;
+
+ # This is stupid. We really really need to move the quip list into the DB!
+ if (open(COMMENTS, "<data/comments")) {
+ my @cdata;
+ push(@cdata, $_) while <COMMENTS>;
+ close COMMENTS;
+ $quip = $cdata[int(rand($#cdata + 1))];
}
- PuntTryAgain($str);
+ $quip ||= "Bugzilla would like to put a random quip here, but nobody has entered any.";
+
+ return $quip;
}
+sub GetGroupsByGroupSet {
+ my ($groupset) = @_;
+
+ return if !$groupset;
+
+ SendSQL("
+ SELECT bit, name, description, isactive
+ FROM groups
+ WHERE (bit & $groupset) != 0
+ AND isbuggroup != 0
+ ORDER BY description ");
+
+ my @groups;
+
+ while (MoreSQLData()) {
+ my $group = {};
+ ($group->{'bit'}, $group->{'name'},
+ $group->{'description'}, $group->{'isactive'}) = FetchSQLData();
+ push(@groups, $group);
+ }
+
+ return \@groups;
+}
+################################################################################
+# Query Generation
+################################################################################
sub GenerateSQL {
my $debug = 0;
- my ($fieldsref, $supptablesref, $wherepartref, $urlstr) = (@_);
+ my ($fieldsref, $urlstr) = (@_);
my @fields;
my @supptables;
my @wherepart;
@fields = @$fieldsref if $fieldsref;
- @supptables = @$supptablesref if $supptablesref;
- @wherepart = @$wherepartref if $wherepartref;
my %F;
my %M;
ParseUrlString($urlstr, \%F, \%M);
@@ -172,10 +307,11 @@ sub GenerateSQL {
my $c = trim($F{'votes'});
if ($c ne "") {
if ($c !~ /^[0-9]*$/) {
- return Error("The 'At least ___ votes' field must be a\n" .
- "simple number. You entered \"" .
- html_quote($c) . "\", which\n" .
- "doesn't cut it.");
+ my $htmlc = html_quote($c);
+ DisplayError("The <em>At least ___ votes</em> field must be
+ a simple number. You entered <kbd>$htmlc</kbd>,
+ which doesn't cut it.");
+ exit;
}
push(@specialchart, ["votes", "greaterthan", $c - 1]);
}
@@ -255,9 +391,10 @@ sub GenerateSQL {
if (@clist) {
push(@specialchart, \@clist);
} else {
- return Error("You must specify one or more fields in which to\n" .
- "search for <tt>" .
- html_quote($email) . "</tt>.\n");
+ my $htmlemail = html_quote($email);
+ DisplayError("You must specify one or more fields in which
+ to search for <tt>$htmlemail</tt>.");
+ exit;
}
}
@@ -266,10 +403,11 @@ sub GenerateSQL {
my $c = trim($F{'changedin'});
if ($c ne "") {
if ($c !~ /^[0-9]*$/) {
- return Error("The 'changed in last ___ days' field must be\n" .
- "a simple number. You entered \"" .
- html_quote($c) . "\", which\n" .
- "doesn't cut it.");
+ my $htmlc = html_quote($c);
+ DisplayError("The <em>changed in last ___ days</em> field
+ must be a simple number. You entered
+ <kbd>$htmlc</kbd>, which doesn't cut it.");
+ exit;
}
push(@specialchart, ["changedin",
"lessthan", $c + 1]);
@@ -417,17 +555,17 @@ sub GenerateSQL {
$t = "greaterthan";
}
if ($field eq "ispatch" && $v ne "0" && $v ne "1") {
- return Error("The only legal values for the 'Attachment is patch' " .
- "field are 0 and 1.");
+ DisplayError("The only legal values for the <em>Attachment is
+ patch</em> field are 0 and 1.");
+ exit;
}
if ($field eq "isobsolete" && $v ne "0" && $v ne "1") {
- return Error("The only legal values for the 'Attachment is obsolete' " .
- "field are 0 and 1.");
+ DisplayError("The only legal values for the <em>Attachment is
+ obsolete</em> field are 0 and 1.");
+ exit;
}
$f = "$table.$field";
},
- # 2001-05-16 myk@mozilla.org: enable querying against attachment status
- # if this installation has enabled use of the attachment tracker.
"^attachstatusdefs.name," => sub {
# When searching for multiple statuses within a single boolean chart,
# we want to match each status record separately. In other words,
@@ -473,12 +611,13 @@ sub GenerateSQL {
my $id = GetKeywordIdFromName($value);
if ($id) {
push(@list, "$table.keywordid = $id");
- } else {
- return Error("Unknown keyword named <code>" .
- html_quote($v) . "</code>.\n" .
- "<P>The legal keyword names are\n" .
- "<A HREF=describekeywords.cgi>" .
- "listed here</A>.\n");
+ }
+ else {
+ my $htmlv = html_quote($v);
+ DisplayError(qq|There is no keyword named <code>$htmlv</code>.
+ To search for keywords, consult the
+ <a href="describekeywords.cgi">list of legal keywords</a>.|);
+ exit;
}
}
my $haveawordterm;
@@ -667,16 +806,16 @@ sub GenerateSQL {
}
-# A boolean chart is a way of representing the terms in a logical
+# A boolean chart is a way of representing the terms in a logical
# expression. Bugzilla builds SQL queries depending on how you enter
-# terms into the boolean chart. Boolean charts are represented in
-# urls as tree-tuples of (chart id, row, column). The query form
+# terms into the boolean chart. Boolean charts are represented in
+# urls as tree-tuples of (chart id, row, column). The query form
# (query.cgi) may contain an arbitrary number of boolean charts where
-# each chart represents a clause in a SQL query.
+# each chart represents a clause in a SQL query.
#
# The query form starts out with one boolean chart containing one
-# row and one column. Extra rows can be created by pressing the
-# AND button at the bottom of the chart. Extra columns are created
+# row and one column. Extra rows can be created by pressing the
+# AND button at the bottom of the chart. Extra columns are created
# by pressing the OR button at the right end of the chart. Extra
# charts are created by pressing "Add another boolean chart".
#
@@ -705,11 +844,11 @@ sub GenerateSQL {
# SELECT blah FROM blah WHERE ( (a1 OR a2)AND(b1 OR b2 OR b3)AND(c1)) AND (d1)
#
# The terms within a single row of a boolean chart are all constraints
-# on a single piece of data. If you're looking for a bug that has two
-# different people cc'd on it, then you need to use two boolean charts.
-# This will find bugs with one CC mathing 'foo@blah.org' and and another
-# CC matching 'bar@blah.org'.
-#
+# on a single piece of data. If you're looking for a bug that has two
+# different people cc'd on it, then you need to use two boolean charts.
+# This will find bugs with one CC mathing 'foo@blah.org' and and another
+# CC matching 'bar@blah.org'.
+#
# --------------------------------------------------------------
# CC | equal to
# foo@blah.org
@@ -717,7 +856,7 @@ sub GenerateSQL {
# CC | equal to
# bar@blah.org
#
-# If you try to do this query by pressing the AND button in the
+# If you try to do this query by pressing the AND button in the
# original boolean chart then what you'll get is an expression that
# looks for a single CC where the login name is both "foo@blah.org",
# and "bar@blah.org". This is impossible.
@@ -740,7 +879,7 @@ sub GenerateSQL {
# $ff = qualified field name (field name prefixed by table)
# e.g. bugs_activity.bug_id
# $t = type of query. e.g. "equal to", "changed after", case sensitive substr"
-# $v = value - value the user typed in to the form
+# $v = value - value the user typed in to the form
# $q = sanitized version of user input (SqlQuote($v))
# @supptables = Tables and/or table aliases used in query
# %suppseen = A hash used to store all the tables in supptables to weed
@@ -814,13 +953,13 @@ sub GenerateSQL {
}
if ($term) {
push(@orlist, $term);
- } else {
- my $errstr = "Can't seem to handle " .
- qq{'<code>$F{"field$chart-$row-$col"}</code>' and } .
- qq{'<code>$F{"type$chart-$row-$col"}</code>' } .
- "together";
- die "Internal error: $errstr" if $chart < 0;
- return Error($errstr);
+ }
+ else {
+ my $errstr =
+ qq|Cannot seem to handle <code>$F{"field$chart-$row-$col"}</code>
+ and <code>$F{"type$chart-$row-$col"}</code> together|;
+ $chart < 0 ? die "Internal error: $errstr"
+ : DisplayError($errstr) && exit;
}
}
if (@orlist) {
@@ -839,931 +978,538 @@ sub GenerateSQL {
$suppseen{$str} = 1;
}
}
-
- my $query = ("SELECT " . join(', ', @fields) .
+ my $query = ("SELECT DISTINCT " . join(', ', @fields) .
" FROM $suppstring" .
- " WHERE " . join(' AND ', (@wherepart, @andlist)) .
- " GROUP BY bugs.bug_id");
+ " WHERE " . join(' AND ', (@wherepart, @andlist)));
$query = SelectVisible($query, $::userid, $::usergroupset);
if ($debug) {
print "<P><CODE>" . value_quote($query) . "</CODE><P>\n";
- exit();
- }
- return $query;
-}
-
-
-
-sub LookupNamedQuery {
- my ($name) = (@_);
- confirm_login();
- my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"});
- SendSQL("SELECT query FROM namedqueries " .
- "WHERE userid = $userid AND name = " . SqlQuote($name));
- my $result = FetchOneColumn();
- if (!defined $result) {
- print "Content-type: text/html\n\n";
- PutHeader("Something weird happened");
- print qq{The named query $name seems to no longer exist.};
- PutFooter();
exit;
}
- return $result;
+ return $query;
}
-$::querytitle = "Bug List";
+################################################################################
+# Command Execution
+################################################################################
+# Figure out if the user wanted to do anything besides just running the query
+# they defined on the query page, and take appropriate action.
CMD: for ($::FORM{'cmdtype'}) {
/^runnamed$/ && do {
$::buffer = LookupNamedQuery($::FORM{"namedcmd"});
- $::querytitle = "Bug List: $::FORM{'namedcmd'}";
+ $vars->{'title'} = "Bug List: $::FORM{'namedcmd'}";
ProcessFormFields($::buffer);
last CMD;
};
+
/^editnamed$/ && do {
my $url = "query.cgi?" . LookupNamedQuery($::FORM{"namedcmd"});
- print qq{Content-type: text/html
-Refresh: 0; URL=$url
-
-<TITLE>What a hack.</TITLE>
-<A HREF="$url">Loading your query named <B>$::FORM{'namedcmd'}</B>...</A>
-};
+ print "Refresh: 0; URL=$url\n";
+ print "Content-Type: text/html\n\n";
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $vars->{'title'} = "Loading your query named $::FORM{'namedcmd'}";
+ $vars->{'url'} = $url;
+ $vars->{'link'} = "Click here if the page does not redisplay automatically.";
+ $template->process("global/message.html.tmpl", $vars)
+ || DisplayError("Template process failed: " . $template->error());
exit;
};
+
/^forgetnamed$/ && do {
confirm_login();
my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"});
- SendSQL("DELETE FROM namedqueries WHERE userid = $userid " .
- "AND name = " . SqlQuote($::FORM{'namedcmd'}));
-
- print "Content-type: text/html\n\n";
- PutHeader("Query is gone", "");
-
- print qq{
-OK, the <B>$::FORM{'namedcmd'}</B> query is gone.
-<P>
-<A HREF="query.cgi">Go back to the query page.</A>
-};
- PutFooter();
+ my $qname = SqlQuote($::FORM{'namedcmd'});
+ SendSQL("DELETE FROM namedqueries WHERE userid = $userid AND name = $qname");
+ print "Content-Type: text/html\n\n";
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $vars->{'title'} = "Query is gone";
+ $vars->{'message'} = "OK, the <b>$::FORM{'namedcmd'}</b> query is gone.";
+ $vars->{'url'} = "query.cgi";
+ $vars->{'link'} = "Go back to the query page.";
+ $template->process("global/message.html.tmpl", $vars)
+ || DisplayError("Template process failed: " . $template->error());
exit;
};
+
/^asdefault$/ && do {
confirm_login();
my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"});
- print "Content-type: text/html\n\n";
- SendSQL("REPLACE INTO namedqueries (userid, name, query) VALUES " .
- "($userid, '$::defaultqueryname'," .
- SqlQuote($::buffer) . ")");
- PutHeader("OK, default is set");
- print qq{
-OK, you now have a new default query. You may also bookmark the result of any
-individual query.
-
-<P><A HREF="query.cgi">Go back to the query page, using the new default.</A>
-};
- PutFooter();
- exit();
+ my $qname = SqlQuote($::defaultqueryname);
+ my $qbuffer = SqlQuote($::buffer);
+ SendSQL("REPLACE INTO namedqueries (userid, name, query)
+ VALUES ($userid, $qname, $qbuffer)");
+ print "Content-Type: text/html\n\n";
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $vars->{'title'} = "OK, default is set";
+ $vars->{'message'} = "OK, you now have a new default query. You may
+ also bookmark the result of any individual query.";
+ $vars->{'url'} = "query.cgi";
+ $vars->{'link'} = "Go back to the query page, using the new default.";
+ $template->process("global/message.html.tmpl", $vars)
+ || DisplayError("Template process failed: " . $template->error());
+ exit;
};
+
/^asnamed$/ && do {
confirm_login();
my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"});
- print "Content-type: text/html\n\n";
+
my $name = trim($::FORM{'newqueryname'});
- if ($name eq "" || $name =~ /[<>&]/) {
- PutHeader("Please pick a valid name for your new query");
- print "Click the <B>Back</B> button and type in a valid name\n";
- print "for this query. (Query names should not contain unusual\n";
- print "characters.)\n";
- PutFooter();
- exit();
- }
- $::buffer =~ s/[\&\?]cmdtype=[a-z]+//;
+ $name
+ || DisplayError("You must enter a name for your query.")
+ && exit;
+ $name =~ /[<>&]/
+ && DisplayError("The name of your query cannot contain any
+ of the following characters: &lt;, &gt;, &amp;.")
+ && exit;
my $qname = SqlQuote($name);
- my $tofooter= ( $::FORM{'tofooter'} ? 1 : 0 );
- SendSQL("SELECT query FROM namedqueries " .
- "WHERE userid = $userid AND name = $qname");
- if (!FetchOneColumn()) {
- SendSQL("REPLACE INTO namedqueries (userid, name, query, linkinfooter) " .
- "VALUES ($userid, $qname, ". SqlQuote($::buffer) .", ". $tofooter .")");
- } else {
- SendSQL("UPDATE namedqueries SET query = " . SqlQuote($::buffer) . "," .
- " linkinfooter = " . $tofooter .
- " WHERE userid = $userid AND name = $qname");
+
+ $::buffer =~ s/[\&\?]cmdtype=[a-z]+//;
+ my $qbuffer = SqlQuote($::buffer);
+
+ my $tofooter= $::FORM{'tofooter'} ? 1 : 0;
+
+ SendSQL("SELECT query FROM namedqueries WHERE userid = $userid AND name = $qname");
+ if (FetchOneColumn()) {
+ SendSQL("UPDATE namedqueries
+ SET query = $qbuffer , linkinfooter = $tofooter
+ WHERE userid = $userid AND name = $qname");
}
- PutHeader("OK, query saved.");
- print qq{
-OK, you have a new query named <code>$name</code>
-<P>
-<BR><A HREF="query.cgi">Go back to the query page</A>
-};
- PutFooter();
+ else {
+ SendSQL("REPLACE INTO namedqueries (userid, name, query, linkinfooter)
+ VALUES ($userid, $qname, $qbuffer, $tofooter)");
+ }
+ print "Content-Type: text/html\n\n";
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $vars->{'title'} = "OK, query saved.";
+ $vars->{'message'} = "OK, you have a new query named <code>$name</code>";
+ $vars->{'url'} = "query.cgi";
+ $vars->{'link'} = "Go back to the query page.";
+ $template->process("global/message.html.tmpl", $vars)
+ || DisplayError("Template process failed: " . $template->error());
exit;
};
}
-if (exists $ENV{'HTTP_USER_AGENT'} && $ENV{'HTTP_USER_AGENT'} =~ /Mozilla.[3-9]/ && $ENV{'HTTP_USER_AGENT'} !~ /[Cc]ompatible/ ) {
- # Search for real Netscape 3 and up. http://www.browsercaps.org used as source of
- # browsers compatbile with server-push. It's a Netscape hack, incompatbile
- # with MSIE and Lynx (at least). Even Communicator 4.51 has bugs with it,
- # especially during page reload.
- $serverpush = 1;
-
- print qq{Content-type: multipart/x-mixed-replace;boundary=thisrandomstring\n
---thisrandomstring
-Content-type: text/html\n
-<html><head><title>Bugzilla is pondering your query</title>
-<style type="text/css">
- .psb { margin-top: 20%; text-align: center; }
-</style></head><body>
-<h1 class="psb">Please stand by ...</h1></body></html>
- };
- # Note! HTML header is complete!
-} else {
- print "Content-type: text/html\n";
- #Changing attachment to inline to resolve 46897
- #zach@zachlipton.com
- print "Content-disposition: inline; filename=bugzilla_bug_list.html\n";
- # Note! Don't finish HTML header yet! Only one newline so far!
+################################################################################
+# Column Definition
+################################################################################
+
+# Define the columns that can be selected in a query and/or displayed in a bug
+# list. Column records include the following fields:
+#
+# 1. ID: a unique identifier by which the column is referred in code;
+#
+# 2. Name: The name of the column in the database (may also be an expression
+# that returns the value of the column);
+#
+# 3. Title: The title of the column as displayed to users.
+#
+# Note: There are a few hacks in the code that deviate from these definitions.
+# In particular, when the list is sorted by the "votes" field the word
+# "DESC" is added to the end of the field to sort in descending order,
+# and the redundant summaryfull column is removed when the client
+# requests "all" columns.
+
+my $columns = {};
+sub DefineColumn {
+ my ($id, $name, $title) = @_;
+ $columns->{$id} = { 'name' => $name , 'title' => $title };
}
-sub DefCol {
- my ($name, $k, $t, $s, $q) = (@_);
- $::key{$name} = $k;
- $::title{$name} = $t;
- if (defined $s && $s ne "") {
- $::sortkey{$name} = $s;
+# Column: ID Name Title
+DefineColumn("id" , "bugs.bug_id" , "ID" );
+DefineColumn("groupset" , "bugs.groupset" , "Groupset" );
+DefineColumn("opendate" , "bugs.creation_ts" , "Opened" );
+DefineColumn("changeddate" , "bugs.delta_ts" , "Changed" );
+DefineColumn("severity" , "bugs.bug_severity" , "Severity" );
+DefineColumn("priority" , "bugs.priority" , "Priority" );
+DefineColumn("platform" , "bugs.rep_platform" , "Platform" );
+DefineColumn("owner" , "map_assigned_to.login_name" , "Owner" );
+DefineColumn("reporter" , "map_reporter.login_name" , "Reporter" );
+DefineColumn("qa_contact" , "map_qa_contact.login_name" , "QA Contact" );
+DefineColumn("status" , "bugs.bug_status" , "State" );
+DefineColumn("resolution" , "bugs.resolution" , "Result" );
+DefineColumn("summary" , "bugs.short_desc" , "Summary" );
+DefineColumn("summaryfull" , "bugs.short_desc" , "Summary" );
+DefineColumn("status_whiteboard" , "bugs.status_whiteboard" , "Status Summary" );
+DefineColumn("component" , "bugs.component" , "Component" );
+DefineColumn("product" , "bugs.product" , "Product" );
+DefineColumn("version" , "bugs.version" , "Version" );
+DefineColumn("os" , "bugs.op_sys" , "OS" );
+DefineColumn("target_milestone" , "bugs.target_milestone" , "Target Milestone" );
+DefineColumn("votes" , "bugs.votes" , "Votes" );
+DefineColumn("keywords" , "bugs.keywords" , "Keywords" );
+
+
+################################################################################
+# Display Column Determination
+################################################################################
+
+# Determine the columns that will be displayed in the bug list via the
+# columnlist CGI parameter, the user's preferences, or the default.
+my @displaycolumns = ();
+if (defined $::FORM{'columnlist'}) {
+ if ($::FORM{'columnlist'} eq "all") {
+ # If the value of the CGI parameter is "all", display all columns,
+ # but remove the redundant "summaryfull" column.
+ @displaycolumns = grep($_ ne 'summaryfull', keys(%$columns));
}
- if (!defined $q || $q eq "") {
- $q = 0;
+ else {
+ @displaycolumns = split(/[ ,]+/, $::FORM{'columnlist'});
}
- $::needquote{$name} = $q;
}
-
-DefCol("opendate", "unix_timestamp(bugs.creation_ts)", "Opened",
- "bugs.creation_ts");
-DefCol("changeddate", "unix_timestamp(bugs.delta_ts)", "Changed",
- "bugs.delta_ts");
-DefCol("severity", "substring(bugs.bug_severity, 1, 3)", "Sev",
- "bugs.bug_severity");
-DefCol("priority", "substring(bugs.priority, 1, 3)", "Pri", "bugs.priority");
-DefCol("platform", "substring(bugs.rep_platform, 1, 3)", "Plt",
- "bugs.rep_platform");
-DefCol("owner", "map_assigned_to.login_name", "Owner",
- "map_assigned_to.login_name");
-DefCol("reporter", "map_reporter.login_name", "Reporter",
- "map_reporter.login_name");
-DefCol("qa_contact", "map_qa_contact.login_name", "QAContact", "map_qa_contact.login_name");
-DefCol("status", "substring(bugs.bug_status,1,4)", "State", "bugs.bug_status");
-DefCol("resolution", "substring(bugs.resolution,1,4)", "Result",
- "bugs.resolution");
-DefCol("summary", "substring(bugs.short_desc, 1, 60)", "Summary", "bugs.short_desc", 1);
-DefCol("summaryfull", "bugs.short_desc", "Summary", "bugs.short_desc", 1);
-DefCol("status_whiteboard", "bugs.status_whiteboard", "StatusSummary", "bugs.status_whiteboard", 1);
-DefCol("component", "substring(bugs.component, 1, 8)", "Comp",
- "bugs.component");
-DefCol("product", "substring(bugs.product, 1, 8)", "Product", "bugs.product");
-DefCol("version", "substring(bugs.version, 1, 5)", "Vers", "bugs.version");
-DefCol("os", "substring(bugs.op_sys, 1, 4)", "OS", "bugs.op_sys");
-DefCol("target_milestone", "bugs.target_milestone", "TargetM",
- "bugs.target_milestone");
-DefCol("votes", "bugs.votes", "Votes", "bugs.votes desc");
-DefCol("keywords", "bugs.keywords", "Keywords", "bugs.keywords", 5);
-
-my @collist;
-if (defined $::FORM{'columnlist'}) {
- @collist = split(/[ ,]+/, $::FORM{'columnlist'});
-} elsif (defined $::COOKIE{'COLUMNLIST'}) {
- @collist = split(/ /, $::COOKIE{'COLUMNLIST'});
-} else {
- @collist = @::default_column_list;
+elsif (defined $::COOKIE{'COLUMNLIST'}) {
+ # Use the columns listed in the user's preferences.
+ @displaycolumns = split(/ /, $::COOKIE{'COLUMNLIST'});
}
-
-my $minvotes;
-if (defined $::FORM{'votes'}) {
- if (trim($::FORM{'votes'}) ne "") {
- if (! (grep {/^votes$/} @collist)) {
- push(@collist, 'votes');
- }
- }
+else {
+ # Use the default list of columns.
+ @displaycolumns = @::default_column_list;
}
+# Weed out columns that don't actually exist to prevent the user
+# from hacking their column list cookie to grab data to which they
+# should not have access. Detaint the data along the way.
+@displaycolumns = grep($columns->{$_} && trick_taint($_), @displaycolumns);
-my $dotweak = defined $::FORM{'tweak'};
-
-if ($dotweak) {
- confirm_login();
- if (!UserInGroup("editbugs")) {
- print qq{
-Sorry; you do not have sufficient privileges to edit a bunch of bugs
-at once.
-};
- PutFooter();
- exit();
- }
-} else {
- quietly_check_login();
-}
+# Remove the "ID" column from the list because bug IDs are always displayed
+# and are hard-coded into the display templates.
+@displaycolumns = grep($_ ne 'id', @displaycolumns);
+# IMPORTANT! Never allow the groupset column to be displayed!
+@displaycolumns = grep($_ ne 'groupset', @displaycolumns);
-my @fields = ("bugs.bug_id", "bugs.groupset");
+# Add the votes column to the list of columns to be displayed
+# in the bug list if the user is searching for bugs with a certain
+# number of votes and the votes column is not already on the list.
+push(@displaycolumns, 'votes')
+ if $::FORM{'votes'} && !grep($_ eq 'votes', @displaycolumns);
-foreach my $c (@collist) {
- if (exists $::needquote{$c}) {
- # The value we are actually using is $::key{$c}, which was created
- # using the DefCol() function earlier. We test for the existance
- # of $::needsquote{$c} to find out if $c is a legitimate key in the
- # hashes that were defined by DefCol(). If $::needsquote{$c} exists,
- # then $c is valid and we can use it to look up our key.
- # If it doesn't exist, then we know the user is screwing with us
- # and we'll just skip it.
- trick_taint($c);
- push(@fields, $::key{$c});
- }
-}
+################################################################################
+# Select Column Determination
+################################################################################
-if ($dotweak) {
- push(@fields, "bugs.product", "bugs.bug_status");
-}
+# Generate the list of columns that will be selected in the SQL query.
+# The bug ID and groupset are always selected because bug IDs are always
+# displayed and we need the groupset to determine whether or not the bug
+# is visible to the user.
+my @selectcolumns = ("id", "groupset");
+# Display columns are selected because otherwise we could not display them.
+push (@selectcolumns, @displaycolumns);
-if ($::FORM{'regetlastlist'}) {
- if (!$::COOKIE{'BUGLIST'}) {
- print qq{
-Sorry, I seem to have lost the cookie that recorded the results of your last
-query. You will have to start over at the <A HREF="query.cgi">query page</A>.
-};
- PutFooter();
- exit;
- }
- my @list = split(/:/, $::COOKIE{'BUGLIST'});
- $::FORM{'bug_id'} = join(',', @list);
- if (!$::FORM{'order'}) {
- $::FORM{'order'} = 'reuse last sort';
- }
- $::buffer = "bug_id=" . $::FORM{'bug_id'} . "&order=" .
- url_quote($::FORM{'order'});
+# If the user is editing multiple bugs, we also make sure to select the product
+# and status because the values of those fields determine what options the user
+# has for modifying the bugs.
+if ($dotweak) {
+ push(@selectcolumns, "product") if !grep($_ eq 'product', @selectcolumns);
+ push(@selectcolumns, "status") if !grep($_ eq 'status', @selectcolumns);
}
+################################################################################
+# Query Generation
+################################################################################
-ReconnectToShadowDatabase();
+# Convert the list of columns being selected into a list of column names.
+my @selectnames = map($columns->{$_}->{'name'}, @selectcolumns);
-my $query = GenerateSQL(\@fields, undef, undef, $::buffer);
+# Generate the basic SQL query that will be used to generate the bug list.
+my $query = GenerateSQL(\@selectnames, $::buffer);
-if ($::COOKIE{'LASTORDER'}) {
- if ((!$::FORM{'order'}) || $::FORM{'order'} =~ /^reuse/i) {
- $::FORM{'order'} = url_decode($::COOKIE{'LASTORDER'});
- }
-}
+################################################################################
+# Sort Order Determination
+################################################################################
-if (defined $::FORM{'order'} && $::FORM{'order'} ne "") {
- $query .= " ORDER BY ";
- $::FORM{'order'} =~ s/votesum/bugs.votes/; # Silly backwards compatability
- # hack.
- $::FORM{'order'} =~ s/assign\.login_name/map_assigned_to.login_name/g;
- # Another backwards compatability hack.
+# Add to the query some instructions for sorting the bug list.
+if ($::COOKIE{'LASTORDER'} && !$order || $order =~ /^reuse/i) {
+ $order = url_decode($::COOKIE{'LASTORDER'});
+}
- ORDER: for ($::FORM{'order'}) {
+if ($order) {
+ # Convert the value of the "order" form field into a list of columns
+ # by which to sort the results.
+ ORDER: for ($order) {
/\./ && do {
- # This (hopefully) already has fieldnames in it, so we're done.
+ # A custom list of columns. Make sure each column is valid.
+ foreach my $fragment (split(/[,\s]+/, $order)) {
+ next if $fragment =~ /^asc|desc$/i;
+ my @columnnames = map($columns->{lc($_)}->{'name'}, keys(%$columns));
+ if (!grep($_ eq $fragment, @columnnames)) {
+ my $qfragment = html_quote($fragment);
+ DisplayError("The custom sort order you specified in your
+ form submission or cookie contains an invalid
+ column name <em>$qfragment</em>.");
+ exit;
+ }
+ }
+ # Now that we have checked that all columns in the order are valid,
+ # detaint the order string.
+ trick_taint($order);
last ORDER;
};
/Number/ && do {
- $::FORM{'order'} = "bugs.bug_id";
+ $order = "bugs.bug_id";
last ORDER;
};
/Import/ && do {
- $::FORM{'order'} = "bugs.priority, bugs.bug_severity";
+ $order = "bugs.priority, bugs.bug_severity";
last ORDER;
};
/Assign/ && do {
- $::FORM{'order'} = "map_assigned_to.login_name, bugs.bug_status, priority, bugs.bug_id";
+ $order = "map_assigned_to.login_name, bugs.bug_status, priority, bugs.bug_id";
last ORDER;
};
/Changed/ && do {
- $::FORM{'order'} = "bugs.delta_ts, bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id";
+ $order = "bugs.delta_ts, bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id";
last ORDER;
};
# DEFAULT
- $::FORM{'order'} = "bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id";
+ $order = "bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id";
}
- die "Invalid order: $::FORM{'order'}" unless
- $::FORM{'order'} =~ /^([a-zA-Z0-9_., ]+)$/;
- $::FORM{'order'} = $1; # detaint this, since we've checked it
# Extra special disgusting hack: if we are ordering by target_milestone,
# change it to order by the sortkey of the target_milestone first.
- my $order = $::FORM{'order'};
if ($order =~ /bugs.target_milestone/) {
- $query =~ s/ WHERE / LEFT JOIN milestones ms_order ON ms_order.value = bugs.target_milestone AND ms_order.product = bugs.product WHERE /;
$order =~ s/bugs.target_milestone/ms_order.sortkey,ms_order.value/;
+ $query =~ s/\sWHERE\s/ LEFT JOIN milestones ms_order ON ms_order.value = bugs.target_milestone AND ms_order.product = bugs.product WHERE /;
}
- $query .= $order;
-}
-
+ # If we are sorting by votes, sort in descending order.
+ if ($order =~ /bugs.votes\s+(asc|desc){0}/i) {
+ $order =~ s/bugs.votes/bugs.votes desc/i;
+ }
-if ($::FORM{'debug'} && $serverpush) {
- print "<P><CODE>" . value_quote($query) . "</CODE><P>\n";
+ $query .= " ORDER BY $order ";
}
-if (Param('expectbigqueries')) {
- SendSQL("set option SQL_BIG_TABLES=1");
-}
+################################################################################
+# Query Execution
+################################################################################
-SendSQL($query);
+# Time to use server push to display an interim message to the user until
+# the query completes and we can display the bug list.
+if ($serverpush) {
+ # Generate HTTP headers.
+ print "Content-Disposition: inline; filename=$filename\n";
+ print "Content-Type: multipart/x-mixed-replace;boundary=thisrandomstring\n\n";
+ print "--thisrandomstring\n";
+ print "Content-Type: text/html\n\n";
-my $count = 0;
-$::bugl = "";
-sub pnl {
- my ($str) = (@_);
- $::bugl .= $str;
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("buglist/server-push.html.tmpl", $vars)
+ || DisplayError("Template process failed: " . $template->error())
+ && exit;
}
-my $fields = $::buffer;
-$fields =~ s/[&?]order=[^&]*//g;
-$fields =~ s/[&?]cmdtype=[^&]*//g;
+# Connect to the shadow database if this installation is using one to improve
+# query performance.
+ReconnectToShadowDatabase();
+# Tell MySQL to store temporary tables on the hard drive instead of memory
+# to avoid "table out of space" errors on MySQL versions less than 3.23.2.
+SendSQL("SET OPTION SQL_BIG_TABLES=1") if Param('expectbigqueries');
-my $orderpart;
-my $oldorder;
+# Execute the query.
+SendSQL($query);
-if (defined $::FORM{'order'} && trim($::FORM{'order'}) ne "") {
- $orderpart = "&order=" . url_quote("$::FORM{'order'}");
- $oldorder = url_quote(", $::FORM{'order'}");
-} else {
- $orderpart = "";
- $oldorder = "";
-}
-if ($dotweak) {
- pnl "<FORM NAME=changeform METHOD=POST ACTION=\"process_bug.cgi\">";
-}
+################################################################################
+# Results Retrieval
+################################################################################
+# Retrieve the query results one row at a time and write the data into a list
+# of Perl records.
-my @th;
-foreach my $c (@collist) {
- if (exists $::needquote{$c}) {
- my $h = "<TH>";
- if (defined $::sortkey{$c}) {
- $h .= "<A HREF=\"buglist.cgi?$fields&order=" . url_quote($::sortkey{$c}) . "$oldorder\">$::title{$c}</A>";
- } else {
- $h .= $::title{$c};
- }
- $h .= "</TH>";
- push(@th, $h);
- }
-}
+my $bugowners = {};
+my $bugproducts = {};
+my $bugstatuses = {};
-my $tablestart = "<TABLE CELLSPACING=0 CELLPADDING=4 WIDTH=100%>
-<TR ALIGN=LEFT><TH>
-<A HREF=\"buglist.cgi?$fields&order=bugs.bug_id\">ID</A>";
+my @bugs; # the list of records
-my $splitheader = 0;
-if ($::COOKIE{'SPLITHEADER'}) {
- $splitheader = 1;
-}
+while (my @row = FetchSQLData()) {
+ my $bug = {}; # a record
-if ($splitheader) {
- $tablestart =~ s/<TH/<TH COLSPAN="2"/;
- for (my $pass=0 ; $pass<2 ; $pass++) {
- if ($pass == 1) {
- $tablestart .= "</TR>\n<TR><TD></TD>";
- }
- for (my $i=1-$pass ; $i<@th ; $i += 2) {
- my $h = $th[$i];
- $h =~ s/TH/TH COLSPAN="2" ALIGN="left"/;
- $tablestart .= $h;
- }
+ # Slurp the row of data into the record.
+ foreach my $column (@selectcolumns) {
+ $bug->{$column} = shift @row;
}
-} else {
- $tablestart .= join("", @th);
-}
-
-
-$tablestart .= "\n";
+ # Process certain values further (i.e. date format conversion).
+ if ($bug->{'changeddate'}) {
+ $bug->{'changeddate'} =~
+ s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/;
+ $bug->{'changeddate'} = DiffDate($bug->{'changeddate'});
+ }
+ ($bug->{'opendate'} = DiffDate($bug->{'opendate'})) if $bug->{'opendate'};
-my @row;
-my %seen;
-my @bugarray;
-my %prodhash;
-my %statushash;
-my %ownerhash;
-my %qahash;
+ # Record the owner, product, and status in the big hashes of those things.
+ $bugowners->{$bug->{'owner'}} = 1 if $bug->{'owner'};
+ $bugproducts->{$bug->{'product'}} = 1 if $bug->{'product'};
+ $bugstatuses->{$bug->{'status'}} = 1 if $bug->{'status'};
-my $pricol = -1;
-my $sevcol = -1;
-for (my $colcount = 0 ; $colcount < @collist ; $colcount++) {
- my $colname = $collist[$colcount];
- if ($colname eq "priority") {
- $pricol = $colcount;
- }
- if ($colname eq "severity") {
- $sevcol = $colcount;
- }
+ # Add the record to the list.
+ push(@bugs, $bug);
}
-my @weekday= qw( Sun Mon Tue Wed Thu Fri Sat );
+# Switch back from the shadow database to the regular database so PutFooter()
+# can determine the current user even if the "logincookies" table is corrupted
+# in the shadow database.
+SendSQL("USE $::db_name");
-# Truncate email to 30 chars per bug #103592
-my $maxemailsize = 30;
-
-while (@row = FetchSQLData()) {
- my $bug_id = shift @row;
- my $g = shift @row; # Bug's group set.
- if (!defined $seen{$bug_id}) {
- $seen{$bug_id} = 1;
- $count++;
- if ($count % 200 == 0) {
- # Too big tables take too much browser memory...
- pnl "</TABLE>$tablestart";
- }
- push @bugarray, $bug_id;
-
- # retrieve this bug's priority and severity, if available,
- # by looping through all column names -- gross but functional
- my $priority = "unknown";
- my $severity;
- if ($pricol >= 0) {
- $priority = $row[$pricol];
- }
- if ($sevcol >= 0) {
- $severity = $row[$sevcol];
- }
- my $customstyle = "";
- if ($severity) {
- if ($severity eq "enh") {
- $customstyle = "style='font-style:italic ! important'";
- }
- if ($severity eq "blo") {
- $customstyle = "style='color:red ! important; font-weight:bold ! important'";
- }
- if ($severity eq "cri") {
- $customstyle = "style='color:red; ! important'";
- }
- }
- pnl "<TR VALIGN=TOP ALIGN=LEFT CLASS=$priority $customstyle><TD>";
- if ($dotweak) {
- pnl "<input type=checkbox name=id_$bug_id>";
- }
- pnl "<A HREF=\"show_bug.cgi?id=$bug_id\">";
- pnl "$bug_id</A>";
- if ($g != "0") { pnl "*"; }
- pnl " ";
- foreach my $c (@collist) {
- if (exists $::needquote{$c}) {
- my $value = shift @row;
- if (!defined $value) {
- pnl "<TD>";
- next;
- }
- if ($c eq "owner") {
- $ownerhash{$value} = 1;
- }
- if ($c eq "qa_contact") {
- $qahash{$value} = 1;
- }
- if ( ($c eq "owner" || $c eq "qa_contact" ) &&
- length $value > $maxemailsize ) {
- my $trunc = substr $value, 0, $maxemailsize;
- $value = value_quote($value);
- $value = qq|<SPAN TITLE="$value">$trunc...</SPAN>|;
- } elsif( $c eq 'changeddate' or $c eq 'opendate' ) {
- my $age = time() - $value;
- my ($s,$m,$h,$d,$mo,$y,$wd)= localtime $value;
- if( $age < 18*60*60 ) {
- $value = sprintf "%02d:%02d:%02d", $h,$m,$s;
- } elsif ( $age < 6*24*60*60 ) {
- $value = sprintf "%s %02d:%02d", $weekday[$wd],$h,$m;
- } else {
- $value = sprintf "%04d-%02d-%02d", 1900+$y,$mo+1,$d;
- }
- }
- if ($::needquote{$c} || $::needquote{$c} == 5) {
- $value = html_quote($value);
- } else {
- $value = "<nobr>$value</nobr>";
- }
- pnl "<td class=$c>$value";
- }
- }
- if ($dotweak) {
- my $value = shift @row;
- $prodhash{$value} = 1;
- $value = shift @row;
- $statushash{$value} = 1;
- }
- pnl "\n";
- }
-}
-my $buglist = join(":", @bugarray);
+################################################################################
+# Template Variable Definition
+################################################################################
+# Define the variables and functions that will be passed to the UI template.
-# This is stupid. We really really need to move the quip list into the DB!
-my $quip;
-if (Param('usequip')){
- if (open (COMMENTS, "<data/comments")) {
- my @cdata;
- while (<COMMENTS>) {
- push @cdata, $_;
- }
- close COMMENTS;
- $quip = $cdata[int(rand($#cdata + 1))];
- }
- $quip ||= "Bugzilla would like to put a random quip here, but nobody has entered any.";
-}
+$vars->{'bugs'} = \@bugs;
+$vars->{'columns'} = $columns;
+$vars->{'displaycolumns'} = \@displaycolumns;
+my @openstates = OpenStates();
+$vars->{'openstates'} = \@openstates;
+$vars->{'closedstates'} = ['CLOSED', 'VERIFIED', 'RESOLVED'];
-# We've done all we can without any output. If we can server push it is time
-# take down the waiting page and put up the real one.
-if ($serverpush) {
- print "\n";
- print "--thisrandomstring\n";
- print "Content-type: text/html\n";
- print "Content-disposition: inline; filename=bugzilla_bug_list.html\n";
- # Note! HTML header not yet closed
-}
-my $toolong = 0;
-if ($::FORM{'order'}) {
- my $q = url_quote($::FORM{'order'});
- my $cookiepath = Param("cookiepath");
- print "Set-Cookie: LASTORDER=$q ; path=$cookiepath; expires=Sun, 30-Jun-2029 00:00:00 GMT\n";
-}
-if (length($buglist) < 4000) {
- print "Set-Cookie: BUGLIST=$buglist\n\n";
-} else {
- print "Set-Cookie: BUGLIST=\n\n";
- $toolong = 1;
-}
-PutHeader($::querytitle, undef, "", "", navigation_links($buglist));
+# The list of query fields in URL query string format, used when creating
+# URLs to the same query results page with different parameters (such as
+# a different sort order or when taking some action on the set of query
+# results). To get this string, we start with the raw URL query string
+# buffer that was created when we initially parsed the URL on script startup,
+# then we remove all non-query fields from it, f.e. the sort order (order)
+# and command type (cmdtype) fields.
+$vars->{'urlquerypart'} = $::buffer;
+$vars->{'urlquerypart'} =~ s/[&?](order|cmdtype)=[^&]*//g;
+$vars->{'order'} = $order;
+# The user's login account name (i.e. email address).
+$vars->{'user'} = $::COOKIE{'Bugzilla_login'};
-print "
-<CENTER>
-<B>" . time2str("%a %b %e %T %Z %Y", time()) . "</B>";
+$vars->{'caneditbugs'} = UserInGroup('editbugs');
+$vars->{'usebuggroups'} = UserInGroup('usebuggroups');
-if (Param('usebuggroups')) {
- print "<BR>* next to a bug number notes a bug not visible to everyone.<BR>";
-}
+# Whether or not this user is authorized to move bugs to another installation.
+$vars->{'ismover'} = 1
+ if Param('move-enabled')
+ && defined($vars->{'user'})
+ && Param('movers') =~ /^(\Q$vars->{'user'}\E[,\s])|([,\s]\Q$vars->{'user'}\E[,\s]+)/;
-if (defined $::FORM{'debug'}) {
- print "<P><CODE>" . value_quote($query) . "</CODE><P>\n";
+my @bugowners = keys %$bugowners;
+if (scalar(@bugowners) > 1 && UserInGroup('editbugs')) {
+ my $suffix = Param('emailsuffix');
+ map(s/$/$suffix/, @bugowners) if $suffix;
+ my $bugowners = join(",", @bugowners);
+ $vars->{'bugowners'} = $bugowners;
}
-if ($toolong) {
- print "<h2>This list is too long for bugzilla's little mind; the\n";
- print "Next/Prev/First/Last buttons won't appear.</h2>\n";
+if ($::FORM{'debug'}) {
+ $vars->{'debug'} = 1;
+ $vars->{'query'} = $query;
}
-if (Param('usequip')){
- print "<HR><A HREF=quips.cgi><I>$quip</I></A></CENTER>\n";
-}
-print "<HR SIZE=10>";
-print "$count bugs found." if $count > 9;
-print $tablestart, "\n";
-print $::bugl;
-print "</TABLE>\n";
-
-if ($count == 0) {
- print "Zarro Boogs found.\n";
- # I've been asked to explain this ... way back when, when Netscape released
- # version 4.0 of its browser, we had a release party. Naturally, there
- # had been a big push to try and fix every known bug before the release.
- # Naturally, that hadn't actually happened. (This is not unique to
- # Netscape or to 4.0; the same thing has happened with every software
- # project I've ever seen.) Anyway, at the release party, T-shirts were
- # handed out that said something like "Netscape 4.0: Zarro Boogs".
- # Just like the software, the T-shirt had no known bugs. Uh-huh.
- #
- # So, when you query for a list of bugs, and it gets no results, you
- # can think of this as a friendly reminder. Of *course* there are bugs
- # matching your query, they just aren't in the bugsystem yet...
-
- print qq{<p><A HREF="query.cgi">Query Page</A>\n};
- print qq{&nbsp;&nbsp;<A HREF="enter_bug.cgi">Enter New Bug</A>\n};
- print qq{<NOBR><A HREF="query.cgi?$::buffer">Edit this query</A></NOBR>\n};
-} elsif ($count == 1) {
- print "One bug found.\n";
-} else {
- print "$count bugs found.\n";
-}
+# Whether or not to split the column titles across two rows to make
+# the list more compact.
+$vars->{'splitheader'} = $::COOKIE{'SPLITHEADER'} ? 1 : 0;
+$vars->{'quip'} = GetQuip() if Param('usequip');
+$vars->{'currenttime'} = time2str("%a %b %e %T %Z %Y", time());
+
+# The following variables are used when the user is making changes to multiple bugs.
if ($dotweak) {
- GetVersionTable();
- print "
-<SCRIPT>
-numelements = document.changeform.elements.length;
-function SetCheckboxes(value) {
- var item;
- for (var i=0 ; i<numelements ; i++) {
- item = document.changeform.elements\[i\];
- item.checked = value;
+ $vars->{'dotweak'} = 1;
+ $vars->{'use_keywords'} = 1 if @::legal_keywords;
+
+ $vars->{'products'} = \@::legal_product;
+ $vars->{'platforms'} = \@::legal_platform;
+ $vars->{'priorities'} = \@::legal_priority;
+ $vars->{'severities'} = \@::legal_severity;
+ $vars->{'resolutions'} = \@::settable_resolution;
+
+ # The value that represents "don't change the value of this field".
+ $vars->{'dontchange'} = $::dontchange;
+
+ $vars->{'unconfirmedstate'} = $::unconfirmedstate;
+
+ $vars->{'bugstatuses'} = [ keys %$bugstatuses ];
+
+ # The groups to which the user belongs.
+ $vars->{'groups'} = GetGroupsByGroupSet($::usergroupset) if $::usergroupset ne '0';
+
+ # If all bugs being changed are in the same product, the user can change
+ # their version and component, so generate a list of products, a list of
+ # versions for the product (if there is only one product on the list of
+ # products), and a list of components for the product.
+ $vars->{'bugproducts'} = [ keys %$bugproducts ];
+ if (scalar(@{$vars->{'bugproducts'}}) == 1) {
+ my $product = $vars->{'bugproducts'}->[0];
+ $vars->{'versions'} = $::versions{$product};
+ $vars->{'components'} = $::components{$product};
+ $vars->{'targetmilestones'} = $::target_milestone{$product} if Param('usetargetmilestone');
}
}
-document.write(\" <input type=button value=\\\"Uncheck All\\\" onclick=\\\"SetCheckboxes(false);\\\"> <input type=button value=\\\"Check All\\\" onclick=\\\"SetCheckboxes(true);\\\">\");
-</SCRIPT>";
-
- my $resolution_popup = make_options(\@::settable_resolution, "FIXED");
- my @prod_list = keys %prodhash;
- my @list = @prod_list;
- my @legal_versions;
- my @legal_component;
- if (1 == @prod_list) {
- @legal_versions = @{$::versions{$prod_list[0]}};
- @legal_component = @{$::components{$prod_list[0]}};
- }
-
- my $version_popup = make_options(\@legal_versions, $::dontchange);
- my $platform_popup = make_options(\@::legal_platform, $::dontchange);
- my $priority_popup = make_options(\@::legal_priority, $::dontchange);
- my $sev_popup = make_options(\@::legal_severity, $::dontchange);
- my $component_popup = make_options(\@legal_component, $::dontchange);
- my $product_popup = make_options(\@::legal_product, $::dontchange);
-
-
- print "
-<hr>
-<TABLE>
-<TR>
- <TD ALIGN=RIGHT><B>Product:</B></TD>
- <TD><SELECT NAME=product>$product_popup</SELECT></TD>
- <TD ALIGN=RIGHT><B>Version:</B></TD>
- <TD><SELECT NAME=version>$version_popup</SELECT></TD>
-<TR>
- <TD ALIGN=RIGHT><B><A HREF=\"bug_status.html#rep_platform\">Platform:</A></B></TD>
- <TD><SELECT NAME=rep_platform>$platform_popup</SELECT></TD>
- <TD ALIGN=RIGHT><B><A HREF=\"bug_status.html#priority\">Priority:</A></B></TD>
- <TD><SELECT NAME=priority>$priority_popup</SELECT></TD>
-</TR>
-<TR>
- <TD ALIGN=RIGHT><B>Component:</B></TD>
- <TD><SELECT NAME=component>$component_popup</SELECT></TD>
- <TD ALIGN=RIGHT><B><A HREF=\"bug_status.html#severity\">Severity:</A></B></TD>
- <TD><SELECT NAME=bug_severity>$sev_popup</SELECT></TD>
-</TR>";
-
- if (Param("usetargetmilestone")) {
- my @legal_milestone;
- if(1 == @prod_list) {
- @legal_milestone = @{$::target_milestone{$prod_list[0]}};
- }
- my $tfm_popup = make_options(\@legal_milestone, $::dontchange);
- print "
- <TR>
- <TD ALIGN=RIGHT><B>Target milestone:</B></TD>
- <TD><SELECT NAME=target_milestone>$tfm_popup</SELECT></TD>
- </TR>";
- }
-
- if (Param("useqacontact")) {
- print "
-<TR>
-<TD><B>QA Contact:</B></TD>
-<TD COLSPAN=3><INPUT NAME=qa_contact SIZE=32 VALUE=\"" .
- value_quote($::dontchange) . "\"></TD>
-</TR>";
- }
-
- print qq{
-<TR><TD ALIGN="RIGHT"><B>CC List:</B></TD>
-<TD COLSPAN=3><INPUT NAME="masscc" SIZE=32 VALUE="">
-<SELECT NAME="ccaction">
-<OPTION VALUE="add">Add these to the CC List
-<OPTION VALUE="remove">Remove these from the CC List
-</SELECT>
-</TD>
-</TR>
-};
- if (@::legal_keywords) {
- print qq{
-<TR><TD><B><A HREF="describekeywords.cgi">Keywords</A>:</TD>
-<TD COLSPAN=3><INPUT NAME=keywords SIZE=32 VALUE="">
-<SELECT NAME="keywordaction">
-<OPTION VALUE="add">Add these keywords
-<OPTION VALUE="delete">Delete these keywords
-<OPTION VALUE="makeexact">Make the keywords be exactly this list
-</SELECT>
-</TD>
-</TR>
-};
- }
+################################################################################
+# HTTP Header Generation
+################################################################################
- print "</TABLE>
+# If we are doing server push, output a separator string.
+print "\n--thisrandomstring\n" if $serverpush;
+
+# Generate HTTP headers
-<INPUT NAME=multiupdate value=Y TYPE=hidden>
+# Suggest a name for the bug list if the user wants to save it as a file.
+# If we are doing server push, then we did this already in the HTTP headers
+# that started the server push, so we don't have to do it again here.
+print "Content-Disposition: inline; filename=$filename\n" unless $serverpush;
-<B>Additional Comments:</B>
-<BR>
-<TEXTAREA WRAP=HARD NAME=comment ROWS=5 COLS=80></TEXTAREA><BR>";
+if ($format->{'extension'} eq "html") {
+ print "Content-Type: text/html\n";
-if($::usergroupset ne '0') {
- SendSQL("select bit, name, description, isactive ".
- "from groups where bit & $::usergroupset != 0 ".
- "and isbuggroup != 0 ".
- "order by description");
- # We only print out a header bit for this section if there are any
- # results.
- my $groupFound = 0;
- my $inactiveFound = 0;
- while (MoreSQLData()) {
- my ($bit, $groupname, $description, $isactive) = (FetchSQLData());
- if(($prodhash{$groupname}) || (!defined($::proddesc{$groupname}))) {
- if(!$groupFound) {
- print "<B>Groupset:</B><BR>\n";
- print "<TABLE BORDER=1><TR>\n";
- print "<TH ALIGN=center VALIGN=middle>Don't<br>change<br>this group<br>restriction</TD>\n";
- print "<TH ALIGN=center VALIGN=middle>Remove<br>bugs<br>from this<br>group</TD>\n";
- print "<TH ALIGN=center VALIGN=middle>Add<br>bugs<br>to this<br>group</TD>\n";
- print "<TH ALIGN=left VALIGN=middle>Group name:</TD></TR>\n";
- $groupFound = 1;
- }
- # Modifying this to use radio buttons instead
- print "<TR>";
- print "<TD ALIGN=center><input type=radio name=\"bit-$bit\" value=\"-1\" checked></TD>\n";
- print "<TD ALIGN=center><input type=radio name=\"bit-$bit\" value=\"0\"></TD>\n";
- if ($isactive) {
- print "<TD ALIGN=center><input type=radio name=\"bit-$bit\" value=\"1\"></TD>\n";
- } else {
- $inactiveFound = 1;
- print "<TD>&nbsp;</TD>\n";
- }
- print "<TD>";
- if(!$isactive) {
- print "<I>";
- }
- print "$description";
- if(!$isactive) {
- print "</I>";
- }
- print "</TD></TR>\n";
- }
+ if ($order) {
+ my $qorder = url_quote($order);
+ print "Set-Cookie: LASTORDER=$qorder ; path=/; expires=Sun, 30-Jun-2029 00:00:00 GMT\n";
}
- # Add in some blank space for legibility
- if($groupFound) {
- print "</TABLE>\n";
- if ($inactiveFound) {
- print "<FONT SIZE=\"-1\">(Note: Bugs may not be added to inactive groups (<I>italicized</I>), only removed)</FONT><BR>\n";
- }
- print "<BR><BR>\n";
+ my $bugids = join(":", map( $_->{'id'}, @bugs));
+ if (length($bugids) < 4000) {
+ print "Set-Cookie: BUGLIST=$bugids\n";
}
-}
-
-
-
-
- # knum is which knob number we're generating, in javascript terms.
-
- my $knum = 0;
- print "
-<INPUT TYPE=radio NAME=knob VALUE=none CHECKED>
- Do nothing else<br>";
- $knum++;
- if ($statushash{$::unconfirmedstate} && 1 == scalar(keys(%statushash))) {
- print "
-<INPUT TYPE=radio NAME=knob VALUE=confirm>
- Confirm bugs (change status to <b>NEW</b>)<br>";
- $knum++;
- }
- print "
-<INPUT TYPE=radio NAME=knob VALUE=accept>
- Accept bugs (change status to <b>ASSIGNED</b>)<br>";
- $knum++;
- if (!defined $statushash{'CLOSED'} &&
- !defined $statushash{'VERIFIED'} &&
- !defined $statushash{'RESOLVED'}) {
- print "
-<INPUT TYPE=radio NAME=knob VALUE=clearresolution>
- Clear the resolution<br>";
- $knum++;
- print "
-<INPUT TYPE=radio NAME=knob VALUE=resolve>
- Resolve bugs, changing <A HREF=\"bug_status.html\">resolution</A> to
- <SELECT NAME=resolution
- ONCHANGE=\"document.changeform.knob\[$knum\].checked=true\">
- $resolution_popup</SELECT><br>";
- $knum++;
- }
- if (!defined $statushash{'NEW'} &&
- !defined $statushash{'ASSIGNED'} &&
- !defined $statushash{'REOPENED'}) {
- print "
-<INPUT TYPE=radio NAME=knob VALUE=reopen> Reopen bugs<br>";
- $knum++;
- }
- my @statuskeys = keys %statushash;
- if (1 == @statuskeys) {
- if (defined $statushash{'RESOLVED'}) {
- print "
-<INPUT TYPE=radio NAME=knob VALUE=verify>
- Mark bugs as <b>VERIFIED</b><br>";
- $knum++;
- }
- if (defined $statushash{'VERIFIED'}) {
- print "
-<INPUT TYPE=radio NAME=knob VALUE=close>
- Mark bugs as <b>CLOSED</b><br>";
- $knum++;
- }
+ else {
+ print "Set-Cookie: BUGLIST=\n";
+ $vars->{'toolong'} = 1;
}
- print "
-<INPUT TYPE=radio NAME=knob VALUE=reassign>
- <A HREF=\"bug_status.html#assigned_to\">Reassign</A> bugs to
- <INPUT NAME=assigned_to SIZE=32
- ONCHANGE=\"document.changeform.knob\[$knum\].checked=true\"
- VALUE=\"$::COOKIE{'Bugzilla_login'}\"><br>";
- $knum++;
- print "<INPUT TYPE=radio NAME=knob VALUE=reassignbycomponent>
- Reassign bugs to owner of selected component<br>";
- $knum++;
-
- print "
-<p>
-<font size=-1>
-To make changes to a bunch of bugs at once:
-<ol>
-<li> Put check boxes next to the bugs you want to change.
-<li> Adjust above form elements. (If the change you are making requires
- an explanation, include it in the comments box).
-<li> Click the below \"Commit\" button.
-</ol></font>
-<INPUT TYPE=SUBMIT VALUE=Commit>";
-
- my $movers = Param("movers");
- $movers =~ s/\s?,\s?/|/g;
- $movers =~ s/@/\@/g;
-
- if ( Param("move-enabled")
- && (defined $::COOKIE{"Bugzilla_login"})
- && ($::COOKIE{"Bugzilla_login"} =~ /($movers)/) ){
- print "<P>";
- print "<INPUT TYPE=\"SUBMIT\" NAME=\"action\" VALUE=\"";
- print Param("move-button-text") . "\">";
- }
-
- print "</FORM><hr>\n";
+}
+else {
+ print "Content-Type: $format->{'contenttype'}\n";
}
+print "\n"; # end HTTP headers
-if ($count > 0) {
- print "<FORM METHOD=POST ACTION=\"long_list.cgi\">
-<INPUT TYPE=HIDDEN NAME=buglist VALUE=$buglist>
-<INPUT TYPE=SUBMIT VALUE=\"Long Format\">
-<NOBR><A HREF=\"query.cgi\">Query Page</A></NOBR>
-&nbsp;&nbsp;
-<NOBR><A HREF=\"enter_bug.cgi\">Enter New Bug</A></NOBR>
-&nbsp;&nbsp;
-<NOBR><A HREF=\"colchange.cgi?$::buffer\">Change columns</A></NOBR>";
- if (!$dotweak && $count > 1 && UserInGroup("editbugs")) {
- print "&nbsp;&nbsp;\n";
- print "<NOBR><A HREF=\"buglist.cgi?$fields$orderpart&tweak=1\">";
- print "Change several bugs at once</A></NOBR>\n";
- }
- my @owners = sort(keys(%ownerhash));
- my $suffix = Param('emailsuffix');
- if (@owners > 1 && UserInGroup("editbugs")) {
- if ($suffix ne "") {
- map(s/$/$suffix/, @owners);
- }
- my $list = join(',', @owners);
- print qq{&nbsp;&nbsp;\n};
- print qq{<A HREF="mailto:$list">Send&nbsp;mail&nbsp;to&nbsp;bug&nbsp;owners</A>\n};
- }
- my @qacontacts = sort(keys(%qahash));
- if (@qacontacts > 1 && UserInGroup("editbugs") && Param("useqacontact")) {
- if ($suffix ne "") {
- map(s/$/$suffix/, @qacontacts);
- }
- my $list = join(',', @qacontacts);
- print qq{&nbsp;&nbsp;\n};
- print qq{<A HREF="mailto:$list">Send&nbsp;mail&nbsp;to&nbsp;bug&nbsp;QA&nbsp;contacts</A>\n};
- }
- print qq{&nbsp;&nbsp;\n};
- print qq{<NOBR><A HREF="query.cgi?$::buffer">Edit this query</A></NOBR>\n};
- print "</FORM>\n";
-}
+################################################################################
+# Content Generation
+################################################################################
-# 2001-06-20, myk@mozilla.org, bug 47914:
-# Switch back from the shadow database to the regular database
-# so that PutFooter() can determine the current user even if
-# the "logincookies" table is corrupted in the shadow database.
-SendSQL("USE $::db_name");
+# Generate and return the UI (HTML page) from the appropriate template.
+$template->process("buglist/$format->{'template'}", $vars)
+ || DisplayError("Template process failed: " . $template->error())
+ && exit;
-PutFooter();
-if ($serverpush) {
- print "\n--thisrandomstring--\n";
-}
+################################################################################
+# Script Conclusion
+################################################################################
+
+print "\n--thisrandomstring--\n" if $serverpush;
diff --git a/checksetup.pl b/checksetup.pl
index 44587f310..ab8b723fa 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -193,6 +193,7 @@ unless (have_vers("Date::Parse",0)) { push @missing,"Date::Parse" }
unless (have_vers("AppConfig","1.52")) { push @missing,"AppConfig" }
unless (have_vers("Template","2.06")) { push @missing,"Template" }
unless (have_vers("Text::Wrap","2001.0131")) { push @missing,"Text::Wrap" }
+unless (have_vers("File::Spec", "0.82")) { push @missing,"File::Spec" }
# If CGI::Carp was loaded successfully for version checking, it changes the
# die and warn handlers, we don't want them changed, so we need to stash the
@@ -488,6 +489,21 @@ LocalVar('platforms', '
+LocalVar('contenttypes', '
+#
+# The types of content that template files can generate, indexed by file extension.
+#
+$contenttypes = {
+ "html" => "text/html" ,
+ "rdf" => "application/xml" ,
+ "xml" => "text/xml" ,
+ "js" => "application/x-javascript" ,
+};
+');
+
+
+
+
if ($newstuff ne "") {
print "\nThis version of Bugzilla contains some variables that you may want\n",
"to change and adapt to your local settings. Please edit the file\n",
diff --git a/css/buglist.css b/css/buglist.css
new file mode 100644
index 000000000..c8a4d13d0
--- /dev/null
+++ b/css/buglist.css
@@ -0,0 +1,38 @@
+/* The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is the Bugzilla Bug Tracking System.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation. Portions created by Netscape are
+ * Copyright (C) 1998 Netscape Communications Corporation. All
+ * Rights Reserved.
+ *
+ * Contributor(s): Myk Melez <myk@mozilla.org>
+ */
+
+/* Right align bug IDs. */
+.bz_id_column { text-align: right; }
+
+/* Style bug rows according to severity. */
+.bz_blocker { color: red; font-weight: bold; }
+.bz_critical { color: red; }
+.bz_enhancement { font-style: italic; }
+
+/* Style secure bugs if the installation is not using bug groups.
+ * Installations that *are* using bug groups are likely to be using
+ * them for almost all bugs, in which case special styling is not
+ * informative and generally a nuisance.
+ */
+.bz_secure { color: black; background-color: lightgrey; }
+
+/* Align columns in the "change multiple bugs" form to the right. */
+table#form tr th { text-align: right; }
+
diff --git a/globals.pl b/globals.pl
index 58dcf301a..417241b50 100644
--- a/globals.pl
+++ b/globals.pl
@@ -35,6 +35,7 @@ sub globals_pl_sillyness {
my $zz;
$zz = @main::SqlStateStack;
$zz = @main::chooseone;
+ $zz = $main::contenttypes;
$zz = @main::default_column_list;
$zz = $main::defaultqueryname;
$zz = @main::dontchange;
@@ -54,8 +55,8 @@ sub globals_pl_sillyness {
$zz = %main::proddesc;
$zz = @main::prodmaxvotes;
$zz = $main::superusergroupset;
- $zz = $main::userid;
$zz = $main::template;
+ $zz = $main::userid;
$zz = $main::vars;
}
@@ -79,6 +80,9 @@ use Date::Parse; # For str2time().
#use Carp; # for confess
use RelationSet;
+# Use standard Perl libraries for cross-platform file/directory manipulation.
+use File::Spec;
+
# Some environment variables are not taint safe
delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
@@ -1611,7 +1615,9 @@ $::template ||= Template->new(
$var =~ s/\n/\\n/g;
$var =~ s/\r/\\r/g;
return $var;
- }
+ } ,
+
+ html => \&html_quote ,
} ,
}
) || DisplayError("Template creation failed: " . Template->error())
@@ -1639,6 +1645,116 @@ $Template::Stash::LIST_OPS->{ containsany } =
return 0;
};
+
+sub GetOutputFormats {
+ # Builds a set of possible output formats for a script by looking for
+ # format files in the appropriate template directories as specified by
+ # the template include path, the sub-directory parameter, and the
+ # template name parameter.
+
+ # This function is relevant for scripts with one basic function whose
+ # results can be represented in multiple formats, f.e. buglist.cgi,
+ # which has one function (query and display of a list of bugs) that can
+ # be represented in multiple formats (i.e. html, rdf, xml, etc.).
+
+ # It is *not* relevant for scripts with several functions but only one
+ # basic output format, f.e. editattachstatuses.cgi, which not only lists
+ # statuses but also provides adding, editing, and deleting functions.
+ # (although it may be possible to make this function applicable under
+ # these circumstances with minimal modification).
+
+ # Format files have names that look like SCRIPT-FORMAT.EXT.tmpl, where
+ # SCRIPT is the name of the CGI script being invoked, SUBDIR is the name
+ # of the template sub-directory, FORMAT is the name of the format, and EXT
+ # is the filename extension identifying the content type of the output.
+
+ # When a format file is found, a record for that format is added to
+ # the hash of format records, indexed by format name, with each record
+ # containing the name of the format file, its filename extension,
+ # and its content type (obtained by reference to the $::contenttypes
+ # hash defined in localconfig).
+
+ my ($subdir, $script) = @_;
+
+ # A set of output format records, indexed by format name, each record
+ # containing template, extension, and contenttype fields.
+ my $formats = {};
+
+ # Get the template include path from the template object.
+ my $includepath = $::template->context->{ LOAD_TEMPLATES }->[0]->include_path();
+
+ # Loop over each include directory in reverse so that format files
+ # earlier in the path override files with the same name later in
+ # the path (i.e. "custom" formats override "default" ones).
+ foreach my $path (reverse @$includepath) {
+ # Get the list of files in the given sub-directory if it exists.
+ my $dirname = File::Spec->catdir($path, $subdir);
+ opendir(SUBDIR, $dirname) || next;
+ my @files = readdir SUBDIR;
+ closedir SUBDIR;
+
+ # Loop over each file in the sub-directory looking for format files
+ # (files whose name looks like SCRIPT-FORMAT.EXT.tmpl).
+ foreach my $file (@files) {
+ if ($file =~ /^$script-(.+)\.(.+)\.(tmpl)$/) {
+ $formats->{$1} = {
+ 'template' => $file ,
+ 'extension' => $2 ,
+ 'contenttype' => $::contenttypes->{$2} || "text/plain" ,
+ };
+ }
+ }
+ }
+ return $formats;
+}
+
+sub ValidateOutputFormat {
+ my ($format, $script, $subdir) = @_;
+
+ # If the script name is undefined, assume the script currently being
+ # executed, deriving its name from Perl's built-in $0 (program name) var.
+ if (!defined($script)) {
+ my ($volume, $dirs, $filename) = File::Spec->splitpath($0);
+ $filename =~ /^(.+)\.cgi$/;
+ $script = $1
+ || DisplayError("Could not determine the name of the script.")
+ && exit;
+ }
+
+ # If the format name is undefined or the default format is specified,
+ # do not do any validation but instead return the default format.
+ if (!defined($format) || $format eq "default") {
+ return
+ {
+ 'template' => "$script.html.tmpl" ,
+ 'extension' => "html" ,
+ 'contenttype' => "text/html" ,
+ };
+ }
+
+ # If the subdirectory name is undefined, assume the script name.
+ $subdir = $script if !defined($subdir);
+
+ # Get the list of output formats supported by this script.
+ my $formats = GetOutputFormats($subdir, $script);
+
+ # Validate the output format requested by the user.
+ if (!$formats->{$format}) {
+ my $escapedname = html_quote($format);
+ DisplayError("The <em>$escapedname</em> output format is not
+ supported by this script. Supported formats (besides the
+ default HTML format) are <em>" .
+ join("</em>, <em>", map(html_quote($_), keys(%$formats))) .
+ "</em>.");
+ exit;
+ }
+
+ # Return the validated output format.
+ return $formats->{$format};
+}
+
+###############################################################################
+
# Add a "substr" method to the Template Toolkit's "scalar" object
# that returns a substring of a string.
$Template::Stash::SCALAR_OPS->{ substr } =
diff --git a/skins/standard/buglist.css b/skins/standard/buglist.css
new file mode 100644
index 000000000..c8a4d13d0
--- /dev/null
+++ b/skins/standard/buglist.css
@@ -0,0 +1,38 @@
+/* The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is the Bugzilla Bug Tracking System.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation. Portions created by Netscape are
+ * Copyright (C) 1998 Netscape Communications Corporation. All
+ * Rights Reserved.
+ *
+ * Contributor(s): Myk Melez <myk@mozilla.org>
+ */
+
+/* Right align bug IDs. */
+.bz_id_column { text-align: right; }
+
+/* Style bug rows according to severity. */
+.bz_blocker { color: red; font-weight: bold; }
+.bz_critical { color: red; }
+.bz_enhancement { font-style: italic; }
+
+/* Style secure bugs if the installation is not using bug groups.
+ * Installations that *are* using bug groups are likely to be using
+ * them for almost all bugs, in which case special styling is not
+ * informative and generally a nuisance.
+ */
+.bz_secure { color: black; background-color: lightgrey; }
+
+/* Align columns in the "change multiple bugs" form to the right. */
+table#form tr th { text-align: right; }
+
diff --git a/template/default/buglist/buglist-rdf.rdf.tmpl b/template/default/buglist/buglist-rdf.rdf.tmpl
new file mode 100644
index 000000000..4cf480dcc
--- /dev/null
+++ b/template/default/buglist/buglist-rdf.rdf.tmpl
@@ -0,0 +1,52 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Myk Melez <myk@mozilla.org>
+ #%]
+
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:bz="http://www.bugzilla.org/rdf#">
+
+<bz:result about="[% Param('urlbase') %]buglist.cgi?[% urlquerypart FILTER html %]">
+
+ <bz:bugs>
+ <Seq>
+ [% FOREACH bug = bugs %]
+ <li>
+
+ <bz:bug about="[% Param('urlbase') %]show_bug.cgi?id=[% bug.id %]">
+
+ <bz:id>[% bug.id %]</bz:id>
+
+ [% FOREACH column = displaycolumns %]
+ <bz:[% column %]>[% bug.$column FILTER html %]</bz:[% column %]>
+ [% END %]
+
+ </bz:bug>
+
+ </li>
+
+ [% END %]
+
+ </Seq>
+
+ </bz:bugs>
+
+</bz:result>
+
+</RDF>
diff --git a/template/default/buglist/buglist-simple.html.tmpl b/template/default/buglist/buglist-simple.html.tmpl
new file mode 100644
index 000000000..c2e4e6823
--- /dev/null
+++ b/template/default/buglist/buglist-simple.html.tmpl
@@ -0,0 +1,44 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Myk Melez <myk@mozilla.org>
+ #%]
+
+[%############################################################################%]
+[%# Initialization #%]
+[%############################################################################%]
+
+[% DEFAULT title = "Bug List" %]
+[% title = title FILTER html %]
+
+
+[%############################################################################%]
+[%# Bug Table #%]
+[%############################################################################%]
+
+<html>
+
+ <head>
+ <title>[% title %]</title>
+ <link href="css/buglist.css" rel="stylesheet" type="text/css" />
+ </head>
+
+ <body>
+ [% PROCESS buglist/table.tmpl %]
+ </body>
+
+</html>
diff --git a/template/default/buglist/buglist.html.tmpl b/template/default/buglist/buglist.html.tmpl
new file mode 100644
index 000000000..f9ea46bec
--- /dev/null
+++ b/template/default/buglist/buglist.html.tmpl
@@ -0,0 +1,160 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Myk Melez <myk@mozilla.org>
+ #%]
+
+[%############################################################################%]
+[%# Template Initialization #%]
+[%############################################################################%]
+
+[% DEFAULT title = "Bug List" %]
+[% style_url = "css/buglist.css" %]
+
+
+[%############################################################################%]
+[%# Page Header #%]
+[%############################################################################%]
+
+[% PROCESS global/header
+ title = title
+ style = style
+%]
+
+<div align="center">
+ <b>[% currenttime %]</b><br />
+
+ [% IF debug %]
+ <p>[% query FILTER html %]</p>
+ [% END %]
+
+ [% IF quip %]
+ <a href="quips.cgi"><i>[% quip %]</i></a>
+ [% END %]
+
+</div>
+
+[% IF toolong %]
+ <h2>
+ This list is too long for Bugzilla's little mind; the
+ Next/Prev/First/Last buttons won't appear on individual bugs.
+ </h2>
+[% END %]
+
+<hr />
+
+
+[%############################################################################%]
+[%# Preceding Status Line #%]
+[%############################################################################%]
+
+[% IF bugs.size > 9 %]
+ [% bugs.size %] bugs found.
+[% END %]
+
+
+[%############################################################################%]
+[%# Start of Change Form #%]
+[%############################################################################%]
+
+[% IF dotweak %]
+ <form name="changeform" method="post" action="process_bug.cgi">
+[% END %]
+
+
+[%############################################################################%]
+[%# Bug Table #%]
+[%############################################################################%]
+
+[% FLUSH %]
+[% PROCESS buglist/table.tmpl %]
+
+[%############################################################################%]
+[%# Succeeding Status Line #%]
+[%############################################################################%]
+
+[% IF bugs.count == 0 %]
+ Zarro Boogs found.
+ <p>
+ <a href="query.cgi">Query Page</a>
+ &nbsp;&nbsp;<a href="enter_bug.cgi">Enter New Bug</a>
+ <a href="query.cgi?[% urlquerypart %]">Edit this query</a>
+ </p>
+
+[% ELSIF bugs.count == 1 %]
+ One bug found.
+
+[% ELSE %]
+ [% bugs.size %] bugs found.
+
+[% END %]
+
+<br />
+
+
+[%############################################################################%]
+[%# Rest of Change Form #%]
+[%############################################################################%]
+
+[% IF dotweak %]
+
+ [% PROCESS "buglist/change-form.tmpl" %]
+
+ </form>
+
+ <hr />
+
+[% END %]
+
+
+[%############################################################################%]
+[%# Navigation Bar #%]
+[%############################################################################%]
+
+[% IF bugs.size > 0 %]
+ <form method="post" action="long_list.cgi">
+ <input type="hidden" name="buglist" value="[% buglist %]">
+ <input type="submit" value="Long Format">
+
+ <a href="query.cgi">Query Page</a> &nbsp;&nbsp;
+ <a href="enter_bug.cgi">Enter New Bug</a> &nbsp;&nbsp;
+ <a href="colchange.cgi?[% urlquerypart %]">Change Columns</a> &nbsp;&nbsp;
+
+ [% IF bugs.size > 1 && caneditbugs && !dotweak %]
+ <a href="buglist.cgi?[% urlquerypart %]
+ [%- "&order=$order" FILTER uri html IF order %]&tweak=1">Change Several
+ Bugs at Once</a>
+ &nbsp;&nbsp;
+ [% END %]
+
+ [% IF bugowners %]
+ <a href="mailto:[% bugowners %]">Send Mail to Bug Owners</a> &nbsp;&nbsp;
+ [% END %]
+
+ <a href="query.cgi?[% urlquerypart %]">Edit this Query</a> &nbsp;&nbsp;
+
+ </form>
+
+[% END %]
+
+
+[%############################################################################%]
+[%# Page Footer #%]
+[%############################################################################%]
+
+[% PROCESS global/footer %]
+
diff --git a/template/default/buglist/change-form.tmpl b/template/default/buglist/change-form.tmpl
new file mode 100644
index 000000000..8498a0ab8
--- /dev/null
+++ b/template/default/buglist/change-form.tmpl
@@ -0,0 +1,339 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Myk Melez <myk@mozilla.org>
+ #%]
+
+<script type="text/javascript" language="JavaScript">
+ var numelements = document.forms.changeform.elements.length;
+ function SetCheckboxes(value) {
+ var item;
+ for (var i=0 ; i<numelements ; i++) {
+ item = document.forms.changeform.elements[i];
+ item.checked = value;
+ }
+ }
+ document.write(' <input type="button" value="Uncheck All" onclick="SetCheckboxes(false);">');
+ document.write(' <input type="button" value="Check All" onclick="SetCheckboxes(true);">');
+</script>
+
+<hr />
+
+<p><font size="-1">
+ To change multiple bugs:
+ <ol>
+ <li>Check the bugs you want to change above.</li>
+ <li>Make your changes in the form fields below. If the change
+ you are making requires an explanation, include it in
+ the comments box.</li>
+ <li>Click the <em>Commit</em> button.</li>
+ </ol>
+</font></p>
+
+<table id="form">
+ <tr>
+
+ <th><label for="product">Product:</label></th>
+ <td>
+ [% PROCESS selectmenu menuname = "product"
+ menuitems = products %]
+ </td>
+
+ <th><label for="version">Version:</label></th>
+ <td>
+ [% PROCESS selectmenu menuname = "version"
+ menuitems = versions %]
+ </td>
+
+ </tr>
+ <tr>
+
+ <th>
+ <label for="rep_platform">
+ <a href="bug_status.html#rep_platform">Platform:</a>
+ </label>
+ </th>
+ <td>
+ [% PROCESS selectmenu menuname = "rep_platform"
+ menuitems = platforms %]
+ </td>
+
+ <th>
+ <label for="priority">
+ <a href="bug_status.html#priority">Priority:</a>
+ </label>
+ </th>
+ <td>
+ [% PROCESS selectmenu menuname = "priority"
+ menuitems = priorities %]
+ </td>
+
+ </tr>
+ <tr>
+
+ <th><label for="component">Component:</label></th>
+ <td>
+ [% PROCESS selectmenu menuname = "component"
+ menuitems = components %]
+ </td>
+
+ <th>
+ <label for="severity">
+ <a href="bug_status.html#severity">Severity:</a>
+ </label>
+ </th>
+ <td>
+ [% PROCESS selectmenu menuname = "severity"
+ menuitems = severities %]
+ </td>
+
+ </tr>
+ <tr>
+
+ <th><label for="target_milestone">Target Milestone:</label></th>
+ <td colspan="3">
+ [% PROCESS selectmenu menuname = "target_milestone"
+ menuitems = targetmilestones %]
+ </td>
+
+ </tr>
+
+ [% IF Param("useqacontact") %]
+ <tr>
+ <th><label for="qa_contact">QA Contact:</label></th>
+ <td colspan="3">
+ <input id="qa_contact"
+ name="qa_contact"
+ value="[% dontchange FILTER html %]"
+ size="32">
+ </td>
+ </tr>
+ [% END %]
+
+ <tr>
+
+ <th><label for="masscc">CC List:</label></th>
+ <td colspan="3">
+ <input id="masscc" name="masscc" size="32">
+ <select name="ccaction">
+ <option value="add">Add these to the CC List</option>
+ <option value="remove">Remove these from the CC List</option>
+ </select>
+ </td>
+
+ </tr>
+
+ [% IF use_keywords %]
+ <tr>
+
+ <th>
+ <label for="keywords">
+ <a href="describekeywords.cgi">Keywords:</a>
+ </label>
+ </th>
+ <td colspan="3">
+ <input id="keywords" name="keywords" size="32">
+ <select name="keywordaction">
+ <option value="add">Add these keywords</option>
+ <option value="delete">Delete these keywords</option>
+ <option value="makeexact">Make the keywords be exactly this list</option>
+ </select>
+ </td>
+
+ </tr>
+ [% END %]
+
+ <tr>
+ <th>Depends on:</th>
+ <td colspan="3">
+ <input id="dependson" name="dependson" size="32">
+ <select name="dependsonaction">
+ <option value="add">Add these dependencies</option>
+ <option value="delete">Remove these dependencies</option>
+ <option value="makeexact">Make the dependencies be exactly this list</option>
+ </select>
+ </td>
+ </tr>
+
+ <tr>
+ <th>Blocks:</th>
+ <td colspan="3">
+ <input id="blocked" name="blocked" size="32">
+ <select name="blockedaction">
+ <option value="add">Add these dependencies</option>
+ <option value="delete">Remove these dependencies</option>
+ <option value="makeexact">Make the dependencies be exactly this list</option>
+ </select>
+ </td>
+ </tr>
+
+</table>
+
+<input type="hidden" name="multiupdate" value="Y">
+
+<label for="comment"><b>Additional Comments:</b></label><br />
+<textarea id="comment" name="comment" rows="5" cols="80" wrap="hard"></textarea><br />
+
+[% IF groups.size > 0 %]
+
+ <b>Groupset:</b><br />
+ <table border="1">
+ <tr>
+ <th>Don't<br />change<br />this group<br />restriction</td>
+ <th>Remove<br />bugs<br />from this<br />group</td>
+ <th>Add<br />bugs<br />to this<br />group</td>
+ <th>Group Name:</td>
+ </tr>
+
+ [% FOREACH group = groups %]
+ <tr>
+ <td align="center">
+ <input type="radio" name="bit-[% group.bit %]" value="-1" checked>
+ </td>
+ <td align="center">
+ <input type="radio" name="bit-[% group.bit %]" value="0">
+ </td>
+ [% IF group.isactive %]
+ <td align="center">
+ <input type="radio" name="bit-[% group.bit %]" value="1">
+ </td>
+ [% ELSE %]
+ <td>&nbsp;</td>
+ [% foundinactive = 1 %]
+ [% END %]
+
+ <td>
+ [% IF group.isactive %]
+ [% group.description %]
+ [% ELSE %]
+ [% group.description FILTER strike %]
+ [% END %]
+ </td>
+
+ </tr>
+ [% END %]
+
+ </table>
+
+ [% IF foundinactive %]
+ <font size="-1">(Note: Bugs may not be added to <strike>inactive
+ groups</strike>, only removed.)</font><br />
+ [% END %]
+
+[% END %]
+
+
+
+[% knum = 0 %]
+<input id="knob-none" type="radio" name="knob" value="none" CHECKED>
+<label for="knob-none">Do nothing else</label><br />
+
+[% IF bugstatuses.size == 1 && bugstatuses.0 == unconfirmedstate %]
+ [% knum = knum + 1 %]
+ <input id="knob-confirm" type="radio" name="knob" value="confirm>
+ <label for="knob-confirm">
+ Confirm bugs (change status to <b>NEW</b>)
+ </label><br />
+[% END %]
+
+[% knum = knum + 1 %]
+<input id="knob-accept" type="radio" name="knob" value="accept">
+<label for="knob-accept">
+ Accept bugs (change status to <b>ASSIGNED</b>)
+</label><br />
+
+[%# If all the bugs being changed are open, allow the user to close them. %]
+[% IF !bugstatuses.containsany(closedstates) %]
+ [% knum = knum + 1 %]
+ <input id="knob-clearresolution" type="radio" name="knob" value="clearresolution">
+ <label for="knob-clearresolution">Clear the resolution</label><br />
+
+ [% knum = knum + 1 %]
+ <input id="knob-resolve" type="radio" name="knob" value="resolve">
+ <label for="knob-resolve">
+ Resolve bugs, changing <A HREF="bug_status.html">resolution</A> to
+ </label>
+ <select name="resolution" onchange="document.forms.changeform.knob[[% knum %]].checked=true">
+ [% FOREACH resolution = resolutions %]
+ [% NEXT IF !resolution %]
+ <option value="[% resolution %]" [% selected IF resolution == "FIXED" %]>
+ [% resolution %]
+ </option>
+ [% END %]
+ </select><br />
+
+[% END %]
+
+[%# If all the bugs are closed, allow the user to reopen them. %]
+[% IF !bugstatuses.containsany(openstates) %]
+ [% knum = knum + 1 %]
+ <input id="knob-reopen" type="radio" name="knob" value="reopen">
+ <label for="knob-reopen">Reopen bugs</label><br />
+[% END %]
+
+[% IF bugstatuses.size == 1 %]
+ [% IF bugstatuses.contains('RESOLVED') %]
+ [% knum = knum + 1 %]
+ <input id="knob-verify" type="radio" name="knob" value="verify">
+ <label for="knob-verify">Mark bugs as <b>VERIFIED</b></label><br />
+ [% ELSIF bugstatuses.contains('VERIFIED') %]
+ [% knum = knum + 1 %]
+ <input id="knob-close" type="radio" name="knob" value="close">
+ <label for="knob-close">Mark bugs as <b>CLOSED</b></label><br />
+ [% END %]
+[% END %]
+
+[% knum = knum + 1 %]
+<input id="knob-reassign" type="radio" name="knob" value="reassign">
+<label for="knob-reassign"><a href="bug_status.html#assigned_to">
+ Reassign</A> bugs to
+</label>
+<input name="assigned_to"
+ value="[% user %]"
+ onchange="document.forms.changeform.knob[[% knum %]].checked = true;"
+ size="32"><br />
+
+[% knum = knum + 1 %]
+<input id="knob-reassignbycomponent"
+ type="radio"
+ name="knob"
+ value="reassignbycomponent">
+<label for="knob-reassignbycomponent">
+ Reassign bugs to owner of selected component
+</label><br />
+
+<input type="submit" value="Commit">
+
+[% IF ismover %]
+ <input type="submit" name="action" value="[% Param('move-button-text') %]">
+[% END %]
+
+
+[%############################################################################%]
+[%# Select Menu Block #%]
+[%############################################################################%]
+
+[% BLOCK selectmenu %]
+ <select id="[% menuname %]" name="[% menuname %]">
+ <option value="[% dontchange FILTER html %]" selected>
+ [% dontchange FILTER html %]
+ </option>
+ [% FOREACH menuitem = menuitems %]
+ <option value="[% menuitem FILTER html %]">[% menuitem FILTER html %]</option>
+ [% END %]
+ </select>
+[% END %]
diff --git a/template/default/buglist/server-push.html.tmpl b/template/default/buglist/server-push.html.tmpl
new file mode 100644
index 000000000..be10f7ab3
--- /dev/null
+++ b/template/default/buglist/server-push.html.tmpl
@@ -0,0 +1,35 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Myk Melez <myk@mozilla.org>
+ #%]
+
+<html>
+ <head>
+ <title>Bugzilla is pondering your query</title>
+ </head>
+ <body>
+ <h1 style="margin-top: 20%; text-align: center;">Please stand by ...</h1>
+
+ [% IF debug %]
+ <p>
+ <code>[% query FILTER html %]</code>
+ </p>
+ [% END %]
+
+ </body>
+</html>
diff --git a/template/default/buglist/table.tmpl b/template/default/buglist/table.tmpl
new file mode 100644
index 000000000..092ff8d2d
--- /dev/null
+++ b/template/default/buglist/table.tmpl
@@ -0,0 +1,142 @@
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Myk Melez <myk@mozilla.org>
+ #%]
+
+[%############################################################################%]
+[%# Initialization #%]
+[%############################################################################%]
+
+[%# Columns whose titles or values should be abbreviated to make the list
+ # more compact. For columns whose titles should be abbreviated,
+ # the shortened title is included. For columns whose values should be
+ # abbreviated, a maximum length is provided along with the ellipsis that
+ # should be added to an abbreviated value, if any.
+ #%]
+[% abbrev =
+ {
+ "severity" => { size => 3 , title => "Sev" } ,
+ "priority" => { size => 3 , title => "Pri" } ,
+ "platform" => { size => 3 , title => "Plt" } ,
+ "status" => { size => 4 } ,
+ "reporter" => { size => 45 , ellipsis => "..." } ,
+ "owner" => { size => 45 , ellipsis => "..." } ,
+ "qa_contact" => { size => 45 , ellipsis => "..." , title => "QAContact" } ,
+ "resolution" => { size => 4 } ,
+ "summary" => { size => 60 , ellipsis => "..." } ,
+ "status_whiteboard" => { title => "StatusSummary" } ,
+ "component" => { size => 8 , title => "Comp" } ,
+ "product" => { size => 8 } ,
+ "version" => { size => 5 , title => "Vers" } ,
+ "os" => { size => 4 } ,
+ "target_milestone" => { title => "TargetM" } ,
+ }
+%]
+
+[%############################################################################%]
+[%# Table Header #%]
+[%############################################################################%]
+
+[% tableheader = BLOCK %]
+ <table class="bz_buglist" cellspacing="0" cellpadding="4" width="100%">
+ <colgroup>
+ <col class="bz_id_column">
+ [% FOREACH id = displaycolumns %]
+ <col class="bz_[% id %]_column">
+ [% END %]
+ </colgroup>
+
+ <tr align="left">
+ <th colspan="[% splitheader ? 2 : 1 %]">
+ <a href="buglist.cgi?[% urlquerypart %]&order=bugs.bug_id">ID</a>
+ </th>
+
+ [% IF splitheader %]
+
+ [% FOREACH id = displaycolumns %]
+ [% NEXT IF loop.count() % 2 == 0 %]
+ [% column = columns.$id %]
+ [% PROCESS columnheader %]
+ [% END %]
+
+ </tr><tr align="left"><th>&nbsp;</th>
+
+ [% FOREACH id = displaycolumns %]
+ [% NEXT UNLESS loop.count() % 2 == 0 %]
+ [% column = columns.$id %]
+ [% PROCESS columnheader %]
+ [% END %]
+
+ [% ELSE %]
+
+ [% FOREACH id = displaycolumns %]
+ [% column = columns.$id %]
+ [% PROCESS columnheader %]
+ [% END %]
+
+ [% END %]
+
+ </tr>
+[% END %]
+
+[% BLOCK columnheader %]
+ <th colspan="[% splitheader ? 2 : 1 %]">
+ <a href="buglist.cgi?[% urlquerypart %]&order=
+ [% column.name FILTER uri html %]
+ [% ",$order" FILTER uri html IF order %]">
+ [%- abbrev.$id.title || column.title -%]</a>
+ </th>
+[% END %]
+
+
+[%############################################################################%]
+[%# Bug Table #%]
+[%############################################################################%]
+
+[% FOREACH bug = bugs %]
+ [% FLUSH IF loop.count() % 10 == 1 %]
+
+ [%# At the beginning of every hundred bugs in the list, start a new table. %]
+ [% IF loop.count() % 100 == 1 %]
+ [% tableheader %]
+ [% END %]
+
+ <tr class="bz_[% bug.severity %] bz_[% bug.priority %] [%+ "bz_secure" IF (bug.groupset && !usebuggroups) %]">
+
+ <td>
+ [% IF dotweak %]<input type="checkbox" name="id_[% bug.id %]">[% END %]
+ <a href="show_bug.cgi?id=[% bug.id %]">[% bug.id %]</a>
+ </td>
+
+ [% FOREACH column = displaycolumns %]
+ <td>
+ [%+ bug.$column.truncate(abbrev.$column.size, abbrev.$column.ellipsis) FILTER html %]
+ </td>
+ [% END %]
+
+ </tr>
+
+ [%# At the end of every hundred bugs in the list, or at the end of the list,
+ # end the current table.
+ #%]
+ [% IF loop.last() || loop.count() % 100 == 0 %]
+ </table>
+ [% END %]
+
+[% END %]
+
diff --git a/template/default/global/header b/template/default/global/header
index 627a52571..05afe2bed 100755
--- a/template/default/global/header
+++ b/template/default/global/header
@@ -11,14 +11,23 @@
<html>
<head>
<title>[% title %]</title>
+
[% Param('headerhtml') %]
+
[% jscript %]
+
[% IF style %]
<style type="text/css">
[% style %]
</style>
[% END %]
+
+ [% IF style_url %]
+ <link href="[% style_url %]" rel="stylesheet" type="text/css" />
+ [% END %]
+
</head>
+
<body [% Param('bodyhtml') %][% " " %][% extra %]>
[% PerformSubsts(Param('bannerhtml')) %]
diff --git a/template/default/global/message.html.tmpl b/template/default/global/message.html.tmpl
index 03253242a..912e9f322 100644
--- a/template/default/global/message.html.tmpl
+++ b/template/default/global/message.html.tmpl
@@ -1,6 +1,6 @@
[% DEFAULT title = "Bugzilla Message" %]
-[% INCLUDE global/header title=title %]
+[% PROCESS global/header %]
[%# The "header" template automatically displays the contents of a "message"
variable if it finds one, so it is not necessary to display the message
@@ -13,4 +13,4 @@
</p>
[% END %]
-[% INCLUDE global/footer %]
+[% PROCESS global/footer %]