#!/usr/bonsaitools/bin/perl -wT # -*- 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): Terry Weissman # Dan Mosedale # Stephan Niemz # Andreas Franke use diagnostics; use strict; use lib qw(.); require "CGI.pl"; use Date::Parse; # Shut up misguided -w warnings about "used only once". "use vars" just # doesn't work for me. sub sillyness { my $zz; $zz = $::db_name; $zz = $::defaultqueryname; $zz = $::unconfirmedstate; $zz = $::userid; $zz = @::components; $zz = @::default_column_list; $zz = @::legal_keywords; $zz = @::legal_platform; $zz = @::legal_priority; $zz = @::legal_product; $zz = @::settable_resolution; $zz = @::legal_severity; $zz = @::versions; $zz = @::target_milestone; $zz = %::proddesc; }; my $serverpush = 0; ConnectToDatabase(); #print "Content-type: text/plain\n\n"; # Handy for debugging. #$::FORM{'debug'} = 1; if (grep(/^cmd-/, keys(%::FORM))) { my $url = "query.cgi?$::buffer#chart"; print qq{Refresh: 0; URL=$url Content-type: text/html Adding field to query page...

Click here if page doesn't redisplay automatically. }; exit(); } if (!defined $::FORM{'cmdtype'}) { # This can happen if there's an old bookmark to a query... $::FORM{'cmdtype'} = 'doit'; } sub SqlifyDate { my ($str) = (@_); if (!defined $str) { $str = ""; } my $date = str2time($str); if (!defined $date) { PuntTryAgain("The string '".html_quote($str)."' is not a legal date."); } 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 Error { my ($str) = (@_); if (!$serverpush) { print "Content-type: text/html\n\n"; } PuntTryAgain($str); } sub GenerateSQL { my $debug = 0; my ($fieldsref, $supptablesref, $wherepartref, $urlstr) = (@_); my @fields; my @supptables; my @wherepart; @fields = @$fieldsref if $fieldsref; @supptables = @$supptablesref if $supptablesref; @wherepart = @$wherepartref if $wherepartref; my %F; my %M; ParseUrlString($urlstr, \%F, \%M); 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]*$/) { return Error("The 'At least ___ votes' field must be a\n" . "simple number. You entered \"" . html_quote($c) . "\", which\n" . "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'}})]); } # 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 { return Error("You must specify one or more fields in which to\n" . "search for " . html_quote($email) . ".\n"); } } if (defined $F{'changedin'}) { my $c = trim($F{'changedin'}); if ($c ne "") { if ($c !~ /^[0-9]*$/) { return Error("The 'changed in last ___ days' field must be\n" . "a simple number. You entered \"" . html_quote($c) . "\", which\n" . "doesn't cut it."); } 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 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") { return Error("The only legal values for the 'Attachment is patch' " . "field are 0 and 1."); } if ($field eq "isobsolete" && $v ne "0" && $v ne "1") { return Error("The only legal values for the 'Attachment is obsolete' " . "field are 0 and 1."); } $f = "$table.$field"; }, # 2001-05-16 myk@mozilla.org: enable querying against attachment status # if this installation has enabled use of the attachment tracker. "^attachstatusdefs.name," => sub { # When searching for multiple statuses within a single boolean chart, # we want to match each status record separately. In other words, # "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" and # "attachstatusdefs" table references. ++$statusid; my $attachtable = "attachments_$chartid"; my $statustable = "attachstatuses_${chartid}_$statusid"; my $statusdefstable = "attachstatusdefs_${chartid}_$statusid"; push(@supptables, "attachments $attachtable"); push(@supptables, "attachstatuses $statustable"); push(@supptables, "attachstatusdefs $statusdefstable"); push(@wherepart, "bugs.bug_id = $attachtable.bug_id"); push(@wherepart, "$attachtable.attach_id = $statustable.attach_id"); push(@wherepart, "$statustable.statusid = $statusdefstable.id"); # When the operator is changedbefore, changedafter, changedto, # or changedby, $f appears in the query as "fielddefs.name = '$f'", # so it must be the exact name of the table/field as they appear # in the fielddefs table (i.e. attachstatusdefs.name). For all # other operators, $f appears in the query as "$f = value", so it # should be the name of the table/field with the correct table # alias for this chart entry (f.e. attachstatusdefs_0.name). $f = ($t =~ /^changed/) ? "attachstatusdefs.name" : "$statusdefstable.name"; }, "^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 { return Error("Unknown keyword named " . html_quote($v) . ".\n" . "

The legal keyword names are\n" . "" . "listed here.\n"); } } 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{

$F{"field$chart-$row-$col"} | $F{"type$chart-$row-$col"} | $F{"value$chart-$row-$col"}*\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 "

$key ($f , $t ) => "; } $ff = $f; if ($f !~ /\./) { $ff = "bugs.$f"; } &$ref; if ($debug) { print "$f , $t , $term"; } if ($term) { last; } } } if ($term) { push(@orlist, $term); } else { my $errstr = "Can't seem to handle " . qq{'$F{"field$chart-$row-$col"}' and } . qq{'$F{"type$chart-$row-$col"}' } . "together"; die "Internal error: $errstr" if $chart < 0; return Error($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 " . join(', ', @fields) . " FROM $suppstring" . " WHERE " . join(' AND ', (@wherepart, @andlist)) . " GROUP BY bugs.bug_id"); $query = SelectVisible($query, $::userid, $::usergroupset); if ($debug) { print "

" . value_quote($query) . "

\n"; exit(); } return $query; } sub LookupNamedQuery { my ($name) = (@_); confirm_login(); my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"}); SendSQL("SELECT query FROM namedqueries " . "WHERE userid = $userid AND name = " . SqlQuote($name)); my $result = FetchOneColumn(); if (!defined $result) { print "Content-type: text/html\n\n"; PutHeader("Something weird happened"); print qq{The named query $name seems to no longer exist.}; PutFooter(); exit; } return $result; } $::querytitle = "Bug List"; CMD: for ($::FORM{'cmdtype'}) { /^runnamed$/ && do { $::buffer = LookupNamedQuery($::FORM{"namedcmd"}); $::querytitle = "Bug List: $::FORM{'namedcmd'}"; ProcessFormFields($::buffer); last CMD; }; /^editnamed$/ && do { my $url = "query.cgi?" . LookupNamedQuery($::FORM{"namedcmd"}); print qq{Content-type: text/html Refresh: 0; URL=$url What a hack. Loading your query named $::FORM{'namedcmd'}... }; exit; }; /^forgetnamed$/ && do { confirm_login(); my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"}); SendSQL("DELETE FROM namedqueries WHERE userid = $userid " . "AND name = " . SqlQuote($::FORM{'namedcmd'})); print "Content-type: text/html\n\n"; PutHeader("Query is gone", ""); print qq{ OK, the $::FORM{'namedcmd'} query is gone.

Go back to the query page. }; PutFooter(); exit; }; /^asdefault$/ && do { confirm_login(); my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"}); print "Content-type: text/html\n\n"; SendSQL("REPLACE INTO namedqueries (userid, name, query) VALUES " . "($userid, '$::defaultqueryname'," . SqlQuote($::buffer) . ")"); PutHeader("OK, default is set"); print qq{ OK, you now have a new default query. You may also bookmark the result of any individual query.

Go back to the query page, using the new default. }; PutFooter(); exit(); }; /^asnamed$/ && do { confirm_login(); my $userid = DBNameToIdAndCheck($::COOKIE{"Bugzilla_login"}); print "Content-type: text/html\n\n"; my $name = trim($::FORM{'newqueryname'}); if ($name eq "" || $name =~ /[<>&]/) { PutHeader("Please pick a valid name for your new query"); print "Click the Back button and type in a valid name\n"; print "for this query. (Query names should not contain unusual\n"; print "characters.)\n"; PutFooter(); exit(); } $::buffer =~ s/[\&\?]cmdtype=[a-z]+//; my $qname = SqlQuote($name); my $tofooter= ( $::FORM{'tofooter'} ? 1 : 0 ); SendSQL("SELECT query FROM namedqueries " . "WHERE userid = $userid AND name = $qname"); if (!FetchOneColumn()) { SendSQL("REPLACE INTO namedqueries (userid, name, query, linkinfooter) " . "VALUES ($userid, $qname, ". SqlQuote($::buffer) .", ". $tofooter .")"); } else { SendSQL("UPDATE namedqueries SET query = " . SqlQuote($::buffer) . "," . " linkinfooter = " . $tofooter . " WHERE userid = $userid AND name = $qname"); } PutHeader("OK, query saved."); print qq{ OK, you have a new query named $name


Go back to the query page }; PutFooter(); exit; }; } if (exists $ENV{'HTTP_USER_AGENT'} && $ENV{'HTTP_USER_AGENT'} =~ /Mozilla.[3-9]/ && $ENV{'HTTP_USER_AGENT'} !~ /[Cc]ompatible/ ) { # Search for real Netscape 3 and up. http://www.browsercaps.org used as source of # browsers compatbile with server-push. It's a Netscape hack, incompatbile # with MSIE and Lynx (at least). Even Communicator 4.51 has bugs with it, # especially during page reload. $serverpush = 1; print qq{Content-type: multipart/x-mixed-replace;boundary=thisrandomstring\n --thisrandomstring Content-type: text/html\n Bugzilla is pondering your query

Please stand by ...

}; # Note! HTML header is complete! } else { print "Content-type: text/html\n"; #Changing attachment to inline to resolve 46897 #zach@zachlipton.com print "Content-disposition: inline; filename=bugzilla_bug_list.html\n"; # Note! Don't finish HTML header yet! Only one newline so far! } sub DefCol { my ($name, $k, $t, $s, $q) = (@_); $::key{$name} = $k; $::title{$name} = $t; if (defined $s && $s ne "") { $::sortkey{$name} = $s; } if (!defined $q || $q eq "") { $q = 0; } $::needquote{$name} = $q; } DefCol("opendate", "unix_timestamp(bugs.creation_ts)", "Opened", "bugs.creation_ts"); DefCol("changeddate", "unix_timestamp(bugs.delta_ts)", "Changed", "bugs.delta_ts"); DefCol("severity", "substring(bugs.bug_severity, 1, 3)", "Sev", "bugs.bug_severity"); DefCol("priority", "substring(bugs.priority, 1, 3)", "Pri", "bugs.priority"); DefCol("platform", "substring(bugs.rep_platform, 1, 3)", "Plt", "bugs.rep_platform"); DefCol("owner", "map_assigned_to.login_name", "Owner", "map_assigned_to.login_name"); DefCol("reporter", "map_reporter.login_name", "Reporter", "map_reporter.login_name"); DefCol("qa_contact", "map_qa_contact.login_name", "QAContact", "map_qa_contact.login_name"); DefCol("status", "substring(bugs.bug_status,1,4)", "State", "bugs.bug_status"); DefCol("resolution", "substring(bugs.resolution,1,4)", "Result", "bugs.resolution"); DefCol("summary", "substring(bugs.short_desc, 1, 60)", "Summary", "bugs.short_desc", 1); DefCol("summaryfull", "bugs.short_desc", "Summary", "bugs.short_desc", 1); DefCol("status_whiteboard", "bugs.status_whiteboard", "StatusSummary", "bugs.status_whiteboard", 1); DefCol("component", "substring(bugs.component, 1, 8)", "Comp", "bugs.component"); DefCol("product", "substring(bugs.product, 1, 8)", "Product", "bugs.product"); DefCol("version", "substring(bugs.version, 1, 5)", "Vers", "bugs.version"); DefCol("os", "substring(bugs.op_sys, 1, 4)", "OS", "bugs.op_sys"); DefCol("target_milestone", "bugs.target_milestone", "TargetM", "bugs.target_milestone"); DefCol("votes", "bugs.votes", "Votes", "bugs.votes desc"); DefCol("keywords", "bugs.keywords", "Keywords", "bugs.keywords", 5); my @collist; if (defined $::FORM{'columnlist'}) { @collist = split(/[ ,]+/, $::FORM{'columnlist'}); } elsif (defined $::COOKIE{'COLUMNLIST'}) { @collist = split(/ /, $::COOKIE{'COLUMNLIST'}); } else { @collist = @::default_column_list; } my $minvotes; if (defined $::FORM{'votes'}) { if (trim($::FORM{'votes'}) ne "") { if (! (grep {/^votes$/} @collist)) { push(@collist, 'votes'); } } } my $dotweak = defined $::FORM{'tweak'}; if ($dotweak) { confirm_login(); if (!UserInGroup("editbugs")) { print qq{ Sorry; you do not have sufficient privileges to edit a bunch of bugs at once. }; PutFooter(); exit(); } } else { quietly_check_login(); } my @fields = ("bugs.bug_id", "bugs.groupset"); foreach my $c (@collist) { if (exists $::needquote{$c}) { # The value we are actually using is $::key{$c}, which was created # using the DefCol() function earlier. We test for the existance # of $::needsquote{$c} to find out if $c is a legitimate key in the # hashes that were defined by DefCol(). If $::needsquote{$c} exists, # then $c is valid and we can use it to look up our key. # If it doesn't exist, then we know the user is screwing with us # and we'll just skip it. trick_taint($c); push(@fields, $::key{$c}); } } if ($dotweak) { push(@fields, "bugs.product", "bugs.bug_status"); } if ($::FORM{'regetlastlist'}) { if (!$::COOKIE{'BUGLIST'}) { print qq{ Sorry, I seem to have lost the cookie that recorded the results of your last query. You will have to start over at the query page. }; PutFooter(); exit; } my @list = split(/:/, $::COOKIE{'BUGLIST'}); $::FORM{'bug_id'} = join(',', @list); if (!$::FORM{'order'}) { $::FORM{'order'} = 'reuse last sort'; } $::buffer = "bug_id=" . $::FORM{'bug_id'} . "&order=" . url_quote($::FORM{'order'}); } ReconnectToShadowDatabase(); my $query = GenerateSQL(\@fields, undef, undef, $::buffer); if ($::COOKIE{'LASTORDER'}) { if ((!$::FORM{'order'}) || $::FORM{'order'} =~ /^reuse/i) { $::FORM{'order'} = url_decode($::COOKIE{'LASTORDER'}); } } if (defined $::FORM{'order'} && $::FORM{'order'} ne "") { $query .= " ORDER BY "; $::FORM{'order'} =~ s/votesum/bugs.votes/; # Silly backwards compatability # hack. $::FORM{'order'} =~ s/assign\.login_name/map_assigned_to.login_name/g; # Another backwards compatability hack. ORDER: for ($::FORM{'order'}) { /\./ && do { # This (hopefully) already has fieldnames in it, so we're done. last ORDER; }; /Number/ && do { $::FORM{'order'} = "bugs.bug_id"; last ORDER; }; /Import/ && do { $::FORM{'order'} = "bugs.priority, bugs.bug_severity"; last ORDER; }; /Assign/ && do { $::FORM{'order'} = "map_assigned_to.login_name, bugs.bug_status, priority, bugs.bug_id"; last ORDER; }; /Changed/ && do { $::FORM{'order'} = "bugs.delta_ts, bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id"; last ORDER; }; # DEFAULT $::FORM{'order'} = "bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id"; } die "Invalid order: $::FORM{'order'}" unless $::FORM{'order'} =~ /^([a-zA-Z0-9_., ]+)$/; $::FORM{'order'} = $1; # detaint this, since we've checked it # Extra special disgusting hack: if we are ordering by target_milestone, # change it to order by the sortkey of the target_milestone first. my $order = $::FORM{'order'}; if ($order =~ /bugs.target_milestone/) { $query =~ s/ WHERE / LEFT JOIN milestones ms_order ON ms_order.value = bugs.target_milestone AND ms_order.product = bugs.product WHERE /; $order =~ s/bugs.target_milestone/ms_order.sortkey,ms_order.value/; } $query .= $order; } if ($::FORM{'debug'} && $serverpush) { print "

