~ubuntu-branches/ubuntu/edgy/bugzilla/edgy

« back to all changes in this revision

Viewing changes to Bugzilla/Search.pm

  • Committer: Bazaar Package Importer
  • Author(s): Alexis Sukrieh
  • Date: 2005-10-03 16:51:01 UTC
  • mfrom: (1.1.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20051003165101-38n0y5qofd68vole
Tags: 2.18.4-1
* New upstream minor release
  + Fixed a security issue: It was possible to bypass the "user
    visibility groups" restrictions if user-matching was turned on
    in "substring" mode.
  + Fixed a security issue: config.cgi exposed information to users who
    weren't logged in, even when "requirelogin" was turned on in Bugzilla.
  (closes: #331206)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- Mode: perl; indent-tabs-mode: nil -*-
 
2
#
 
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/
 
7
#
 
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.
 
12
#
 
13
# The Original Code is the Bugzilla Bug Tracking System.
 
14
#
 
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
 
18
# Rights Reserved.
 
19
#
 
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>
 
27
 
 
28
use strict;
 
29
 
 
30
# The caller MUST require CGI.pl and globals.pl before using this
 
31
 
 
32
use vars qw($userid);
 
33
 
 
34
package Bugzilla::Search;
 
35
use base qw(Exporter);
 
36
@Bugzilla::Search::EXPORT = qw(IsValidQueryType);
 
37
 
 
38
use Bugzilla::Config;
 
39
use Bugzilla::Error;
 
40
use Bugzilla::Util;
 
41
 
 
42
use Date::Format;
 
43
use Date::Parse;
 
44
 
 
45
# Create a new Search
 
46
# Note that the param argument may be modified by Bugzilla::Search
 
47
sub new {
 
48
    my $invocant = shift;
 
49
    my $class = ref($invocant) || $invocant;
 
50
  
 
51
    my $self = { @_ };  
 
52
    bless($self, $class);
 
53
    
 
54
    $self->init();
 
55
    
 
56
    return $self;
 
57
}
 
58
 
 
59
sub init {
 
60
    my $self = shift;
 
61
    my $fieldsref = $self->{'fields'};
 
62
    my $params = $self->{'params'};
 
63
    my $user = $self->{'user'} || Bugzilla->user;
 
64
 
 
65
    my $debug = 0;
 
66
        
 
67
    my @fields;
 
68
    my @supptables;
 
69
    my @wherepart;
 
70
    my @having;
 
71
    @fields = @$fieldsref if $fieldsref;
 
72
    my @specialchart;
 
73
    my @andlist;
 
74
 
 
75
    &::GetVersionTable();
 
76
    
 
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";
 
82
    }
 
83
 
 
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";
 
88
    }
 
89
 
 
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";
 
93
    }
 
94
 
 
95
    if (lsearch($fieldsref, 'map_products.name') >= 0) {
 
96
        push @supptables, "products AS map_products";
 
97
        push @wherepart, "bugs.product_id = map_products.id";
 
98
    }
 
99
 
 
100
    if (lsearch($fieldsref, 'map_components.name') >= 0) {
 
101
        push @supptables, "components AS map_components";
 
102
        push @wherepart, "bugs.component_id = map_components.id";
 
103
    }
 
104
    
 
105
    if (grep($_ =~ /AS (actual_time|percentage_complete)$/, @$fieldsref)) {
 
106
        push(@supptables, "longdescs AS ldtime");
 
107
        push(@wherepart, "ldtime.bug_id = bugs.bug_id");
 
108
    }
 
109
    
 
110
    my $minvotes;
 
111
    if (defined $params->param('votes')) {
 
112
        my $c = trim($params->param('votes'));
 
113
        if ($c ne "") {
 
114
            if ($c !~ /^[0-9]*$/) {
 
115
                ThrowUserError("illegal_at_least_x_votes",
 
116
                                  { value => $c });
 
117
            }
 
118
            push(@specialchart, ["votes", "greaterthan", $c - 1]);
 
119
        }
 
120
    }
 
121
 
 
122
    if ($params->param('bug_id')) {
 
123
        my $type = "anyexact";
 
124
        if ($params->param('bugidtype') && $params->param('bugidtype') eq 'exclude') {
 
125
            $type = "nowords";
 
126
        }
 
127
        push(@specialchart, ["bug_id", $type, join(',', $params->param('bug_id'))]);
 
128
    }
 
129
 
 
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__")
 
138
        {
 
139
            $params->delete('bug_status');
 
140
        }
 
141
        elsif ($bug_statuses[0] eq '__open__') {
 
142
            $params->param('bug_status', map(&::IsOpenedState($_) ? $_ : undef, 
 
143
                                             @::legal_bug_status));
 
144
        }
 
145
        elsif ($bug_statuses[0] eq "__closed__") {
 
146
            $params->param('bug_status', map(&::IsOpenedState($_) ? undef : $_, 
 
147
                                             @::legal_bug_status));
 
148
        }
 
149
    }
 
150
    
 
151
    if ($params->param('resolution')) {
 
152
        my @resolutions = $params->param('resolution');
 
153
        
 
154
        if (scalar(@resolutions) == scalar(@::legal_resolution)) {
 
155
            $params->delete('resolution');
 
156
        }
 
157
    }
 
158
    
 
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");
 
163
 
 
164
    foreach my $field ($params->param()) {
 
165
        if (lsearch(\@legal_fields, $field) != -1) {
 
166
            push(@specialchart, [$field, "anyexact",
 
167
                                 join(',', $params->param($field))]);
 
168
        }
 
169
    }
 
170
 
 
171
    if ($params->param('keywords')) {
 
172
        my $t = $params->param('keywords_type');
 
173
        if (!$t || $t eq "or") {
 
174
            $t = "anywords";
 
175
        }
 
176
        push(@specialchart, ["keywords", $t, $params->param('keywords')]);
 
177
    }
 
178
 
 
179
    foreach my $id ("1", "2") {
 
180
        if (!defined ($params->param("email$id"))) {
 
181
            next;
 
182
        }
 
183
        my $email = trim($params->param("email$id"));
 
184
        if ($email eq "") {
 
185
            next;
 
186
        }
 
187
        my $type = $params->param("emailtype$id");
 
188
        if ($type eq "exact") {
 
189
            $type = "anyexact";
 
190
            foreach my $name (split(',', $email)) {
 
191
                $name = trim($name);
 
192
                if ($name) {
 
193
                    &::DBNameToIdAndCheck($name);
 
194
                }
 
195
            }
 
196
        }
 
197
 
 
198
        my @clist;
 
199
        foreach my $field ("assigned_to", "reporter", "cc", "qa_contact") {
 
200
            if ($params->param("email$field$id")) {
 
201
                push(@clist, $field, $type, $email);
 
202
            }
 
203
        }
 
204
        if ($params->param("emaillongdesc$id")) {
 
205
                push(@clist, "commenter", $type, $email);
 
206
        }
 
207
        if (@clist) {
 
208
            push(@specialchart, \@clist);
 
209
        } else {
 
210
            ThrowUserError("missing_email_type",
 
211
                           { email => $email });
 
212
        }
 
213
    }
 
214
 
 
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')) || '';
 
221
 
 
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')) || '';
 
225
    if ($changedin) {
 
226
        if ($changedin !~ /^[0-9]*$/) {
 
227
            ThrowUserError("illegal_changed_in_last_x_days",
 
228
                              { value => $changedin });
 
229
        }
 
230
 
 
231
        if (!$chfieldfrom
 
232
            && !$chfieldto
 
233
            && scalar(@chfield) == 1
 
234
            && $chfield[0] eq "[Bug creation]")
 
235
        {
 
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";
 
240
        }
 
241
        else {
 
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]);
 
245
        }
 
246
    }
 
247
 
 
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) : '';
 
252
        if(!@chfield) {
 
253
            push(@wherepart, "bugs.delta_ts >= $sql_chfrom") if ($sql_chfrom);
 
254
            push(@wherepart, "bugs.delta_ts <= $sql_chto") if ($sql_chto);
 
255
        } else {
 
256
            my $bug_creation_clause;
 
257
            my @list;
 
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.
 
262
                    my @l;
 
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) . ")";
 
266
                } else {
 
267
                    push(@list, "\nactcheck.fieldid = " . &::GetFieldID($f));
 
268
                }
 
269
            }
 
270
 
 
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).
 
273
            if(@list) {
 
274
                push(@supptables, "bugs_activity actcheck");
 
275
                push(@wherepart, "actcheck.bug_id = bugs.bug_id");
 
276
                if($sql_chfrom) {
 
277
                    push(@wherepart, "actcheck.bug_when >= $sql_chfrom");
 
278
                }
 
279
                if($sql_chto) {
 
280
                    push(@wherepart, "actcheck.bug_when <= $sql_chto");
 
281
                }
 
282
                if($sql_chvalue) {
 
283
                    push(@wherepart, "actcheck.added = $sql_chvalue");
 
284
                }
 
285
            }
 
