1
# -*- Mode: perl; indent-tabs-mode: nil -*-
3
# The contents of this file are subject to the Mozilla Public
4
# License Version 1.1 (the "License"); you may not use this file
5
# except in compliance with the License. You may obtain a copy of
6
# the License at http://www.mozilla.org/MPL/
8
# Software distributed under the License is distributed on an "AS
9
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
10
# implied. See the License for the specific language governing
11
# rights and limitations under the License.
13
# The Original Code is the Bugzilla Bug Tracking System.
15
# The Initial Developer of the Original Code is Netscape Communications
16
# Corporation. Portions created by Netscape are
17
# Copyright (C) 1998 Netscape Communications Corporation. All
20
# Contributor(s): Gervase Markham <gerv@gerv.net>
21
# Terry Weissman <terry@mozilla.org>
22
# Dan Mosedale <dmose@mozilla.org>
23
# Stephan Niemz <st.n@gmx.net>
24
# Andreas Franke <afranke@mathweb.org>
25
# Myk Melez <myk@mozilla.org>
26
# Michael Schindler <michael@compressconsult.com>
30
# The caller MUST require CGI.pl and globals.pl before using this
34
package Bugzilla::Search;
35
use base qw(Exporter);
36
@Bugzilla::Search::EXPORT = qw(IsValidQueryType);
46
# Note that the param argument may be modified by Bugzilla::Search
49
my $class = ref($invocant) || $invocant;
61
my $fieldsref = $self->{'fields'};
62
my $params = $self->{'params'};
63
my $user = $self->{'user'} || Bugzilla->user;
71
@fields = @$fieldsref if $fieldsref;
77
# First, deal with all the old hard-coded non-chart-based poop.
78
if (lsearch($fieldsref, 'map_assigned_to.login_name') >= 0 ||
79
lsearch($fieldsref, 'map_assigned_to.realname') >= 0) {
80
push @supptables, "profiles AS map_assigned_to";
81
push @wherepart, "bugs.assigned_to = map_assigned_to.userid";
84
if (lsearch($fieldsref, 'map_reporter.login_name') >= 0 ||
85
lsearch($fieldsref, 'map_reporter.realname') >= 0) {
86
push @supptables, "profiles AS map_reporter";
87
push @wherepart, "bugs.reporter = map_reporter.userid";
90
if (lsearch($fieldsref, 'map_qa_contact.login_name') >= 0 ||
91
lsearch($fieldsref, 'map_qa_contact.realname') >= 0) {
92
push @supptables, "LEFT JOIN profiles map_qa_contact ON bugs.qa_contact = map_qa_contact.userid";
95
if (lsearch($fieldsref, 'map_products.name') >= 0) {
96
push @supptables, "products AS map_products";
97
push @wherepart, "bugs.product_id = map_products.id";
100
if (lsearch($fieldsref, 'map_components.name') >= 0) {
101
push @supptables, "components AS map_components";
102
push @wherepart, "bugs.component_id = map_components.id";
105
if (grep($_ =~ /AS (actual_time|percentage_complete)$/, @$fieldsref)) {
106
push(@supptables, "longdescs AS ldtime");
107
push(@wherepart, "ldtime.bug_id = bugs.bug_id");
111
if (defined $params->param('votes')) {
112
my $c = trim($params->param('votes'));
114
if ($c !~ /^[0-9]*$/) {
115
ThrowUserError("illegal_at_least_x_votes",
118
push(@specialchart, ["votes", "greaterthan", $c - 1]);
122
if ($params->param('bug_id')) {
123
my $type = "anyexact";
124
if ($params->param('bugidtype') && $params->param('bugidtype') eq 'exclude') {
127
push(@specialchart, ["bug_id", $type, join(',', $params->param('bug_id'))]);
130
# If the user has selected all of either status or resolution, change to
131
# selecting none. This is functionally equivalent, but quite a lot faster.
132
# Also, if the status is __open__ or __closed__, translate those
133
# into their equivalent lists of open and closed statuses.
134
if ($params->param('bug_status')) {
135
my @bug_statuses = $params->param('bug_status');
136
if (scalar(@bug_statuses) == scalar(@::legal_bug_status)
137
|| $bug_statuses[0] eq "__all__")
139
$params->delete('bug_status');
141
elsif ($bug_statuses[0] eq '__open__') {
142
$params->param('bug_status', map(&::IsOpenedState($_) ? $_ : undef,
143
@::legal_bug_status));
145
elsif ($bug_statuses[0] eq "__closed__") {
146
$params->param('bug_status', map(&::IsOpenedState($_) ? undef : $_,
147
@::legal_bug_status));
151
if ($params->param('resolution')) {
152
my @resolutions = $params->param('resolution');
154
if (scalar(@resolutions) == scalar(@::legal_resolution)) {
155
$params->delete('resolution');
159
my @legal_fields = ("product", "version", "rep_platform", "op_sys",
160
"bug_status", "resolution", "priority", "bug_severity",
161
"assigned_to", "reporter", "component",
162
"target_milestone", "bug_group");
164
foreach my $field ($params->param()) {
165
if (lsearch(\@legal_fields, $field) != -1) {
166
push(@specialchart, [$field, "anyexact",
167
join(',', $params->param($field))]);
171
if ($params->param('keywords')) {
172
my $t = $params->param('keywords_type');
173
if (!$t || $t eq "or") {
176
push(@specialchart, ["keywords", $t, $params->param('keywords')]);
179
foreach my $id ("1", "2") {
180
if (!defined ($params->param("email$id"))) {
183
my $email = trim($params->param("email$id"));
187
my $type = $params->param("emailtype$id");
188
if ($type eq "exact") {
190
foreach my $name (split(',', $email)) {
193
&::DBNameToIdAndCheck($name);
199
foreach my $field ("assigned_to", "reporter", "cc", "qa_contact") {
200
if ($params->param("email$field$id")) {
201
push(@clist, $field, $type, $email);
204
if ($params->param("emaillongdesc$id")) {
205
push(@clist, "commenter", $type, $email);
208
push(@specialchart, \@clist);
210
ThrowUserError("missing_email_type",
211
{ email => $email });
215
my $chfieldfrom = trim(lc($params->param('chfieldfrom'))) || '';
216
my $chfieldto = trim(lc($params->param('chfieldto'))) || '';
217
$chfieldfrom = '' if ($chfieldfrom eq 'now');
218
$chfieldto = '' if ($chfieldto eq 'now');
219
my @chfield = $params->param('chfield');
220
my $chvalue = trim($params->param('chfieldvalue')) || '';
222
# 2003-05-20: The 'changedin' field is no longer in the UI, but we continue
223
# to process it because it will appear in stored queries and bookmarks.
224
my $changedin = trim($params->param('changedin')) || '';
226
if ($changedin !~ /^[0-9]*$/) {
227
ThrowUserError("illegal_changed_in_last_x_days",
228
{ value => $changedin });
233
&& scalar(@chfield) == 1
234
&& $chfield[0] eq "[Bug creation]")
236
# Deal with the special case where the query is using changedin
237
# to get bugs created in the last n days by converting the value
238
# into its equivalent for the chfieldfrom parameter.
239
$chfieldfrom = "-" . ($changedin - 1) . "d";
242
# Oh boy, the general case. Who knows why the user included
243
# the changedin parameter, but do our best to comply.
244
push(@specialchart, ["changedin", "lessthan", $changedin + 1]);
248
if ($chfieldfrom ne '' || $chfieldto ne '') {
249
my $sql_chfrom = $chfieldfrom ? &::SqlQuote(SqlifyDate($chfieldfrom)):'';
250
my $sql_chto = $chfieldto ? &::SqlQuote(SqlifyDate($chfieldto)) :'';
251
my $sql_chvalue = $chvalue ne '' ? &::SqlQuote($chvalue) : '';
253
push(@wherepart, "bugs.delta_ts >= $sql_chfrom") if ($sql_chfrom);
254
push(@wherepart, "bugs.delta_ts <= $sql_chto") if ($sql_chto);
256
my $bug_creation_clause;
258
foreach my $f (@chfield) {
259
if ($f eq "[Bug creation]") {
260
# Treat [Bug creation] differently because we need to look
261
# at bugs.creation_ts rather than the bugs_activity table.
263
push(@l, "bugs.creation_ts >= $sql_chfrom") if($sql_chfrom);
264
push(@l, "bugs.creation_ts <= $sql_chto") if($sql_chto);
265
$bug_creation_clause = "(" . join(' AND ', @l) . ")";
267
push(@list, "\nactcheck.fieldid = " . &::GetFieldID($f));
271
# @list won't have any elements if the only field being searched
272
# is [Bug creation] (in which case we don't need bugs_activity).
274
push(@supptables, "bugs_activity actcheck");
275
push(@wherepart, "actcheck.bug_id = bugs.bug_id");
277
push(@wherepart, "actcheck.bug_when >= $sql_chfrom");
280
push(@wherepart, "actcheck.bug_when <= $sql_chto");
283
push(@wherepart, "actcheck.added = $sql_chvalue");
287
# Now that we're done using @list to determine if there are any
288
# regular fields to search (and thus we need bugs_activity),
289
# add the [Bug creation] criterion to the list so we can OR it
290
# together with the others.
291
push(@list, $bug_creation_clause) if $bug_creation_clause;
293
push(@wherepart, "(" . join(" OR ", @list) . ")");
297
foreach my $f ("short_desc", "long_desc", "bug_file_loc",
298
"status_whiteboard") {
299
if (defined $params->param($f)) {
300
my $s = trim($params->param($f));
303
my $q = &::SqlQuote($s);
304
my $type = $params->param($f . "_type");
305
push(@specialchart, [$f, $type, $s]);
310
if (defined $params->param('content')) {
311
# Append a new chart implementing content quicksearch
313
for ($chart = 0 ; $params->param("field$chart-0-0") ; $chart++) {};
314
$params->param("field$chart-0-0", 'content');
315
$params->param("type$chart-0-0", 'matches');
316
$params->param("value$chart-0-0", $params->param('content'));
317
$params->param("field$chart-0-1", 'short_desc');
318
$params->param("type$chart-0-1", 'allwords');
319
$params->param("value$chart-0-1", $params->param('content'));
324
# $type_id is used by the code that queries for attachment flags.
335
"^(assigned_to|reporter)," => sub {
336
push(@supptables, "profiles AS map_$f");
337
push(@wherepart, "bugs.$f = map_$f.userid");
338
$f = "map_$f.login_name";
340
"^qa_contact," => sub {
342
"LEFT JOIN profiles map_qa_contact ON bugs.qa_contact = map_qa_contact.userid");
343
$f = "map_$f.login_name";
346
"^cc,(anyexact|substring)" => sub {
348
$list = $self->ListIDsForEmail($t, $v);
350
$chartseq = $chartid;
351
if ($chartid eq "") {
352
$chartseq = "CC$sequence";
356
push(@supptables, "LEFT JOIN cc cc_$chartseq ON bugs.bug_id = cc_$chartseq.bug_id AND cc_$chartseq.who IN($list)");
357
$term = "cc_$chartseq.who IS NOT NULL";
359
push(@supptables, "LEFT JOIN cc cc_$chartseq ON bugs.bug_id = cc_$chartseq.bug_id");
361
push(@supptables, "LEFT JOIN profiles map_cc_$chartseq ON cc_$chartseq.who = map_cc_$chartseq.userid");
362
$ff = $f = "map_cc_$chartseq.login_name";
363
my $ref = $funcsbykey{",$t"};
369
$chartseq = $chartid;
370
if ($chartid eq "") {
371
$chartseq = "CC$sequence";
374
push(@supptables, "LEFT JOIN cc cc_$chartseq ON bugs.bug_id = cc_$chartseq.bug_id");
376
push(@supptables, "LEFT JOIN profiles map_cc_$chartseq ON cc_$chartseq.who = map_cc_$chartseq.userid");
377
$f = "map_cc_$chartseq.login_name";
380
"^long_?desc,changedby" => sub {
381
my $table = "longdescs_$chartid";
382
push(@supptables, "longdescs $table");
383
push(@wherepart, "$table.bug_id = bugs.bug_id");
384
my $id = &::DBNameToIdAndCheck($v);
385
$term = "$table.who = $id";
387
"^long_?desc,changedbefore" => sub {
388
my $table = "longdescs_$chartid";
389
push(@supptables, "longdescs $table");
390
push(@wherepart, "$table.bug_id = bugs.bug_id");
391
$term = "$table.bug_when < " . &::SqlQuote(SqlifyDate($v));
393
"^long_?desc,changedafter" => sub {
394
my $table = "longdescs_$chartid";
395
push(@supptables, "longdescs $table");
396
push(@wherepart, "$table.bug_id = bugs.bug_id");
397
$term = "$table.bug_when > " . &::SqlQuote(SqlifyDate($v));
399
"^content,matches" => sub {
400
# "content" is an alias for columns containing text for which we
401
# can search a full-text index and retrieve results by relevance,
402
# currently just bug comments (and summaries to some degree).
403
# There's only one way to search a full-text index
404
# ("MATCH (...) AGAINST (...)"), so we only accept the "matches"
405
# operator, which is specific to full-text index searches.
407
# Add the longdescs table to the query so we can search comments.
408
my $table = "longdescs_$chartid";
409
push(@supptables, "INNER JOIN longdescs $table ON bugs.bug_id " .
411
if (Param("insidergroup")
412
&& !&::UserInGroup(Param("insidergroup")))
414
push(@wherepart, "$table.isprivate < 1");
417
# Create search terms to add to the SELECT and WHERE clauses.
418
# $term1 searches comments.
419
# $term2 searches summaries, which contributes to the relevance
420
# ranking in SELECT but doesn't limit which bugs get retrieved.
421
my $term1 = "MATCH($table.thetext) AGAINST(".&::SqlQuote($v).")";
422
my $term2 = "MATCH(bugs.short_desc) AGAINST(".&::SqlQuote($v).")";
424
# The term to use in the WHERE clause.
427
# In order to sort by relevance (in case the user requests it),
428
# we SELECT the relevance value and give it an alias so we can
429
# add it to the SORT BY clause when we build it in buglist.cgi.
431
# Note: MySQL calculates relevance for each comment separately,
432
# so we need to do some additional calculations to get an overall
433
# relevance value, which we do by calculating the average (mean)
434
# comment relevance and then adding the summary relevance, if any.
435
# This weights summary relevance heavily, which makes sense
436
# since summaries are short and thus highly significant.
438
# Note: We should be calculating the average relevance of all
439
# comments for a bug, not just matching comments, but that's hard
440
# (see http://bugzilla.mozilla.org/show_bug.cgi?id=145588#c35).
442
"(SUM($term1)/COUNT($term1) + $term2) AS relevance";
444
# Users can specify to display the relevance field, in which case
445
# it'll show up in the list of fields being selected, and we need
446
# to replace that occurrence with our select term. Otherwise
447
# we can just add the term to the list of fields being selected.
448
if (grep($_ eq "relevance", @fields)) {
449
@fields = map($_ eq "relevance" ? $select_term : $_ , @fields);
452
push(@fields, $select_term);
456
ThrowUserError("search_content_without_matches");
458
"^commenter," => sub {
461
$list = $self->ListIDsForEmail($t, $v);
462
$chartseq = $chartid;
463
if ($chartid eq "") {
464
$chartseq = "LD$sequence";
467
my $table = "longdescs_$chartseq";
469
if (Param("insidergroup") && !&::UserInGroup(Param("insidergroup"))) {
470
$extra = "AND $table.isprivate < 1";
473
push(@supptables, "LEFT JOIN longdescs $table ON $table.bug_id = bugs.bug_id $extra AND $table.who IN ($list)");
474
$term = "$table.who IS NOT NULL";
476
push(@supptables, "LEFT JOIN longdescs $table ON $table.bug_id = bugs.bug_id $extra");
477
push(@supptables, "LEFT JOIN profiles map_$table ON $table.who = map_$table.userid");
478
$ff = $f = "map_$table.login_name";
479
my $ref = $funcsbykey{",$t"};
483
"^long_?desc," => sub {
484
my $table = "longdescs_$chartid";
485
push(@supptables, "longdescs $table");
486
if (Param("insidergroup") && !&::UserInGroup(Param("insidergroup"))) {
487
push(@wherepart, "$table.isprivate < 1") ;
489
push(@wherepart, "$table.bug_id = bugs.bug_id");
490
$f = "$table.thetext";
492
"^work_time,changedby" => sub {
493
my $table = "longdescs_$chartid";
494
push(@supptables, "longdescs $table");
495
push(@wherepart, "$table.bug_id = bugs.bug_id");
496
my $id = &::DBNameToIdAndCheck($v);
497
$term = "(($table.who = $id";
498
$term .= ") AND ($table.work_time <> 0))";
500
"^work_time,changedbefore" => sub {
501
my $table = "longdescs_$chartid";
502
push(@supptables, "longdescs $table");
503
push(@wherepart, "$table.bug_id = bugs.bug_id");
504
$term = "(($table.bug_when < " . &::SqlQuote(SqlifyDate($v));
505
$term .= ") AND ($table.work_time <> 0))";
507
"^work_time,changedafter" => sub {
508
my $table = "longdescs_$chartid";
509
push(@supptables, "longdescs $table");
510
push(@wherepart, "$table.bug_id = bugs.bug_id");
511
$term = "(($table.bug_when > " . &::SqlQuote(SqlifyDate($v));
512
$term .= ") AND ($table.work_time <> 0))";
514
"^work_time," => sub {
515
my $table = "longdescs_$chartid";
516
push(@supptables, "longdescs $table");
517
push(@wherepart, "$table.bug_id = bugs.bug_id");
518
$f = "$table.work_time";
520
"^percentage_complete," => sub {
522
if ($t eq "equals") {
524
} elsif ($t eq "greaterthan") {
526
} elsif ($t eq "lessthan") {
528
} elsif ($t eq "notequal") {
530
} elsif ($t eq "regexp") {
532
} elsif ($t eq "notregexp") {
533
$oper = "NOT REGEXP";
537
if ($oper ne "noop") {
538
my $table = "longdescs_$chartid";
539
push(@supptables, "longdescs $table");
540
push(@wherepart, "$table.bug_id = bugs.bug_id");
541
my $field = "(100*((SUM($table.work_time)*COUNT(DISTINCT $table.bug_when)/COUNT(bugs.bug_id))/((SUM($table.work_time)*COUNT(DISTINCT $table.bug_when)/COUNT(bugs.bug_id))+bugs.remaining_time))) AS percentage_complete_$table";
542
push(@fields, $field);
544
"percentage_complete_$table $oper " . &::SqlQuote($v));
548
"^bug_group,(?!changed)" => sub {
549
push(@supptables, "LEFT JOIN bug_group_map bug_group_map_$chartid ON bugs.bug_id = bug_group_map_$chartid.bug_id");
551
push(@supptables, "LEFT JOIN groups groups_$chartid ON groups_$chartid.id = bug_group_map_$chartid.group_id");
552
$f = "groups_$chartid.name";
554
"^attachments\..*," => sub {
555
my $table = "attachments_$chartid";
556
push(@supptables, "attachments $table");
557
if (Param("insidergroup") && !&::UserInGroup(Param("insidergroup"))) {
558
push(@wherepart, "$table.isprivate = 0") ;
560
push(@wherepart, "bugs.bug_id = $table.bug_id");
561
$f =~ m/^attachments\.(.*)$/;
563
if ($t eq "changedby") {
564
$v = &::DBNameToIdAndCheck($v);
565
$q = &::SqlQuote($v);
566
$field = "submitter_id";
568
} elsif ($t eq "changedbefore") {
570
$q = &::SqlQuote($v);
571
$field = "creation_ts";
573
} elsif ($t eq "changedafter") {
575
$q = &::SqlQuote($v);
576
$field = "creation_ts";
579
if ($field eq "ispatch" && $v ne "0" && $v ne "1") {
580
ThrowUserError("illegal_attachment_is_patch");
582
if ($field eq "isobsolete" && $v ne "0" && $v ne "1") {
583
ThrowUserError("illegal_is_obsolete");
585
$f = "$table.$field";
587
"^flagtypes.name," => sub {
588
# Matches bugs by flag name/status.
589
# Note that--for the purposes of querying--a flag comprises
590
# its name plus its status (i.e. a flag named "review"
591
# with a status of "+" can be found by searching for "review+").
593
# Don't do anything if this condition is about changes to flags,
594
# as the generic change condition processors can handle those.
595
return if ($t =~ m/^changed/);
597
# Add the flags and flagtypes tables to the query. We do
598
# a left join here so bugs without any flags still match
599
# negative conditions (f.e. "flag isn't review+").
600
my $flags = "flags_$chartid";
601
push(@supptables, "LEFT JOIN flags $flags " .
602
"ON bugs.bug_id = $flags.bug_id " .
603
"AND $flags.is_active = 1");
604
my $flagtypes = "flagtypes_$chartid";
605
push(@supptables, "LEFT JOIN flagtypes $flagtypes " .
606
"ON $flags.type_id = $flagtypes.id");
608
# Generate the condition by running the operator-specific function.
609
# Afterwards the condition resides in the global $term variable.
610
$ff = "CONCAT($flagtypes.name, $flags.status)";
611
&{$funcsbykey{",$t"}};
613
# If this is a negative condition (f.e. flag isn't "review+"),
614
# we only want bugs where all flags match the condition, not
615
# those where any flag matches, which needs special magic.
616
# Instead of adding the condition to the WHERE clause, we select
617
# the number of flags matching the condition and the total number
618
# of flags on each bug, then compare them in a HAVING clause.
619
# If the numbers are the same, all flags match the condition,
620
# so this bug should be included.
622
push(@fields, "SUM($ff IS NOT NULL) AS allflags_$chartid");
623
push(@fields, "SUM($term) AS matchingflags_$chartid");
624
push(@having, "allflags_$chartid = matchingflags_$chartid");
628
"^requestees.login_name," => sub {
629
my $flags = "flags_$chartid";
630
push(@supptables, "LEFT JOIN flags $flags " .
631
"ON bugs.bug_id = $flags.bug_id " .
632
"AND $flags.is_active = 1");
633
push(@supptables, "LEFT JOIN profiles requestees_$chartid " .
634
"ON $flags.requestee_id = requestees_$chartid.userid");
635
$f = "requestees_$chartid.login_name";
637
"^setters.login_name," => sub {
638
my $flags = "flags_$chartid";
639
push(@supptables, "LEFT JOIN flags $flags " .
640
"ON bugs.bug_id = $flags.bug_id " .
641
"AND $flags.is_active = 1");
642
push(@supptables, "LEFT JOIN profiles setters_$chartid " .
643
"ON $flags.setter_id = setters_$chartid.userid");
644
$f = "setters_$chartid.login_name";
647
"^changedin," => sub {
648
$f = "(to_days(now()) - to_days(bugs.delta_ts))";
651
"^component,(?!changed)" => sub {
652
$f = $ff = "components.name";
653
$funcsbykey{",$t"}->();
654
$term = build_subselect("bugs.component_id",
660
"^product,(?!changed)" => sub {
661
# Generate the restriction condition
662
$f = $ff = "products.name";
663
$funcsbykey{",$t"}->();
664
$term = build_subselect("bugs.product_id",
670
"^keywords," => sub {
671
&::GetVersionTable();
673
my $table = "keywords_$chartid";
674
foreach my $value (split(/[\s,]+/, $v)) {
678
my $id = &::GetKeywordIdFromName($value);
680
push(@list, "$table.keywordid = $id");
683
ThrowUserError("unknown_keyword",
689
$haveawordterm = "(" . join(' OR ', @list) . ")";
690
if ($t eq "anywords") {
691
$term = $haveawordterm;
692
} elsif ($t eq "allwords") {
693
my $ref = $funcsbykey{",$t"};
695
if ($term && $haveawordterm) {
696
$term = "(($term) AND $haveawordterm)";
701
push(@supptables, "keywords $table");
702
push(@wherepart, "$table.bug_id = bugs.bug_id");
706
"^dependson," => sub {
707
my $table = "dependson_" . $chartid;
708
push(@supptables, "dependencies $table");
710
my $ref = $funcsbykey{",$t"};
712
push(@wherepart, "$table.blocked = bugs.bug_id");
716
my $table = "blocked_" . $chartid;
717
push(@supptables, "dependencies $table");
719
my $ref = $funcsbykey{",$t"};
721
push(@wherepart, "$table.dependson = bugs.bug_id");
724
"^owner_idle_time,(greaterthan|lessthan)" => sub {
725
my $table = "idle_" . $chartid;
726
$v =~ /^(\d+)\s*([hHdDwWmMyY])?$/;
729
my $unitinterval = 'DAY';
731
$unitinterval = 'HOUR';
732
} elsif ($unit eq 'w') {
733
$unitinterval = ' * 7 DAY';
734
} elsif ($unit eq 'm') {
735
$unitinterval = 'MONTH';
736
} elsif ($unit eq 'y') {
737
$unitinterval = 'YEAR';
739
my $cutoff = "DATE_SUB(NOW(),
740
INTERVAL $quantity $unitinterval)";
741
my $assigned_fieldid = &::GetFieldID('assigned_to');
742
push(@supptables, "LEFT JOIN longdescs comment_$table " .
743
"ON comment_$table.who = bugs.assigned_to " .
744
"AND comment_$table.bug_id = bugs.bug_id " .
745
"AND comment_$table.bug_when > $cutoff");
746
push(@supptables, "LEFT JOIN bugs_activity activity_$table " .
747
"ON (activity_$table.who = bugs.assigned_to " .
748
"OR activity_$table.fieldid = $assigned_fieldid) " .
749
"AND activity_$table.bug_id = bugs.bug_id " .
750
"AND activity_$table.bug_when > $cutoff");
751
if ($t =~ /greater/) {
752
push(@wherepart, "(comment_$table.who IS NULL " .
753
"AND activity_$table.who IS NULL)");
755
push(@wherepart, "(comment_$table.who IS NOT NULL " .
756
"OR activity_$table.who IS NOT NULL)");
764
",notequals" => sub {
767
",casesubstring" => sub {
768
# mysql 4.0.1 and lower do not support CAST
769
# mysql 3.*.* had a case-sensitive INSTR
770
# (checksetup has a check for unsupported versions)
771
my $server_version = Bugzilla::DB->server_version;
772
if ($server_version =~ /^3\./) {
773
$term = "INSTR($ff ,$q)";
775
$term = "INSTR(CAST($ff AS BINARY), CAST($q AS BINARY))";
778
",substring" => sub {
779
$term = "INSTR(LOWER($ff), " . lc($q) . ")";
782
$funcsbykey{",substring"}->();
784
",notsubstring" => sub {
785
$term = "INSTR(LOWER($ff), " . lc($q) . ") = 0";
788
$term = "LOWER($ff) REGEXP $q";
790
",notregexp" => sub {
791
$term = "LOWER($ff) NOT REGEXP $q";
797
ThrowUserError("search_content_without_matches");
799
",greaterthan" => sub {
804
foreach my $w (split(/,/, $v)) {
805
if ($w eq "---" && $f !~ /milestone/) {
808
push(@list, &::SqlQuote($w));
811
$term = "$ff IN (" . join (',', @list) . ")";
814
",anywordssubstr" => sub {
815
$term = join(" OR ", @{GetByWordListSubstr($ff, $v)});
817
",allwordssubstr" => sub {
818
$term = join(" AND ", @{GetByWordListSubstr($ff, $v)});
820
",nowordssubstr" => sub {
821
my @list = @{GetByWordListSubstr($ff, $v)};
823
$term = "NOT (" . join(" OR ", @list) . ")";
827
$term = join(" OR ", @{GetByWordList($ff, $v)});
830
$term = join(" AND ", @{GetByWordList($ff, $v)});
833
my @list = @{GetByWordList($ff, $v)};
835
$term = "NOT (" . join(" OR ", @list) . ")";
838
",changedbefore" => sub {
839
my $table = "act_$chartid";
840
my $ftable = "fielddefs_$chartid";
841
push(@supptables, "bugs_activity $table");
842
push(@supptables, "fielddefs $ftable");
843
push(@wherepart, "$table.bug_id = bugs.bug_id");
844
push(@wherepart, "$table.fieldid = $ftable.fieldid");
845
$term = "($ftable.name = '$f' AND $table.bug_when < $q)";
847
",changedafter" => sub {
848
my $table = "act_$chartid";
849
my $ftable = "fielddefs_$chartid";
850
push(@supptables, "bugs_activity $table");
851
push(@supptables, "fielddefs $ftable");
852
push(@wherepart, "$table.bug_id = bugs.bug_id");
853
push(@wherepart, "$table.fieldid = $ftable.fieldid");
854
$term = "($ftable.name = '$f' AND $table.bug_when > $q)";
856
",changedfrom" => sub {
857
my $table = "act_$chartid";
858
my $ftable = "fielddefs_$chartid";
859
push(@supptables, "bugs_activity $table");
860
push(@supptables, "fielddefs $ftable");
861
push(@wherepart, "$table.bug_id = bugs.bug_id");
862
push(@wherepart, "$table.fieldid = $ftable.fieldid");
863
$term = "($ftable.name = '$f' AND $table.removed = $q)";
865
",changedto" => sub {
866
my $table = "act_$chartid";
867
my $ftable = "fielddefs_$chartid";
868
push(@supptables, "bugs_activity $table");
869
push(@supptables, "fielddefs $ftable");
870
push(@wherepart, "$table.bug_id = bugs.bug_id");
871
push(@wherepart, "$table.fieldid = $ftable.fieldid");
872
$term = "($ftable.name = '$f' AND $table.added = $q)";
874
",changedby" => sub {
875
my $table = "act_$chartid";
876
my $ftable = "fielddefs_$chartid";
877
push(@supptables, "bugs_activity $table");
878
push(@supptables, "fielddefs $ftable");
879
push(@wherepart, "$table.bug_id = bugs.bug_id");
880
push(@wherepart, "$table.fieldid = $ftable.fieldid");
881
my $id = &::DBNameToIdAndCheck($v);
882
$term = "($ftable.name = '$f' AND $table.who = $id)";
887
my $key = shift(@funcdefs);
888
my $value = shift(@funcdefs);
889
if ($key =~ /^[^,]*$/) {
890
die "All defs in %funcs must have a comma in their name: $key";
892
if (exists $funcsbykey{$key}) {
893
die "Duplicate key in %funcs: $key";
895
$funcsbykey{$key} = $value;
896
push(@funcnames, $key);
899
# first we delete any sign of "Chart #-1" from the HTML form hash
900
# since we want to guarantee the user didn't hide something here
901
my @badcharts = grep /^(field|type|value)-1-/, $params->param();
902
foreach my $field (@badcharts) {
903
$params->delete($field);
906
# now we take our special chart and stuff it into the form hash
909
foreach my $ref (@specialchart) {
912
$params->param("field$chart-$row-$col", shift(@$ref));
913
$params->param("type$chart-$row-$col", shift(@$ref));
914
$params->param("value$chart-$row-$col", shift(@$ref));
916
print qq{<p>$params->param("field$chart-$row-$col") | $params->param("type$chart-$row-$col") | $params->param("value$chart-$row-$col")*</p>\n};
925
# A boolean chart is a way of representing the terms in a logical
926
# expression. Bugzilla builds SQL queries depending on how you enter
927
# terms into the boolean chart. Boolean charts are represented in
928
# urls as tree-tuples of (chart id, row, column). The query form
929
# (query.cgi) may contain an arbitrary number of boolean charts where
930
# each chart represents a clause in a SQL query.
932
# The query form starts out with one boolean chart containing one
933
# row and one column. Extra rows can be created by pressing the
934
# AND button at the bottom of the chart. Extra columns are created
935
# by pressing the OR button at the right end of the chart. Extra
936
# charts are created by pressing "Add another boolean chart".
938
# Each chart consists of an arbitrary number of rows and columns.
939
# The terms within a row are ORed together. The expressions represented
940
# by each row are ANDed together. The expressions represented by each
941
# chart are ANDed together.
943
# ----------------------
944
# | col2 | col2 | col3 |
945
# --------------|------|------|
946
# | row1 | a1 | a2 | |
947
# |------|------|------|------| => ((a1 OR a2) AND (b1 OR b2 OR b3) AND (c1))
948
# | row2 | b1 | b2 | b3 |
949
# |------|------|------|------|
951
# -----------------------------
956
# | row1 | d1 | => (d1)
959
# Together, these two charts represent a SQL expression like this
960
# SELECT blah FROM blah WHERE ( (a1 OR a2)AND(b1 OR b2 OR b3)AND(c1)) AND (d1)
962
# The terms within a single row of a boolean chart are all constraints
963
# on a single piece of data. If you're looking for a bug that has two
964
# different people cc'd on it, then you need to use two boolean charts.
965
# This will find bugs with one CC matching 'foo@blah.org' and and another
966
# CC matching 'bar@blah.org'.
968
# --------------------------------------------------------------
971
# --------------------------------------------------------------
975
# If you try to do this query by pressing the AND button in the
976
# original boolean chart then what you'll get is an expression that
977
# looks for a single CC where the login name is both "foo@blah.org",
978
# and "bar@blah.org". This is impossible.
980
# --------------------------------------------------------------
986
# --------------------------------------------------------------
988
# $chartid is the number of the current chart whose SQL we're constructing
989
# $row is the current row of the current chart
991
# names for table aliases are constructed using $chartid and $row
992
# SELECT blah FROM $table "$table_$chartid_$row" WHERE ....
994
# $f = field of table in bug db (e.g. bug_id, reporter, etc)
995
# $ff = qualified field name (field name prefixed by table)
996
# e.g. bugs_activity.bug_id
997
# $t = type of query. e.g. "equal to", "changed after", case sensitive substr"
998
# $v = value - value the user typed in to the form
999
# $q = sanitized version of user input (SqlQuote($v))
1000
# @supptables = Tables and/or table aliases used in query
1001
# %suppseen = A hash used to store all the tables in supptables to weed
1003
# @supplist = A list used to accumulate all the JOIN clauses for each
1004
# chart to merge the ON sections of each.
1005
# $suppstring = String which is pasted into query containing all table names
1007
# get a list of field names to verify the user-submitted chart fields against
1009
&::SendSQL("SELECT name FROM fielddefs");
1010
while (&::MoreSQLData()) {
1011
my ($name) = &::FetchSQLData();
1012
$chartfields{$name} = 1;
1017
$chart < 0 || $params->param("field$chart-0-0") ;
1019
$chartid = $chart >= 0 ? $chart : "";
1021
$params->param("field$chart-$row-0") ;
1025
$params->param("field$chart-$row-$col") ;
1027
$f = $params->param("field$chart-$row-$col") || "noop";
1028
$t = $params->param("type$chart-$row-$col") || "noop";
1029
$v = $params->param("value$chart-$row-$col");
1030
$v = "" if !defined $v;
1032
if ($f eq "noop" || $t eq "noop" || $v eq "") {
1035
# chart -1 is generated by other code above, not from the user-
1036
# submitted form, so we'll blindly accept any values in chart -1
1037
if ((!$chartfields{$f}) && ($chart != -1)) {
1038
ThrowCodeError("invalid_field_name", {field => $f});
1041
# This is either from the internal chart (in which case we
1042
# already know about it), or it was in %chartfields, so it is
1043
# a valid field name, which means that it's ok.
1045
$q = &::SqlQuote($v);
1048
foreach my $key (@funcnames) {
1049
if ("$f,$t" =~ m/$key/) {
1050
my $ref = $funcsbykey{$key};
1052
print "<p>$key ($f , $t ) => ";
1060
print "$f , $t , $term</p>";
1068
push(@orlist, $term);
1071
# This field and this type don't work together.
1072
ThrowCodeError("field_type_mismatch",
1073
{ field => $params->param("field$chart-$row-$col"),
1074
type => $params->param("type$chart-$row-$col"),
1079
@orlist = map("($_)", @orlist) if (scalar(@orlist) > 1);
1080
push(@andlist, "(" . join(" OR ", @orlist) . ")");
1084
my %suppseen = ("bugs" => 1);
1085
my $suppstring = "bugs";
1086
my @supplist = (" ");
1087
foreach my $str (@supptables) {
1088
if (!$suppseen{$str}) {
1089
if ($str =~ /^(LEFT|INNER) JOIN/i) {
1090
$str =~ /^(.*?)\s+ON\s+(.*)$/i;
1091
my ($leftside, $rightside) = ($1, $2);
1092
if ($suppseen{$leftside}) {
1093
$supplist[$suppseen{$leftside}] .= " AND ($rightside)";
1095
$suppseen{$leftside} = scalar @supplist;
1096
push @supplist, " $leftside ON ($rightside)";
1099
$suppstring .= ", $str";
1100
$suppseen{$str} = 1;
1104
$suppstring .= join('', @supplist);
1106
# Make sure we create a legal SQL query.
1107
@andlist = ("1 = 1") if !@andlist;
1109
my $query = "SELECT " . join(', ', @fields) .
1110
" FROM $suppstring" .
1111
" LEFT JOIN bug_group_map " .
1112
" ON bug_group_map.bug_id = bugs.bug_id ";
1115
if (%{$user->groups}) {
1116
$query .= " AND bug_group_map.group_id NOT IN (" . join(',', values(%{$user->groups})) . ") ";
1119
$query .= " LEFT JOIN cc ON cc.bug_id = bugs.bug_id AND cc.who = " . $user->id;
1122
$query .= " WHERE " . join(' AND ', (@wherepart, @andlist)) .
1123
" AND bugs.creation_ts IS NOT NULL AND ((bug_group_map.group_id IS NULL)";
1126
my $userid = $user->id;
1127
$query .= " OR (bugs.reporter_accessible = 1 AND bugs.reporter = $userid) " .
1128
" OR (bugs.cclist_accessible = 1 AND cc.who IS NOT NULL) " .
1129
" OR (bugs.assigned_to = $userid) ";
1130
if (Param('useqacontact')) {
1131
$query .= "OR (bugs.qa_contact = $userid) ";
1135
$query .= ") GROUP BY bugs.bug_id";
1138
$query .= " HAVING " . join(" AND ", @having);
1142
print "<p><code>" . value_quote($query) . "</code></p>\n";
1146
$self->{'sql'} = $query;
1149
###############################################################################
1150
# Helper functions for the init() method.
1151
###############################################################################
1154
$str = "" if !defined $str;
1156
my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time());
1157
return sprintf("%4d-%02d-%02d 00:00:00", $year+1900, $month+1, $mday);
1159
if ($str =~ /^-?(\d+)([dDwWmMyY])$/) { # relative date
1160
my ($amount, $unit, $date) = ($1, lc $2, time);
1161
my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date);
1162
if ($unit eq 'w') { # convert weeks to days
1163
$amount = 7*$amount + $wday;
1167
$date -= $sec + 60*$min + 3600*$hour + 24*3600*$amount;
1168
return time2str("%Y-%m-%d %H:%M:%S", $date);
1170
elsif ($unit eq 'y') {
1171
return sprintf("%4d-01-01 00:00:00", $year+1900-$amount);
1173
elsif ($unit eq 'm') {
1175
while ($month<0) { $year--; $month += 12; }
1176
return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1);
1178
return undef; # should not happen due to regexp at top
1180
my $date = str2time($str);
1181
if (!defined($date)) {
1182
ThrowUserError("illegal_date", { date => $str });
1184
return time2str("%Y-%m-%d %H:%M:%S", $date);
1187
# ListIDsForEmail returns a string with a comma-joined list
1188
# of userids matching email addresses
1189
# according to the type specified.
1190
# Currently, this only supports exact, anyexact, and substring matches.
1191
# Substring matches will return up to 50 matching userids
1192
# If a match type is unsupported or returns too many matches,
1193
# ListIDsForEmail returns an undef.
1194
sub ListIDsForEmail {
1195
my ($self, $type, $email) = (@_);
1196
my $old = $self->{"emailcache"}{"$type,$email"};
1197
return undef if ($old && $old eq "---");
1198
return $old if $old;
1201
if ($type eq 'anyexact') {
1202
foreach my $w (split(/,/, $email)) {
1204
my $id = &::DBname_to_id($w);
1209
$list = join(',', @list);
1210
} elsif ($type eq 'substring') {
1211
&::SendSQL("SELECT userid FROM profiles WHERE INSTR(login_name, " .
1212
&::SqlQuote($email) . ") LIMIT 51");
1213
while (&::MoreSQLData()) {
1214
my ($id) = &::FetchSQLData();
1218
$list = join(',', @list);
1221
$self->{"emailcache"}{"$type,$email"} = $list;
1222
return undef if ($list eq "---");
1226
sub build_subselect {
1227
my ($outer, $inner, $table, $cond) = @_;
1228
my $q = "SELECT $inner FROM $table WHERE $cond";
1229
#return "$outer IN ($q)";
1232
while (&::MoreSQLData()) {
1233
push (@list, &::FetchOneColumn());
1235
return "1=2" unless @list; # Could use boolean type on dbs which support it
1236
return "$outer IN (" . join(',', @list) . ")";
1240
my ($field, $strs) = (@_);
1243
foreach my $w (split(/[\s,]+/, $strs)) {
1246
$word =~ tr/A-Z/a-z/;
1247
$word = &::SqlQuote(quotemeta($word));
1250
$word = '(^|[^a-z0-9])' . $word . '($|[^a-z0-9])';
1251
push(@list, "lower($field) regexp '$word'");
1258
# Support for "any/all/nowordssubstr" comparison type ("words as substrings")
1259
sub GetByWordListSubstr {
1260
my ($field, $strs) = (@_);
1263
foreach my $word (split(/[\s,]+/, $strs)) {
1265
push(@list, "INSTR(LOWER($field), " . lc(&::SqlQuote($word)) . ")");
1274
return $self->{'sql'};
1277
# Define if the Query Type passed in is a valid query type that we can deal with
1278
sub IsValidQueryType
1280
my ($queryType) = @_;
1281
if (grep { $_ eq $queryType } qw(specific advanced)) {