142
143
$searchstring =~ s/(^[\s,]+|[\s,]+$)//g;
143
144
ThrowUserError('buglist_parameters_required') unless ($searchstring);
145
$fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
147
146
if ($searchstring =~ m/^[0-9,\s]*$/) {
148
147
_bug_numbers_only($searchstring);
151
150
_handle_alias($searchstring);
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;
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.
158
|| ThrowUserError('quicksearch_unbalanced_quotes', {string => $searchstring});
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)$/);
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';
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';
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]});
188
unshift(@words, "-$word");
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));
202
_handle_status_and_resolution(\@qswords);
161
204
my (@unknownFields, %ambiguous_fields);
205
$fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
163
207
# Loop over all main-level QuickSearch words.
164
foreach my $qsword (@words) {
165
my $negate = substr($qsword, 0, 1) eq '-';
167
$qsword = substr($qsword, 1);
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,
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);
184
_handle_urls($word, $negate);
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 '-';
213
$term = substr($term, 1);
216
next if _handle_special_first_chars($term, $negate);
217
next if _handle_field_names($term, $negate, \@unknownFields,
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);
228
_handle_urls($word, $negate);
194
236
# Inform user about any unknown fields
195
237
if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) {
355
397
addChart('requestees.login_name', 'substring', $2, $negate);
359
# generic field1,field2,field3:value1,value2 notation
360
if ($or_operand =~ /^([^:]+):([^:]+)$/) {
361
my @fields = split(/,/, $1);
362
my @values = split(/,/, $2);
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);
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;
376
foreach my $value (@values) {
377
my $operator = FIELD_OPERATOR->{$translated} || 'substring';
378
addChart($translated, $operator, $value, $negate);
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
424
if ($value =~ /^(["'])(.+)\g1$/) {
426
$value =~ s/\\(["'])/$1/g;
428
addChart($translated, $operator, $value, $negate);
514
564
###########################################################################
516
# Split string on whitespace, retaining quoted strings as one
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
528
$part = url_quote($part);
529
# Protect the minus sign from being considered
530
# as negation, in quotes.
531
$part =~ s/(?<=^)\-/%2D/;
535
$string = join('"', @quoteparts);
537
# Now split on unescaped whitespace
538
@parts = split(/\s+/, $string);
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.
551
566
# Quote and escape a phrase appropriately for a "content matches" search.
552
567
sub _matches_phrase {
553
568
my ($phrase) = @_;