diff options
author | myk%mozilla.org <> | 2002-03-12 22:54:53 +0100 |
---|---|---|
committer | myk%mozilla.org <> | 2002-03-12 22:54:53 +0100 |
commit | 496013d2cc0586cde9db0ace761292594fcae995 (patch) | |
tree | 619f2c7f7f253ce069595b09865d1bfb3c33e0f0 | |
parent | 38551035718d027fb8794f56a15fe1bf5a63676b (diff) | |
download | bugzilla-496013d2cc0586cde9db0ace761292594fcae995.tar.gz bugzilla-496013d2cc0586cde9db0ace761292594fcae995.tar.xz |
Fix for bug 103778: Rewrites and templatizes buglist.cgi.
Patch by Myk Melez <myk@mozilla.org>.
r=bbaetz,gerv
-rw-r--r-- | CGI.pl | 3 | ||||
-rwxr-xr-x | buglist.cgi | 1466 | ||||
-rwxr-xr-x | checksetup.pl | 16 | ||||
-rw-r--r-- | css/buglist.css | 38 | ||||
-rw-r--r-- | globals.pl | 120 | ||||
-rw-r--r-- | skins/standard/buglist.css | 38 | ||||
-rw-r--r-- | template/default/buglist/buglist-rdf.rdf.tmpl | 52 | ||||
-rw-r--r-- | template/default/buglist/buglist-simple.html.tmpl | 44 | ||||
-rw-r--r-- | template/default/buglist/buglist.html.tmpl | 160 | ||||
-rw-r--r-- | template/default/buglist/change-form.tmpl | 339 | ||||
-rw-r--r-- | template/default/buglist/server-push.html.tmpl | 35 | ||||
-rw-r--r-- | template/default/buglist/table.tmpl | 142 | ||||
-rwxr-xr-x | template/default/global/header | 9 | ||||
-rw-r--r-- | template/default/global/message.html.tmpl | 4 |
14 files changed, 1601 insertions, 865 deletions
@@ -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: <, >, &.") + && 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{ <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> </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> - -<NOBR><A HREF=\"enter_bug.cgi\">Enter New Bug</A></NOBR> - -<NOBR><A HREF=\"colchange.cgi?$::buffer\">Change columns</A></NOBR>"; - if (!$dotweak && $count > 1 && UserInGroup("editbugs")) { - print " \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{ \n}; - print qq{<A HREF="mailto:$list">Send mail to bug 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{ \n}; - print qq{<A HREF="mailto:$list">Send mail to bug QA contacts</A>\n}; - } - print qq{ \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> + <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> + <a href="enter_bug.cgi">Enter New Bug</a> + <a href="colchange.cgi?[% urlquerypart %]">Change Columns</a> + + [% 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> + + [% END %] + + [% IF bugowners %] + <a href="mailto:[% bugowners %]">Send Mail to Bug Owners</a> + [% END %] + + <a href="query.cgi?[% urlquerypart %]">Edit this Query</a> + + </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> </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> </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 %] |