286
 
 
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;
 
292
 
 
293
            push(@wherepart, "(" . join(" OR ", @list) . ")");
 
294
        }
 
295
    }
 
296
 
 
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));
 
301
            if ($s ne "") {
 
302
                my $n = $f;
 
303
                my $q = &::SqlQuote($s);
 
304
                my $type = $params->param($f . "_type");
 
305
                push(@specialchart, [$f, $type, $s]);
 
306
            }
 
307
        }
 
308
    }
 
309
 
 
310
    if (defined $params->param('content')) {
 
311
        # Append a new chart implementing content quicksearch
 
312
        my $chart;
 
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'));
 
320
    }
 
321
 
 
322
    my $chartid;
 
323
    my $sequence = 0;
 
324
    # $type_id is used by the code that queries for attachment flags.
 
325
    my $type_id = 0;
 
326
    my $f;
 
327
    my $ff;
 
328
    my $t;
 
329
    my $q;
 
330
    my $v;
 
331
    my $term;
 
332
    my %funcsbykey;
 
333
    my @funcdefs =
 
334
        (
 
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";
 
339
         },
 
340
         "^qa_contact," => sub {
 
341
             push(@supptables,
 
342
                  "LEFT JOIN profiles map_qa_contact ON bugs.qa_contact = map_qa_contact.userid");
 
343
             $f = "map_$f.login_name";
 
344
         },
 
345
 
 
346
         "^cc,(anyexact|substring)" => sub {
 
347
             my $list;
 
348
             $list = $self->ListIDsForEmail($t, $v);
 
349
             my $chartseq;
 
350
             $chartseq = $chartid;
 
351
             if ($chartid eq "") {
 
352
                 $chartseq = "CC$sequence";
 
353
                 $sequence++;
 
354
             }
 
355
             if ($list) {
 
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";
 
358
             } else {
 
359
                 push(@supptables, "LEFT JOIN cc cc_$chartseq ON bugs.bug_id = cc_$chartseq.bug_id");
 
360
 
 
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"};
 
364
                 &$ref;
 
365
             }
 
366
         },
 
367
         "^cc," => sub {
 
368
             my $chartseq;
 
369
             $chartseq = $chartid;
 
370
             if ($chartid eq "") {
 
371
                 $chartseq = "CC$sequence";
 
372
                 $sequence++;
 
373
             }
 
374
            push(@supptables, "LEFT JOIN cc cc_$chartseq ON bugs.bug_id = cc_$chartseq.bug_id");
 
375
 
 
376
            push(@supptables, "LEFT JOIN profiles map_cc_$chartseq ON cc_$chartseq.who = map_cc_$chartseq.userid");
 
377
            $f = "map_cc_$chartseq.login_name";
 
378
         },
 
379
 
 
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";
 
386
         },
 
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));
 
392
         },
 
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));
 
398
         },
 
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.
 
406
 
 
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 " . 
 
410
                               "= $table.bug_id");
 
411
             if (Param("insidergroup") 
 
412
                 && !&::UserInGroup(Param("insidergroup")))
 
413
             {
 
414
                 push(@wherepart, "$table.isprivate < 1");
 
415
             }
 
416
 
 
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).")";
 
423
 
 
424
             # The term to use in the WHERE clause.
 
425
             $term = $term1;
 
426
 
 
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.
 
430
             #
 
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.
 
437
             #
 
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).
 
441
             my $select_term =
 
442
               "(SUM($term1)/COUNT($term1) + $term2) AS relevance";
 
443
 
 
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);
 
450
             }
 
451
             else {
 
452
                 push(@fields, $select_term);
 
453
             }
 
454
         },
 
455
         "^content," => sub {
 
456
             ThrowUserError("search_content_without_matches");
 
457
         },
 
