RSS

(root)/bugzilla/4.2 : 8060 : Bugzilla/Search/Quicksearch.pm

To get this branch, use:
bzr branch /bugzilla/4.2

« back to all changes in this revision

Viewing changes to Bugzilla/Search/Quicksearch.pm

Frédéric Buclin
2012-03-29 10:56:41
Revision ID: lpsolit@gmail.com-20120329175641-u3ah0nr0io2psvi0
Bug 554819: Quicksearch should be using Text::ParseWords instead of custom code in splitString
Also fixes QS with accented characters (bug 730207)
r=dkl a=LpSolit

Show diffs side-by-side

added added

removed removed

32
32
 
33
33
use List::Util qw(min max);
34
34
use List::MoreUtils qw(firstidx);
 
35
use Text::ParseWords qw(parse_line);
35
36
 
36
37
use base qw(Exporter);
37
38
@Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch);
142
143
    $searchstring =~ s/(^[\s,]+|[\s,]+$)//g;
143
144
    ThrowUserError('buglist_parameters_required') unless ($searchstring);
144
145
 
145
 
    $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
146
 
 
147
146
    if ($searchstring =~ m/^[0-9,\s]*$/) {
148
147
        _bug_numbers_only($searchstring);
149
148
    }
150
149
    else {
151
150
        _handle_alias($searchstring);
152
151
 
153
 
        # Globally translate " AND ", " OR ", " NOT " to space, pipe, dash.
154
 
        $searchstring =~ s/\s+AND\s+/ /g;
155
 
        $searchstring =~ s/\s+OR\s+/|/g;
156
 
        $searchstring =~ s/\s+NOT\s+/ -/g;
157
 
 
158
 
        my @words = splitString($searchstring);
159
 
        _handle_status_and_resolution(\@words);
 
152
        # Retain backslashes and quotes, to know which strings are quoted,
 
153
        # and which ones are not.
 
154
        my @words = parse_line('\s+', 1, $searchstring);
 
155
        # If parse_line() returns no data, this means strings are badly quoted.
 
156
        # Rather than trying to guess what the user wanted to do, we throw an error.
 
157
        scalar(@words)
 
158
          || ThrowUserError('quicksearch_unbalanced_quotes', {string => $searchstring});
 
159
 
 
160
        # A query cannot start with AND or OR, nor can it end with AND, OR or NOT.
 
161
        ThrowUserError('quicksearch_invalid_query')
 
162
          if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/);
 
163
 
 
164
        my (@qswords, @or_group);
 
165
        while (scalar @words) {
 
166
            my $word = shift @words;
 
167
            # AND is the default word separator, similar to a whitespace,
 
168
            # but |a AND OR b| is not a valid combination.
 
169
            if ($word eq 'AND') {
 
170
                ThrowUserError('quicksearch_invalid_query', {operators => ['AND', 'OR']})
 
171
                  if $words[0] eq 'OR';
 
172
            }
 
173
            # |a OR AND b| is not a valid combination.
 
174
            # |a OR OR b| is equivalent to |a OR b| and so is harmless.
 
175
            elsif ($word eq 'OR') {
 
176
                ThrowUserError('quicksearch_invalid_query', {operators => ['OR', 'AND']})
 
177
                  if $words[0] eq 'AND';
 
178
            }
 
179
            # NOT negates the following word.
 
180
            # |NOT AND| and |NOT OR| are not valid combinations.
 
181
            # |NOT NOT| is fine but has no effect as they cancel themselves.
 
182
            elsif ($word eq 'NOT') {
 
183
                $word = shift @words;
 
184
                next if $word eq 'NOT';
 
185
                if ($word eq 'AND' || $word eq 'OR') {
 
186
                    ThrowUserError('quicksearch_invalid_query', {operators => ['NOT', $word]});
 
187
                }
 
188
                unshift(@words, "-$word");
 
189
            }
 
190
            else {
 
191
                # OR groups words together, as OR has higher precedence than AND.
 
192
                push(@or_group, $word);
 
193
                # If the next word is not OR, then we are not in a OR group,
 
194
                # or we are leaving it.
 
195
                if (!defined $words[0] || $words[0] ne 'OR') {
 
196
                    push(@qswords, join('|', @or_group));
 
197
                    @or_group = ();
 
198
                }
 
199
            }
 
200
        }
 
