]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 98707. Query.cgi rewrite. Patch by me, layout by mpt and others, r=justdave,...
authorgerv%gerv.net <>
Mon, 19 Nov 2001 06:23:19 +0000 (06:23 +0000)
committergerv%gerv.net <>
Mon, 19 Nov 2001 06:23:19 +0000 (06:23 +0000)
CGI.pl
query.cgi

diff --git a/CGI.pl b/CGI.pl
index 57f3ee96787ed3ad3227b09714d3caa236c02695..da06191e43f99a4fb7f66dc231930ec941bd5fac 100644 (file)
--- a/CGI.pl
+++ b/CGI.pl
@@ -29,6 +29,8 @@
 
 use diagnostics;
 use strict;
+use lib ".";
+
 # use Carp;                       # for confess
 
 # commented out the following snippet of code. this tosses errors into the
index 30709275d149a86d9652c3069d107eaac65a4fcf..3795c98ea29cb026fe4b562c3aff9f8965a84909 100755 (executable)
--- a/query.cgi
+++ b/query.cgi
@@ -1,4 +1,4 @@
-#!/usr/bonsaitools/bin/perl -w
+#!/usr/bonsaitools/bin/perl -wT
 # -*- Mode: perl; indent-tabs-mode: nil -*-
 #
 # The contents of this file are subject to the Mozilla Public
 # Contributor(s): Terry Weissman <terry@mozilla.org>
 #                 David Gardiner <david.gardiner@unisa.edu.au>
 #                 Matthias Radestock <matthias@sorted.org>
-#                 Chris Lahey <clahey@ximian.com> [javascript fixes]
-#                 Christian Reis <kiko@async.com.br> [javascript rewrite]
+#                 Gervase Markham <gerv@gerv.net>
 
 use diagnostics;
 use strict;
+use lib ".";
 
 require "CGI.pl";
 
-$::CheckOptionValues = 0;       # It's OK if we have some bogus things in the
-                                # pop-up lists here, from a remembered query
-                                # that is no longer quite valid.  We don't
-                                # want to crap out in the query page.
-
-# Shut up misguided -w warnings about "used only once":
+# Prevents &make_options in CGI.pl from throwing an error if we give it
+# an invalid list of selections (from a remembered query containing values
+# that no longer exist). We don't want to die in the query page even if
+# strict value checks are turned on.
+$::CheckOptionValues = 0;
 
 use vars