458
         "^commenter," => sub {    
 
459
             my $chartseq;
 
460
             my $list;
 
461
             $list = $self->ListIDsForEmail($t, $v);
 
462
             $chartseq = $chartid;
 
463
             if ($chartid eq "") {
 
464
                 $chartseq = "LD$sequence";
 
465
                 $sequence++;
 
466
             }
 
467
             my $table = "longdescs_$chartseq";
 
468
             my $extra = "";
 
469
             if (Param("insidergroup") && !&::UserInGroup(Param("insidergroup"))) {
 
470
                 $extra = "AND $table.isprivate < 1";
 
471
             }
 
472
             if ($list) {
 
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";
 
475
             } else {
 
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"};
 
480
                 &$ref;
 
481
             }
 
482
         },
 
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") ;
 
488
             }
 
489
             push(@wherepart, "$table.bug_id = bugs.bug_id");
 
490
             $f = "$table.thetext";
 
491
         },
 
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))";
 
499
         },
 
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))";
 
506
         },
 
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))";
 
513
         },
 
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";
 
519
         },
 
520
         "^percentage_complete," => sub {
 
521
             my $oper;
 
522
             if ($t eq "equals") {
 
523
                 $oper = "=";
 
524
             } elsif ($t eq "greaterthan") {
 
525
                 $oper = ">";
 
526
             } elsif ($t eq "lessthan") {
 
527
                 $oper = "<";
 
528
             } elsif ($t eq "notequal") {
 
529
                 $oper = "<>";
 
530
             } elsif ($t eq "regexp") {
 
531
                 $oper = "REGEXP";
 
532
             } elsif ($t eq "notregexp") {
 
533
                 $oper = "NOT REGEXP";
 
534
             } else {
 
535
                 $oper = "noop";
 
536
             }
 
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);
 
543
                 push(@having, 
 
544
                      "percentage_complete_$table $oper " . &::SqlQuote($v));
 
545
             }
 
546
             $term = "0=0";
 
547
         },
 
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");
 
550
 
 
551
            push(@supptables, "LEFT JOIN groups groups_$chartid ON groups_$chartid.id = bug_group_map_$chartid.group_id");
 
552
            $f = "groups_$chartid.name";
 
553
         },
 
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") ;
 
559
             }
 
560
             push(@wherepart, "bugs.bug_id = $table.bug_id");
 
561
             $f =~ m/^attachments\.(.*)$/;
 
562
             my $field = $1;
 
563
             if ($t eq "changedby") {
 
564
                 $v = &::DBNameToIdAndCheck($v);
 
565
                 $q = &::SqlQuote($v);
 
566
                 $field = "submitter_id";
 
567
                 $t = "equals";
 
568
             } elsif ($t eq "changedbefore") {
 
569
                 $v = SqlifyDate($v);
 
570
                 $q = &::SqlQuote($v);
 
571
                 $field = "creation_ts";
 
572
                 $t = "lessthan";
 
573
             } elsif ($t eq "changedafter") {
 
574
                 $v = SqlifyDate($v);
 
575
                 $q = &::SqlQuote($v);
 
576
                 $field = "creation_ts";
 
577
                 $t = "greaterthan";
 
578
             }
 
579
             if ($field eq "ispatch" && $v ne "0" && $v ne "1") {
 
580
                 ThrowUserError("illegal_attachment_is_patch");
 
581
             }
 
582
             if ($field eq "isobsolete" && $v ne "0" && $v ne "1") {
 
583
                 ThrowUserError("illegal_is_obsolete");
 
584
             }
 
585
             $f = "$table.$field";
 
586
         },
 
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+").
 
592
             
 
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/);
 
596
             
 
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");
 
607
             
 
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"}};
 
612
             
 
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.
 
621
             if ($t =~ m/not/) {
 
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");
 
625
                $term = "0=0";
 
626
             }
 
627
         },
 
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";
 
636
         },
 
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";
 
645
         },
 
646
         
 
647
         "^changedin," => sub {
 
648
             $f = "(to_days(now()) - to_days(bugs.delta_ts))";
 
649
         },
 
650
 
 
651
         "^component,(?!changed)" => sub {
 
652
             $f = $ff = "components.name";
 
653
             $funcsbykey{",$t"}->();
 
654
             $term = build_subselect("bugs.component_id",
 
655
                                     "components.id",
 
656
                                     "components",
 
657
                                     $term);
 
658
         },
 
659
 
 
660
         "^product,(?!changed)" => sub {
 
661
             # Generate the restriction condition
 
662
             $f = $ff = "products.name";
 
663
             $funcsbykey{",$t"}->();
 
664
             $term = build_subselect("bugs.product_id",
 
665
                                     "products.id",
 
666
                                     "products",
 
667
                                     $term);
 
668
         },
 
