Commit c124d368 authored by mkanat%bugzilla.org's avatar mkanat%bugzilla.org

Bug 490551: Refactor Bugzilla::Search::QuickSearch::quicksearch into a series of subroutines

Patch by Max Kanat-Alexander <mkanat@bugzilla.org> r=wicked, a=mkanat
parent 50642a73
...@@ -114,7 +114,6 @@ our ($chart, $and, $or); ...@@ -114,7 +114,6 @@ our ($chart, $and, $or);
sub quicksearch { sub quicksearch {
my ($searchstring) = (@_); my ($searchstring) = (@_);
my $cgi = Bugzilla->cgi; my $cgi = Bugzilla->cgi;
my $urlbase = correct_urlbase();
$chart = 0; $chart = 0;
$and = 0; $and = 0;
...@@ -125,14 +124,93 @@ sub quicksearch { ...@@ -125,14 +124,93 @@ sub quicksearch {
ThrowUserError('buglist_parameters_required') unless ($searchstring); ThrowUserError('buglist_parameters_required') unless ($searchstring);
if ($searchstring =~ m/^[0-9,\s]*$/) { if ($searchstring =~ m/^[0-9,\s]*$/) {
# Bug number(s) only. _bug_numbers_only($searchstring);
}
else {
_handle_alias($searchstring);
# Globally translate " AND ", " OR ", " NOT " to space, pipe, dash.
$searchstring =~ s/\s+AND\s+/ /g;
$searchstring =~ s/\s+OR\s+/|/g;
$searchstring =~ s/\s+NOT\s+/ -/g;
my @words = splitString($searchstring);
_handle_status_and_resolution(\@words);
my @unknownFields;
# Loop over all main-level QuickSearch words.
foreach my $qsword (@words) {
my $negate = substr($qsword, 0, 1) eq '-';
if ($negate) {
$qsword = substr($qsword, 1);
}
# No special first char
if (!_handle_special_first_chars($qsword, $negate)) {
# Split by '|' to get all operands for a boolean OR.
foreach my $or_operand (split(/\|/, $qsword)) {
if (!_handle_field_names($or_operand, $negate,
\@unknownFields))
{
# Having ruled out the special cases, we may now split
# by comma, which is another legal boolean OR indicator.
foreach my $word (split(/,/, $or_operand)) {
if (!_special_field_syntax($word, $negate)) {
_default_quicksearch_word($word, $negate);
}
_handle_urls($word, $negate);
}
}
}
}
$chart++;
$and = 0;
$or = 0;
} # foreach (@words)
# Inform user about any unknown fields
if (scalar(@unknownFields)) {
ThrowUserError("quicksearch_unknown_field",
{ fields => \@unknownFields });
}
# Make sure we have some query terms left
scalar($cgi->param())>0 || ThrowUserError("buglist_parameters_required");
}
# List of quicksearch-specific CGI parameters to get rid of.
my @params_to_strip = ('quicksearch', 'load', 'run');
my $modified_query_string = $cgi->canonicalise_query(@params_to_strip);
if ($cgi->param('load')) {
my $urlbase = correct_urlbase();
# Param 'load' asks us to display the query in the advanced search form.
print $cgi->redirect(-uri => "${urlbase}query.cgi?format=advanced&amp;"
. $modified_query_string);
}
# Otherwise, pass the modified query string to the caller.
# We modified $cgi->params, so the caller can choose to look at that, too,
# and disregard the return value.
$cgi->delete(@params_to_strip);
return $modified_query_string;
}
##########################
# Parts of quicksearch() #
##########################
sub _bug_numbers_only {
my $searchstring = shift;
my $cgi = Bugzilla->cgi;
# Allow separation by comma or whitespace. # Allow separation by comma or whitespace.
$searchstring =~ s/[,\s]+/,/g; $searchstring =~ s/[,\s]+/,/g;
if (index($searchstring, ',') < $[) { if ($searchstring !~ /,/) {
# Single bug number; shortcut to show_bug.cgi. # Single bug number; shortcut to show_bug.cgi.
print $cgi->redirect(-uri => "${urlbase}show_bug.cgi?id=$searchstring"); print $cgi->redirect(
-uri => correct_urlbase() . "show_bug.cgi?id=$searchstring");
exit; exit;
} }
else { else {
...@@ -141,56 +219,51 @@ sub quicksearch { ...@@ -141,56 +219,51 @@ sub quicksearch {
$cgi->param('order', 'bugs.bug_id'); $cgi->param('order', 'bugs.bug_id');
$cgi->param('bugidtype', 'include'); $cgi->param('bugidtype', 'include');
} }
} }
else {
# It's not just a bug number or a list of bug numbers. sub _handle_alias {
# Maybe it's an alias? my $searchstring = shift;
if ($searchstring =~ /^([^,\s]+)$/) { if ($searchstring =~ /^([^,\s]+)$/) {
if (Bugzilla->dbh->selectrow_array(q{SELECT COUNT(*) my $alias = $1;
FROM bugs # We use this direct SQL because we want quicksearch to be VERY fast.
WHERE alias = ?}, my $is_alias = Bugzilla->dbh->selectrow_array(
undef, q{SELECT 1 FROM bugs WHERE alias = ?}, undef, $alias);
$1)) { if ($is_alias) {
print $cgi->redirect(-uri => "${urlbase}show_bug.cgi?id=$1"); print Bugzilla->cgi->redirect(
-uri => correct_urlbase() . "show_bug.cgi?id=$alias");
exit; exit;
} }
} }
}
# It's no alias either, so it's a more complex query. sub _handle_status_and_resolution {
my ($words) = @_;
my $legal_statuses = get_legal_field_values('bug_status'); my $legal_statuses = get_legal_field_values('bug_status');
my $legal_resolutions = get_legal_field_values('resolution'); my $legal_resolutions = get_legal_field_values('resolution');
my $legal_priorities = get_legal_field_values('priority');
# Globally translate " AND ", " OR ", " NOT " to space, pipe, dash.
$searchstring =~ s/\s+AND\s+/ /g;
$searchstring =~ s/\s+OR\s+/|/g;
$searchstring =~ s/\s+NOT\s+/ -/g;
my @words = splitString($searchstring);
my @openStates = BUG_STATE_OPEN; my @openStates = BUG_STATE_OPEN;
my @closedStates; my @closedStates;
my @unknownFields;
my (%states, %resolutions); my (%states, %resolutions);
foreach (@$legal_statuses) { foreach (@$legal_statuses) {
push(@closedStates, $_) unless is_open_state($_); push(@closedStates, $_) unless is_open_state($_);
} }
foreach (@openStates) { $states{$_} = 1 } foreach (@openStates) { $states{$_} = 1 }
if ($words[0] eq 'ALL') { if ($words->[0] eq 'ALL') {
foreach (@$legal_statuses) { $states{$_} = 1 } foreach (@$legal_statuses) { $states{$_} = 1 }
shift @words; shift @$words;
} }
elsif ($words[0] eq 'OPEN') { elsif ($words->[0] eq 'OPEN') {
shift @words; shift @$words;
} }
elsif ($words[0] =~ /^\+[A-Z]+(,[A-Z]+)*$/) { elsif ($words->[0] =~ /^\+[A-Z]+(,[A-Z]+)*$/) {
# e.g. +DUP,FIX # e.g. +DUP,FIX
if (matchPrefixes(\%states, if (matchPrefixes(\%states,
\%resolutions, \%resolutions,
[split(/,/, substr($words[0], 1))], [split(/,/, substr($words->[0], 1))],
\@closedStates, \@closedStates,
$legal_resolutions)) { $legal_resolutions)) {
shift @words; shift @$words;
# Allowing additional resolutions means we need to keep # Allowing additional resolutions means we need to keep
# the "no resolution" resolution. # the "no resolution" resolution.
$resolutions{'---'} = 1; $resolutions{'---'} = 1;
...@@ -199,15 +272,15 @@ sub quicksearch { ...@@ -199,15 +272,15 @@ sub quicksearch {
# Carry on if no match found. # Carry on if no match found.
} }
} }
elsif ($words[0] =~ /^[A-Z]+(,[A-Z]+)*$/) { elsif ($words->[0] =~ /^[A-Z]+(,[A-Z]+)*$/) {
# e.g. NEW,ASSI,REOP,FIX # e.g. NEW,ASSI,REOP,FIX
undef %states; undef %states;
if (matchPrefixes(\%states, if (matchPrefixes(\%states,
\%resolutions, \%resolutions,
[split(/,/, $words[0])], [split(/,/, $words->[0])],
$legal_statuses, $legal_statuses,
$legal_resolutions)) { $legal_resolutions)) {
shift @words; shift @$words;
} }
else { else {
# Carry on if no match found # Carry on if no match found
...@@ -224,69 +297,75 @@ sub quicksearch { ...@@ -224,69 +297,75 @@ sub quicksearch {
foreach (@closedStates) { $states{$_} = 1 } foreach (@closedStates) { $states{$_} = 1 }
} }
$cgi->param('bug_status', keys(%states)); Bugzilla->cgi->param('bug_status', keys(%states));
$cgi->param('resolution', keys(%resolutions)); Bugzilla->cgi->param('resolution', keys(%resolutions));
}
# Loop over all main-level QuickSearch words.
foreach my $qsword (@words) { sub _handle_special_first_chars {
my $negate = substr($qsword, 0, 1) eq '-'; my ($qsword, $negate) = @_;
if ($negate) {
$qsword = substr($qsword, 1);
}
my $firstChar = substr($qsword, 0, 1); my $firstChar = substr($qsword, 0, 1);
my $baseWord = substr($qsword, 1); my $baseWord = substr($qsword, 1);
my @subWords = split(/[\|,]/, $baseWord); my @subWords = split(/[\|,]/, $baseWord);
if ($firstChar eq '+') { if ($firstChar eq '+') {
foreach (@subWords) { addChart('short_desc', 'substring', $_, $negate) foreach (@subWords);
addChart('short_desc', 'substring', $_, $negate); return 1;
} }
} if ($firstChar eq '#') {
elsif ($firstChar eq '#') {
addChart('short_desc', 'substring', $baseWord, $negate); addChart('short_desc', 'substring', $baseWord, $negate);
addChart('content', 'matches', $baseWord, $negate); addChart('content', 'matches', $baseWord, $negate);
return 1;
} }
elsif ($firstChar eq ':') { if ($firstChar eq ':') {
foreach (@subWords) { foreach (@subWords) {
addChart('product', 'substring', $_, $negate); addChart('product', 'substring', $_, $negate);
addChart('component', 'substring', $_, $negate); addChart('component', 'substring', $_, $negate);
} }
return 1;
} }
elsif ($firstChar eq '@') { if ($firstChar eq '@') {
foreach (@subWords) { addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords);
addChart('assigned_to', 'substring', $_, $negate); return 1;
}
} }
elsif ($firstChar eq '[') { if ($firstChar eq '[') {
addChart('short_desc', 'substring', $baseWord, $negate); addChart('short_desc', 'substring', $baseWord, $negate);
addChart('status_whiteboard', 'substring', $baseWord, $negate); addChart('status_whiteboard', 'substring', $baseWord, $negate);
return 1;
} }
elsif ($firstChar eq '!') { if ($firstChar eq '!') {
addChart('keywords', 'anywords', $baseWord, $negate); addChart('keywords', 'anywords', $baseWord, $negate);
return 1;
} }
else { # No special first char return 0;
}
sub _handle_field_names {
my ($or_operand, $negate, $unknownFields) = @_;
# Split by '|' to get all operands for a boolean OR.
foreach my $or_operand (split(/\|/, $qsword)) {
if ($or_operand =~ /^votes:([0-9]+)$/) {
# votes:xx ("at least xx votes") # votes:xx ("at least xx votes")
if ($or_operand =~ /^votes:([0-9]+)$/) {
addChart('votes', 'greaterthan', $1 - 1, $negate); addChart('votes', 'greaterthan', $1 - 1, $negate);
return 1;
} }
elsif ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
# Flag and requestee shortcut # Flag and requestee shortcut
if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
addChart('flagtypes.name', 'substring', $1, $negate); addChart('flagtypes.name', 'substring', $1, $negate);
$chart++; $and = $or = 0; # Next chart for boolean AND $chart++; $and = $or = 0; # Next chart for boolean AND
addChart('requestees.login_name', 'substring', $2, $negate); addChart('requestees.login_name', 'substring', $2, $negate);
return 1;
} }
elsif ($or_operand =~ /^([^:]+):([^:]+)$/) {
# generic field1,field2,field3:value1,value2 notation # generic field1,field2,field3:value1,value2 notation
if ($or_operand =~ /^([^:]+):([^:]+)$/) {
my @fields = split(/,/, $1); my @fields = split(/,/, $1);
my @values = split(/,/, $2); my @values = split(/,/, $2);
foreach my $field (@fields) { foreach my $field (@fields) {
# Skip and record any unknown fields # Skip and record any unknown fields
if (!defined(MAPPINGS->{$field})) { if (!defined(MAPPINGS->{$field})) {
push(@unknownFields, $field); push(@$unknownFields, $field);
next; next;
} }
$field = MAPPINGS->{$field}; $field = MAPPINGS->{$field};
...@@ -294,29 +373,32 @@ sub quicksearch { ...@@ -294,29 +373,32 @@ sub quicksearch {
addChart($field, 'substring', $_, $negate); addChart($field, 'substring', $_, $negate);
} }
} }
return 1;
} }
else {
# Having ruled out the special cases, we may now split return 0;
# by comma, which is another legal boolean OR indicator. }
foreach my $word (split(/,/, $or_operand)) {
sub _special_field_syntax {
my ($word, $negate) = @_;
# Platform and operating system # Platform and operating system
if (grep({lc($word) eq $_} PLATFORMS) if (grep { lc($word) eq $_ } PLATFORMS
|| grep({lc($word) eq $_} OPSYSTEMS)) { or grep { lc($word) eq $_ } OPSYSTEMS)
addChart('rep_platform', 'substring', {
$word, $negate); addChart('rep_platform', 'substring', $word, $negate);
addChart('op_sys', 'substring', addChart('op_sys', 'substring', $word, $negate);
$word, $negate); return 1;
} }
# Priority # Priority
elsif (grep { lc($_) eq lc($word) } my $legal_priorities = get_legal_field_values('priority');
@$legal_priorities) if (grep { lc($_) eq lc($word) } @$legal_priorities) {
{
addChart('priority', 'equals', $word, $negate); addChart('priority', 'equals', $word, $negate);
return 1;
} }
# P1-5 Syntax # P1-5 Syntax
elsif ($word =~ m/^P(\d+)(?:-(\d+))?$/i) { if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) {
my $start = $1 - 1; my $start = $1 - 1;
$start = 0 if $start < 0; $start = 0 if $start < 0;
my $end = $2 - 1; my $end = $2 - 1;
...@@ -326,110 +408,66 @@ sub quicksearch { ...@@ -326,110 +408,66 @@ sub quicksearch {
if ($end) { if ($end) {
$prios = join(',', @$legal_priorities[$start..$end]) $prios = join(',', @$legal_priorities[$start..$end])
} }
addChart('priority', 'anyexact', $prios, addChart('priority', 'anyexact', $prios, $negate);
$negate); return 1;
} }
# Severity # Severity
elsif (grep({lc($word) eq substr($_, 0, 3)} my $legal_severities = get_legal_field_values('bug_severity');
@{get_legal_field_values('bug_severity')})) { if (grep { lc($word) eq substr($_, 0, 3)} @$legal_severities) {
addChart('bug_severity', 'substring', addChart('bug_severity', 'substring', $word, $negate);
$word, $negate); return 1;
} }
# Votes (votes>xx) # Votes (votes>xx)
elsif ($word =~ m/^votes>([0-9]+)$/) { if ($word =~ m/^votes>([0-9]+)$/) {
addChart('votes', 'greaterthan', addChart('votes', 'greaterthan', $1, $negate);
$1, $negate); return 1;
} }
# Votes (votes>=xx, votes=>xx)
elsif ($word =~ m/^votes(>=|=>)([0-9]+)$/) {
addChart('votes', 'greaterthan',
$2-1, $negate);
# Votes (votes>=xx, votes=>xx)
if ($word =~ m/^votes(>=|=>)([0-9]+)$/) {
addChart('votes', 'greaterthan', $2-1, $negate);
return 1;
} }
else { # Default QuickSearch word
if (!grep({lc($word) eq $_} return 0;
PRODUCT_EXCEPTIONS) && }
length($word)>2
) { sub _default_quicksearch_word {
addChart('product', 'substring', my ($word, $negate) = @_;
$word, $negate);
} if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) {
if (!grep({lc($word) eq $_} addChart('product', 'substring', $word, $negate);
COMPONENT_EXCEPTIONS) &&
length($word)>2
) {
addChart('component', 'substring',
$word, $negate);
}
if (grep({lc($word) eq lc($_)}
map($_->name, Bugzilla::Keyword->get_all))) {
addChart('keywords', 'substring',
$word, $negate);
if (length($word)>2) {
addChart('short_desc', 'substring',
$word, $negate);
addChart('status_whiteboard',
'substring',
$word, $negate);
} }
if (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) {
addChart('component', 'substring', $word, $negate);
} }
else {
addChart('short_desc', 'substring', my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all);
$word, $negate); if (grep { lc($word) eq lc($_) } @legal_keywords) {
addChart('status_whiteboard', 'substring', addChart('keywords', 'substring', $word, $negate);
$word, $negate);
} }
addChart('short_desc', 'substring', $word, $negate);
addChart('status_whiteboard', 'substring', $word, $negate);
addChart('content', 'matches', $word, $negate); addChart('content', 'matches', $word, $negate);
} }
sub _handle_urls {
my ($word, $negate) = @_;
# URL field (for IP addrs, host.names, # URL field (for IP addrs, host.names,
# scheme://urls) # scheme://urls)
if ($word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ if ($word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/
|| $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/ || $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/
|| $word =~ /:[\\\/][\\\/]/ || $word =~ /:[\\\/][\\\/]/
|| $word =~ /localhost/ || $word =~ /localhost/
|| $word =~ /mailto[:]?/ || $word =~ /mailto[:]?/)
# || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port # || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port
) { {
addChart('bug_file_loc', 'substring', addChart('bug_file_loc', 'substring', $word, $negate);
$word, $negate);
}
} # foreach my $word (split(/,/, $qsword))
} # votes and generic field detection
} # foreach (split(/\|/, $_))
} # "switch" $firstChar
$chart++;
$and = 0;
$or = 0;
} # foreach (@words)
# Inform user about any unknown fields
if (scalar(@unknownFields)) {
ThrowUserError("quicksearch_unknown_field",
{ fields => \@unknownFields });
}
# Make sure we have some query terms left
scalar($cgi->param())>0 || ThrowUserError("buglist_parameters_required");
}
# List of quicksearch-specific CGI parameters to get rid of.
my @params_to_strip = ('quicksearch', 'load', 'run');
my $modified_query_string = $cgi->canonicalise_query(@params_to_strip);
if ($cgi->param('load')) {
# Param 'load' asks us to display the query in the advanced search form.
print $cgi->redirect(-uri => "${urlbase}query.cgi?format=advanced&amp;"
. $modified_query_string);
} }
# Otherwise, pass the modified query string to the caller.
# We modified $cgi->params, so the caller can choose to look at that, too,
# and disregard the return value.
$cgi->delete(@params_to_strip);
return $modified_query_string;
} }
########################################################################### ###########################################################################
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment