r=dkl, a=sgreen git-svn-id: svn://10.0.0.236/trunk@265493 18797224-902f-48f8-a5cc-f745e15eee43
699 lines
23 KiB
Perl
699 lines
23 KiB
Perl
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
#
|
|
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
# defined by the Mozilla Public License, v. 2.0.
|
|
|
|
package Bugzilla::Search::Quicksearch;
|
|
|
|
use 5.10.1;
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Bugzilla::Error;
|
|
use Bugzilla::Constants;
|
|
use Bugzilla::Keyword;
|
|
use Bugzilla::Status;
|
|
use Bugzilla::Field;
|
|
use Bugzilla::Util;
|
|
|
|
use List::Util qw(min max);
|
|
use List::MoreUtils qw(firstidx);
|
|
use Text::ParseWords qw(parse_line);
|
|
|
|
use parent qw(Exporter);
|
|
@Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch);
|
|
|
|
# Custom mappings for some fields.
|
|
use constant MAPPINGS => {
|
|
# Status, Resolution, Platform, OS, Priority, Severity
|
|
"status" => "bug_status",
|
|
"platform" => "rep_platform",
|
|
"os" => "op_sys",
|
|
"severity" => "bug_severity",
|
|
|
|
# People: AssignedTo, Reporter, QA Contact, CC, etc.
|
|
"assignee" => "assigned_to",
|
|
"owner" => "assigned_to",
|
|
|
|
# Product, Version, Component, Target Milestone
|
|
"milestone" => "target_milestone",
|
|
|
|
# Summary, Description, URL, Status whiteboard, Keywords
|
|
"summary" => "short_desc",
|
|
"description" => "longdesc",
|
|
"comment" => "longdesc",
|
|
"url" => "bug_file_loc",
|
|
"whiteboard" => "status_whiteboard",
|
|
"sw" => "status_whiteboard",
|
|
"kw" => "keywords",
|
|
"group" => "bug_group",
|
|
|
|
# Flags
|
|
"flag" => "flagtypes.name",
|
|
"requestee" => "requestees.login_name",
|
|
"setter" => "setters.login_name",
|
|
|
|
# Attachments
|
|
"attachment" => "attachments.description",
|
|
"attachmentdesc" => "attachments.description",
|
|
"attachdesc" => "attachments.description",
|
|
"attachmentdata" => "attach_data.thedata",
|
|
"attachdata" => "attach_data.thedata",
|
|
"attachmentmimetype" => "attachments.mimetype",
|
|
"attachmimetype" => "attachments.mimetype"
|
|
};
|
|
|
|
sub FIELD_MAP {
|
|
my $cache = Bugzilla->request_cache;
|
|
return $cache->{quicksearch_fields} if $cache->{quicksearch_fields};
|
|
|
|
# Get all the fields whose names don't contain periods. (Fields that
|
|
# contain periods are always handled in MAPPINGS.)
|
|
my @db_fields = grep { $_->name !~ /\./ }
|
|
@{ Bugzilla->fields({ obsolete => 0 }) };
|
|
my %full_map = (%{ MAPPINGS() }, map { $_->name => $_->name } @db_fields);
|
|
|
|
# Eliminate the fields that start with bug_ or rep_, because those are
|
|
# handled by the MAPPINGS instead, and we don't want too many names
|
|
# for them. (Also, otherwise "rep" doesn't match "reporter".)
|
|
#
|
|
# Remove "status_whiteboard" because we have "whiteboard" for it in
|
|
# the mappings, and otherwise "stat" can't match "status".
|
|
#
|
|
# Also, don't allow searching the _accessible stuff via quicksearch
|
|
# (both because it's unnecessary and because otherwise
|
|
# "reporter_accessible" and "reporter" both match "rep".
|
|
delete @full_map{qw(rep_platform bug_status bug_file_loc bug_group
|
|
bug_severity bug_status
|
|
status_whiteboard
|
|
cclist_accessible reporter_accessible)};
|
|
|
|
Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map} );
|
|
|
|
$cache->{quicksearch_fields} = \%full_map;
|
|
|
|
return $cache->{quicksearch_fields};
|
|
}
|
|
|
|
# Certain fields, when specified like "field:value" get an operator other
|
|
# than "substring"
|
|
use constant FIELD_OPERATOR => {
|
|
content => 'matches',
|
|
owner_idle_time => 'greaterthan',
|
|
};
|
|
|
|
# Mappings for operators symbols to support operators other than "substring"
|
|
use constant OPERATOR_SYMBOLS => {
|
|
':' => 'substring',
|
|
'=' => 'equals',
|
|
'!=' => 'notequals',
|
|
'>=' => 'greaterthaneq',
|
|
'<=' => 'lessthaneq',
|
|
'>' => 'greaterthan',
|
|
'<' => 'lessthan',
|
|
};
|
|
|
|
# We might want to put this into localconfig or somewhere
|
|
use constant PRODUCT_EXCEPTIONS => (
|
|
'row', # [Browser]
|
|
# ^^^
|
|
'new', # [MailNews]
|
|
# ^^^
|
|
);
|
|
use constant COMPONENT_EXCEPTIONS => (
|
|
'hang' # [Bugzilla: Component/Keyword Changes]
|
|
# ^^^^
|
|
);
|
|
|
|
# Quicksearch-wide globals for boolean charts.
|
|
our ($chart, $and, $or, $fulltext, $bug_status_set);
|
|
|
|
sub quicksearch {
|
|
my ($searchstring) = (@_);
|
|
my $cgi = Bugzilla->cgi;
|
|
|
|
$chart = 0;
|
|
$and = 0;
|
|
$or = 0;
|
|
|
|
# Remove leading and trailing commas and whitespace.
|
|
$searchstring =~ s/(^[\s,]+|[\s,]+$)//g;
|
|
ThrowUserError('buglist_parameters_required') unless ($searchstring);
|
|
|
|
if ($searchstring =~ m/^[0-9,\s]*$/) {
|
|
_bug_numbers_only($searchstring);
|
|
}
|
|
else {
|
|
_handle_alias($searchstring);
|
|
|
|
# Retain backslashes and quotes, to know which strings are quoted,
|
|
# and which ones are not.
|
|
my @words = _parse_line('\s+', 1, $searchstring);
|
|
# If parse_line() returns no data, this means strings are badly quoted.
|
|
# Rather than trying to guess what the user wanted to do, we throw an error.
|
|
scalar(@words)
|
|
|| ThrowUserError('quicksearch_unbalanced_quotes', {string => $searchstring});
|
|
|
|
# A query cannot start with AND or OR, nor can it end with AND, OR or NOT.
|
|
ThrowUserError('quicksearch_invalid_query')
|
|
if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/);
|
|
|
|
my (@qswords, @or_group);
|
|
while (scalar @words) {
|
|
my $word = shift @words;
|
|
# AND is the default word separator, similar to a whitespace,
|
|
# but |a AND OR b| is not a valid combination.
|
|
if ($word eq 'AND') {
|
|
ThrowUserError('quicksearch_invalid_query', {operators => ['AND', 'OR']})
|
|
if $words[0] eq 'OR';
|
|
}
|
|
# |a OR AND b| is not a valid combination.
|
|
# |a OR OR b| is equivalent to |a OR b| and so is harmless.
|
|
elsif ($word eq 'OR') {
|
|
ThrowUserError('quicksearch_invalid_query', {operators => ['OR', 'AND']})
|
|
if $words[0] eq 'AND';
|
|
}
|
|
# NOT negates the following word.
|
|
# |NOT AND| and |NOT OR| are not valid combinations.
|
|
# |NOT NOT| is fine but has no effect as they cancel themselves.
|
|
elsif ($word eq 'NOT') {
|
|
$word = shift @words;
|
|
next if $word eq 'NOT';
|
|
if ($word eq 'AND' || $word eq 'OR') {
|
|
ThrowUserError('quicksearch_invalid_query', {operators => ['NOT', $word]});
|
|
}
|
|
unshift(@words, "-$word");
|
|
}
|
|
else {
|
|
# OR groups words together, as OR has higher precedence than AND.
|
|
push(@or_group, $word);
|
|
# If the next word is not OR, then we are not in a OR group,
|
|
# or we are leaving it.
|
|
if (!defined $words[0] || $words[0] ne 'OR') {
|
|
push(@qswords, join('|', @or_group));
|
|
@or_group = ();
|
|
}
|
|
}
|
|
}
|
|
|
|
_handle_status_and_resolution($qswords[0]);
|
|
shift(@qswords) if $bug_status_set;
|
|
|
|
my (@unknownFields, %ambiguous_fields);
|
|
$fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
|
|
|
|
# Loop over all main-level QuickSearch words.
|
|
foreach my $qsword (@qswords) {
|
|
my @or_operand = _parse_line('\|', 1, $qsword);
|
|
foreach my $term (@or_operand) {
|
|
next unless defined $term;
|
|
my $negate = substr($term, 0, 1) eq '-';
|
|
if ($negate) {
|
|
$term = substr($term, 1);
|
|
}
|
|
|
|
next if _handle_special_first_chars($term, $negate);
|
|
next if _handle_field_names($term, $negate, \@unknownFields,
|
|
\%ambiguous_fields);
|
|
|
|
# Having ruled out the special cases, we may now split
|
|
# by comma, which is another legal boolean OR indicator.
|
|
# Remove quotes from quoted words, if any.
|
|
@words = _parse_line(',', 0, $term);
|
|
foreach my $word (@words) {
|
|
if (!_special_field_syntax($word, $negate)) {
|
|
_default_quicksearch_word($word, $negate);
|
|
}
|
|
_handle_urls($word, $negate);
|
|
}
|
|
}
|
|
$chart++;
|
|
$and = 0;
|
|
$or = 0;
|
|
}
|
|
|
|
# If there is no mention of a bug status, we restrict the query
|
|
# to open bugs by default.
|
|
unless ($bug_status_set) {
|
|
$cgi->param('bug_status', BUG_STATE_OPEN);
|
|
}
|
|
|
|
# Inform user about any unknown fields
|
|
if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) {
|
|
ThrowUserError("quicksearch_unknown_field",
|
|
{ unknown => \@unknownFields,
|
|
ambiguous => \%ambiguous_fields });
|
|
}
|
|
|
|
# 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&"
|
|
. $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 _parse_line {
|
|
my ($delim, $keep, $line) = @_;
|
|
return () unless defined $line;
|
|
|
|
# parse_line always treats ' as a quote character, making it impossible
|
|
# to sanely search for contractions. As this behavour isn't
|
|
# configurable, we replace ' with a placeholder to hide it from the
|
|
# parser.
|
|
|
|
# only treat ' at the start or end of words as quotes
|
|
# it's easier to do this in reverse with regexes
|
|
$line =~ s/(^|\s|:)'/$1\001/g;
|
|
$line =~ s/'($|\s)/\001$1/g;
|
|
$line =~ s/\\?'/\000/g;
|
|
$line =~ tr/\001/'/;
|
|
|
|
my @words = parse_line($delim, $keep, $line);
|
|
foreach my $word (@words) {
|
|
$word =~ tr/\000/'/ if defined $word;
|
|
}
|
|
return @words;
|
|
}
|
|
|
|
sub _bug_numbers_only {
|
|
my $searchstring = shift;
|
|
my $cgi = Bugzilla->cgi;
|
|
# Allow separation by comma or whitespace.
|
|
$searchstring =~ s/[,\s]+/,/g;
|
|
|
|
if ($searchstring !~ /,/ && !i_am_webservice()) {
|
|
# Single bug number; shortcut to show_bug.cgi.
|
|
print $cgi->redirect(
|
|
-uri => correct_urlbase() . "show_bug.cgi?id=$searchstring");
|
|
exit;
|
|
}
|
|
else {
|
|
# List of bug numbers.
|
|
$cgi->param('bug_id', $searchstring);
|
|
$cgi->param('order', 'bugs.bug_id');
|
|
$cgi->param('bug_id_type', 'anyexact');
|
|
}
|
|
}
|
|
|
|
sub _handle_alias {
|
|
my $searchstring = shift;
|
|
if ($searchstring =~ /^([^,\s]+)$/) {
|
|
my $alias = $1;
|
|
# We use this direct SQL because we want quicksearch to be VERY fast.
|
|
my $bug_id = Bugzilla->dbh->selectrow_array(
|
|
q{SELECT bug_id FROM bugs_aliases WHERE alias = ?}, undef, $alias);
|
|
# If the user cannot see the bug or if we are using a webservice,
|
|
# do not resolve its alias.
|
|
if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) {
|
|
$alias = url_quote($alias);
|
|
print Bugzilla->cgi->redirect(
|
|
-uri => correct_urlbase() . "show_bug.cgi?id=$alias");
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
|
|
sub _handle_status_and_resolution {
|
|
my $word = shift;
|
|
my $legal_statuses = get_legal_field_values('bug_status');
|
|
my (%states, %resolutions);
|
|
$bug_status_set = 1;
|
|
|
|
if ($word eq 'OPEN') {
|
|
$states{$_} = 1 foreach BUG_STATE_OPEN;
|
|
}
|
|
# If we want all bugs, then there is nothing to do.
|
|
elsif ($word ne 'ALL'
|
|
&& !matchPrefixes(\%states, \%resolutions, $word, $legal_statuses))
|
|
{
|
|
$bug_status_set = 0;
|
|
}
|
|
|
|
# If we have wanted resolutions, allow closed states
|
|
if (keys(%resolutions)) {
|
|
foreach my $status (@$legal_statuses) {
|
|
$states{$status} = 1 unless is_open_state($status);
|
|
}
|
|
}
|
|
|
|
Bugzilla->cgi->param('bug_status', keys(%states));
|
|
Bugzilla->cgi->param('resolution', keys(%resolutions));
|
|
}
|
|
|
|
|
|
sub _handle_special_first_chars {
|
|
my ($qsword, $negate) = @_;
|
|
return 0 if !defined $qsword || length($qsword) <= 1;
|
|
|
|
my $firstChar = substr($qsword, 0, 1);
|
|
my $baseWord = substr($qsword, 1);
|
|
my @subWords = split(/,/, $baseWord);
|
|
|
|
if ($firstChar eq '#') {
|
|
addChart('short_desc', 'substring', $baseWord, $negate);
|
|
addChart('content', 'matches', _matches_phrase($baseWord), $negate) if $fulltext;
|
|
return 1;
|
|
}
|
|
if ($firstChar eq ':') {
|
|
foreach (@subWords) {
|
|
addChart('product', 'substring', $_, $negate);
|
|
addChart('component', 'substring', $_, $negate);
|
|
}
|
|
return 1;
|
|
}
|
|
if ($firstChar eq '@') {
|
|
addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords);
|
|
return 1;
|
|
}
|
|
if ($firstChar eq '[') {
|
|
addChart('short_desc', 'substring', $baseWord, $negate);
|
|
addChart('status_whiteboard', 'substring', $baseWord, $negate);
|
|
return 1;
|
|
}
|
|
if ($firstChar eq '!') {
|
|
addChart('keywords', 'anywords', $baseWord, $negate);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub _handle_field_names {
|
|
my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_;
|
|
|
|
# Generic field1,field2,field3:value1,value2 notation.
|
|
# We have to correctly ignore commas and colons in quotes.
|
|
# Longer operators must be tested first as we don't want single character
|
|
# operators such as <, > and = to be tested before <=, >= and !=.
|
|
my @operators = sort { length($b) <=> length($a) } keys %{ OPERATOR_SYMBOLS() };
|
|
|
|
foreach my $symbol (@operators) {
|
|
my @field_values = _parse_line($symbol, 1, $or_operand);
|
|
next unless scalar @field_values == 2;
|
|
my @fields = _parse_line(',', 1, $field_values[0]);
|
|
my @values = _parse_line(',', 1, $field_values[1]);
|
|
foreach my $field (@fields) {
|
|
my $translated = _translate_field_name($field);
|
|
# Skip and record any unknown fields
|
|
if (!defined $translated) {
|
|
push(@$unknownFields, $field);
|
|
}
|
|
# If we got back an array, that means the substring is
|
|
# ambiguous and could match more than field name
|
|
elsif (ref $translated) {
|
|
$ambiguous_fields->{$field} = $translated;
|
|
}
|
|
else {
|
|
if ($translated eq 'bug_status' || $translated eq 'resolution') {
|
|
$bug_status_set = 1;
|
|
}
|
|
foreach my $value (@values) {
|
|
my $operator = FIELD_OPERATOR->{$translated}
|
|
|| OPERATOR_SYMBOLS->{$symbol}
|
|
|| 'substring';
|
|
# If the string was quoted to protect some special
|
|
# characters such as commas and colons, we need
|
|
# to remove quotes.
|
|
if ($value =~ /^(["'])(.+)\1$/) {
|
|
$value = $2;
|
|
$value =~ s/\\(["'])/$1/g;
|
|
}
|
|
# If a requestee is set, we need to handle it separately.
|
|
if ($translated eq 'flagtypes.name' && $value =~ /^([^\?]+\?)([^\?]+)$/) {
|
|
_handle_flags($1, $2, $negate);
|
|
next;
|
|
}
|
|
addChart($translated, $operator, $value, $negate);
|
|
}
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
# Do not look inside quoted strings.
|
|
return 0 if ($or_operand =~ /^(["']).*\1$/);
|
|
|
|
# Flag and requestee shortcut.
|
|
if ($or_operand =~ /^([^\?]+\?)([^\?]*)$/) {
|
|
_handle_flags($1, $2, $negate);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub _handle_flags {
|
|
my ($flag, $requestee, $negate) = @_;
|
|
|
|
addChart('flagtypes.name', 'substring', $flag, $negate);
|
|
if ($requestee) {
|
|
# FIXME - Every time a requestee is involved and you use OR somewhere
|
|
# in your quick search, the logic will be wrong because boolean charts
|
|
# are unable to run queries of the form (a AND b) OR c. In our case:
|
|
# (flag name is foo AND requestee is bar) OR (any other criteria).
|
|
# But this has never been possible, so this is not a regression. If one
|
|
# needs to run such queries, they must use the Custom Search section of
|
|
# the Advanced Search page.
|
|
$chart++;
|
|
$and = $or = 0;
|
|
addChart('requestees.login_name', 'substring', $requestee, $negate);
|
|
}
|
|
}
|
|
|
|
sub _translate_field_name {
|
|
my $field = shift;
|
|
$field = lc($field);
|
|
my $field_map = FIELD_MAP;
|
|
|
|
# If the field exactly matches a mapping, just return right now.
|
|
return $field_map->{$field} if exists $field_map->{$field};
|
|
|
|
# Check if we match, as a starting substring, exactly one field.
|
|
my @field_names = keys %$field_map;
|
|
my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names;
|
|
# Eliminate duplicates that are actually the same field
|
|
# (otherwise "assi" matches both "assignee" and "assigned_to", and
|
|
# the lines below fail when they shouldn't.)
|
|
my %match_unique = map { $field_map->{$_} => $_ } @matches;
|
|
@matches = values %match_unique;
|
|
|
|
if (scalar(@matches) == 1) {
|
|
return $field_map->{$matches[0]};
|
|
}
|
|
elsif (scalar(@matches) > 1) {
|
|
return \@matches;
|
|
}
|
|
|
|
# Check if we match exactly one custom field, ignoring the cf_ on the
|
|
# custom fields (to allow people to type things like "build" for
|
|
# "cf_build").
|
|
my %cfless;
|
|
foreach my $name (@field_names) {
|
|
my $no_cf = $name;
|
|
if ($no_cf =~ s/^cf_//) {
|
|
if ($field eq $no_cf) {
|
|
return $field_map->{$name};
|
|
}
|
|
$cfless{$no_cf} = $name;
|
|
}
|
|
}
|
|
|
|
# See if we match exactly one substring of any of the cf_-less fields.
|
|
my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless);
|
|
|
|
if (scalar(@cfless_matches) == 1) {
|
|
my $match = $cfless_matches[0];
|
|
my $actual_field = $cfless{$match};
|
|
return $field_map->{$actual_field};
|
|
}
|
|
elsif (scalar(@matches) > 1) {
|
|
return \@matches;
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub _special_field_syntax {
|
|
my ($word, $negate) = @_;
|
|
|
|
# P1-5 Syntax
|
|
if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) {
|
|
my ($p_start, $p_end) = ($1, $2);
|
|
my $legal_priorities = get_legal_field_values('priority');
|
|
|
|
# If Pn exists explicitly, use it.
|
|
my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities;
|
|
my $end;
|
|
$end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end;
|
|
|
|
# If Pn doesn't exist explicitly, then we mean the nth priority.
|
|
if ($start == -1) {
|
|
$start = max(0, $p_start - 1);
|
|
}
|
|
my $prios = $legal_priorities->[$start];
|
|
|
|
if (defined $end) {
|
|
# If Pn doesn't exist explicitly, then we mean the nth priority.
|
|
if ($end == -1) {
|
|
$end = min(scalar(@$legal_priorities), $p_end) - 1;
|
|
$end = max(0, $end); # Just in case the user typed P0.
|
|
}
|
|
($start, $end) = ($end, $start) if $end < $start;
|
|
$prios = join(',', @$legal_priorities[$start..$end])
|
|
}
|
|
|
|
addChart('priority', 'anyexact', $prios, $negate);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub _default_quicksearch_word {
|
|
my ($word, $negate) = @_;
|
|
|
|
if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) {
|
|
addChart('product', 'substring', $word, $negate);
|
|
}
|
|
|
|
if (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) {
|
|
addChart('component', 'substring', $word, $negate);
|
|
}
|
|
|
|
my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all);
|
|
if (grep { lc($word) eq lc($_) } @legal_keywords) {
|
|
addChart('keywords', 'substring', $word, $negate);
|
|
}
|
|
|
|
addChart('alias', 'substring', $word, $negate);
|
|
addChart('short_desc', 'substring', $word, $negate);
|
|
addChart('status_whiteboard', 'substring', $word, $negate);
|
|
addChart('content', 'matches', _matches_phrase($word), $negate) if $fulltext;
|
|
}
|
|
|
|
sub _handle_urls {
|
|
my ($word, $negate) = @_;
|
|
# URL field (for IP addrs, host.names,
|
|
# scheme://urls)
|
|
if ($word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/
|
|
|| $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/
|
|
|| $word =~ /:[\\\/][\\\/]/
|
|
|| $word =~ /localhost/
|
|
|| $word =~ /mailto[:]?/)
|
|
# || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port
|
|
{
|
|
addChart('bug_file_loc', 'substring', $word, $negate);
|
|
}
|
|
}
|
|
|
|
###########################################################################
|
|
# Helpers
|
|
###########################################################################
|
|
|
|
# Quote and escape a phrase appropriately for a "content matches" search.
|
|
sub _matches_phrase {
|
|
my ($phrase) = @_;
|
|
$phrase =~ s/"/\\"/g;
|
|
return "\"$phrase\"";
|
|
}
|
|
|
|
# Expand found prefixes to states or resolutions
|
|
sub matchPrefixes {
|
|
my ($hr_states, $hr_resolutions, $word, $ar_check_states) = @_;
|
|
return unless $word =~ /^[A-Z_]+(,[A-Z_]+)*$/;
|
|
|
|
my @ar_prefixes = split(/,/, $word);
|
|
my $ar_check_resolutions = get_legal_field_values('resolution');
|
|
my $foundMatch = 0;
|
|
|
|
foreach my $prefix (@ar_prefixes) {
|
|
foreach (@$ar_check_states) {
|
|
if (/^$prefix/) {
|
|
$$hr_states{$_} = 1;
|
|
$foundMatch = 1;
|
|
}
|
|
}
|
|
foreach (@$ar_check_resolutions) {
|
|
if (/^$prefix/) {
|
|
$$hr_resolutions{$_} = 1;
|
|
$foundMatch = 1;
|
|
}
|
|
}
|
|
}
|
|
return $foundMatch;
|
|
}
|
|
|
|
# Negate comparison type
|
|
sub negateComparisonType {
|
|
my $comparisonType = shift;
|
|
|
|
if ($comparisonType eq 'anywords') {
|
|
return 'nowords';
|
|
}
|
|
return "not$comparisonType";
|
|
}
|
|
|
|
# Add a boolean chart
|
|
sub addChart {
|
|
my ($field, $comparisonType, $value, $negate) = @_;
|
|
|
|
$negate && ($comparisonType = negateComparisonType($comparisonType));
|
|
makeChart("$chart-$and-$or", $field, $comparisonType, $value);
|
|
if ($negate) {
|
|
$and++;
|
|
$or = 0;
|
|
}
|
|
else {
|
|
$or++;
|
|
}
|
|
}
|
|
|
|
# Create the CGI parameters for a boolean chart
|
|
sub makeChart {
|
|
my ($expr, $field, $type, $value) = @_;
|
|
|
|
my $cgi = Bugzilla->cgi;
|
|
$cgi->param("field$expr", $field);
|
|
$cgi->param("type$expr", $type);
|
|
$cgi->param("value$expr", $value);
|
|
}
|
|
|
|
1;
|
|
|
|
=head1 B<Methods in need of POD>
|
|
|
|
=over
|
|
|
|
=item FIELD_MAP
|
|
|
|
=item quicksearch
|
|
|
|
=item negateComparisonType
|
|
|
|
=item makeChart
|
|
|
|
=item addChart
|
|
|
|
=item matchPrefixes
|
|
|
|
=back
|