669
 
 
670
         "^keywords," => sub {
 
671
             &::GetVersionTable();
 
672
             my @list;
 
673
             my $table = "keywords_$chartid";
 
674
             foreach my $value (split(/[\s,]+/, $v)) {
 
675
                 if ($value eq '') {
 
676
                     next;
 
677
                 }
 
678
                 my $id = &::GetKeywordIdFromName($value);
 
679
                 if ($id) {
 
680
                     push(@list, "$table.keywordid = $id");
 
681
                 }
 
682
                 else {
 
683
                     ThrowUserError("unknown_keyword",
 
684
                                    { keyword => $v });
 
685
                 }
 
686
             }
 
687
             my $haveawordterm;
 
688
             if (@list) {
 
689
                 $haveawordterm = "(" . join(' OR ', @list) . ")";
 
690
                 if ($t eq "anywords") {
 
691
                     $term = $haveawordterm;
 
692
                 } elsif ($t eq "allwords") {
 
693
                     my $ref = $funcsbykey{",$t"};
 
694
                     &$ref;
 
695
                     if ($term && $haveawordterm) {
 
696
                         $term = "(($term) AND $haveawordterm)";
 
697
                     }
 
698
                 }
 
699
             }
 
700
             if ($term) {
 
701
                 push(@supptables, "keywords $table");
 
702
                 push(@wherepart, "$table.bug_id = bugs.bug_id");
 
703
             }
 
704
         },
 
705
 
 
706
         "^dependson," => sub {
 
707
                my $table = "dependson_" . $chartid;
 
708
                push(@supptables, "dependencies $table");
 
709
                $ff = "$table.$f";
 
710
                my $ref = $funcsbykey{",$t"};
 
711
                &$ref;
 
712
                push(@wherepart, "$table.blocked = bugs.bug_id");
 
713
         },
 
714
 
 
715
         "^blocked," => sub {
 
716
                my $table = "blocked_" . $chartid;
 
717
                push(@supptables, "dependencies $table");
 
718
                $ff = "$table.$f";
 
719
                my $ref = $funcsbykey{",$t"};
 
720
                &$ref;
 
721
                push(@wherepart, "$table.dependson = bugs.bug_id");
 
722
         },
 
723
 
 
724
         "^owner_idle_time,(greaterthan|lessthan)" => sub {
 
725
                my $table = "idle_" . $chartid;
 
726
                $v =~ /^(\d+)\s*([hHdDwWmMyY])?$/;
 
727
                my $quantity = $1;
 
728
                my $unit = lc $2;
 
729
                my $unitinterval = 'DAY';
 
730
                if ($unit eq 'h') {
 
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';
 
738
                }
 
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)");
 
754
                } else {
 
755
                    push(@wherepart, "(comment_$table.who IS NOT NULL " .
 
756
                                     "OR activity_$table.who IS NOT NULL)");
 
757
                }
 
758
                $term = "0=0";
 
759
         },
 
760
 
 
761
         ",equals" => sub {
 
762
             $term = "$ff = $q";
 
763
         },
 
764
         ",notequals" => sub {
 
765
             $term = "$ff != $q";
 
766
         },
 
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)";
 
774
             } else {
 
775
                 $term = "INSTR(CAST($ff AS BINARY), CAST($q AS BINARY))";
 
776
             }
 
777
         },
 
778
         ",substring" => sub {
 
779
             $term = "INSTR(LOWER($ff), " . lc($q) . ")";
 
780
         },
 
781
         ",substr" => sub {
 
782
             $funcsbykey{",substring"}->();
 
783
         },
 
784
         ",notsubstring" => sub {
 
785
             $term = "INSTR(LOWER($ff), " . lc($q) . ") = 0";
 
786
         },
 
787
         ",regexp" => sub {
 
788
             $term = "LOWER($ff) REGEXP $q";
 
789
         },
 
790
         ",notregexp" => sub {
 
791
             $term = "LOWER($ff) NOT REGEXP $q";
 
792
         },
 
793
         ",lessthan" => sub {
 
794
             $term = "$ff < $q";
 
795
         },
 
796
         ",matches" => sub {
 
797
             ThrowUserError("search_content_without_matches");
 
798
         },
 
799
         ",greaterthan" => sub {
 
800
             $term = "$ff > $q";
 
801
         },
 