201
 
 
202
        _handle_status_and_resolution(\@qswords);
160
203
 
161
204
        my (@unknownFields, %ambiguous_fields);
 
205
        $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
162
206
 
163
207
        # Loop over all main-level QuickSearch words.
164
 
        foreach my $qsword (@words) {
165
 
            my $negate = substr($qsword, 0, 1) eq '-';
166
 
            if ($negate) {
167
 
                $qsword = substr($qsword, 1);
168
 
            }
169
 
 
170
 
            # No special first char
171
 
            if (!_handle_special_first_chars($qsword, $negate)) {
172
 
                # Split by '|' to get all operands for a boolean OR.
173
 
                foreach my $or_operand (split(/\|/, $qsword)) {
174
 
                    if (!_handle_field_names($or_operand, $negate,
175
 
                                             \@unknownFields, 
176
 
                                             \%ambiguous_fields))
177
 
                    {
178
 
                        # Having ruled out the special cases, we may now split
179
 
                        # by comma, which is another legal boolean OR indicator.
180
 
                        foreach my $word (split(/,/, $or_operand)) {
181
 
                            if (!_special_field_syntax($word, $negate)) {
182
 
                                _default_quicksearch_word($word, $negate);
183
 
                            }
184
 
                            _handle_urls($word, $negate);
185
 
                        }
 
208
        foreach my $qsword (@qswords) {
 
209
            my @or_operand = parse_line('\|', 1, $qsword);
 
210
            foreach my $term (@or_operand) {
 
211
                my $negate = substr($term, 0, 1) eq '-';
 
212
                if ($negate) {
 
213
                    $term = substr($term, 1);
 
214
                }
 
215
 
 
216
                next if _handle_special_first_chars($term, $negate);
 
217
                next if _handle_field_names($term, $negate, \@unknownFields,
 
218
                                            \%ambiguous_fields);
 
219
 
 
220
                # Having ruled out the special cases, we may now split
 
221
                # by comma, which is another legal boolean OR indicator.
 
222
                # Remove quotes from quoted words, if any.
 
223
                @words = parse_line(',', 0, $term);
 
224
                foreach my $word (@words) {
 
225
                    if (!_special_field_syntax($word, $negate)) {
 
226
                        _default_quicksearch_word($word, $negate);
186
227
                    }
 
228
                    _handle_urls($word, $negate);
187
229
                }
188
230
            }
189
231
            $chart++;
190
232
            $and = 0;
191
233
            $or = 0;
192
 
        } # foreach (@words)
 
234
        }
193
235
 
194
236
        # Inform user about any unknown fields
195
237
        if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) {
315
357
 
316
358
    my $firstChar = substr($qsword, 0, 1);
317
359
    my $baseWord = substr($qsword, 1);
318
 
    my @subWords = split(/[\|,]/, $baseWord);
 
360
    my @subWords = split(/,/, $baseWord);
319
361
 
320
362
    if ($firstChar eq '#') {
321
363
        addChart('short_desc', 'substring', $baseWord, $negate);
347
389
 
348
390
sub _handle_field_names {
349
391
    my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_;
350
 
    
 
392
 
351
393
    # Flag and requestee shortcut
352
394
    if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
353
395
        addChart('flagtypes.name', 'substring', $1, $negate);
355
397
        addChart('requestees.login_name', 'substring', $2, $negate);
356
398
        return 1;
357
399
    }
358
 
    
359
 
    # generic field1,field2,field3:value1,value2 notation
360
 
    if ($or_operand =~ /^([^:]+):([^:]+)$/) {
361
 
        my @fields = split(/,/, $1);
362
 
        my @values = split(/,/, $2);
 
400
 
 
401
    # Generic field1,field2,field3:value1,value2 notation.
 
402
    # We have to correctly ignore commas and colons in quotes.
 
403
    my @field_values = parse_line(':', 1, $or_operand);
 
404
    if (scalar @field_values == 2) {
 
405
        my @fields = parse_line(',', 1, $field_values[0]);
 
406
        my @values = parse_line(',', 1, $field_values[1]);
363
407
        foreach my $field (@fields) {
364
408
            my $translated = _translate_field_name($field);
365
409
            # Skip and record any unknown fields
366
410
            if (!defined $translated) {
367
411
                push(@$unknownFields, $field);
368
 
                next;
369
412
            }
370
413
            # If we got back an array, that means the substring is
371
414
            # ambiguous and could match more than field name
372
415
            elsif (ref $translated) {
373
416
                $ambiguous_fields->{$field} = $translated;
374
 
                next;
375
417
            }
376
 
            foreach my $value (@values) {
377
 
                my $operator = FIELD_OPERATOR->{$translated} || 'substring';
378
 
                addChart($translated, $operator, $value, $negate);
 
418
            else {
 
419
                foreach my $value (@values) {
 
420
                    my $operator = FIELD_OPERATOR->{$translated} || 'substring';
 
421
                    # If the string was quoted to protect some special
 
422
                    # characters such as commas and colons, we need
 
423
                    # to remove quotes.
 
424
                    if ($value =~ /^(["'])(.+)\g1$/) {
 
425
                        $value = $2;
 
426
                        $value =~ s/\\(["'])/$1/g;
 
427
                    }
 
428
                    addChart($translated, $operator, $value, $negate);
 
429
                }
379
430
            }
380
431
        }
381
432
        return 1;
382
433
    }
383
 
    
384
434
    return 0;
385
435
}
386
436
 
513
563
# Helpers
514
564
###########################################################################
515
565
 
516
 
# Split string on whitespace, retaining quoted strings as one
517
 
sub splitString {
518
 
    my $string = shift;
519
 
    my @quoteparts;
520
 
    my @parts;
521
 
    my $i = 0;
522
 
 
523
 
    # Now split on quote sign; be tolerant about unclosed quotes
524
 
    @quoteparts = split(/"/, $string);
525
 
    foreach my $part (@quoteparts) {
526
 
        # After every odd quote, quote special chars
527
 
        if ($i++ %2) {
528
 
            $part = url_quote($part);
529
 
            # Protect the minus sign from being considered
530
 
            # as negation, in quotes.
531
 
            $part =~ s/(?<=^)\-/%2D/;
532
 
        }
533
 
    }
534
 
    # Join again
535
 
    $string = join('"', @quoteparts);
536
 
 
537
 
    # Now split on unescaped whitespace
538
 
    @parts = split(/\s+/, $string);
539
 
    foreach (@parts) {
540
 
        # Protect plus signs from becoming a blank.
541
 
        # If "+" appears as the first character, leave it alone
542
 
        # as it has a special meaning. Strings which start with
543
 
        # "+" must be quoted.
544
 
        s/(?<!^)\+/%2B/g;
545
 
        # Remove quotes
546
 
        s/"//g;
547
 
    }
548
 
    return @parts;
549
 
}
550
 
 
551
566
# Quote and escape a phrase appropriately for a "content matches" search.
552
567
sub _matches_phrase {
553
568
    my ($phrase) = @_;
613
628
    my $cgi = Bugzilla->cgi;
614
629
    $cgi->param("field$expr", $field);
615
630
    $cgi->param("type$expr",  $type);
616
 
    $cgi->param("value$expr", url_decode($value));
 
631
    $cgi->param("value$expr", $value);
617
632
}
618
633
 
619
634
1;

Loggerhead 1.18.1 is a web-based interface for Bazaar branches