]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 158474 - Abstract out GenerateSQL into perl module. Patch by gerv; r=jouni.
authorgerv%gerv.net <>
Sat, 10 Aug 2002 05:12:09 +0000 (05:12 +0000)
committergerv%gerv.net <>
Sat, 10 Aug 2002 05:12:09 +0000 (05:12 +0000)
Bugzilla/Search.pm [new file with mode: 0644]
buglist.cgi

diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
new file mode 100644 (file)
index 0000000..8626026
--- /dev/null
@@ -0,0 +1,860 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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): Gervase Markham <gerv@gerv.net>
+#                 Terry Weissman <terry@mozilla.org>
+#                 Dan Mosedale <dmose@mozilla.org>
+#                 Stephan Niemz <st.n@gmx.net>
+#                 Andreas Franke <afranke@mathweb.org>
+#                 Myk Melez <myk@mozilla.org>
+#                 Michael Schindler <michael@compressconsult.com>
+
+use diagnostics;
+use strict;
+
+require "globals.pl";
+require "CGI.pl";
+
+use vars qw($userid $usergroupset);
+
+package Bugzilla::Search;
+
+# Create a new Search
+sub new {
+    my $invocant = shift;
+    my $class = ref($invocant) || $invocant;
+  
+    my $self = { @_ };  
+    bless($self, $class);
+    
+    $self->init();
+    
+    return $self;
+}
+
+sub init {
+    my $self = shift;
+    my $fieldsref = $self->{'fields'};
+    my $urlstr = $self->{'url'};
+
+    my $debug = 0;
+        
+    my @fields;
+    my @supptables;
+    my @wherepart;
+    @fields = @$fieldsref if $fieldsref;
+    my %F;
+    my %M;
+    &::ParseUrlString($urlstr, \%F, \%M);
+    my @specialchart;
+    my @andlist;
+
+    # First, deal with all the old hard-coded non-chart-based poop.
+    unshift(@supptables,
+            ("profiles map_assigned_to",
+             "profiles map_reporter",
+             "LEFT JOIN profiles map_qa_contact ON bugs.qa_contact = map_qa_contact.userid"));
+    unshift(@wherepart,
+            ("bugs.assigned_to = map_assigned_to.userid",
+             "bugs.reporter = map_reporter.userid"));
+
+    my $minvotes;
+    if (defined $F{'votes'}) {
+        my $c = &::trim($F{'votes'});
+        if ($c ne "") {
+            if ($c !~ /^[0-9]*$/) {
+                my $htmlc = html_quote($c);
+                &::ThrowUserError("The <em>At least ___ votes</em> field must 
+                                   be a simple number.  You entered
+                                   <tt>$htmlc</tt>, which doesn't cut it.");
+            }
+            push(@specialchart, ["votes", "greaterthan", $c - 1]);
+        }
+    }
+
+    if ($M{'bug_id'}) {
+        my $type = "anyexact";
+        if ($F{'bugidtype'} && $F{'bugidtype'} eq 'exclude') {
+            $type = "nowords";
+        }
+        push(@specialchart, ["bug_id", $type, join(',', @{$M{'bug_id'}})]);
+    }
+
+    my @legal_fields = ("product", "version", "rep_platform", "op_sys",
+                        "bug_status", "resolution", "priority", "bug_severity",
+                        "assigned_to", "reporter", "component",
+                        "target_milestone", "groupset");
+
+    foreach my $field (keys %F) {
+        if (&::lsearch(\@legal_fields, $field) != -1) {
+            push(@specialchart, [$field, "anyexact",
+                                 join(',', @{$M{$field}})]);
+        }
+    }
+
+    if ($F{'keywords'}) {
+        my $t = $F{'keywords_type'};
+        if (!$t || $t eq "or") {
+            $t = "anywords";
+        }
+        push(@specialchart, ["keywords", $t, $F{'keywords'}]);
+    }
+
+    foreach my $id ("1", "2") {
+        if (!defined ($F{"email$id"})) {
+            next;
+        }
+        my $email = &::trim($F{"email$id"});
+        if ($email eq "") {
+            next;
+        }
+        my $type = $F{"emailtype$id"};
+        if ($type eq "exact") {
+            $type = "anyexact";
+            foreach my $name (split(',', $email)) {
+                $name = &::trim($name);
+                if ($name) {
+                    &::DBNameToIdAndCheck($name);
+                }
+            }
+        }
+
+        my @clist;
+        foreach my $field ("assigned_to", "reporter", "cc", "qa_contact") {
+            if ($F{"email$field$id"}) {
+                push(@clist, $field, $type, $email);
+            }
+        }
+        if ($F{"emaillongdesc$id"}) {
+            my $table = "longdescs_";
+            push(@supptables, "longdescs $table");
+            push(@wherepart, "$table.bug_id = bugs.bug_id");
+            my $ptable = "longdescnames_";
+            push(@supptables, "profiles $ptable");
+            push(@wherepart, "$table.who = $ptable.userid");
+            push(@clist, "$ptable.login_name", $type, $email);
+        }
+        if (@clist) {
+            push(@specialchart, \@clist);
+        } else {
+            my $htmlemail = html_quote($email);
+            &::ThrowUserError("You must specify one or more fields in which
+                               to search for <tt>$htmlemail</tt>.");
+        }
+    }
+
+
+    if (defined $F{'changedin'}) {
+        my $c = &::trim($F{'changedin'});
+        if ($c ne "") {
+            if ($c !~ /^[0-9]*$/) {
+                my $htmlc = &::html_quote($c);
+                &::ThrowUserError("The <em>changed in last ___ days</em> field
+                                   must be a simple number.  You entered
+                                   <tt>$htmlc</tt>, which doesn't cut it.");
+            }
+            push(@specialchart, ["changedin",
+                                 "lessthan", $c + 1]);
+        }
+    }
+
+    my $ref = $M{'chfield'};
+
+    if (defined $ref) {
+        my $which = &::lsearch($ref, "[Bug creation]");
+        if ($which >= 0) {
+            splice(@$ref, $which, 1);
+            push(@specialchart, ["creation_ts", "greaterthan",
+                                 SqlifyDate($F{'chfieldfrom'})]);
+            my $to = $F{'chfieldto'};
+            if (defined $to) {
+                $to = &::trim($to);
+                if ($to ne "" && $to !~ /^now$/i) {
+                    push(@specialchart, ["creation_ts", "lessthan",
+                                         SqlifyDate($to)]);
+                }
+            }
+        }
+    }
+
+    if (defined $ref && 0 < @$ref) {
+        push(@supptables, "bugs_activity actcheck");
+
+        my @list;
+        foreach my $f (@$ref) {
+            push(@list, "\nactcheck.fieldid = " . &::GetFieldID($f));
+        }
+        push(@wherepart, "actcheck.bug_id = bugs.bug_id");
+        push(@wherepart, "(" . join(' OR ', @list) . ")");
+        push(@wherepart, "actcheck.bug_when >= " .
+             &::SqlQuote(SqlifyDate($F{'chfieldfrom'})));
+        my $to = $F{'chfieldto'};
+        if (defined $to) {
+            $to = &::trim($to);
+            if ($to ne "" && $to !~ /^now$/i) {
+                push(@wherepart, "actcheck.bug_when <= " .
+                     &::SqlQuote(SqlifyDate($to)));
+            }
+        }
+        my $value = $F{'chfieldvalue'};
+        if (defined $value) {
+            $value = &::trim($value);
+            if ($value ne "") {
+                push(@wherepart, "actcheck.added = " .
+                     &::SqlQuote($value))
+            }
+        }
+    }
+
+    foreach my $f ("short_desc", "long_desc", "bug_file_loc",
+                   "status_whiteboard") {
+        if (defined $F{$f}) {
+            my $s = &::trim($F{$f});
+            if ($s ne "") {
+                my $n = $f;
+                my $q = &::SqlQuote($s);
+                my $type = $F{$f . "_type"};
+                push(@specialchart, [$f, $type, $s]);
+            }
+        }
+    }
+
+    my $chartid;
+    # $statusid is used by the code that queries for attachment statuses.
+    my $statusid = 0;
+    my $f;
+    my $ff;
+    my $t;
+    my $q;
+    my $v;
+    my $term;
+    my %funcsbykey;
+    my @funcdefs =
+        (
+         "^(assigned_to|reporter)," => sub {
+             push(@supptables, "profiles map_$f");
+             push(@wherepart, "bugs.$f = map_$f.userid");
+             $f = "map_$f.login_name";
+         },
+         "^qa_contact," => sub {
+             push(@supptables,
+                  "LEFT JOIN profiles map_qa_contact ON bugs.qa_contact = map_qa_contact.userid");
+             $f = "map_$f.login_name";
+         },
+
+         "^cc," => sub {
+            push(@supptables, "LEFT JOIN cc cc_$chartid ON bugs.bug_id = cc_$chartid.bug_id");
+
+            push(@supptables, "LEFT JOIN profiles map_cc_$chartid ON cc_$chartid.who = map_cc_$chartid.userid");
+            $f = "map_cc_$chartid.login_name";
+         },
+
+         "^long_?desc,changedby" => sub {
+             my $table = "longdescs_$chartid";
+             push(@supptables, "longdescs $table");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             my $id = &::DBNameToIdAndCheck($v);
+             $term = "$table.who = $id";
+         },
+         "^long_?desc,changedbefore" => sub {
+             my $table = "longdescs_$chartid";
+             push(@supptables, "longdescs $table");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             $term = "$table.bug_when < " . &::SqlQuote(SqlifyDate($v));
+         },
+         "^long_?desc,changedafter" => sub {
+             my $table = "longdescs_$chartid";
+             push(@supptables, "longdescs $table");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             $term = "$table.bug_when > " . &::SqlQuote(SqlifyDate($v));
+         },
+         "^long_?desc," => sub {
+             my $table = "longdescs_$chartid";
+             push(@supptables, "longdescs $table");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             $f = "$table.thetext";
+         },
+         "^attachments\..*," => sub {
+             my $table = "attachments_$chartid";
+             push(@supptables, "attachments $table");
+             push(@wherepart, "bugs.bug_id = $table.bug_id");
+             $f =~ m/^attachments\.(.*)$/;
+             my $field = $1;
+             if ($t eq "changedby") {
+                 $v = &::DBNameToIdAndCheck($v);
+                 $q = &::SqlQuote($v);
+                 $field = "submitter_id";
+                 $t = "equals";
+             } elsif ($t eq "changedbefore") {
+                 $v = SqlifyDate($v);
+                 $q = &::SqlQuote($v);
+                 $field = "creation_ts";
+                 $t = "lessthan";
+             } elsif ($t eq "changedafter") {
+                 $v = SqlifyDate($v);
+                 $q = &::SqlQuote($v);
+                 $field = "creation_ts";
+                 $t = "greaterthan";
+             }
+             if ($field eq "ispatch" && $v ne "0" && $v ne "1") {
+                 &::ThrowUserError("The only legal values for the 
+                                    <em>Attachment is patch</em> field are 
+                                    0 and 1.");
+             }
+             if ($field eq "isobsolete" && $v ne "0" && $v ne "1") {
+                 &::ThrowUserError("The only legal values for the 
+                                    <em>Attachment is obsolete</em> field are 
+                                    0 and 1.");
+             }
+             $f = "$table.$field";
+         },
+         "^attachstatusdefs.name," => sub {
+             # The below has Fun with the names for attachment statuses. This
+             # isn't needed for changed* queries, so exclude those - the
+             # generic stuff will cope
+             return if ($t =~ m/^changed/);
+
+             # Searching for "status != 'bar'" wants us to look for an
+             # attachment without the 'bar' status, not for an attachment with
+             # a status not equal to 'bar' (Which would pick up an attachment
+             # with more than one status). We do this by LEFT JOINS, after
+             # grabbing the matching attachment status ids.
+             # Note that this still won't find bugs with no attachments, since
+             # that isn't really what people would expect.
+
+             # First, get the attachment status ids, using the other funcs
+             # to match the WHERE term.
+             # Note that we need to reverse the negated bits for this to work
+             # This somewhat abuses the definitions of the various terms -
+             # eg, does 'contains all' mean that the status has to contain all
+             # those words, or that all those words must be exact matches to
+             # statuses, which must all be on a single attachment, or should
+             # the match on the status descriptions be a contains match, too?
+
+             my $inverted = 0;
+             if ($t =~ m/not(.*)/) {
+                 $t = $1;
+                 $inverted = 1;
+             }
+
+             $ref = $funcsbykey{",$t"};
+             &$ref;
+             &::SendSQL("SELECT id FROM attachstatusdefs WHERE $term");
+
+             my @as_ids;
+             while (&::MoreSQLData()) {
+                 push @as_ids, &::FetchOneColumn();
+             }
+
+             # When searching for multiple statuses within a single boolean chart,
+             # we want to match each status record separately.  In other words,
+             # "status = 'foo' AND status = 'bar'" should match attachments with
+             # one status record equal to "foo" and another one equal to "bar",
+             # not attachments where the same status record equals both "foo" and
+             # "bar" (which is nonsensical).  In order to do this we must add an
+             # additional counter to the end of the "attachstatuses" table
+             # reference.
+             ++$statusid;
+
+             my $attachtable = "attachments_$chartid";
+             my $statustable = "attachstatuses_${chartid}_$statusid";
+
+             push(@supptables, "attachments $attachtable");
+             my $join = "LEFT JOIN attachstatuses $statustable ON ".
+               "($attachtable.attach_id = $statustable.attach_id AND " .
+                "$statustable.statusid IN (" . join(",", @as_ids) . "))";
+             push(@supptables, $join);
+             push(@wherepart, "bugs.bug_id = $attachtable.bug_id");
+             if ($inverted) {
+                 $term = "$statustable.statusid IS NULL";
+             } else {
+                 $term = "$statustable.statusid IS NOT NULL";
+             }
+         },
+         "^changedin," => sub {
+             $f = "(to_days(now()) - to_days(bugs.delta_ts))";
+         },
+
+         "^keywords," => sub {
+             &::GetVersionTable();
+             my @list;
+             my $table = "keywords_$chartid";
+             foreach my $value (split(/[\s,]+/, $v)) {
+                 if ($value eq '') {
+                     next;
+                 }
+                 my $id = &::GetKeywordIdFromName($value);
+                 if ($id) {
+                     push(@list, "$table.keywordid = $id");
+                 }
+                 else {
+                     my $htmlv = &::html_quote($v);
+                     &::ThrowUserError("There is no keyword named<code>$htmlv
+                                        </code>. To search for keywords, consult
+                                        the <a href='describekeywords.cgi'>list
+                                        of legal keywords</a>.");
+                 }
+             }
+             my $haveawordterm;
+             if (@list) {
+                 $haveawordterm = "(" . join(' OR ', @list) . ")";
+                 if ($t eq "anywords") {
+                     $term = $haveawordterm;
+                 } elsif ($t eq "allwords") {
+                     $ref = $funcsbykey{",$t"};
+                     &$ref;
+                     if ($term && $haveawordterm) {
+                         $term = "(($term) AND $haveawordterm)";
+                     }
+                 }
+             }
+             if ($term) {
+                 push(@supptables, "keywords $table");
+                 push(@wherepart, "$table.bug_id = bugs.bug_id");
+             }
+         },
+
+         "^dependson," => sub {
+                my $table = "dependson_" . $chartid;
+                push(@supptables, "dependencies $table");
+                $ff = "$table.$f";
+                $ref = $funcsbykey{",$t"};
+                &$ref;
+                push(@wherepart, "$table.blocked = bugs.bug_id");
+         },
+
+         "^blocked," => sub {
+                my $table = "blocked_" . $chartid;
+                push(@supptables, "dependencies $table");
+                $ff = "$table.$f";
+                $ref = $funcsbykey{",$t"};
+                &$ref;
+                push(@wherepart, "$table.dependson = bugs.bug_id");
+         },
+
+
+         ",equals" => sub {
+             $term = "$ff = $q";
+         },
+         ",notequals" => sub {
+             $term = "$ff != $q";
+         },
+         ",casesubstring" => sub {
+             $term = "INSTR($ff, $q)";
+         },
+         ",(substring|substr)" => sub {
+             $term = "INSTR(LOWER($ff), " . lc($q) . ")";
+         },
+         ",notsubstring" => sub {
+             $term = "INSTR(LOWER($ff), " . lc($q) . ") = 0";
+         },
+         ",regexp" => sub {
+             $term = "LOWER($ff) REGEXP $q";
+         },
+         ",notregexp" => sub {
+             $term = "LOWER($ff) NOT REGEXP $q";
+         },
+         ",lessthan" => sub {
+             $term = "$ff < $q";
+         },
+         ",greaterthan" => sub {
+             $term = "$ff > $q";
+         },
+         ",anyexact" => sub {
+             my @list;
+             foreach my $w (split(/,/, $v)) {
+                 if ($w eq "---" && $f !~ /milestone/) {
+                     $w = "";
+                 }
+                 push(@list, "$ff = " . &::SqlQuote($w));
+             }
+             $term = join(" OR ", @list);
+         },
+         ",anywordssubstr" => sub {
+             $term = join(" OR ", @{GetByWordListSubstr($ff, $v)});
+         },
+         ",allwordssubstr" => sub {
+             $term = join(" AND ", @{GetByWordListSubstr($ff, $v)});
+         },
+         ",nowordssubstr" => sub {
+             my @list = @{GetByWordListSubstr($ff, $v)};
+             if (@list) {
+                 $term = "NOT (" . join(" OR ", @list) . ")";
+             }
+         },
+         ",anywords" => sub {
+             $term = join(" OR ", @{GetByWordList($ff, $v)});
+         },
+         ",allwords" => sub {
+             $term = join(" AND ", @{GetByWordList($ff, $v)});
+         },
+         ",nowords" => sub {
+             my @list = @{GetByWordList($ff, $v)};
+             if (@list) {
+                 $term = "NOT (" . join(" OR ", @list) . ")";
+             }
+         },
+         ",changedbefore" => sub {
+             my $table = "act_$chartid";
+             my $ftable = "fielddefs_$chartid";
+             push(@supptables, "bugs_activity $table");
+             push(@supptables, "fielddefs $ftable");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             push(@wherepart, "$table.fieldid = $ftable.fieldid");
+             $term = "($ftable.name = '$f' AND $table.bug_when < $q)";
+         },
+         ",changedafter" => sub {
+             my $table = "act_$chartid";
+             my $ftable = "fielddefs_$chartid";
+             push(@supptables, "bugs_activity $table");
+             push(@supptables, "fielddefs $ftable");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             push(@wherepart, "$table.fieldid = $ftable.fieldid");
+             $term = "($ftable.name = '$f' AND $table.bug_when > $q)";
+         },
+         ",changedfrom" => sub {
+             my $table = "act_$chartid";
+             my $ftable = "fielddefs_$chartid";
+             push(@supptables, "bugs_activity $table");
+             push(@supptables, "fielddefs $ftable");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             push(@wherepart, "$table.fieldid = $ftable.fieldid");
+             $term = "($ftable.name = '$f' AND $table.removed = $q)";
+         },
+         ",changedto" => sub {
+             my $table = "act_$chartid";
+             my $ftable = "fielddefs_$chartid";
+             push(@supptables, "bugs_activity $table");
+             push(@supptables, "fielddefs $ftable");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             push(@wherepart, "$table.fieldid = $ftable.fieldid");
+             $term = "($ftable.name = '$f' AND $table.added = $q)";
+         },
+         ",changedby" => sub {
+             my $table = "act_$chartid";
+             my $ftable = "fielddefs_$chartid";
+             push(@supptables, "bugs_activity $table");
+             push(@supptables, "fielddefs $ftable");
+             push(@wherepart, "$table.bug_id = bugs.bug_id");
+             push(@wherepart, "$table.fieldid = $ftable.fieldid");
+             my $id = &::DBNameToIdAndCheck($v);
+             $term = "($ftable.name = '$f' AND $table.who = $id)";
+         },
+         );
+    my @funcnames;
+    while (@funcdefs) {
+        my $key = shift(@funcdefs);
+        my $value = shift(@funcdefs);
+        if ($key =~ /^[^,]*$/) {
+            die "All defs in %funcs must have a comma in their name: $key";
+        }
+        if (exists $funcsbykey{$key}) {
+            die "Duplicate key in %funcs: $key";
+        }
+        $funcsbykey{$key} = $value;
+        push(@funcnames, $key);
+    }
+
+    # first we delete any sign of "Chart #-1" from the HTML form hash
+    # since we want to guarantee the user didn't hide something here
+    my @badcharts = grep /^(field|type|value)-1-/, (keys %F);
+    foreach my $field (@badcharts) {
+        delete $F{$field};
+    }
+
+    # now we take our special chart and stuff it into the form hash
+    my $chart = -1;
+    my $row = 0;
+    foreach my $ref (@specialchart) {
+        my $col = 0;
+        while (@$ref) {
+            $F{"field$chart-$row-$col"} = shift(@$ref);
+            $F{"type$chart-$row-$col"} = shift(@$ref);
+            $F{"value$chart-$row-$col"} = shift(@$ref);
+            if ($debug) {
+                print qq{<p>$F{"field$chart-$row-$col"} | $F{"type$chart-$row-$col"} | $F{"value$chart-$row-$col"}*</p>\n};
+            }
+            $col++;
+
+        }
+        $row++;
+    }
+
+
+# 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
+# (query.cgi) may contain an arbitrary number of boolean charts where
+# 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
+# by pressing the OR button at the right end of the chart. Extra
+# charts are created by pressing "Add another boolean chart".
+#
+# Each chart consists of an artibrary number of rows and columns.
+# The terms within a row are ORed together. The expressions represented
+# by each row are ANDed together. The expressions represented by each
+# chart are ANDed together.
+#
+#        ----------------------
+#        | col2 | col2 | col3 |
+# --------------|------|------|
+# | row1 |  a1  |  a2  |      |
+# |------|------|------|------|  => ((a1 OR a2) AND (b1 OR b2 OR b3) AND (c1))
+# | row2 |  b1  |  b2  |  b3  |
+# |------|------|------|------|
+# | row3 |  c1  |      |      |
+# -----------------------------
+#
+#        --------
+#        | col2 |
+# --------------|
+# | row1 |  d1  | => (d1)
+# ---------------
+#
+# Together, these two charts represent a SQL expression like this
+# 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'.
+#
+# --------------------------------------------------------------
+# CC    | equal to
+# foo@blah.org
+# --------------------------------------------------------------
+# CC    | equal to
+# bar@blah.org
+#
+# 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.
+#
+# --------------------------------------------------------------
+# CC    | equal to
+# foo@blah.org
+# AND
+# CC    | equal to
+# bar@blah.org
+# --------------------------------------------------------------
+
+# $chartid is the number of the current chart whose SQL we're contructing
+# $row is the current row of the current chart
+
+# names for table aliases are constructed using $chartid and $row
+#   SELECT blah  FROM $table "$table_$chartid_$row" WHERE ....
+
+# $f  = field of table in bug db (e.g. bug_id, reporter, etc)
+# $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
+# $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
+#               out duplicates.
+# $suppstring = String which is pasted into query containing all table names
+
+    # get a list of field names to verify the user-submitted chart fields against
+    my %chartfields;
+    &::SendSQL("SELECT name FROM fielddefs");
+    while (&::MoreSQLData()) {
+        my ($name) = &::FetchSQLData();
+        $chartfields{$name} = 1;
+    }
+
+    $row = 0;
+    for ($chart=-1 ;
+         $chart < 0 || exists $F{"field$chart-0-0"} ;
+         $chart++) {
+        $chartid = $chart >= 0 ? $chart : "";
+        for ($row = 0 ;
+             exists $F{"field$chart-$row-0"} ;
+             $row++) {
+            my @orlist;
+            for (my $col = 0 ;
+                 exists $F{"field$chart-$row-$col"} ;
+                 $col++) {
+                $f = $F{"field$chart-$row-$col"} || "noop";
+                $t = $F{"type$chart-$row-$col"} || "noop";
+                $v = $F{"value$chart-$row-$col"};
+                $v = "" if !defined $v;
+                $v = &::trim($v);
+                if ($f eq "noop" || $t eq "noop" || $v eq "") {
+                    next;
+                }
+                # chart -1 is generated by other code above, not from the user-
+                # submitted form, so we'll blindly accept any values in chart -1
+                if ((!$chartfields{$f}) && ($chart != -1)) {
+                    my $errstr = "Can't use " . &::html_quote($f) . " as a field name.  " .
+                        "If you think you're getting this in error, please copy the " .
+                        "entire URL out of the address bar at the top of your browser " .
+                        "window and email it to <109679\@bugzilla.org>";
+                    die "Internal error: $errstr" if $chart < 0;
+                    return Error($errstr);
+                }
+
+                # This is either from the internal chart (in which case we
+                # already know about it), or it was in %chartfields, so it is
+                # a valid field name, which means that its ok.
+                &::trick_taint($f);
+                $q = &::SqlQuote($v);
+                my $func;
+                $term = undef;
+                foreach my $key (@funcnames) {
+                    if ("$f,$t" =~ m/$key/) {
+                        my $ref = $funcsbykey{$key};
+                        if ($debug) {
+                            print "<p>$key ($f , $t ) => ";
+                        }
+                        $ff = $f;
+                        if ($f !~ /\./) {
+                            $ff = "bugs.$f";
+                        }
+                        &$ref;
+                        if ($debug) {
+                            print "$f , $t , $term</p>";
+                        }
+                        if ($term) {
+                            last;
+                        }
+                    }
+                }
+                if ($term) {
+                    push(@orlist, $term);
+                }
+                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"
+                               : &::ThrowCodeError($errstr);
+                }
+            }
+            if (@orlist) {
+                push(@andlist, "(" . join(" OR ", @orlist) . ")");
+            }
+        }
+    }
+    my %suppseen = ("bugs" => 1);
+    my $suppstring = "bugs";
+    foreach my $str (@supptables) {
+        if (!$suppseen{$str}) {
+            if ($str !~ /^(LEFT|INNER) JOIN/i) {
+                $suppstring .= ",";
+            }
+            $suppstring .= " $str";
+            $suppseen{$str} = 1;
+        }
+    }
+    my $query =  ("SELECT DISTINCT " . join(', ', @fields) .
+                  " FROM $suppstring" .
+                  " WHERE " . join(' AND ', (@wherepart, @andlist)));
+
+    $query = &::SelectVisible($query, $::userid, $::usergroupset);
+
+    if ($debug) {
+        print "<p><code>" . &::value_quote($query) . "</code></p>\n";
+        exit;
+    }
+    
+    $self->{'sql'} = $query;
+}
+
+###############################################################################
+# Helper functions for the init() method.
+###############################################################################
+sub SqlifyDate {
+    my ($str) = @_;
+    $str = "" if !defined $str;
+    if ($str =~ /^-?(\d+)([dDwWmMyY])$/) {   # relative date
+        my ($amount, $unit, $date) = ($1, lc $2, time);
+        my ($sec, $min, $hour, $mday, $month, $year, $wday)  = localtime($date);
+        if ($unit eq 'w') {                  # convert weeks to days
+            $amount = 7*$amount + $wday;
+            $unit = 'd';
+        }
+        if ($unit eq 'd') {
+            $date -= $sec + 60*$min + 3600*$hour + 24*3600*$amount;
+            return time2str("%Y-%m-%d %H:%M:%S", $date);
+        }
+        elsif ($unit eq 'y') {
+            return sprintf("%4d-01-01 00:00:00", $year+1900-$amount);
+        }
+        elsif ($unit eq 'm') {
+            $month -= $amount;
+            while ($month<0) { $year--; $month += 12; }
+            return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1);
+        }
+        return undef;                      # should not happen due to regexp at top
+    }
+    my $date = str2time($str);
+    if (!defined($date)) {
+        my $htmlstr = html_quote($str);
+        ThrowUserError("The string <tt>$htmlstr</tt> is not a legal date.");
+        exit;
+    }
+    return time2str("%Y-%m-%d %H:%M:%S", $date);
+}
+
+sub GetByWordList {
+    my ($field, $strs) = (@_);
+    my @list;
+
+    foreach my $w (split(/[\s,]+/, $strs)) {
+        my $word = $w;
+        if ($word ne "") {
+            $word =~ tr/A-Z/a-z/;
+            $word = SqlQuote(quotemeta($word));
+            $word =~ s/^'//;
+            $word =~ s/'$//;
+            $word = '(^|[^a-z0-9])' . $word . '($|[^a-z0-9])';
+            push(@list, "lower($field) regexp '$word'");
+        }
+    }
+
+    return \@list;
+}
+
+# Support for "any/all/nowordssubstr" comparison type ("words as substrings")
+sub GetByWordListSubstr {
+    my ($field, $strs) = (@_);
+    my @list;
+
+    foreach my $word (split(/[\s,]+/, $strs)) {
+        if ($word ne "") {
+            push(@list, "INSTR(LOWER($field), " . lc(SqlQuote($word)) . ")");
+        }
+    }
+
+    return \@list;
+}
+
+sub getSQL {
+    my $self = shift;
+    return $self->{'sql'};
+}
+
+1;
index ce824ea57a01688e4c45a780e5a8a54f10352b0b..58971996235009e01d1473ad8a5c4a9d5a6198c5 100755 (executable)
@@ -23,7 +23,6 @@
 #                 Stephan Niemz  <st.n@gmx.net>
 #                 Andreas Franke <afranke@mathweb.org>
 #                 Myk Melez <myk@mozilla.org>
-#                 Michael Schindler <michael@compressconsult.com>
 
 ################################################################################
 # Script Initialization
@@ -35,31 +34,28 @@ use strict;
 
 use lib qw(.);
 
-use vars qw( $template $vars );
+use vars qw($template $vars);
+
+use Bugzilla::Search;
 
 # Include the Bugzilla CGI and general utility library.
 require "CGI.pl";
 
-# 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 = @::components;
-    $zz = @::default_column_list;
-    $zz = $::defaultqueryname;
-    $zz = @::dontchange;
-    $zz = @::legal_keywords;
-    $zz = @::legal_platform;
-    $zz = @::legal_priority;
-    $zz = @::legal_product;
-    $zz = @::legal_severity;
-    $zz = @::settable_resolution;
-    $zz = @::target_milestone;
-    $zz = $::unconfirmedstate;
-    $zz = $::userid;
-    $zz = @::versions;
-};
+use vars qw($db_name
+            @components
+            @default_column_list
+            $defaultqueryname
+            @dontchange
+            @legal_keywords
+            @legal_platform
+            @legal_priority
+            @legal_product
+            @legal_severity
+            @settable_resolution
+            @target_milestone
+            $unconfirmedstate
+            $userid
+            @versions);
 
 if (length($::buffer) == 0) {
     $vars->{'title'} = "Parameters Required";
@@ -170,39 +166,6 @@ if ($::FORM{'cmdtype'} eq 'runnamed') {
 # Utilities
 ################################################################################
 
-sub SqlifyDate {
-    my ($str) = @_;
-    $str = "" if !defined $str;
-    if ($str =~ /^-?(\d+)([dDwWmMyY])$/) {   # relative date
-        my ($amount, $unit, $date) = ($1, lc $2, time);
-        my ($sec, $min, $hour, $mday, $month, $year, $wday)  = localtime($date);
-        if ($unit eq 'w') {                  # convert weeks to days
-            $amount = 7*$amount + $wday;
-            $unit = 'd';
-        }
-        if ($unit eq 'd') {
-            $date -= $sec + 60*$min + 3600*$hour + 24*3600*$amount;
-            return time2str("%Y-%m-%d %H:%M:%S", $date);
-        }
-        elsif ($unit eq 'y') {
-            return sprintf("%4d-01-01 00:00:00", $year+1900-$amount);
-        }
-        elsif ($unit eq 'm') {
-            $month -= $amount;
-            while ($month<0) { $year--; $month += 12; }
-            return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1);
-        }
-        return undef;                      # should not happen due to regexp at top
-    }
-    my $date = str2time($str);
-    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);
-}
-
 my @weekday= qw( Sun Mon Tue Wed Thu Fri Sat );
 sub DiffDate {
     my ($datestr) = @_;
@@ -219,41 +182,6 @@ sub DiffDate {
     return $date;
 }
 
-sub GetByWordList {
-    my ($field, $strs) = (@_);
-    my @list;
-
-    foreach my $w (split(/[\s,]+/, $strs)) {
-        my $word = $w;
-        if ($word ne "") {
-            $word =~ tr/A-Z/a-z/;
-            $word = SqlQuote(quotemeta($word));
-            $word =~ s/^'//;
-            $word =~ s/'$//;
-            $word = '(^|[^a-z0-9])' . $word . '($|[^a-z0-9])';
-            push(@list, "lower($field) regexp '$word'");
-        }
-    }
-
-    return \@list;
-}
-
-#
-# support for "any/all/nowordssubstr" comparison type ("words as substrings")
-#
-sub GetByWordListSubstr {
-    my ($field, $strs) = (@_);
-    my @list;
-
-    foreach my $word (split(/[\s,]+/, $strs)) {
-        if ($word ne "") {
-            push(@list, "INSTR(LOWER($field), " . lc(SqlQuote($word)) . ")");
-        }
-    }
-
-    return \@list;
-}
-
 sub LookupNamedQuery {
     my ($name) = @_;
     confirm_login();
@@ -307,759 +235,6 @@ sub GetGroupsByGroupSet {
 }
 
 
-
-################################################################################
-# Query Generation
-################################################################################
-
-sub GenerateSQL {
-    my $debug = 0;
-    my ($fieldsref, $urlstr) = (@_);
-    my @fields;
-    my @supptables;
-    my @wherepart;
-    @fields = @$fieldsref if $fieldsref;
-    my %F;
-    my %M;
-    ParseUrlString($urlstr, \%F, \%M);
-    my @specialchart;
-    my @andlist;
-
-    # First, deal with all the old hard-coded non-chart-based poop.
-
-    unshift(@supptables,
-            ("profiles map_assigned_to",
-             "profiles map_reporter",
-             "LEFT JOIN profiles map_qa_contact ON bugs.qa_contact = map_qa_contact.userid"));
-    unshift(@wherepart,
-            ("bugs.assigned_to = map_assigned_to.userid",
-             "bugs.reporter = map_reporter.userid"));
-
-    my $minvotes;
-    if (defined $F{'votes'}) {
-        my $c = trim($F{'votes'});
-        if ($c ne "") {
-            if ($c !~ /^[0-9]*$/) {
-                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]);
-        }
-    }
-
-    if ($M{'bug_id'}) {
-        my $type = "anyexact";
-        if ($F{'bugidtype'} && $F{'bugidtype'} eq 'exclude') {
-            $type = "nowords";
-        }
-        push(@specialchart, ["bug_id", $type, join(',', @{$M{'bug_id'}})]);
-    }
-
-# This is evil.  We should never allow a user to directly append SQL to
-# any query without a huge amount of validation.  Even then, it would
-# be a bad idea.  Beware that uncommenting this will allow someone to
-# peak at virtually anything they want in the bugs database.
-#    if (defined $F{'sql'}) {
-#        die "Invalid sql: $F{'sql'}" if $F{'sql'} =~ /;/;
-#        push(@wherepart, "( $F{'sql'} )");
-#    }
-
-    my @legal_fields = ("product", "version", "rep_platform", "op_sys",
-                        "bug_status", "resolution", "priority", "bug_severity",
-                        "assigned_to", "reporter", "component",
-                        "target_milestone", "groupset");
-
-    foreach my $field (keys %F) {
-        if (lsearch(\@legal_fields, $field) != -1) {
-            push(@specialchart, [$field, "anyexact",
-                                 join(',', @{$M{$field}})]);
-        }
-    }
-
-    if ($F{'keywords'}) {
-        my $t = $F{'keywords_type'};
-        if (!$t || $t eq "or") {
-            $t = "anywords";
-        }
-        push(@specialchart, ["keywords", $t, $F{'keywords'}]);
-    }
-
-    foreach my $id ("1", "2") {
-        if (!defined ($F{"email$id"})) {
-            next;
-        }
-        my $email = trim($F{"email$id"});
-        if ($email eq "") {
-            next;
-        }
-        my $type = $F{"emailtype$id"};
-        if ($type eq "exact") {
-            $type = "anyexact";
-            foreach my $name (split(',', $email)) {
-                $name = trim($name);
-                if ($name) {
-                    DBNameToIdAndCheck($name);
-                }
-            }
-        }
-
-        my @clist;
-        foreach my $field ("assigned_to", "reporter", "cc", "qa_contact") {
-            if ($F{"email$field$id"}) {
-                push(@clist, $field, $type, $email);
-            }
-        }
-        if ($F{"emaillongdesc$id"}) {
-            my $table = "longdescs_";
-            push(@supptables, "longdescs $table");
-            push(@wherepart, "$table.bug_id = bugs.bug_id");
-            my $ptable = "longdescnames_";
-            push(@supptables, "profiles $ptable");
-            push(@wherepart, "$table.who = $ptable.userid");
-            push(@clist, "$ptable.login_name", $type, $email);
-        }
-        if (@clist) {
-            push(@specialchart, \@clist);
-        } else {
-            my $htmlemail = html_quote($email);
-            DisplayError("You must specify one or more fields in which
-                          to search for <tt>$htmlemail</tt>.");
-            exit;
-        }
-    }
-
-
-    if (defined $F{'changedin'}) {
-        my $c = trim($F{'changedin'});
-        if ($c ne "") {
-            if ($c !~ /^[0-9]*$/) {
-                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]);
-        }
-    }
-
-    my $ref = $M{'chfield'};
-
-    if (defined $ref) {
-        my $which = lsearch($ref, "[Bug creation]");
-        if ($which >= 0) {
-            splice(@$ref, $which, 1);
-            push(@specialchart, ["creation_ts", "greaterthan",
-                                 SqlifyDate($F{'chfieldfrom'})]);
-            my $to = $F{'chfieldto'};
-            if (defined $to) {
-                $to = trim($to);
-                if ($to ne "" && $to !~ /^now$/i) {
-                    push(@specialchart, ["creation_ts", "lessthan",
-                                         SqlifyDate($to)]);
-                }
-            }
-        }
-    }
-
-
-
-    if (defined $ref && 0 < @$ref) {
-        push(@supptables, "bugs_activity actcheck");
-
-        my @list;
-        foreach my $f (@$ref) {
-            push(@list, "\nactcheck.fieldid = " . GetFieldID($f));
-        }
-        push(@wherepart, "actcheck.bug_id = bugs.bug_id");
-        push(@wherepart, "(" . join(' OR ', @list) . ")");
-        push(@wherepart, "actcheck.bug_when >= " .
-             SqlQuote(SqlifyDate($F{'chfieldfrom'})));
-        my $to = $F{'chfieldto'};
-        if (defined $to) {
-            $to = trim($to);
-            if ($to ne "" && $to !~ /^now$/i) {
-                push(@wherepart, "actcheck.bug_when <= " .
-                     SqlQuote(SqlifyDate($to)));
-            }
-        }
-        my $value = $F{'chfieldvalue'};
-        if (defined $value) {
-            $value = trim($value);
-            if ($value ne "") {
-                push(@wherepart, "actcheck.added = " .
-                     SqlQuote($value))
-            }
-        }
-    }
-
-
-    foreach my $f ("short_desc", "long_desc", "bug_file_loc",
-                   "status_whiteboard") {
-        if (defined $F{$f}) {
-            my $s = trim($F{$f});
-            if ($s ne "") {
-                my $n = $f;
-                my $q = SqlQuote($s);
-                my $type = $F{$f . "_type"};
-                push(@specialchart, [$f, $type, $s]);
-            }
-        }
-    }
-
-    my $chartid;
-    # $statusid is used by the code that queries for attachment statuses.
-    my $statusid = 0;
-    my $f;
-    my $ff;
-    my $t;
-    my $q;
-    my $v;
-    my $term;
-    my %funcsbykey;
-    my @funcdefs =
-        (
-         "^(assigned_to|reporter)," => sub {
-             push(@supptables, "profiles map_$f");
-             push(@wherepart, "bugs.$f = map_$f.userid");
-             $f = "map_$f.login_name";
-         },
-         "^qa_contact," => sub {
-             push(@supptables,
-                  "LEFT JOIN profiles map_qa_contact ON bugs.qa_contact = map_qa_contact.userid");
-             $f = "map_$f.login_name";
-         },
-
-         "^cc," => sub {
-            push(@supptables, "LEFT JOIN cc cc_$chartid ON bugs.bug_id = cc_$chartid.bug_id");
-
-            push(@supptables, "LEFT JOIN profiles map_cc_$chartid ON cc_$chartid.who = map_cc_$chartid.userid");
-            $f = "map_cc_$chartid.login_name";
-         },
-
-         "^long_?desc,changedby" => sub {
-             my $table = "longdescs_$chartid";
-             push(@supptables, "longdescs $table");
-             push(@wherepart, "$table.bug_id = bugs.bug_id");
-             my $id = DBNameToIdAndCheck($v);
-             $term = "$table.who = $id";
-         },
-         "^long_?desc,changedbefore" => sub {
-             my $table = "longdescs_$chartid";
-             push(@supptables, "longdescs $table");
-             push(@wherepart, "$table.bug_id = bugs.bug_id");
-             $term = "$table.bug_when < " . SqlQuote(SqlifyDate($v));
-         },
-         "^long_?desc,changedafter" => sub {
-             my $table = "longdescs_$chartid";
-             push(@supptables, "longdescs $table");
-             push(@wherepart, "$table.bug_id = bugs.bug_id");
-             $term = "$table.bug_when > " . SqlQuote(SqlifyDate($v));
-         },
-         "^long_?desc," => sub {
-             my $table = "longdescs_$chartid";
-             push(@supptables, "longdescs $table");
-             push(@wherepart, "$table.bug_id = bugs.bug_id");
-             $f = "$table.thetext";
-         },
-         "^attachments\..*," => sub {
-             my $table = "attachments_$chartid";
-             push(@supptables, "attachments $table");
-             push(@wherepart, "bugs.bug_id = $table.bug_id");
-             $f =~ m/^attachments\.(.*)$/;
-             my $field = $1;
-             if ($t eq "changedby") {
-                 $v = DBNameToIdAndCheck($v);
-                 $q = SqlQuote($v);
-                 $field = "submitter_id";
-                 $t = "equals";
-             } elsif ($t eq "changedbefore") {
-                 $v = SqlifyDate($v);
-                 $q = SqlQuote($v);
-                 $field = "creation_ts";
-                 $t = "lessthan";
-             } elsif ($t eq "changedafter") {
-                 $v = SqlifyDate($v);
-                 $q = SqlQuote($v);
-                 $field = "creation_ts";
-                 $t = "greaterthan";
-             }
-             if ($field eq "ispatch" && $v ne "0" && $v ne "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") {
-                 DisplayError("The only legal values for the <em>Attachment is
-                               obsolete</em> field are 0 and 1.");
-                 exit;
-             }
-             $f = "$table.$field";
-         },
-         "^attachstatusdefs.name," => sub {
-             # The below has Fun with the names for attachment statuses. This
-             # isn't needed for changed* queries, so exclude those - the
-             # generic stuff will cope
-             return if ($t =~ m/^changed/);
-
-             # Searching for "status != 'bar'" wants us to look for an
-             # attachment without the 'bar' status, not for an attachment with
-             # a status not equal to 'bar' (Which would pick up an attachment
-             # with more than one status). We do this by LEFT JOINS, after
-             # grabbing the matching attachment status ids.
-             # Note that this still won't find bugs with no attachments, since
-             # that isn't really what people would expect.
-
-             # First, get the attachment status ids, using the other funcs
-             # to match the WHERE term.
-             # Note that we need to reverse the negated bits for this to work
-             # This somewhat abuses the definitions of the various terms -
-             # eg, does 'contains all' mean that the status has to contain all
-             # those words, or that all those words must be exact matches to
-             # statuses, which must all be on a single attachment, or should
-             # the match on the status descriptions be a contains match, too?
-
-             my $inverted = 0;
-             if ($t =~ m/not(.*)/) {
-                 $t = $1;
-                 $inverted = 1;
-             }
-
-             $ref = $funcsbykey{",$t"};
-             &$ref;
-             SendSQL("SELECT id FROM attachstatusdefs WHERE $term");
-
-             my @as_ids;
-             while (MoreSQLData()) {
-                 push @as_ids, FetchOneColumn();
-             }
-
-             # When searching for multiple statuses within a single boolean chart,
-             # we want to match each status record separately.  In other words,
-             # "status = 'foo' AND status = 'bar'" should match attachments with
-             # one status record equal to "foo" and another one equal to "bar",
-             # not attachments where the same status record equals both "foo" and
-             # "bar" (which is nonsensical).  In order to do this we must add an
-             # additional counter to the end of the "attachstatuses" table
-             # reference.
-             ++$statusid;
-
-             my $attachtable = "attachments_$chartid";
-             my $statustable = "attachstatuses_${chartid}_$statusid";
-
-             push(@supptables, "attachments $attachtable");
-             my $join = "LEFT JOIN attachstatuses $statustable ON ".
-               "($attachtable.attach_id = $statustable.attach_id AND " .
-                "$statustable.statusid IN (" . join(",", @as_ids) . "))";
-             push(@supptables, $join);
-             push(@wherepart, "bugs.bug_id = $attachtable.bug_id");
-             if ($inverted) {
-                 $term = "$statustable.statusid IS NULL";
-             } else {
-                 $term = "$statustable.statusid IS NOT NULL";
-             }
-         },
-         "^changedin," => sub {
-             $f = "(to_days(now()) - to_days(bugs.delta_ts))";
-         },
-
-         "^keywords," => sub {
-             GetVersionTable();
-             my @list;
-             my $table = "keywords_$chartid";
-             foreach my $value (split(/[\s,]+/, $v)) {
-                 if ($value eq '') {
-                     next;
-                 }
-                 my $id = GetKeywordIdFromName($value);
-                 if ($id) {
-                     push(@list, "$table.keywordid = $id");
-                 }
-                 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;
-             if (@list) {
-                 $haveawordterm = "(" . join(' OR ', @list) . ")";
-                 if ($t eq "anywords") {
-                     $term = $haveawordterm;
-                 } elsif ($t eq "allwords") {
-                     $ref = $funcsbykey{",$t"};
-                     &$ref;
-                     if ($term && $haveawordterm) {
-                         $term = "(($term) AND $haveawordterm)";
-                     }
-                 }
-             }
-             if ($term) {
-                 push(@supptables, "keywords $table");
-                 push(@wherepart, "$table.bug_id = bugs.bug_id");
-             }
-         },
-
-         "^dependson," => sub {
-                my $table = "dependson_" . $chartid;
-                push(@supptables, "dependencies $table");
-                $ff = "$table.$f";
-                $ref = $funcsbykey{",$t"};
-                &$ref;
-                push(@wherepart, "$table.blocked = bugs.bug_id");
-         },
-
-         "^blocked," => sub {
-                my $table = "blocked_" . $chartid;
-                push(@supptables, "dependencies $table");
-                $ff = "$table.$f";
-                $ref = $funcsbykey{",$t"};
-                &$ref;
-                push(@wherepart, "$table.dependson = bugs.bug_id");
-         },
-
-
-         ",equals" => sub {
-             $term = "$ff = $q";
-         },
-         ",notequals" => sub {
-             $term = "$ff != $q";
-         },
-         ",casesubstring" => sub {
-             $term = "INSTR($ff, $q)";
-         },
-         ",(substring|substr)" => sub {
-             $term = "INSTR(LOWER($ff), " . lc($q) . ")";
-         },
-         ",notsubstring" => sub {
-             $term = "INSTR(LOWER($ff), " . lc($q) . ") = 0";
-         },
-         ",regexp" => sub {
-             $term = "LOWER($ff) REGEXP $q";
-         },
-         ",notregexp" => sub {
-             $term = "LOWER($ff) NOT REGEXP $q";
-         },
-         ",lessthan" => sub {
-             $term = "$ff < $q";
-         },
-         ",greaterthan" => sub {
-             $term = "$ff > $q";
-         },
-         ",anyexact" => sub {
-             my @list;
-             foreach my $w (split(/,/, $v)) {
-                 if ($w eq "---" && $f !~ /milestone/) {
-                     $w = "";
-                 }
-                 push(@list, "$ff = " . SqlQuote($w));
-             }
-             $term = join(" OR ", @list);
-         },
-         ",anywordssubstr" => sub {
-             $term = join(" OR ", @{GetByWordListSubstr($ff, $v)});
-         },
-         ",allwordssubstr" => sub {
-             $term = join(" AND ", @{GetByWordListSubstr($ff, $v)});
-         },
-         ",nowordssubstr" => sub {
-             my @list = @{GetByWordListSubstr($ff, $v)};
-             if (@list) {
-                 $term = "NOT (" . join(" OR ", @list) . ")";
-             }
-         },
-         ",anywords" => sub {
-             $term = join(" OR ", @{GetByWordList($ff, $v)});
-         },
-         ",allwords" => sub {
-             $term = join(" AND ", @{GetByWordList($ff, $v)});
-         },
-         ",nowords" => sub {
-             my @list = @{GetByWordList($ff, $v)};
-             if (@list) {
-                 $term = "NOT (" . join(" OR ", @list) . ")";
-             }
-         },
-         ",changedbefore" => sub {
-             my $table = "act_$chartid";
-             my $ftable = "fielddefs_$chartid";
-             push(@supptables, "bugs_activity $table");
-             push(@supptables, "fielddefs $ftable");
-             push(@wherepart, "$table.bug_id = bugs.bug_id");
-             push(@wherepart, "$table.fieldid = $ftable.fieldid");
-             $term = "($ftable.name = '$f' AND $table.bug_when < $q)";
-         },
-         ",changedafter" => sub {
-             my $table = "act_$chartid";
-             my $ftable = "fielddefs_$chartid";
-             push(@supptables, "bugs_activity $table");
-             push(@supptables, "fielddefs $ftable");
-             push(@wherepart, "$table.bug_id = bugs.bug_id");
-             push(@wherepart, "$table.fieldid = $ftable.fieldid");
-             $term = "($ftable.name = '$f' AND $table.bug_when > $q)";
-         },
-         ",changedfrom" => sub {
-             my $table = "act_$chartid";
-             my $ftable = "fielddefs_$chartid";
-             push(@supptables, "bugs_activity $table");
-             push(@supptables, "fielddefs $ftable");
-             push(@wherepart, "$table.bug_id = bugs.bug_id");
-             push(@wherepart, "$table.fieldid = $ftable.fieldid");
-             $term = "($ftable.name = '$f' AND $table.removed = $q)";
-         },
-         ",changedto" => sub {
-             my $table = "act_$chartid";
-             my $ftable = "fielddefs_$chartid";
-             push(@supptables, "bugs_activity $table");
-             push(@supptables, "fielddefs $ftable");
-             push(@wherepart, "$table.bug_id = bugs.bug_id");
-             push(@wherepart, "$table.fieldid = $ftable.fieldid");
-             $term = "($ftable.name = '$f' AND $table.added = $q)";
-         },
-         ",changedby" => sub {
-             my $table = "act_$chartid";
-             my $ftable = "fielddefs_$chartid";
-             push(@supptables, "bugs_activity $table");
-             push(@supptables, "fielddefs $ftable");
-             push(@wherepart, "$table.bug_id = bugs.bug_id");
-             push(@wherepart, "$table.fieldid = $ftable.fieldid");
-             my $id = DBNameToIdAndCheck($v);
-             $term = "($ftable.name = '$f' AND $table.who = $id)";
-         },
-         );
-    my @funcnames;
-    while (@funcdefs) {
-        my $key = shift(@funcdefs);
-        my $value = shift(@funcdefs);
-        if ($key =~ /^[^,]*$/) {
-            die "All defs in %funcs must have a comma in their name: $key";
-        }
-        if (exists $funcsbykey{$key}) {
-            die "Duplicate key in %funcs: $key";
-        }
-        $funcsbykey{$key} = $value;
-        push(@funcnames, $key);
-    }
-
-    # first we delete any sign of "Chart #-1" from the HTML form hash
-    # since we want to guarantee the user didn't hide something here
-    my @badcharts = grep /^(field|type|value)-1-/, (keys %F);
-    foreach my $field (@badcharts) {
-        delete $F{$field};
-    }
-
-    # now we take our special chart and stuff it into the form hash
-    my $chart = -1;
-    my $row = 0;
-    foreach my $ref (@specialchart) {
-        my $col = 0;
-        while (@$ref) {
-            $F{"field$chart-$row-$col"} = shift(@$ref);
-            $F{"type$chart-$row-$col"} = shift(@$ref);
-            $F{"value$chart-$row-$col"} = shift(@$ref);
-            if ($debug) {
-                print qq{<p>$F{"field$chart-$row-$col"} | $F{"type$chart-$row-$col"} | $F{"value$chart-$row-$col"}*</p>\n};
-            }
-            $col++;
-
-        }
-        $row++;
-    }
-
-
-# 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
-# (query.cgi) may contain an arbitrary number of boolean charts where
-# 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
-# by pressing the OR button at the right end of the chart. Extra
-# charts are created by pressing "Add another boolean chart".
-#
-# Each chart consists of an artibrary number of rows and columns.
-# The terms within a row are ORed together. The expressions represented
-# by each row are ANDed together. The expressions represented by each
-# chart are ANDed together.
-#
-#        ----------------------
-#        | col2 | col2 | col3 |
-# --------------|------|------|
-# | row1 |  a1  |  a2  |      |
-# |------|------|------|------|  => ((a1 OR a2) AND (b1 OR b2 OR b3) AND (c1))
-# | row2 |  b1  |  b2  |  b3  |
-# |------|------|------|------|
-# | row3 |  c1  |      |      |
-# -----------------------------
-#
-#        --------
-#        | col2 |
-# --------------|
-# | row1 |  d1  | => (d1)
-# ---------------
-#
-# Together, these two charts represent a SQL expression like this
-# 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'.
-#
-# --------------------------------------------------------------
-# CC    | equal to
-# foo@blah.org
-# --------------------------------------------------------------
-# CC    | equal to
-# bar@blah.org
-#
-# 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.
-#
-# --------------------------------------------------------------
-# CC    | equal to
-# foo@blah.org
-# AND
-# CC    | equal to
-# bar@blah.org
-# --------------------------------------------------------------
-
-# $chartid is the number of the current chart whose SQL we're contructing
-# $row is the current row of the current chart
-
-# names for table aliases are constructed using $chartid and $row
-#   SELECT blah  FROM $table "$table_$chartid_$row" WHERE ....
-
-# $f  = field of table in bug db (e.g. bug_id, reporter, etc)
-# $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
-# $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
-#               out duplicates.
-# $suppstring = String which is pasted into query containing all table names
-
-    # get a list of field names to verify the user-submitted chart fields against
-    my %chartfields;
-    SendSQL("SELECT name FROM fielddefs");
-    while (MoreSQLData()) {
-        my ($name) = FetchSQLData();
-        $chartfields{$name} = 1;
-    }
-
-    $row = 0;
-    for ($chart=-1 ;
-         $chart < 0 || exists $F{"field$chart-0-0"} ;
-         $chart++) {
-        $chartid = $chart >= 0 ? $chart : "";
-        for ($row = 0 ;
-             exists $F{"field$chart-$row-0"} ;
-             $row++) {
-            my @orlist;
-            for (my $col = 0 ;
-                 exists $F{"field$chart-$row-$col"} ;
-                 $col++) {
-                $f = $F{"field$chart-$row-$col"} || "noop";
-                $t = $F{"type$chart-$row-$col"} || "noop";
-                $v = $F{"value$chart-$row-$col"};
-                $v = "" if !defined $v;
-                $v = trim($v);
-                if ($f eq "noop" || $t eq "noop" || $v eq "") {
-                    next;
-                }
-                # chart -1 is generated by other code above, not from the user-
-                # submitted form, so we'll blindly accept any values in chart -1
-                if ((!$chartfields{$f}) && ($chart != -1)) {
-                    my $errstr = "Can't use " . html_quote($f) . " as a field name.  " .
-                        "If you think you're getting this in error, please copy the " .
-                        "entire URL out of the address bar at the top of your browser " .
-                        "window and email it to <109679\@bugzilla.org>";
-                    die "Internal error: $errstr" if $chart < 0;
-                    return Error($errstr);
-                }
-
-                # This is either from the internal chart (in which case we
-                # already know about it), or it was in %chartfields, so it is
-                # a valid field name, which means that its ok.
-                trick_taint($f);
-                $q = SqlQuote($v);
-                my $func;
-                $term = undef;
-                foreach my $key (@funcnames) {
-                    if ("$f,$t" =~ m/$key/) {
-                        my $ref = $funcsbykey{$key};
-                        if ($debug) {
-                            print "<p>$key ($f , $t ) => ";
-                        }
-                        $ff = $f;
-                        if ($f !~ /\./) {
-                            $ff = "bugs.$f";
-                        }
-                        &$ref;
-                        if ($debug) {
-                            print "$f , $t , $term</p>";
-                        }
-                        if ($term) {
-                            last;
-                        }
-                    }
-                }
-                if ($term) {
-                    push(@orlist, $term);
-                }
-                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) {
-                push(@andlist, "(" . join(" OR ", @orlist) . ")");
-            }
-        }
-    }
-    my %suppseen = ("bugs" => 1);
-    my $suppstring = "bugs";
-    foreach my $str (@supptables) {
-        if (!$suppseen{$str}) {
-            if ($str !~ /^(LEFT|INNER) JOIN/i) {
-                $suppstring .= ",";
-            }
-            $suppstring .= " $str";
-            $suppseen{$str} = 1;
-        }
-    }
-    my $query =  ("SELECT DISTINCT " . join(', ', @fields) .
-                  " FROM $suppstring" .
-                  " WHERE " . join(' AND ', (@wherepart, @andlist)));
-
-    $query = SelectVisible($query, $::userid, $::usergroupset);
-
-    if ($debug) {
-        print "<p><code>" . value_quote($query) . "</code></p>\n";
-        exit;
-    }
-    return $query;
-}
-
-
-
 ################################################################################
 # Command Execution
 ################################################################################
@@ -1312,7 +487,9 @@ if ($dotweak) {
 my @selectnames = map($columns->{$_}->{'name'}, @selectcolumns);
 
 # Generate the basic SQL query that will be used to generate the bug list.
-my $query = GenerateSQL(\@selectnames, $::buffer);
+my $search = new Bugzilla::Search('fields' => \@selectnames, 
+                                  'url' => $::buffer);
+my $query = $search->getSQL();
 
 
 ################################################################################