802
         ",anyexact" => sub {
 
803
             my @list;
 
804
             foreach my $w (split(/,/, $v)) {
 
805
                 if ($w eq "---" && $f !~ /milestone/) {
 
806
                     $w = "";
 
807
                 }
 
808
                 push(@list, &::SqlQuote($w));
 
809
             }
 
810
             if (@list) {
 
811
                 $term = "$ff IN (" . join (',', @list) . ")";
 
812
             }
 
813
         },
 
814
         ",anywordssubstr" => sub {
 
815
             $term = join(" OR ", @{GetByWordListSubstr($ff, $v)});
 
816
         },
 
817
         ",allwordssubstr" => sub {
 
818
             $term = join(" AND ", @{GetByWordListSubstr($ff, $v)});
 
819
         },
 
820
         ",nowordssubstr" => sub {
 
821
             my @list = @{GetByWordListSubstr($ff, $v)};
 
822
             if (@list) {
 
823
                 $term = "NOT (" . join(" OR ", @list) . ")";
 
824
             }
 
825
         },
 
826
         ",anywords" => sub {
 
827
             $term = join(" OR ", @{GetByWordList($ff, $v)});
 
828
         },
 
829
         ",allwords" => sub {
 
830
             $term = join(" AND ", @{GetByWordList($ff, $v)});
 
831
         },
 
832
         ",nowords" => sub {
 
833
             my @list = @{GetByWordList($ff, $v)};
 
834
             if (@list) {
 
835
                 $term = "NOT (" . join(" OR ", @list) . ")";
 
836
             }
 
837
         },
 
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)";
 
846
         },
 
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)";
 
855
         },
 
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)";
 
864
         },
 
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)";
 
873
         },
 
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)";
 
883
         },
 
884
         );
 
885
    my @funcnames;
 
886
    while (@funcdefs) {
 
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";
 
891
        }
 
892
        if (exists $funcsbykey{$key}) {
 
893
            die "Duplicate key in %funcs: $key";
 
894
        }
 
895
        $funcsbykey{$key} = $value;
 
896
        push(@funcnames, $key);
 
897
    }
 
898
 
 
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);
 
904
    }
 
905
 
 
906
    # now we take our special chart and stuff it into the form hash
 
907
    my $chart = -1;
 
908
    my $row = 0;
 
909
    foreach my $ref (@specialchart) {
 
910
        my $col = 0;
 
911
        while (@$ref) {
 
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));
 
915
            if ($debug) {
 
916
                print qq{<p>$params->param("field$chart-$row-$col") | $params->param("type$chart-$row-$col") | $params->param("value$chart-$row-$col")*</p>\n};
 
917
            }
 
918
            $col++;
 
919
 
 
920
        }
 
921
        $row++;
 
922
    }
 
923
 
 
924
 
 
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.
 
931
#
 
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".
 
937
#
 
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.
 
942
#
 
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
# |------|------|------|------|
 
950
# | row3 |  c1  |      |      |
 
951
# -----------------------------
 
952
#
 
953
#        --------
 
954
#        | col2 |
 
955
# --------------|
 
956
# | row1 |  d1  | => (d1)
 
957
# ---------------
 
958
#
 
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)
 
961
#
 
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'.
 
967
#
 
968
# --------------------------------------------------------------
 
969
# CC    | equal to
 
970
# foo@blah.org
 
971
# --------------------------------------------------------------
 
972
# CC    | equal to
 
973
# bar@blah.org
 
974
#
 
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.
 
979
#
 
980
# --------------------------------------------------------------
 
981
# CC    | equal to
 
982
# foo@blah.org
 
983
# AND
 
984
# CC    | equal to
 
985
# bar@blah.org
 
986
# --------------------------------------------------------------
 
987
 
 
988
# $chartid is the number of the current chart whose SQL we're constructing
 
989
# $row is the current row of the current chart
 
990
 
 
991
# names for table aliases are constructed using $chartid and $row
 
992
#   SELECT blah  FROM $table "$table_$chartid_$row" WHERE ....
 
993
 
 
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
 
1002
#               out duplicates.
 
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
 
1006
 
 
1007
    # get a list of field names to verify the user-submitted chart fields against
 
1008
    my %chartfields;
 
1009
    &::SendSQL("SELECT name FROM fielddefs");
 
1010
    while (&::MoreSQLData()) {
 
1011
        my ($name) = &::FetchSQLData();
 
1012
        $chartfields{$name} = 1;
 
1013
    }
 