" . value_quote($query) . "

\n"; } if (Param('expectbigqueries')) { SendSQL("set option SQL_BIG_TABLES=1"); } SendSQL($query); my $count = 0; $::bugl = ""; sub pnl { my ($str) = (@_); $::bugl .= $str; } my $fields = $::buffer; $fields =~ s/[&?]order=[^&]*//g; $fields =~ s/[&?]cmdtype=[^&]*//g; my $orderpart; my $oldorder; if (defined $::FORM{'order'} && trim($::FORM{'order'}) ne "") { $orderpart = "&order=" . url_quote("$::FORM{'order'}"); $oldorder = url_quote(", $::FORM{'order'}"); } else { $orderpart = ""; $oldorder = ""; } if ($dotweak) { pnl "

"; } my @th; foreach my $c (@collist) { if (exists $::needquote{$c}) { my $h = ""; if (defined $::sortkey{$c}) { $h .= "$::title{$c}"; } else { $h .= $::title{$c}; } $h .= ""; push(@th, $h); } } my $tablestart = "
ID"; my $splitheader = 0; if ($::COOKIE{'SPLITHEADER'}) { $splitheader = 1; } if ($splitheader) { $tablestart =~ s/$tablestart"; } push @bugarray, $bug_id; # retrieve this bug's priority and severity, if available, # by looping through all column names -- gross but functional my $priority = "unknown"; my $severity; if ($pricol >= 0) { $priority = $row[$pricol]; } if ($sevcol >= 0) { $severity = $row[$sevcol]; } my $customstyle = ""; if ($severity) { if ($severity eq "enh") { $customstyle = "style='font-style:italic ! important'"; } if ($severity eq "blo") { $customstyle = "style='color:red ! important; font-weight:bold ! important'"; } if ($severity eq "cri") { $customstyle = "style='color:red; ! important'"; } } pnl "
"; if ($dotweak) { pnl ""; } pnl ""; pnl "$bug_id"; if ($g != "0") { pnl "*"; } pnl " "; foreach my $c (@collist) { if (exists $::needquote{$c}) { my $value = shift @row; if (!defined $value) { pnl ""; next; } if ($c eq "owner") { $ownerhash{$value} = 1; } if ($c eq "qa_contact") { $qahash{$value} = 1; } if ( ($c eq "owner" || $c eq "qa_contact" ) && length $value > $maxemailsize ) { my $trunc = substr $value, 0, $maxemailsize; $value = value_quote($value); $value = qq|$trunc...|; } elsif( $c eq 'changeddate' or $c eq 'opendate' ) { my $age = time() - $value; my ($s,$m,$h,$d,$mo,$y,$wd)= localtime $value; if( $age < 18*60*60 ) { $value = sprintf "%02d:%02d:%02d", $h,$m,$s; } elsif ( $age < 6*24*60*60 ) { $value = sprintf "%s %02d:%02d", $weekday[$wd],$h,$m; } else { $value = sprintf "%04d-%02d-%02d", 1900+$y,$mo+1,$d; } } if ($::needquote{$c} || $::needquote{$c} == 5) { $value = html_quote($value); } else { $value = "$value"; } pnl "$value"; } } if ($dotweak) { my $value = shift @row; $prodhash{$value} = 1; $value = shift @row; $statushash{$value} = 1; } pnl "\n"; } } my $buglist = join(":", @bugarray); # This is stupid. We really really need to move the quip list into the DB! my $quip; if (Param('usequip')){ if (open (COMMENTS, ") { push @cdata, $_; } close COMMENTS; $quip = $cdata[int(rand($#cdata + 1))]; } $quip ||= "Bugzilla would like to put a random quip here, but nobody has entered any."; } # We've done all we can without any output. If we can server push it is time # take down the waiting page and put up the real one. if ($serverpush) { print "\n"; print "--thisrandomstring\n"; print "Content-type: text/html\n"; print "Content-disposition: inline; filename=bugzilla_bug_list.html\n"; # Note! HTML header not yet closed } my $toolong = 0; if ($::FORM{'order'}) { my $q = url_quote($::FORM{'order'}); my $cookiepath = Param("cookiepath"); print "Set-Cookie: LASTORDER=$q ; path=$cookiepath; expires=Sun, 30-Jun-2029 00:00:00 GMT\n"; } if (length($buglist) < 4000) { print "Set-Cookie: BUGLIST=$buglist\n\n"; } else { print "Set-Cookie: BUGLIST=\n\n"; $toolong = 1; } PutHeader($::querytitle, undef, "", "", navigation_links($buglist)); print "
" . time2str("%a %b %e %T %Z %Y", time()) . ""; if (Param('usebuggroups')) { print "
* next to a bug number notes a bug not visible to everyone.
"; } if (defined $::FORM{'debug'}) { print "

" . value_quote($query) . "

\n"; } if ($toolong) { print "

This list is too long for bugzilla's little mind; the\n"; print "Next/Prev/First/Last buttons won't appear.

\n"; } if (Param('usequip')){ print "
$quip
\n"; } print "
"; print "$count bugs found." if $count > 9; print $tablestart, "\n"; print $::bugl; print "
\n"; if ($count == 0) { print "Zarro Boogs found.\n"; # I've been asked to explain this ... way back when, when Netscape released # version 4.0 of its browser, we had a release party. Naturally, there # had been a big push to try and fix every known bug before the release. # Naturally, that hadn't actually happened. (This is not unique to # Netscape or to 4.0; the same thing has happened with every software # project I've ever seen.) Anyway, at the release party, T-shirts were # handed out that said something like "Netscape 4.0: Zarro Boogs". # Just like the software, the T-shirt had no known bugs. Uh-huh. # # So, when you query for a list of bugs, and it gets no results, you # can think of this as a friendly reminder. Of *course* there are bugs # matching your query, they just aren't in the bugsystem yet... print qq{

Query Page\n}; print qq{  Enter New Bug\n}; print qq{Edit this query\n}; } elsif ($count == 1) { print "One bug found.\n"; } else { print "$count bugs found.\n"; } if ($dotweak) { GetVersionTable(); print " "; my $resolution_popup = make_options(\@::settable_resolution, "FIXED"); my @prod_list = keys %prodhash; my @list = @prod_list; my @legal_versions; my @legal_component; if (1 == @prod_list) { @legal_versions = @{$::versions{$prod_list[0]}}; @legal_component = @{$::components{$prod_list[0]}}; } my $version_popup = make_options(\@legal_versions, $::dontchange); my $platform_popup = make_options(\@::legal_platform, $::dontchange); my $priority_popup = make_options(\@::legal_priority, $::dontchange); my $sev_popup = make_options(\@::legal_severity, $::dontchange); my $component_popup = make_options(\@legal_component, $::dontchange); my $product_popup = make_options(\@::legal_product, $::dontchange); print "


"; if (Param("usetargetmilestone")) { my @legal_milestone; if(1 == @prod_list) { @legal_milestone = @{$::target_milestone{$prod_list[0]}}; } my $tfm_popup = make_options(\@legal_milestone, $::dontchange); print " "; } if (Param("useqacontact")) { print " "; } print qq{ }; if (@::legal_keywords) { print qq{ }; } print "
Product: Version:
Platform: Priority:
Component: Severity:
Target milestone:
QA Contact:
CC List:
Keywords:
Additional Comments:

"; if($::usergroupset ne '0') { SendSQL("select bit, name, description, isactive ". "from groups where bit & $::usergroupset != 0 ". "and isbuggroup != 0 ". "order by description"); # We only print out a header bit for this section if there are any # results. my $groupFound = 0; my $inactiveFound = 0; while (MoreSQLData()) { my ($bit, $groupname, $description, $isactive) = (FetchSQLData()); if(($prodhash{$groupname}) || (!defined($::proddesc{$groupname}))) { if(!$groupFound) { print "Groupset:
\n"; print "\n"; print "\n"; $groupFound = 1; } # Modifying this to use radio buttons instead print ""; print "\n"; print "\n"; if ($isactive) { print "\n"; } else { $inactiveFound = 1; print "\n"; } print "\n"; } } # Add in some blank space for legibility if($groupFound) { print "
Don't
change
this group
restriction\n"; print "
Remove
bugs
from this
group\n"; print "
Add
bugs
to this
group\n"; print "
Group name:
 "; if(!$isactive) { print ""; } print "$description"; if(!$isactive) { print ""; } print "
\n"; if ($inactiveFound) { print "(Note: Bugs may not be added to inactive groups (italicized), only removed)
\n"; } print "

\n"; } } # knum is which knob number we're generating, in javascript terms. my $knum = 0; print " Do nothing else
"; $knum++; if ($statushash{$::unconfirmedstate} && 1 == scalar(keys(%statushash))) { print " Confirm bugs (change status to NEW)
"; $knum++; } print " Accept bugs (change status to ASSIGNED)
"; $knum++; if (!defined $statushash{'CLOSED'} && !defined $statushash{'VERIFIED'} && !defined $statushash{'RESOLVED'}) { print " Clear the resolution
"; $knum++; print " Resolve bugs, changing resolution to
"; $knum++; } if (!defined $statushash{'NEW'} && !defined $statushash{'ASSIGNED'} && !defined $statushash{'REOPENED'}) { print " Reopen bugs
"; $knum++; } my @statuskeys = keys %statushash; if (1 == @statuskeys) { if (defined $statushash{'RESOLVED'}) { print " Mark bugs as VERIFIED
"; $knum++; } if (defined $statushash{'VERIFIED'}) { print " Mark bugs as CLOSED
"; $knum++; } } print " Reassign bugs to
"; $knum++; print " Reassign bugs to owner of selected component
"; $knum++; print "

To make changes to a bunch of bugs at once:

  1. Put check boxes next to the bugs you want to change.
  2. Adjust above form elements. (If the change you are making requires an explanation, include it in the comments box).
  3. Click the below \"Commit\" button.
"; my $movers = Param("movers"); $movers =~ s/\s?,\s?/|/g; $movers =~ s/@/\@/g; if ( Param("move-enabled") && (defined $::COOKIE{"Bugzilla_login"}) && ($::COOKIE{"Bugzilla_login"} =~ /($movers)/) ){ print "

"; print ""; } print "


\n"; } if ($count > 0) { print "
Query Page    Enter New Bug    Change columns"; if (!$dotweak && $count > 1 && UserInGroup("editbugs")) { print "  \n"; print ""; print "Change several bugs at once\n"; } my @owners = sort(keys(%ownerhash)); my $suffix = Param('emailsuffix'); if (@owners > 1 && UserInGroup("editbugs")) { if ($suffix ne "") { map(s/$/$suffix/, @owners); } my $list = join(',', @owners); print qq{  \n}; print qq{Send mail to bug owners\n}; } my @qacontacts = sort(keys(%qahash)); if (@qacontacts > 1 && UserInGroup("editbugs") && Param("useqacontact")) { if ($suffix ne "") { map(s/$/$suffix/, @qacontacts); } my $list = join(',', @qacontacts); print qq{  \n}; print qq{Send mail to bug QA contacts\n}; } print qq{  \n}; print qq{Edit this query\n}; print "
\n"; } # 2001-06-20, myk@mozilla.org, bug 47914: # Switch back from the shadow database to the regular database # so that PutFooter() can determine the current user even if # the "logincookies" table is corrupted in the shadow database. SendSQL("USE $::db_name"); PutFooter(); if ($serverpush) { print "\n--thisrandomstring--\n"; }