diff options
-rw-r--r-- | Bugzilla/Search.pm | 860 | ||||
-rwxr-xr-x | buglist.cgi | 865 |
2 files changed, 881 insertions, 844 deletions
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm new file mode 100644 index 000000000..862602685 --- /dev/null +++ b/Bugzilla/Search.pm @@ -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; diff --git a/buglist.cgi b/buglist.cgi index ce824ea57..589719962 100755 --- a/buglist.cgi +++ b/buglist.cgi @@ -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(); ################################################################################ |