1014
 
 
1015
    $row = 0;
 
1016
    for ($chart=-1 ;
 
1017
         $chart < 0 || $params->param("field$chart-0-0") ;
 
1018
         $chart++) {
 
1019
        $chartid = $chart >= 0 ? $chart : "";
 
1020
        for ($row = 0 ;
 
1021
             $params->param("field$chart-$row-0") ;
 
1022
             $row++) {
 
1023
            my @orlist;
 
1024
            for (my $col = 0 ;
 
1025
                 $params->param("field$chart-$row-$col") ;
 
1026
                 $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;
 
1031
                $v = trim($v);
 
1032
                if ($f eq "noop" || $t eq "noop" || $v eq "") {
 
1033
                    next;
 
1034
                }
 
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});
 
1039
                }
 
1040
 
 
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.
 
1044
                trick_taint($f);
 
1045
                $q = &::SqlQuote($v);
 
1046
                my $func;
 
1047
                $term = undef;
 
1048
                foreach my $key (@funcnames) {
 
1049
                    if ("$f,$t" =~ m/$key/) {
 
1050
                        my $ref = $funcsbykey{$key};
 
1051
                        if ($debug) {
 
1052
                            print "<p>$key ($f , $t ) => ";
 
1053
                        }
 
1054
                        $ff = $f;
 
1055
                        if ($f !~ /\./) {
 
1056
                            $ff = "bugs.$f";
 
1057
                        }
 
1058
                        &$ref;
 
1059
                        if ($debug) {
 
1060
                            print "$f , $t , $term</p>";
 
1061
                        }
 
1062
                        if ($term) {
 
1063
                            last;
 
1064
                        }
 
1065
                    }
 
1066
                }
 
1067
                if ($term) {
 
1068
                    push(@orlist, $term);
 
1069
                }
 
1070
                else {
 
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"),
 
1075
                                   });
 
1076
                }
 
1077
            }
 
1078
            if (@orlist) {
 
1079
                @orlist = map("($_)", @orlist) if (scalar(@orlist) > 1);
 
1080
                push(@andlist, "(" . join(" OR ", @orlist) . ")");
 
1081
            }
 
1082
        }
 
1083
    }
 
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)";
 
1094
                } else {
 
1095
                    $suppseen{$leftside} = scalar @supplist;
 
1096
                    push @supplist, " $leftside ON ($rightside)";
 
1097
                }
 
1098
            } else {
 
1099
                $suppstring .= ", $str";
 
1100
                $suppseen{$str} = 1;
 
1101
            }
 
1102
        }
 
1103
    }
 
1104
    $suppstring .= join('', @supplist);
 
1105
    
 
1106
    # Make sure we create a legal SQL query.
 
1107
    @andlist = ("1 = 1") if !@andlist;
 
1108
   
 
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 ";
 
1113
 
 
1114
    if ($user) {
 
1115
        if (%{$user->groups}) {
 
1116
            $query .= " AND bug_group_map.group_id NOT IN (" . join(',', values(%{$user->groups})) . ") ";
 
1117
        }
 
1118
 
 
1119
        $query .= " LEFT JOIN cc ON cc.bug_id = bugs.bug_id AND cc.who = " . $user->id;
 
1120
    }
 
1121
 
 
1122
    $query .= " WHERE " . join(' AND ', (@wherepart, @andlist)) .
 
1123
              " AND bugs.creation_ts IS NOT NULL AND ((bug_group_map.group_id IS NULL)";
 
1124
 
 
1125
    if ($user) {
 
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) ";
 
1132
        }
 
1133
    }
 
1134
 
 
1135
    $query .= ") GROUP BY bugs.bug_id";
 
1136
 
 
1137
    if (@having) {
 
1138
        $query .= " HAVING " . join(" AND ", @having);
 
1139
    }
 
1140
 
 
1141
    if ($debug) {
 
1142
        print "<p><code>" . value_quote($query) . "</code></p>\n";
 
1143
        exit;
 
1144
    }
 
1145
    
 
1146
    $self->{'sql'} = $query;
 
1147
}
 
1148
 
 
1149
###############################################################################
 
1150
# Helper functions for the init() method.
 
1151
###############################################################################
 
1152
sub SqlifyDate {
 
1153
    my ($str) = @_;
 
1154
    $str = "" if !defined $str;
 
1155
    if ($str eq "") {
 
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);
 
1158
    }
 
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;
 