-  @::CheckOptionValues,
-  @::legal_resolution,
-  @::legal_bug_status,
-  @::legal_components,
-  @::legal_keywords,
-  @::legal_opsys,
-  @::legal_platform,
-  @::legal_priority,
-  @::legal_product,
-  @::legal_severity,
-  @::legal_target_milestone,
-  @::legal_versions,
-  @::log_columns,
-  %::versions,
-  %::components,
-  %::FORM;
-
+    @::CheckOptionValues,
+    @::legal_resolution,
+    @::legal_bug_status,
+    @::legal_components,
+    @::legal_keywords,
+    @::legal_opsys,
+    @::legal_platform,
+    @::legal_priority,
+    @::legal_product,
+    @::legal_severity,
+    @::legal_target_milestone,
+    @::legal_versions,
+    @::log_columns,
+    %::versions,
+    %::components,
+    %::FORM;
+
+# Use the template toolkit (http://www.template-toolkit.org/) to generate
+# the user interface (HTML pages and mail messages) using templates in the
+# "template/" subdirectory.
+use Template;
+
+# Create the global template object that processes templates and specify
+# configuration parameters that apply to all templates processed in this script.
+my $template = Template->new(
+{
+    # Colon-separated list of directories containing templates.
+    INCLUDE_PATH => "template/custom:template/default",
+    # Allow templates to be specified with relative paths.
+    RELATIVE => 1,
+    PRE_CHOMP => 1,
+    FILTERS =>
+    {
+         # Returns the text with backslashes, single/double quotes,
+         # and newlines/carriage returns escaped for use in JS strings.
+        'js' => sub
+        {
+            my ($var) = @_;
+            $var =~ s/([\\\'\"])/\\$1/g; 
+            $var =~ s/\n/\\n/g; 
+            $var =~ s/\r/\\r/g; 
+            return $var;
+        }
+    },
+});
+
+# Define the global variables and functions that will be passed to the UI 
+# template.  Individual functions add their own values to this hash before
+# sending them to the templates they process.
+my $vars = 
+{
+    # Function for retrieving global parameters.
+    'Param' => \&Param, 
+
+    # Function for processing global parameters that contain references
+    # to other global parameters.
+    'PerformSubsts' => \&PerformSubsts,
+    # Function to search an array for a value   
+    'lsearch' => \&lsearch,
+};
 
 if (defined $::FORM{"GoAheadAndLogIn"}) {
     # We got here from a login page, probably from relogin.cgi.  We better
@@ -62,15 +105,11 @@ if (defined $::FORM{"GoAheadAndLogIn"}) {
 } else {
     quietly_check_login();
 }
-my $userid = 0;
-if (defined $::COOKIE{"Bugzilla_login"}) {
-    $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"});
-}
 
-# Backwards compatability hack -- if there are any of the old QUERY_*
+# Backwards compatibility hack -- if there are any of the old QUERY_*
 # cookies around, and we are logged in, then move them into the database
-# and nuke the cookie.
-if ($userid) {
+# and nuke the cookie. This is required for Bugzilla 2.8 and earlier.
+if ($::userid) {
     my @oldquerycookies;
     foreach my $i (keys %::COOKIE) {
         if ($i =~ /^QUERY_(.*)$/) {
@@ -87,65 +126,68 @@ if ($userid) {
             if ($value) {
                 my $qname = SqlQuote($name);
                 SendSQL("SELECT query FROM namedqueries " .
-                        "WHERE userid = $userid AND name = $qname");
+                        "WHERE userid = $::userid AND name = $qname");
                 my $query = FetchOneColumn();
                 if (!$query) {
                     SendSQL("REPLACE INTO namedqueries " .
                             "(userid, name, query) VALUES " .
-                            "($userid, $qname, " . SqlQuote($value) . ")");
+                            "($::userid, $qname, " . SqlQuote($value) . ")");
                 }
             }
-            print "Set-Cookie: $cookiename= ; path=" . Param("cookiepath"). "; expires=Sun, 30-Jun-1980 00:00:00 GMT\n";
+            print "Set-Cookie: $cookiename= ; path=" . Param("cookiepath") . 
+                  "; expires=Sun, 30-Jun-1980 00:00:00 GMT\n";
         }
     }
 }
-                
-
-
 
 if ($::FORM{'nukedefaultquery'}) {
-    if ($userid) {
+    if ($::userid) {
         SendSQL("DELETE FROM namedqueries " .
-                "WHERE userid = $userid AND name = '$::defaultqueryname'");
+                "WHERE userid = $::userid AND name = '$::defaultqueryname'");
     }
     $::buffer = "";
 }
 
-
 my $userdefaultquery;
-if ($userid) {
+if ($::userid) {
     SendSQL("SELECT query FROM namedqueries " .
-            "WHERE userid = $userid AND name = '$::defaultqueryname'");
+            "WHERE userid = $::userid AND name = '$::defaultqueryname'");
     $userdefaultquery = FetchOneColumn();
 }
 
 my %default;
-my %type;
 
-sub ProcessFormStuff {
+# We pass the defaults as a hash of references to arrays. For those
+# Items which are single-valued, the template should only reference [0]
+# and ignore any multiple values.
+sub PrefillForm {
     my ($buf) = (@_);
     my $foundone = 0;
+
+    # Nothing must be undef, otherwise the template complains.
     foreach my $name ("bug_status", "resolution", "assigned_to",
                       "rep_platform", "priority", "bug_severity",
                       "product", "reporter", "op_sys",
                       "component", "version", "chfield", "chfieldfrom",
                       "chfieldto", "chfieldvalue", "target_milestone",
-                      "email1", "emailtype1", "emailreporter1",
-                      "emailassigned_to1", "emailcc1", "emailqa_contact1",
-                      "emaillongdesc1",
-                      "email2", "emailtype2", "emailreporter2",
-                      "emailassigned_to2", "emailcc2", "emailqa_contact2",
-                      "emaillongdesc2",
+                      "email", "emailtype", "emailreporter",
+                      "emailassigned_to", "emailcc", "emailqa_contact",
+                      "emaillongdesc",
                       "changedin", "votes", "short_desc", "short_desc_type",
                       "long_desc", "long_desc_type", "bug_file_loc",
                       "bug_file_loc_type", "status_whiteboard",
                       "status_whiteboard_type", "bug_id",
                       "bugidtype", "keywords", "keywords_type") {
-        $default{$name} = "";
-        $type{$name} = 0;
+        # This is a bit of a hack. The default, empty list has 
+        # three entries to accommodate the needs of the email fields -
+        # we use each position to denote the relevant field. Array
+        # position 0 is unused for email fields because the form 
+        # parameters historically started at 1.
+        $default{$name} = ["", "", ""];
     }
-
-
+    # Iterate over the URL parameters
     foreach my $item (split(/\&/, $buf)) {
         my @el = split(/=/, $item);
         my $name = $el[0];
@@ -155,870 +197,216 @@ sub ProcessFormStuff {
         } else {
             $value = "";
         }
-        if (defined $default{$name}) {
+        
+        # If the name ends in a number (which it does for the fields which
+        # are part of the email searching), we use the array
+        # positions to show the defaults for that number field.
+        if ($name =~ m/^(.+)(\d)$/ && defined($default{$1})) {
             $foundone = 1;
-            if ($default{$name} ne "") {
-                $default{$name} .= "|$value";
-                $type{$name} = 1;
-            } else {
-                $default{$name} = $value;
-            }
+            $default{$1}->[$2] = $value;
         }
-    }
+        # If there's no default yet, we replace the blank string.
+        elsif (defined($default{$name}) && $default{$name}->[0] eq "") {
+            $foundone = 1;
+            $default{$name} = [$value]; 
+        } 
+        # If there's already a default, we push on the new value.
+        elsif (defined($default{$name})) {
+            push (@{$default{$name}}, $value);
+        }        
+    }        
     return $foundone;
 }
 
 
-if (!ProcessFormStuff($::buffer)) {
+if (!PrefillForm($::buffer)) {
     # Ah-hah, there was no form stuff specified.  Do it again with the
     # default query.
     if ($userdefaultquery) {
-        ProcessFormStuff($userdefaultquery);
+        PrefillForm($userdefaultquery);
     } else {
-        ProcessFormStuff(Param("defaultquery"));
+        PrefillForm(Param("defaultquery"));
     }
 }
 
-
-                 
-
-if ($default{'chfieldto'} eq "") {
-    $default{'chfieldto'} = "Now";
+if ($default{'chfieldto'}->[0] eq "") {
+    $default{'chfieldto'} = ["Now"];
 }
 
-
-
-print "Set-Cookie: BUGLIST=
-Content-type: text/html\n\n";
-
 GetVersionTable();
 
-sub GenerateEmailInput {
-    my ($id) = (@_);
-    my $defstr = value_quote($default{"email$id"});
-    my $deftype = $default{"emailtype$id"};
-    if ($deftype eq "") {
-        $deftype = "substring";
-    }
-    my $assignedto = ($default{"emailassigned_to$id"} eq "1") ? "checked" : "";
-    my $reporter = ($default{"emailreporter$id"} eq "1") ? "checked" : "";
-    my $cc = ($default{"emailcc$id"} eq "1") ? "checked" : "";
-    my $longdesc = ($default{"emaillongdesc$id"} eq "1") ? "checked" : "";
-
-    my $qapart = "";
-    my $qacontact = "";
-    if (Param("useqacontact")) {
-        $qacontact = ($default{"emailqa_contact$id"} eq "1") ? "checked" : "";
-        $qapart = qq|
-<tr>
-<td></td>
-<td>
-<input type="checkbox" name="emailqa_contact$id" value=1 $qacontact>QA Contact
-</td>
-</tr>
-|;
-    }
-    if ($assignedto eq "" && $reporter eq "" && $cc eq "" &&
-          $qacontact eq "") {
-        if ($id eq "1") {
-            $assignedto = "checked";
-        } else {
-            $reporter = "checked";
-        }
-    }
-
-
-    $default{"emailtype$id"} ||= "substring";
-
-    return qq{
-<table border=1 cellspacing=0 cellpadding=0>
-<tr><td>
-<table cellspacing=0 cellpadding=0>
-<tr>
-<td rowspan=2 valign=top><a href="queryhelp.cgi#peopleinvolved">Email:</a>
-<input name="email$id" size="30" value="$defstr">&nbsp;matching as
-} . BuildPulldown("emailtype$id",
-                  [["regexp", "regexp"],
-                   ["notregexp", "not regexp"],
-                   ["substring", "substring"],
-                   ["exact", "exact"]],
-                  $default{"emailtype$id"}) . qq{
-</td>
-<td>
-<input type="checkbox" name="emailassigned_to$id" value=1 $assignedto>Assigned To
-</td>
-</tr>
-<tr>
-<td>
-<input type="checkbox" name="emailreporter$id" value=1 $reporter>Reporter
-</td>
-</tr>$qapart
-<tr>
-<td align=right>(Will match any of the selected fields)</td>
-<td>
-<input type="checkbox" name="emailcc$id" value=1 $cc>CC
-</td>
-</tr>
-<tr>
-<td></td>
-<td>
-<input type="checkbox" name="emaillongdesc$id" value=1 $longdesc>Added comment
-</td>
-</tr>
-</table>
-</table>
-};
-}
-
-
-            
-
-
-my $emailinput1 = GenerateEmailInput(1);
-my $emailinput2 = GenerateEmailInput(2);
-
 # if using usebuggroups, then we don't want people to see products they don't
-# have access to.  remove them from the list.
+# have access to. Remove them from the list.
 
-@::product_list = ();
+my @products = ();
 my %component_set;
 my %version_set;
 my %milestone_set;
 foreach my $p (@::legal_product) {
-  if(Param("usebuggroups")
-     && GroupExists($p)
-     && !UserInGroup($p)) {
     # If we're using bug groups to restrict entry on products, and
     # this product has a bug group, and the user is not in that
     # group, we don't want to include that product in this list.
-    next;
-  }
-  push @::product_list, $p;
-  if ($::components{$p}) {
-    foreach my $c (@{$::components{$p}}) {
-      $component_set{$c} = 1;
-    }
-  }
-  foreach my $v (@{$::versions{$p}}) {
-    $version_set{$v} = 1;
-  }
-  foreach my $m (@{$::target_milestone{$p}}) {
-    $milestone_set{$m} = 1;
-  }
-}
-
-@::component_list = ();
-@::version_list = ();
-@::milestone_list = ();
-foreach my $c (@::legal_components) {
-  if ($component_set{$c}) {
-    push @::component_list, $c;
-  }
-}
-foreach my $v (@::legal_versions) {
-  if ($version_set{$v}) {
-    push @::version_list, $v;
-  }
-}
-foreach my $m (@::legal_target_milestone) {
-  if ($milestone_set{$m}) {
-    push @::milestone_list, $m;
-  }
-}
-
-# SELECT box javascript handling. This is done to make the component,
-# versions and milestone SELECTs repaint automatically when a product is
-# selected. Refactored for bug 96534.
-    
-# make_js_array: iterates through the product array creating a
-# javascript array keyed by product with an alphabetically ordered array
-# for the corresponding elements in the components array passed in.
-# return a string with javascript definitions for the product in a nice
-# arrays which can be linearly appended later on.
-
-# make_js_array ( \@products, \%[components/versions/milestones], $array )
-
-sub make_js_array {
-    my @prods = @{$_[0]};
-    my %data = %{$_[1]};
-    my $arr = $_[2];
-
-    my $ret = "\nvar $arr = new Array();\n";
-    foreach my $p ( @prods ) {
-        # join each element with a "," case-insensitively alpha sorted
-        if ( $data{$p} ) {
-            $ret .= $arr."[".SqlQuote($p)."] = [";
-            # the SqlQuote() protects our 's.
-            my @tmp = map( SqlQuote( $_ ), @{ $data{$p} } );
-            # do the join on a sorted, quoted list
-            @tmp = sort { lc( $a ) cmp lc( $b ) } @tmp;
-            $ret .= join( ", ", @tmp );
-            $ret .= "];\n";
+    next if (Param("usebuggroups") && GroupExists($p) && !UserInGroup($p));
+
+    # We build up boolean hashes in the "-set" hashes for each of these things 
+    # before making a list because there may be duplicates names across products.
+    push @products, $p;
+    if ($::components{$p}) {
+        foreach my $c (@{$::components{$p}}) {
+            $component_set{$c} = 1;
         }
     }
-    return $ret;
-}
-
-my $jscript = '<script language="JavaScript" type="text/javascript">';
-$jscript .= "\n<!--\n\n";
-
-# Add the javascript code for the arrays of components and versions
-# This is used in our javascript functions
-
-$jscript .= "var usetms = 0; // do we have target milestone?\n";
-$jscript .= "var first_load = 1; // is this the first time we load the page?\n";
-$jscript .= "var last_sel = []; // caches last selection\n";
-$jscript .= make_js_array( \@::product_list, \%::components, "cpts" );
-$jscript .= make_js_array( \@::product_list, \%::versions, "vers" );
-
-if ( Param( "usetargetmilestone" ) ) {
-    $jscript .= make_js_array(\@::product_list, \%::target_milestone, "tms");
-    $jscript .= "\nusetms = 1; // hooray, we use target milestones\n";
-}
-
-$jscript .= << 'ENDSCRIPT';
-
-// Adds to the target select object all elements in array that
-// correspond to the elements selected in source.
-//     - array should be a array of arrays, indexed by product name. the
-//       array should contain the elements that correspont to that
-//       product. Example:
-//         var array = Array();
-//         array['ProductOne'] = [ 'ComponentA', 'ComponentB' ];
-//         updateSelect(array, source, target);
-//     - sel is a list of selected items, either whole or a diff
-//       depending on sel_is_diff.
-//     - sel_is_diff determines if we are sending in just a diff or the
-//       whole selection. a diff is used to optimize adding selections.
-//     - target should be the target select object.
-//     - single specifies if we selected a single item. if we did, no
-//       need to merge.
-
-function updateSelect( array, sel, target, sel_is_diff, single ) {
-        
-    var i, comp;
-
-    // if single, even if it's a diff (happens when you have nothing
-    // selected and select one item alone), skip this.
-    if ( ! single ) {
-
-        // array merging/sorting in the case of multiple selections
-        if ( sel_is_diff ) {
-        
-            // merge in the current options with the first selection
-            comp = merge_arrays( array[sel[0]], target.options, 1 );
-
-            // merge the rest of the selection with the results
-            for ( i = 1 ; i < sel.length ; i++ ) {
-                comp = merge_arrays( array[sel[i]], comp, 0 );
-            }
-        } else {
-            // here we micro-optimize for two arrays to avoid merging with a
-            // null array 
-            comp = merge_arrays( array[sel[0]],array[sel[1]], 0 );
-
-            // merge the arrays. not very good for multiple selections.
-            for ( i = 2; i < sel.length; i++ ) {
-                comp = merge_arrays( comp, array[sel[i]], 0 );
-            }
-        }
-    } else {
-        // single item in selection, just get me the list
-        comp = array[sel[0]];
+    foreach my $v (@{$::versions{$p}}) {
+        $version_set{$v} = 1;
     }
-
-    // clear select
-    target.options.length = 0;
-
-    // load elements of list into select
-    for ( i = 0; i < comp.length; i++ ) {
-        target.options[i] = new Option( comp[i], comp[i] );
+    foreach my $m (@{$::target_milestone{$p}}) {
+        $milestone_set{$m} = 1;
     }
 }
 
-// Returns elements in a that are not in b.
-// NOT A REAL DIFF: does not check the reverse.
-//     - a,b: arrays of values to be compare.
-
-function fake_diff_array( a, b ) {
-    var newsel = new Array();
+# @products is now all the products we are ever concerned with, as a list
+# %x_set is now a unique "list" of the relevant components/versions/tms
+@products = sort { lc($a) cmp lc($b) } @products;
 
-    // do a boring array diff to see who's new
-    for ( var ia in a ) {
-        var found = 0;
-        for ( var ib in b ) {
-            if ( a[ia] == b[ib] ) {
-                found = 1;
-            }
-        }
-        if ( ! found ) {
-            newsel[newsel.length] = a[ia];
-        }
-        found = 0;
+# Create the component, version and milestone lists.
+my @components = ();
+my @versions = ();
+my @milestones = ();
+foreach my $c (@::legal_components) {
+    if ($component_set{$c}) {
+        push @components, $c;
     }
-    return newsel;
 }
-
-// takes two arrays and sorts them by string, returning a new, sorted
-// array. the merge removes dupes, too.
-//     - a, b: arrays to be merge.
-//     - b_is_select: if true, then b is actually an optionitem and as
-//       such we need to use item.value on it.
-
-function merge_arrays( a, b, b_is_select ) {
-    var pos_a = 0;
-    var pos_b = 0;
-    var ret = new Array();
-    var bitem, aitem;
-
-    // iterate through both arrays and add the larger item to the return
-    // list. remove dupes, too. Use toLowerCase to provide
-    // case-insensitivity.
-
-    while ( ( pos_a < a.length ) && ( pos_b < b.length ) ) {
-
-        if ( b_is_select ) {
-            bitem = b[pos_b].value;
-        } else {
-            bitem = b[pos_b];
-        }
-        aitem = a[pos_a];
-
-        // smaller item in list a
-        if ( aitem.toLowerCase() < bitem.toLowerCase() ) {
-            ret[ret.length] = aitem;
-            pos_a++;
-        } else {
-            // smaller item in list b
-            if ( aitem.toLowerCase() > bitem.toLowerCase() ) {
-                ret[ret.length] = bitem;
-                pos_b++;
-            } else {
-                // list contents are equal, inc both counters. 
-                ret[ret.length] = aitem;
-                pos_a++;
-                pos_b++;
-            }
-        }
-    }
-
-    // catch leftovers here. these sections are ugly code-copying.
-    if ( pos_a < a.length ) {
-        for ( ; pos_a < a.length ; pos_a++ ) {
-            ret[ret.length] = a[pos_a];
-        }
-    }
-
-    if ( pos_b < b.length ) {
-        for ( ; pos_b < b.length; pos_b++ ) {
-            if ( b_is_select ) {
-                bitem = b[pos_b].value;
-            } else {
-                bitem = b[pos_b];
-            }
-            ret[ret.length] = bitem;
-        }
+foreach my $v (@::legal_versions) {
+    if ($version_set{$v}) {
+        push @versions, $v;
     }
-    return ret;
 }
-
-// selectProduct reads the selection from f.product and updates
-// f.version, component and target_milestone accordingly.
-//     - f: a form containing product, component, varsion and
-//       target_milestone select boxes. 
-// globals (3vil!):
-//     - cpts, vers, tms: array of arrays, indexed by product name. the
-//       subarrays contain a list of names to be fed to the respective
-//       selectboxes. For bugzilla, these are generated with perl code
-//       at page start.
-//     - usetms: this is a global boolean that is defined if the
-//       bugzilla installation has it turned on. generated in perl too.
-//     - first_load: boolean, specifying if it's the first time we load
-//       the query page.
-//     - last_sel: saves our last selection list so we know what has
-//       changed, and optimize for additions.
-
-function selectProduct( f ) {
-
-    // this is to avoid handling events that occur before the form
-    // itself is ready, which happens in buggy browsers.
-
-    if ( ( !f ) || ( ! f.product ) ) {
-        return;
-    }
-
-    // if this is the first load and nothing is selected, no need to
-    // merge and sort all components; perl gives it to us sorted.
-
-    if ( ( first_load ) && ( f.product.selectedIndex == -1 ) ) {
-            first_load = 0;
-            return;
-    }
-    
-    // turn first_load off. this is tricky, since it seems to be
-    // redundant with the above clause. It's not: if when we first load
-    // the page there is _one_ element selected, it won't fall into that
-    // clause, and first_load will remain 1. Then, if we unselect that
-    // item, selectProduct will be called but the clause will be valid
-    // (since selectedIndex == -1), and we will return - incorrectly -
-    // without merge/sorting.
-
-    first_load = 0;
-
-    // - sel keeps the array of products we are selected. 
-    // - is_diff says if it's a full list or just a list of products that
-    //   were added to the current selection. 
-    // - single indicates if a single item was selected
-    var sel = Array();
-    var is_diff = 0;
-    var single;
-
-    // if nothing selected, pick all
-    if ( f.product.selectedIndex == -1 ) {
-        for ( var i = 0 ; i < f.product.length ; i++ ) {
-            sel[sel.length] = f.product.options[i].value;
-        }
-        single = 0;
-    } else {
-
-        for ( i = 0 ; i < f.product.length ; i++ ) {
-            if ( f.product.options[i].selected ) {
-                sel[sel.length] = f.product.options[i].value;
-            }
-        }
-
-        single = ( sel.length == 1 );
-
-        // save last_sel before we kill it
-        var tmp = last_sel;
-        last_sel = sel;
-    
-        // this is an optimization: if we've added components, no need
-        // to remerge them; just merge the new ones with the existing
-        // options.
-
-        if ( ( tmp ) && ( tmp.length < sel.length ) ) {
-            sel = fake_diff_array(sel, tmp);
-            is_diff = 1;
-        }
-    }
-
-    // do the actual fill/update
-    updateSelect( cpts, sel, f.component, is_diff, single );
-    updateSelect( vers, sel, f.version, is_diff, single );
-    if ( usetms ) {
-        updateSelect( tms, sel, f.target_milestone, is_diff, single );
+foreach my $m (@::legal_target_milestone) {
+    if ($milestone_set{$m}) {
+        push @milestones, $m;
     }
 }
 
-// -->
-</script>
-ENDSCRIPT
-
-#
-# End the fearsome Javascript section. 
-#
-
-# Muck the "legal product" list so that the default one is always first (and
-# is therefore visibly selected.
-
-# Commented out, until we actually have enough products for this to matter.
-
-# set w [lsearch $legal_product $default{"product"}]
-# if {$w >= 0} {
-#    set legal_product [concat $default{"product"} [lreplace $legal_product $w $w]]
-# }
-
-PutHeader("Bugzilla Query Page", "Query", 
-          "This page lets you search the database for recorded bugs.",
-          q{onLoad="selectProduct(document.forms['queryform']);"}, $jscript);
-
-push @::legal_resolution, "---"; # Oy, what a hack.
-
-my @logfields = ("[Bug creation]", @::log_columns);
-
-print"<P>Give me a <A HREF=\"queryhelp.cgi\">clue</A> about how to use this form.<P>";
-
-print qq{
-<FORM METHOD=GET ACTION="buglist.cgi" NAME="queryform">
-
-<table>
-<tr>
-<th align=left><A HREF="queryhelp.cgi#status">Status</a>:</th>
-<th align=left><A HREF="queryhelp.cgi#resolution">Resolution</a>:</th>
-<th align=left><A HREF="queryhelp.cgi#platform">Platform</a>:</th>
-<th align=left><A HREF="queryhelp.cgi#opsys">OpSys</a>:</th>
-<th align=left><A HREF="queryhelp.cgi#priority">Priority</a>:</th>
-<th align=left><A HREF="queryhelp.cgi#severity">Severity</a>:</th>
-};
-
-print "
-</tr>
-<tr>
-<td align=left valign=top>
-
-@{[make_selection_widget(\"bug_status\",\@::legal_bug_status,$default{'bug_status'}, $type{'bug_status'}, 1)]}
-
-</td>
-<td align=left valign=top>
-@{[make_selection_widget(\"resolution\",\@::legal_resolution,$default{'resolution'}, $type{'resolution'}, 1)]}
-
-</td>
-<td align=left valign=top>
-@{[make_selection_widget(\"rep_platform\",\@::legal_platform,$default{'platform'}, $type{'platform'}, 1)]}
-
-</td>
-<td align=left valign=top>
-@{[make_selection_widget(\"op_sys\",\@::legal_opsys,$default{'op_sys'}, $type{'op_sys'}, 1)]}
-
-</td>
-<td align=left valign=top>
-@{[make_selection_widget(\"priority\",\@::legal_priority,$default{'priority'}, $type{'priority'}, 1)]}
-
-</td>
-<td align=left valign=top>
-@{[make_selection_widget(\"bug_severity\",\@::legal_severity,$default{'bug_severity'}, $type{'bug_severity'}, 1)]}
-
-</tr>
-</table>
-
-<p>
-
-<table>
-<tr><td colspan=2>
-$emailinput1<p>
-</td></tr><tr><td colspan=2>
-$emailinput2<p>
-</td></tr>";
-
-my $inclselected = "SELECTED";
-my $exclselected = "";
-
-    
-if ($default{'bugidtype'} eq "exclude") {
-    $inclselected = "";
-    $exclselected = "SELECTED";
-}
-my $bug_id = value_quote($default{'bug_id'}); 
-
-print qq{
-<TR>
-<TD COLSPAN="3">
-<SELECT NAME="bugidtype">
-<OPTION VALUE="include" $inclselected>Only
-<OPTION VALUE="exclude" $exclselected>Exclude
-</SELECT>
-bugs numbered: 
-<INPUT TYPE="text" NAME="bug_id" VALUE="$bug_id" SIZE=30>
-</TD>
-</TR>
-};
-
-print "
-<tr>
-<td>
-Changed in the <NOBR>last <INPUT NAME=changedin SIZE=2 VALUE=\"$default{'changedin'}\"> days.</NOBR>
-</td>
-<td align=right>
-At <NOBR>least <INPUT NAME=votes SIZE=3 VALUE=\"$default{'votes'}\"> votes.</NOBR>
-</tr>
-</table>
-
-
-<table>
-<tr>
-<td rowspan=2 align=right>Where the field(s)
-</td><td rowspan=2>
-<SELECT NAME=\"chfield\" MULTIPLE SIZE=4>
-@{[make_options(\@logfields, $default{'chfield'}, $type{'chfield'})]}
-</SELECT>
-</td><td rowspan=2>
-changed.
-</td><td>
-<nobr>dates <INPUT NAME=chfieldfrom SIZE=10 VALUE=\"$default{'chfieldfrom'}\"></nobr>
-<nobr>to <INPUT NAME=chfieldto SIZE=10 VALUE=\"$default{'chfieldto'}\"></nobr>
-</td>
-</tr>
-<tr>
-<td>changed to value <nobr><INPUT NAME=chfieldvalue SIZE=10> (optional)</nobr>
-</td>
-</table>
-
-
-<P>
-
-<table>
-<tr>
-<TH ALIGN=LEFT VALIGN=BOTTOM>Program:</th>
-<TH ALIGN=LEFT VALIGN=BOTTOM>Version:</th>
-<TH ALIGN=LEFT VALIGN=BOTTOM><A HREF=describecomponents.cgi>Component:</a></th>
-";
-
-if (Param("usetargetmilestone")) {
-    print "<TH ALIGN=LEFT VALIGN=BOTTOM>Target Milestone:</th>";
-}
-
-print "
-</tr>
-<tr>
-
-<td align=left valign=top>
-<SELECT NAME=\"product\" MULTIPLE SIZE=5 onChange=\"selectProduct(this.form);\">
-@{[make_options(\@::product_list, $default{'product'}, $type{'product'})]}
-</SELECT>
-</td>
+# Sort the component list...
+my $comps = \%::components;
+foreach my $p (@products) {
+    my @tmp = sort { lc($a) cmp lc($b) } @{$comps->{$p}};
+    $comps->{$p} = \@tmp;
+}    
 
-<td align=left valign=top>
-<SELECT NAME=\"version\" MULTIPLE SIZE=5>
-@{[make_options(\@::version_list, $default{'version'}, $type{'version'})]}
-</SELECT>
-</td>
+# and the version list...
+my $vers = \%::versions;
+foreach my $p (@products) {
+    my @tmp = sort { lc($a) cmp lc($b) } @{$vers->{$p}};
+    $vers->{$p} = \@tmp;
+}    
 
-<td align=left valign=top>
-<SELECT NAME=\"component\" MULTIPLE SIZE=5>
-@{[make_options(\@::component_list, $default{'component'}, $type{'component'})]}
-</SELECT>
-</td>";
-
-if (Param("usetargetmilestone")) {
-    print "
-<td align=left valign=top>
-<SELECT NAME=\"target_milestone\" MULTIPLE SIZE=5>
-@{[make_options(\@::milestone_list, $default{'target_milestone'}, $type{'target_milestone'})]}
-</SELECT>
-</td>";
+# and the milestone list.
+my $mstones;
+if (Param('usetargetmilestone')) {
+    $mstones = \%::target_milestone;
+    foreach my $p (@products) {
+        my @tmp = sort { lc($a) cmp lc($b) } @{$mstones->{$p}};
+        $mstones->{$p} = \@tmp;
+    }    
 }
 
+# "foo" or "foos" is a list of all the possible (or legal) products, 
+# components, versions or target milestones.
+# "foobyproduct" is a hash, keyed by product, of sorted lists
+# of the same data.
 
-sub StringSearch {
-    my ($desc, $name) = (@_);
-    my $type = $name . "_type";
-    my $def = value_quote($default{$name});
-    print qq{<tr>
-<td align=right>$desc:</td>
-<td><input name=$name size=30 value="$def"></td>
-<td><SELECT NAME=$type>
-};
-    if ($default{$type} eq "") {
-        $default{$type} = "allwordssubstr";
-    }
-    foreach my $i (["substring", "case-insensitive substring"],
-                   ["casesubstring", "case-sensitive substring"],
-                   ["allwordssubstr", "all words as substrings"],
-                   ["anywordssubstr", "any words as substrings"],
-                   ["allwords", "all words"],
-                   ["anywords", "any words"],
-                   ["regexp", "regular expression"],
-                   ["notregexp", "not ( regular expression )"]) {
-        my ($n, $d) = (@$i);
-        my $sel = "";
-        if ($default{$type} eq $n) {
-            $sel = " SELECTED";
-        }
-        print qq{<OPTION VALUE="$n"$sel>$d\n};
-    }
-    print "</SELECT></TD>
-</tr>
-";
-}
+$vars->{'product'} = \@products;
 
-print "
-</tr>
-</table>
+# We use 'component_' because 'component' is a Template Toolkit reserved word.
+$vars->{'componentsbyproduct'} = $comps;
+$vars->{'component_'} = \@components;
 
-<table border=0>
-";
-
-StringSearch("Summary", "short_desc");
-StringSearch("A description entry", "long_desc");
-StringSearch("URL", "bug_file_loc");
-
-if (Param("usestatuswhiteboard")) {
-    StringSearch("Status whiteboard", "status_whiteboard");
-}
+$vars->{'versionsbyproduct'} = $vers;
+$vars->{'version'} = \@versions;
 
-if (@::legal_keywords) {
-    my $def = value_quote($default{'keywords'});
-    print qq{
-<TR>
-<TD ALIGN="right"><A HREF="describekeywords.cgi">Keywords</A>:</TD>
-<TD><INPUT NAME="keywords" SIZE=30 VALUE="$def"></TD>
-<TD>
-};
-    my $type = $default{"keywords_type"};
-    if ($type eq "or") {        # Backward compatability hack.
-        $type = "anywords";
-    }
-    print BuildPulldown("keywords_type",
-                        [["anywords", "Any of the listed keywords set"],
-                         ["allwords", "All of the listed keywords set"],
-                         ["nowords", "None of the listed keywords set"]],
-                        $type);
-    print qq{</TD></TR>};
+if (Param('usetargetmilestone')) {
+    $vars->{'milestonesbyproduct'} = $mstones;
+    $vars->{'target_milestone'} = \@milestones;
 }
 
-print "
-</table>
-<p>
-";
-
+$vars->{'have_keywords'} = scalar(%::legal_keywords);
 
+push @::legal_resolution, "---"; # Oy, what a hack.
+shift @::legal_resolution; 
+      # Another hack - this array contains "" for some reason. See bug 106589.
+$vars->{'resolution'} = \@::legal_resolution;
+
+$vars->{'chfield'} = ["[Bug creation]", @::log_columns];
+$vars->{'bug_status'} = \@::legal_bug_status;
+$vars->{'rep_platform'} = \@::legal_platform;
+$vars->{'op_sys'} = \@::legal_opsys;
+$vars->{'priority'} = \@::legal_priority;
+$vars->{'bug_severity'} = \@::legal_severity;
+$vars->{'userid'} = $::userid;
+
+# Boolean charts
 my @fields;
-push(@fields, ["noop", "---"]);
+push(@fields, { name => "noop", description => "---" });
 ConnectToDatabase();
 SendSQL("SELECT name, description FROM fielddefs ORDER BY sortkey");
 while (MoreSQLData()) {
-    my ($name, $description) = (FetchSQLData());
-    push(@fields, [$name, $description]);
+    my ($name, $description) = FetchSQLData();
+    push(@fields, { name => $name, description => $description });
 }
 
-my @types = (
-             ["noop", "---"],
-             ["equals", "equal to"],
-             ["notequals", "not equal to"],
-             ["casesubstring", "contains (case-sensitive) substring"],
-             ["substring", "contains (case-insensitive) substring"],
-             ["notsubstring", "does not contain (case-insensitive) substring"],
-             ["allwordssubstr", "all words as (case-insensitive) substrings"],
-             ["anywordssubstr", "any words as (case-insensitive) substrings"],
-             ["regexp", "contains regexp"],
-             ["notregexp", "does not contain regexp"],
-             ["lessthan", "less than"],
-             ["greaterthan", "greater than"],
-             ["anywords", "any words"],
-             ["allwords", "all words"],
-             ["nowords", "none of the words"],
-             ["changedbefore", "changed before"],
-             ["changedafter", "changed after"],
-             ["changedfrom", "changed from"],
-             ["changedto", "changed to"],
-             ["changedby", "changed by"],
-             );
-
-
-print qq{<A NAME="chart"> </A>\n};
+$vars->{'fields'} = \@fields;
 
+# Creating new charts - if the cmd-add value is there, we define the field
+# value so the code sees it and creates the chart. It will attempt to select
+# "xyzzy" as the default, and fail. This is the correct behaviour.
 foreach my $cmd (grep(/^cmd-/, keys(%::FORM))) {
     if ($cmd =~ /^cmd-add(\d+)-(\d+)-(\d+)$/) {
         $::FORM{"field$1-$2-$3"} = "xyzzy";
     }
 }
 
-#  foreach my $i (sort(keys(%::FORM))) {
-#      print "$i : " . value_quote($::FORM{$i}) . "<BR>\n";
-#  }
-
-
 if (!exists $::FORM{'field0-0-0'}) {
     $::FORM{'field0-0-0'} = "xyzzy";
 }
 
-my $jsmagic = qq{ONCLICK="document.forms[0].action='query.cgi#chart' ; document.forms[0].method='POST' ; return 1;"};
-
-my $chart;
-for ($chart=0 ; exists $::FORM{"field$chart-0-0"} ; $chart++) {
+# Create data structure of boolean chart info. It's an array of arrays of
+# arrays - with the inner arrays having three members - field, type and
+# value.
+my @charts;
+for (my $chart = 0; $::FORM{"field$chart-0-0"}; $chart++) {
     my @rows;
-    my $row;
-    for ($row = 0 ; exists $::FORM{"field$chart-$row-0"} ; $row++) {
+    for (my $row = 0; $::FORM{"field$chart-$row-0"}; $row++) {
         my @cols;
-        my $col;
-        for ($col = 0 ; exists $::FORM{"field$chart-$row-$col"} ; $col++) {
-            my $key = "$chart-$row-$col";
-            my $deffield = $::FORM{"field$key"} || "";
-            my $deftype = $::FORM{"type$key"} || "";
-            my $defvalue = value_quote($::FORM{"value$key"} || "");
-            my $line = "";
-            $line .= "<TD>";
-            $line .= BuildPulldown("field$key", \@fields, $deffield);
-            $line .= BuildPulldown("type$key", \@types, $deftype);
-            $line .= qq{<INPUT NAME="value$key" VALUE="$defvalue">};
-            $line .= "</TD>\n";
-            push(@cols, $line);
+        for (my $col = 0; $::FORM{"field$chart-$row-$col"}; $col++) {
+            push(@cols, { field => $::FORM{"field$chart-$row-$col"},
+                          type => $::FORM{"type$chart-$row-$col"},
+                          value => $::FORM{"value$chart-$row-$col"} });
         }
-        push(@rows, "<TR>" . join(qq{<TD ALIGN="center"> or </TD>\n}, @cols) .
-             qq{<TD><INPUT TYPE="submit" VALUE="Or" NAME="cmd-add$chart-$row-$col" $jsmagic></TD></TR>});
-    }
-    print qq{
-<HR>
-<TABLE>
-};
-    print join('<TR><TD>And</TD></TR>', @rows);
-    print qq{
-<TR><TD><INPUT TYPE="submit" VALUE="And" NAME="cmd-add$chart-$row-0" $jsmagic>
-};
-    my $n = $chart + 1;
-    if (!exists $::FORM{"field$n-0-0"}) {
-        print qq{
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-<INPUT TYPE="submit" VALUE="Add another boolean chart" NAME="cmd-add$n-0-0" $jsmagic>
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-<NOBR><A HREF="queryhelp.cgi#advancedquerying">What is this stuff?</A></NOBR>
-};
+        push(@rows, \@cols);
     }
-    print qq{
-</TD>
-</TR>
-</TABLE>
-    };
+    push(@charts, \@rows);
 }
-print qq{<HR>};
 
+$default{'charts'} = \@charts;
 
-
-
-if (!$userid) {
-    print qq{<INPUT TYPE="hidden" NAME="cmdtype" VALUE="doit">};
-} else {
-    print "
-<BR>
-<INPUT TYPE=radio NAME=cmdtype VALUE=doit CHECKED> Run this query
-<BR>
-";
-
+# Named queries
+if ($::userid) {
     my @namedqueries;
-    if ($userid) {
-        SendSQL("SELECT name FROM namedqueries " .
-                "WHERE userid = $userid AND name != '$::defaultqueryname' " .
-                "ORDER BY name");
-        while (MoreSQLData()) {
-            push(@namedqueries, FetchOneColumn());
-        }
+    SendSQL("SELECT name FROM namedqueries " .
+            "WHERE userid = $::userid AND name != '$::defaultqueryname' " .
+            "ORDER BY name");
+    while (MoreSQLData()) {
+        push(@namedqueries, FetchOneColumn());
     }
     
-    
-    
-    
-    if (@namedqueries) {
-        my $namelist = make_options(\@namedqueries);
-        print qq{
-<table cellspacing=0 cellpadding=0><tr>
-<td><INPUT TYPE=radio NAME=cmdtype VALUE=editnamed> Load the remembered query:</td>
-<td rowspan=3><select name=namedcmd>$namelist</select>
-</tr><tr>
-<td><INPUT TYPE=radio NAME=cmdtype VALUE=runnamed> Run the remembered query:</td>
-</tr><tr>
-<td><INPUT TYPE=radio NAME=cmdtype VALUE=forgetnamed> Forget the remembered query:</td>
-</tr></table>};
-    }
-
-    print qq{
-<INPUT TYPE=radio NAME=cmdtype VALUE=asdefault> Remember this as the default query
-<BR>
-<INPUT TYPE=radio NAME=cmdtype VALUE=asnamed> Remember this query, and name it:
-<INPUT TYPE=text NAME=newqueryname>
-<br>&nbsp;&nbsp;&nbsp;&nbsp;<INPUT TYPE="checkbox" NAME="tofooter" VALUE="1">
-    and put it in my page footer.
-<BR>
-    };
+    $vars->{'namedqueries'} = \@namedqueries;    
 }
 
-print qq{
-<NOBR><B>Sort By:</B>
-<SELECT NAME=\"order\">
-};
-
-my $deforder = "'Importance'";
-my @orders = ('Bug Number', $deforder, 'Assignee', 'Last Changed');
+# Sort order
+my $deforder;
+my @orders = ('Bug Number', 'Importance', 'Assignee', 'Last Changed');
 
 if ($::COOKIE{'LASTORDER'}) {
     $deforder = "Reuse same sort as last time";
@@ -1027,43 +415,15 @@ if ($::COOKIE{'LASTORDER'}) {
 
 if ($::FORM{'order'}) { $deforder = $::FORM{'order'} }
 
-my $defquerytype = $userdefaultquery ? "my" : "the";
-
-print make_options(\@orders, $deforder);
-print "</SELECT></NOBR>
-<INPUT TYPE=\"submit\" VALUE=\"Submit query\">
-<INPUT TYPE=\"reset\" VALUE=\"Reset back to $defquerytype default query\">
-";
-
-if ($userdefaultquery) {
-    print qq{<BR><A HREF="query.cgi?nukedefaultquery=1">Set my default query back to the system default</A>};
-}
-
-print "
-</FORM>
-";
-
-###
-### I really hate this redudancy, but if somebody for some inexplicable reason doesn't like using
-### the footer for these links, they can uncomment this section. 
-###
-
-# if (UserInGroup("tweakparams")) {
-#     print "<a href=editparams.cgi>Edit Bugzilla operating parameters</a><br>\n";
-# }
-# if (UserInGroup("editcomponents")) {
-#     print "<a href=editproducts.cgi>Edit Bugzilla products and components</a><br>\n";
-# }
-# if (UserInGroup("editkeywords")) {
-#     print "<a href=editkeywords.cgi>Edit Bugzilla keywords</a><br>\n";
-# }
-# if ($userid) {
-#     print "<a href=relogin.cgi>Log in as someone besides <b>$::COOKIE{'Bugzilla_login'}</b></a><br>\n";
-# }
-# print "<a href=userprefs.cgi>Change your password or preferences.</a><br>\n";
-# print "<a href=\"enter_bug.cgi\">Report a new bug.</a><br>\n";
-# print "<a href=\"createaccount.cgi\">Open a new Bugzilla account</a><br>\n";
-# print "<a href=\"reports.cgi\">Bug reports</a><br>\n";
+$vars->{'userdefaultquery'} = $userdefaultquery;
+$vars->{'orders'} = \@orders;
+$default{'querytype'} = $deforder || 'Importance';
 
+# Add in the defaults.
+$vars->{'default'} = \%default;
 
-PutFooter();
+# Generate and return the UI (HTML page) from the appropriate template.
+print "Content-type: text/html\n\n";
+$template->process("query/query.atml", $vars)
+  || DisplayError("Template process failed: " . $template->error())
+  && exit;