1164
            $unit = 'd';
 
1165
        }
 
1166
        if ($unit eq 'd') {
 
1167
            $date -= $sec + 60*$min + 3600*$hour + 24*3600*$amount;
 
1168
            return time2str("%Y-%m-%d %H:%M:%S", $date);
 
1169
        }
 
1170
        elsif ($unit eq 'y') {
 
1171
            return sprintf("%4d-01-01 00:00:00", $year+1900-$amount);
 
1172
        }
 
1173
        elsif ($unit eq 'm') {
 
1174
            $month -= $amount;
 
1175
            while ($month<0) { $year--; $month += 12; }
 
1176
            return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1);
 
1177
        }
 
1178
        return undef;                      # should not happen due to regexp at top
 
1179
    }
 
1180
    my $date = str2time($str);
 
1181
    if (!defined($date)) {
 
1182
        ThrowUserError("illegal_date", { date => $str });
 
1183
    }
 
1184
    return time2str("%Y-%m-%d %H:%M:%S", $date);
 
1185
}
 
1186
 
 
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;
 
1199
    my @list = ();
 
1200
    my $list = "---";
 
1201
    if ($type eq 'anyexact') {
 
1202
        foreach my $w (split(/,/, $email)) {
 
1203
            $w = trim($w);
 
1204
            my $id = &::DBname_to_id($w);
 
1205
            if ($id > 0) {
 
1206
                push(@list,$id)
 
1207
            }
 
1208
        }
 
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();
 
1215
            push(@list, $id);
 
1216
        }
 
1217
        if (@list < 50) {
 
1218
            $list = join(',', @list);
 
1219
        }
 
1220
    }
 
1221
    $self->{"emailcache"}{"$type,$email"} = $list;
 
1222
    return undef if ($list eq "---");
 
1223
    return $list;
 
1224
}
 
1225
 
 
1226
sub build_subselect {
 
1227
    my ($outer, $inner, $table, $cond) = @_;
 
1228
    my $q = "SELECT $inner FROM $table WHERE $cond";
 
1229
    #return "$outer IN ($q)";
 
1230
    &::SendSQL($q);
 
1231
    my @list;
 
1232
    while (&::MoreSQLData()) {
 
1233
        push (@list, &::FetchOneColumn());
 
1234
    }
 
1235
    return "1=2" unless @list; # Could use boolean type on dbs which support it
 
1236
    return "$outer IN (" . join(',', @list) . ")";
 
1237
}
 
1238
 
 
1239
sub GetByWordList {
 
1240
    my ($field, $strs) = (@_);
 
1241
    my @list;
 
1242
 
 
1243
    foreach my $w (split(/[\s,]+/, $strs)) {
 
1244
        my $word = $w;
 
1245
        if ($word ne "") {
 
1246
            $word =~ tr/A-Z/a-z/;
 
1247
            $word = &::SqlQuote(quotemeta($word));
 
1248
            $word =~ s/^'//;
 
1249
            $word =~ s/'$//;
 
1250
            $word = '(^|[^a-z0-9])' . $word . '($|[^a-z0-9])';
 
1251
            push(@list, "lower($field) regexp '$word'");
 
1252
        }
 
1253
    }
 
1254
 
 
1255
    return \@list;
 
1256
}
 
1257
 
 
1258
# Support for "any/all/nowordssubstr" comparison type ("words as substrings")
 
1259
sub GetByWordListSubstr {
 
1260
    my ($field, $strs) = (@_);
 
1261
    my @list;
 
1262
 
 
1263
    foreach my $word (split(/[\s,]+/, $strs)) {
 
1264
        if ($word ne "") {
 
1265
            push(@list, "INSTR(LOWER($field), " . lc(&::SqlQuote($word)) . ")");
 
1266
        }
 
1267
    }
 
1268
 
 
1269
    return \@list;
 
1270
}
 
1271
 
 
1272
sub getSQL {
 
1273
    my $self = shift;
 
1274
    return $self->{'sql'};
 
1275
}
 
1276
 
 
1277
# Define if the Query Type passed in is a valid query type that we can deal with
 
1278
sub IsValidQueryType
 
1279
{
 
1280
    my ($queryType) = @_;
 
1281
    if (grep { $_ eq $queryType } qw(specific advanced)) {
 
1282
        return 1;
 
1283
    }
 
1284
    return 0;
 
1285
}
 
1286
 
 
1287
1;