summaryrefslogtreecommitdiffstats
path: root/Bugzilla/Search.pm
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/Search.pm')
-rw-r--r--Bugzilla/Search.pm693
1 files changed, 617 insertions, 76 deletions
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 8e419c0ee..0c52553bb 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -38,7 +38,6 @@ use base qw(Exporter);
@Bugzilla::Search::EXPORT = qw(
IsValidQueryType
split_order_term
- translate_old_column
);
use Bugzilla::Error;
@@ -48,6 +47,7 @@ use Bugzilla::Group;
use Bugzilla::User;
use Bugzilla::Field;
use Bugzilla::Search::Clause;
+use Bugzilla::Search::ClauseGroup;
use Bugzilla::Search::Condition qw(condition);
use Bugzilla::Status;
use Bugzilla::Keyword;
@@ -56,9 +56,10 @@ use Data::Dumper;
use Date::Format;
use Date::Parse;
use Scalar::Util qw(blessed);
-use List::MoreUtils qw(all part uniq);
+use List::MoreUtils qw(all firstidx part uniq);
use POSIX qw(INT_MAX);
use Storable qw(dclone);
+use Time::HiRes qw(gettimeofday tv_interval);
# Description Of Boolean Charts
# -----------------------------
@@ -130,8 +131,14 @@ use Storable qw(dclone);
# Constants #
#############
+# BMO - product aliases for searching
+use constant PRODUCT_ALIASES => {
+ 'Boot2Gecko' => 'Firefox OS',
+};
+
# When doing searches, NULL datetimes are treated as this date.
use constant EMPTY_DATETIME => '1970-01-01 00:00:00';
+use constant EMPTY_DATE => '1970-01-01';
# This is the regex for real numbers from Regexp::Common, modified to be
# more readable.
@@ -182,6 +189,8 @@ use constant OPERATORS => {
changedfrom => \&_changedfrom_changedto,
changedto => \&_changedfrom_changedto,
changedby => \&_changedby,
+ isempty => \&_isempty,
+ isnotempty => \&_isnotempty,
};
# Some operators are really just standard SQL operators, and are
@@ -208,6 +217,8 @@ use constant OPERATOR_REVERSE => {
lessthaneq => 'greaterthan',
greaterthan => 'lessthaneq',
greaterthaneq => 'lessthan',
+ isempty => 'isnotempty',
+ isnotempty => 'isempty',
# The following don't currently have reversals:
# casesubstring, anyexact, allwords, allwordssubstr
};
@@ -223,6 +234,12 @@ use constant NON_NUMERIC_OPERATORS => qw(
notregexp
);
+# These operators ignore the entered value
+use constant NO_VALUE_OPERATORS => qw(
+ isempty
+ isnotempty
+);
+
use constant MULTI_SELECT_OVERRIDE => {
notequals => \&_multiselect_negative,
notregexp => \&_multiselect_negative,
@@ -336,6 +353,7 @@ use constant OPERATOR_FIELD_OVERRIDE => {
FIELD_TYPE_FREETEXT, { _non_changed => \&_nullable },
FIELD_TYPE_BUG_ID, { _non_changed => \&_nullable_int },
FIELD_TYPE_DATETIME, { _non_changed => \&_nullable_datetime },
+ FIELD_TYPE_DATE, { _non_changed => \&_nullable_date },
FIELD_TYPE_TEXTAREA, { _non_changed => \&_nullable },
FIELD_TYPE_MULTI_SELECT, MULTI_SELECT_OVERRIDE,
FIELD_TYPE_BUG_URLS, MULTI_SELECT_OVERRIDE,
@@ -343,18 +361,29 @@ use constant OPERATOR_FIELD_OVERRIDE => {
# These are fields where special action is taken depending on the
# *value* passed in to the chart, sometimes.
-use constant SPECIAL_PARSING => {
- # Pronoun Fields (Ones that can accept %user%, etc.)
- assigned_to => \&_contact_pronoun,
- cc => \&_cc_pronoun,
- commenter => \&_commenter_pronoun,
- qa_contact => \&_contact_pronoun,
- reporter => \&_contact_pronoun,
-
- # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format.
- creation_ts => \&_timestamp_translate,
- deadline => \&_timestamp_translate,
- delta_ts => \&_timestamp_translate,
+# This is a sub because custom fields are dynamic
+sub SPECIAL_PARSING {
+ my $map = {
+ # Pronoun Fields (Ones that can accept %user%, etc.)
+ assigned_to => \&_contact_pronoun,
+ cc => \&_cc_pronoun,
+ commenter => \&_commenter_pronoun,
+ qa_contact => \&_contact_pronoun,
+ reporter => \&_contact_pronoun,
+
+ # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format.
+ creation_ts => \&_datetime_translate,
+ deadline => \&_date_translate,
+ delta_ts => \&_datetime_translate,
+ };
+ foreach my $field (Bugzilla->active_custom_fields) {
+ if ($field->type == FIELD_TYPE_DATETIME) {
+ $map->{$field->name} = \&_datetime_translate;
+ } elsif ($field->type == FIELD_TYPE_DATE) {
+ $map->{$field->name} = \&_date_translate;
+ }
+ }
+ return $map;
};
# Information about fields that represent "users", used by _user_nonchanged.
@@ -485,6 +514,14 @@ use constant COLUMN_JOINS => {
to => 'id',
},
},
+ blocked => {
+ table => 'dependencies',
+ to => 'dependson',
+ },
+ dependson => {
+ table => 'dependencies',
+ to => 'blocked',
+ },
'longdescs.count' => {
table => 'longdescs',
join => 'INNER',
@@ -552,15 +589,18 @@ sub COLUMNS {
. $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status')),
'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'),
+
+ blocked => $dbh->sql_group_concat('DISTINCT map_blocked.blocked'),
+ dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'),
'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)',
);
# Backward-compatibility for old field names. Goes new_name => old_name.
- # These are here and not in translate_old_column because the rest of the
+ # These are here and not in _translate_old_column because the rest of the
# code actually still uses the old names, while the fielddefs table uses
# the new names (which is not the case for the fields handled by
- # translate_old_column).
+ # _translate_old_column).
my %old_names = (
creation_ts => 'opendate',
delta_ts => 'changeddate',
@@ -647,7 +687,9 @@ sub REPORT_COLUMNS {
# is here because it *always* goes into the GROUP BY as the first item,
# so it should be skipped when determining extra GROUP BY columns.
use constant GROUP_BY_SKIP => qw(
+ blocked
bug_id
+ dependson
flagtypes.name
keywords
longdescs.count
@@ -688,7 +730,128 @@ sub new {
# Public Accessors #
####################
-sub sql {
+sub data {
+ my $self = shift;
+ return $self->{data} if $self->{data};
+ my $dbh = Bugzilla->dbh;
+
+ # If all fields belong to the 'bugs' table, there is no need to split
+ # the original query into two pieces. Else we override the 'fields'
+ # argument to first get bug IDs based on the search criteria defined
+ # by the caller, and the desired fields are collected in the 2nd query.
+ my @orig_fields = $self->_input_columns;
+ my $all_in_bugs_table = 1;
+ foreach my $field (@orig_fields) {
+ next if $self->COLUMNS->{$field}->{name} =~ /^bugs\.\w+$/;
+ $self->{fields} = ['bug_id'];
+ $all_in_bugs_table = 0;
+ last;
+ }
+
+ # BMO - to avoid massive amounts of joins, if we're selecting a lot of
+ # tracking flags, replace them with placeholders. the values will be
+ # retrieved later and injected into the result.
+ my %tf_map = map { $_ => 1 } Bugzilla::Extension::TrackingFlags::Flag->get_all_names();
+ my @tf_selected = grep { exists $tf_map{$_} } @orig_fields;
+ # mysql has a limit of 61 joins, and we want to avoid massive amounts of joins
+ # 30 ensures we won't hit the limit, nor generate too many joins
+ if (scalar @tf_selected > 30) {
+ foreach my $column (@tf_selected) {
+ $self->COLUMNS->{$column}->{name} = "'---'";
+ }
+ $self->{tracking_flags} = \@tf_selected;
+ }
+ else {
+ $self->{tracking_flags} = [];
+ }
+
+ my $start_time = [gettimeofday()];
+ my $sql = $self->_sql;
+ # Do we just want bug IDs to pass to the 2nd query or all the data immediately?
+ my $func = $all_in_bugs_table ? 'selectall_arrayref' : 'selectcol_arrayref';
+ my $bug_ids = $dbh->$func($sql);
+ my @extra_data = ({sql => $sql, time => tv_interval($start_time)});
+ # Restore the original 'fields' argument, just in case.
+ $self->{fields} = \@orig_fields unless $all_in_bugs_table;
+
+ # BMO if the caller only wants the count, that's all we need to return
+ return $bug_ids->[0]->[0] if $self->_params->{count_only};
+
+ # If there are no bugs found, or all fields are in the 'bugs' table,
+ # there is no need for another query.
+ if (!scalar @$bug_ids || $all_in_bugs_table) {
+ $self->{data} = $bug_ids;
+ return wantarray ? ($self->{data}, \@extra_data) : $self->{data};
+ }
+
+ # Make sure the bug_id will be returned. If not, append it to the list.
+ my $pos = firstidx { $_ eq 'bug_id' } @orig_fields;
+ if ($pos < 0) {
+ push(@orig_fields, 'bug_id');
+ $pos = $#orig_fields;
+ }
+
+ # Now create a query with the buglist above as the single criteria
+ # and the fields that the caller wants. No need to redo security checks;
+ # the list has already been validated above.
+ my $search = $self->new('fields' => \@orig_fields,
+ 'params' => {bug_id => $bug_ids, bug_id_type => 'anyexact'},
+ 'sharer' => $self->_sharer_id,
+ 'user' => $self->_user,
+ 'allow_unlimited' => 1,
+ '_no_security_check' => 1);
+
+ $start_time = [gettimeofday()];
+ $sql = $search->_sql;
+ my $unsorted_data = $dbh->selectall_arrayref($sql);
+ push(@extra_data, {sql => $sql, time => tv_interval($start_time)});
+ # Let's sort the data. We didn't do it in the query itself because
+ # we already know in which order to sort bugs thanks to the first query,
+ # and this avoids additional table joins in the SQL query.
+ my %data = map { $_->[$pos] => $_ } @$unsorted_data;
+ $self->{data} = [map { $data{$_} } @$bug_ids];
+
+ # BMO - get tracking flags values, and insert into result
+ if (@{ $self->{tracking_flags} }) {
+ # read values
+ my $values;
+ $sql = "
+ SELECT bugs.bug_id, tracking_flags.name, tracking_flags_bugs.value
+ FROM bugs
+ LEFT JOIN tracking_flags_bugs ON tracking_flags_bugs.bug_id = bugs.bug_id
+ LEFT JOIN tracking_flags ON tracking_flags.id = tracking_flags_bugs.tracking_flag_id
+ WHERE " . $dbh->sql_in('bugs.bug_id', $bug_ids);
+ $start_time = [gettimeofday()];
+ my $rows = $dbh->selectall_arrayref($sql);
+ push(@extra_data, {sql => $sql, time => tv_interval($start_time)});
+ foreach my $row (@$rows) {
+ $values->{$row->[0]}{$row->[1]} = $row->[2] if defined($row->[2]);
+ }
+
+ # find the columns of the tracking flags
+ my %tf_pos;
+ for (my $i = 0; $i <= $#orig_fields; $i++) {
+ if (grep { $_ eq $orig_fields[$i] } @{ $self->{tracking_flags} }) {
+ $tf_pos{$orig_fields[$i]} = $i;
+ }
+ }
+
+ # replace the placeholder value with the field's value
+ foreach my $row (@{ $self->{data} }) {
+ my $bug_id = $row->[$pos];
+ next unless exists $values->{$bug_id};
+ foreach my $field (keys %{ $values->{$bug_id} }) {
+ if (exists $tf_pos{$field}) {
+ $row->[$tf_pos{$field}] = $values->{$bug_id}{$field};
+ }
+ }
+ }
+ }
+
+ return wantarray ? ($self->{data}, \@extra_data) : $self->{data};
+}
+
+sub _sql {
my ($self) = @_;
return $self->{sql} if $self->{sql};
my $dbh = Bugzilla->dbh;
@@ -710,7 +873,15 @@ sub sql {
? "\nORDER BY " . join(', ', $self->_sql_order_by) : '';
my $limit = $self->_sql_limit;
$limit = "\n$limit" if $limit;
-
+
+ # BMO allow fetching just the number of matching bugs
+ if ($self->_params->{count_only}) {
+ $select = 'COUNT(*) AS count';
+ $group_by = '';
+ $order_by = '';
+ $limit = '';
+ }
+
my $query = <<END;
SELECT $select
FROM $from
@@ -725,12 +896,30 @@ sub search_description {
my ($self, $params) = @_;
my $desc = $self->{'search_description'} ||= [];
if ($params) {
+
+ # BMO - product aliases
+ # display the new product name on the search results name to avoid a
+ # disparity between the search summary and the results.
+ if ($params->{field} eq 'product') {
+ my $aliased;
+ my @values = split(/,/, $params->{value});
+ foreach my $value (@values) {
+ if (exists PRODUCT_ALIASES->{lc($value)}) {
+ $value = PRODUCT_ALIASES->{lc($value)};
+ $aliased = 1;
+ }
+ }
+ if ($aliased) {
+ $params->{value} = join(',', @values);
+ }
+ }
+
push(@$desc, $params);
}
# Make sure that the description has actually been generated if
# people are asking for the whole thing.
else {
- $self->sql;
+ $self->_sql;
}
return $self->{'search_description'};
}
@@ -754,6 +943,21 @@ sub boolean_charts_to_custom_search {
}
}
+sub invalid_order_columns {
+ my ($self) = @_;
+ my @invalid_columns;
+ foreach my $order ($self->_input_order) {
+ next if defined $self->_validate_order_column($order);
+ push(@invalid_columns, $order);
+ }
+ return \@invalid_columns;
+}
+
+sub order {
+ my ($self) = @_;
+ return $self->_valid_order;
+}
+
######################
# Internal Accessors #
######################
@@ -819,7 +1023,7 @@ sub _extra_columns {
my ($self) = @_;
# Everything that's going to be in the ORDER BY must also be
# in the SELECT.
- push(@{ $self->{extra_columns} }, $self->_input_order_columns);
+ push(@{ $self->{extra_columns} }, $self->_valid_order_columns);
return @{ $self->{extra_columns} };
}
@@ -889,10 +1093,32 @@ sub _sql_select {
# The "order" that was requested by the consumer, exactly as it was
# requested.
sub _input_order { @{ $_[0]->{'order'} || [] } }
-# The input order with just the column names, and no ASC or DESC.
-sub _input_order_columns {
+# Requested order with invalid values removed and old names translated
+sub _valid_order {
my ($self) = @_;
- return map { (split_order_term($_))[0] } $self->_input_order;
+ return map { ($self->_validate_order_column($_)) } $self->_input_order;
+}
+# The valid order with just the column names, and no ASC or DESC.
+sub _valid_order_columns {
+ my ($self) = @_;
+ return map { (split_order_term($_))[0] } $self->_valid_order;
+}
+
+sub _validate_order_column {
+ my ($self, $order_item) = @_;
+
+ # Translate old column names
+ my ($field, $direction) = split_order_term($order_item);
+ $field = $self->_translate_old_column($field);
+
+ # Only accept valid columns
+ return if (!exists $self->COLUMNS->{$field});
+
+ # Relevance column can be used only with one or more fulltext searches
+ return if ($field eq 'relevance' && !$self->COLUMNS->{$field}->{name});
+
+ $direction = " $direction" if $direction;
+ return "$field$direction";
}
# A hashref that describes all the special stuff that has to be done
@@ -924,7 +1150,7 @@ sub _sql_order_by {
my ($self) = @_;
if (!$self->{sql_order_by}) {
my @order_by = map { $self->_translate_order_by_column($_) }
- $self->_input_order;
+ $self->_valid_order;
$self->{sql_order_by} = \@order_by;
}
return @{ $self->{sql_order_by} };
@@ -1069,7 +1295,7 @@ sub _select_order_joins {
my @column_join = $self->_column_join($field);
push(@joins, @column_join);
}
- foreach my $field ($self->_input_order_columns) {
+ foreach my $field ($self->_valid_order_columns) {
my $join_info = $self->_special_order->{$field}->{join};
if ($join_info) {
# Don't let callers modify SPECIAL_ORDER.
@@ -1088,6 +1314,7 @@ sub _standard_joins {
my ($self) = @_;
my $user = $self->_user;
my @joins;
+ return () if $self->{_no_security_check};
my $security_join = {
table => 'bug_group_map',
@@ -1126,8 +1353,8 @@ sub _translate_join {
die "join with no table: " . Dumper($join_info) if !$join_info->{table};
die "join with no 'as': " . Dumper($join_info) if !$join_info->{as};
-
- my $from_table = "bugs";
+
+ my $from_table = $join_info->{bugs_table} || "bugs";
my $from = $join_info->{from} || "bug_id";
if ($from =~ /^(\w+)\.(\w+)$/) {
($from_table, $from) = ($1, $2);
@@ -1164,6 +1391,7 @@ sub _translate_join {
# group security.
sub _standard_where {
my ($self) = @_;
+ return ('1=1') if $self->{_no_security_check};
# If replication lags badly between the shadow db and the main DB,
# it's possible for bugs to show up in searches before their group
# controls are properly set. To prevent this, when initially creating
@@ -1225,7 +1453,7 @@ sub _sql_group_by {
# And all items from ORDER BY must be in the GROUP BY. The above loop
# doesn't catch items that were put into the ORDER BY from SPECIAL_ORDER.
- foreach my $column ($self->_input_order_columns) {
+ foreach my $column ($self->_valid_order_columns) {
my $special_order = $self->_special_order->{$column}->{order};
next if !$special_order;
push(@extra_group_by, @$special_order);
@@ -1525,7 +1753,7 @@ sub _charts_to_conditions {
my $clause = $self->_charts;
my @joins;
$clause->walk_conditions(sub {
- my ($condition) = @_;
+ my ($clause, $condition) = @_;
return if !$condition->translated;
push(@joins, @{ $condition->translated->{joins} });
});
@@ -1545,7 +1773,7 @@ sub _params_to_data_structure {
my ($self) = @_;
# First we get the "special" charts, representing all the normal
- # field son the search page. This may modify _params, so it needs to
+ # fields on the search page. This may modify _params, so it needs to
# happen first.
my $clause = $self->_special_charts;
@@ -1554,7 +1782,7 @@ sub _params_to_data_structure {
# And then process the modern "custom search" format.
$clause->add( $self->_custom_search );
-
+
return $clause;
}
@@ -1585,7 +1813,9 @@ sub _boolean_charts {
my $identifier = "$chart_id-$and_id-$or_id";
my $field = $params->{"field$identifier"};
my $operator = $params->{"type$identifier"};
- my $value = $params->{"value$identifier"};
+ my $value = $params->{"value$identifier"};
+ # no-value operators ignore the value, however a value needs to be set
+ $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS;
$or_clause->add($field, $operator, $value);
}
$and_clause->add($or_clause);
@@ -1601,13 +1831,18 @@ sub _custom_search {
my ($self) = @_;
my $params = $self->_params;
- my $current_clause = new Bugzilla::Search::Clause($params->{j_top});
+ my $joiner = $params->{j_top} || '';
+ my $current_clause = $joiner eq 'AND_G'
+ ? new Bugzilla::Search::ClauseGroup()
+ : new Bugzilla::Search::Clause($joiner);
my @clause_stack;
foreach my $id ($self->_field_ids) {
my $field = $params->{"f$id"};
if ($field eq 'OP') {
- my $joiner = $params->{"j$id"};
- my $new_clause = new Bugzilla::Search::Clause($joiner);
+ my $joiner = $params->{"j$id"} || '';
+ my $new_clause = $joiner eq 'AND_G'
+ ? new Bugzilla::Search::ClauseGroup()
+ : new Bugzilla::Search::Clause($joiner);
$new_clause->negate($params->{"n$id"});
$current_clause->add($new_clause);
push(@clause_stack, $current_clause);
@@ -1623,6 +1858,8 @@ sub _custom_search {
my $operator = $params->{"o$id"};
my $value = $params->{"v$id"};
+ # no-value operators ignore the value, however a value needs to be set
+ $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS;
my $condition = condition($field, $operator, $value);
$condition->negate($params->{"n$id"});
$current_clause->add($condition);
@@ -1646,14 +1883,12 @@ sub _field_ids {
}
sub _handle_chart {
- my ($self, $chart_id, $condition) = @_;
+ my ($self, $chart_id, $clause, $condition) = @_;
my $dbh = Bugzilla->dbh;
my $params = $self->_params;
my ($field, $operator, $value) = $condition->fov;
-
- $field = FIELD_MAP->{$field} || $field;
-
return if (!defined $field or !defined $operator or !defined $value);
+ $field = FIELD_MAP->{$field} || $field;
my $string_value;
if (ref $value eq 'ARRAY') {
@@ -1684,16 +1919,20 @@ sub _handle_chart {
# on multiple values, like anyexact.
my %search_args = (
- chart_id => $chart_id,
- sequence => $chart_id,
- field => $field,
- full_field => $full_field,
- operator => $operator,
- value => $string_value,
- all_values => $value,
- joins => [],
- condition => $condition,
+ chart_id => $chart_id,
+ sequence => $chart_id,
+ field => $field,
+ full_field => $full_field,
+ operator => $operator,
+ value => $string_value,
+ all_values => $value,
+ joins => [],
+ bugs_table => 'bugs',
+ table_suffix => '',
+ condition => $condition,
);
+ $clause->update_search_args(\%search_args);
+
$search_args{quoted} = $self->_quote_unless_numeric(\%search_args);
# This should add a "term" selement to %search_args.
$self->do_search_function(\%search_args);
@@ -1709,7 +1948,12 @@ sub _handle_chart {
field => $field, type => $operator,
value => $string_value, term => $search_args{term},
});
-
+
+ foreach my $join (@{ $search_args{joins} }) {
+ $join->{bugs_table} = $search_args{bugs_table};
+ $join->{table_suffix} = $search_args{table_suffix};
+ }
+
$condition->translated(\%search_args);
}
@@ -1830,8 +2074,9 @@ sub _get_column_joins {
return $cache->{column_joins} if defined $cache->{column_joins};
my %column_joins = %{ COLUMN_JOINS() };
+ # BMO - add search object to hook
Bugzilla::Hook::process('buglist_column_joins',
- { column_joins => \%column_joins });
+ { column_joins => \%column_joins, search => $self });
$cache->{column_joins} = \%column_joins;
return $cache->{column_joins};
@@ -1951,22 +2196,29 @@ sub _word_terms {
#####################################
sub _timestamp_translate {
- my ($self, $args) = @_;
+ my ($self, $ignore_time, $args) = @_;
my $value = $args->{value};
my $dbh = Bugzilla->dbh;
return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i;
- # By default, the time is appended to the date, which we don't want
- # for deadlines.
$value = SqlifyDate($value);
- if ($args->{field} eq 'deadline') {
+ # By default, the time is appended to the date, which we don't always want.
+ if ($ignore_time) {
($value) = split(/\s/, $value);
}
$args->{value} = $value;
$args->{quoted} = $dbh->quote($value);
}
+sub _datetime_translate {
+ return shift->_timestamp_translate(0, @_);
+}
+
+sub _date_translate {
+ return shift->_timestamp_translate(1, @_);
+}
+
sub SqlifyDate {
my ($str) = @_;
my $fmt = "%Y-%m-%d %H:%M:%S";
@@ -2259,7 +2511,7 @@ sub _user_nonchanged {
# For negative operators, the system we're using here
# only works properly if we reverse the operator and check IS NULL
# in the WHERE.
- my $is_negative = $operator =~ /^no/ ? 1 : 0;
+ my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0;
if ($is_negative) {
$args->{operator} = $self->_reverse_operator($operator);
}
@@ -2339,8 +2591,13 @@ sub _long_desc_changedbefore_after {
sub _long_desc_nonchanged {
my ($self, $args) = @_;
- my ($chart_id, $operator, $value, $joins) =
- @$args{qw(chart_id operator value joins)};
+ my ($chart_id, $operator, $value, $joins, $bugs_table) =
+ @$args{qw(chart_id operator value joins bugs_table)};
+
+ if ($operator =~ /^is(not)?empty$/) {
+ $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty');
+ return;
+ }
my $dbh = Bugzilla->dbh;
my $table = "longdescs_$chart_id";
@@ -2354,6 +2611,7 @@ sub _long_desc_nonchanged {
all_values => $value,
quoted => $dbh->quote($value),
joins => [],
+ bugs_table => $bugs_table,
};
$self->_do_operator_function($join_args);
@@ -2501,7 +2759,28 @@ sub _component_nonchanged {
sub _product_nonchanged {
my ($self, $args) = @_;
-
+
+ # BMO - product aliases
+ # swap out old product names for new ones
+ if (ref($args->{all_values})) {
+ my $aliased;
+ foreach my $value (@{ $args->{all_values} }) {
+ if (exists PRODUCT_ALIASES->{lc($value)}) {
+ $value = PRODUCT_ALIASES->{lc($value)};
+ $aliased = 1;
+ }
+ }
+ if ($aliased) {
+ $args->{value} = join(',', @{ $args->{all_values} });
+ $args->{quoted} = Bugzilla->dbh->quote($args->{value});
+ }
+ }
+ elsif (exists PRODUCT_ALIASES->{lc($args->{value})}) {
+ $args->{value} = PRODUCT_ALIASES->{lc($args->{value})};
+ $args->{all_values} = $args->{value};
+ $args->{quoted} = Bugzilla->dbh->quote($args->{value});
+ }
+
# Generate the restriction condition
$args->{full_field} = "products.name";
$self->_do_operator_function($args);
@@ -2544,6 +2823,13 @@ sub _nullable_datetime {
$args->{full_field} = "COALESCE($field, $empty)";
}
+sub _nullable_date {
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+ my $empty = Bugzilla->dbh->quote(EMPTY_DATE);
+ $args->{full_field} = "COALESCE($field, $empty)";
+}
+
sub _deadline {
my ($self, $args) = @_;
my $field = $args->{full_field};
@@ -2586,7 +2872,7 @@ sub _owner_idle_time_greater_less {
"$ld_table.who IS NULL AND $act_table.who IS NULL";
} else {
$args->{term} =
- "$ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL";
+ "($ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL)";
}
}
@@ -2630,8 +2916,14 @@ sub _multiselect_multiple {
sub _flagtypes_nonchanged {
my ($self, $args) = @_;
- my ($chart_id, $operator, $value, $joins, $condition) =
- @$args{qw(chart_id operator value joins condition)};
+ my ($chart_id, $operator, $value, $joins, $bugs_table, $condition) =
+ @$args{qw(chart_id operator value joins bugs_table condition)};
+
+ if ($operator =~ /^is(not)?empty$/) {
+ $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty');
+ return;
+ }
+
my $dbh = Bugzilla->dbh;
# For 'not' operators, we need to negate the whole term.
@@ -2654,6 +2946,7 @@ sub _flagtypes_nonchanged {
all_values => $value,
quoted => $dbh->quote($value),
joins => [],
+ bugs_table => "bugs_$chart_id",
};
$self->_do_operator_function($subselect_args);
my $subselect_term = $subselect_args->{term};
@@ -2661,7 +2954,7 @@ sub _flagtypes_nonchanged {
# don't call build_subselect as this must run as a true sub-select
$args->{term} = "EXISTS (
SELECT 1
- FROM bugs bugs_$chart_id
+ FROM $bugs_table bugs_$chart_id
LEFT JOIN attachments AS attachments_$chart_id
ON bugs_$chart_id.bug_id = attachments_$chart_id.bug_id
LEFT JOIN flags AS flags_$chart_id
@@ -2670,7 +2963,7 @@ sub _flagtypes_nonchanged {
OR flags_$chart_id.attach_id IS NULL)
LEFT JOIN flagtypes AS flagtypes_$chart_id
ON flags_$chart_id.type_id = flagtypes_$chart_id.id
- WHERE bugs_$chart_id.bug_id = bugs.bug_id
+ WHERE bugs_$chart_id.bug_id = $bugs_table.bug_id
AND $subselect_term
)";
}
@@ -2747,12 +3040,126 @@ sub _multiselect_table {
sub _multiselect_term {
my ($self, $args, $not) = @_;
+ my ($operator) = $args->{operator};
+ # 'empty' operators require special handling
+ return $self->_multiselect_isempty($args, $not)
+ if $operator =~ /^is(not)?empty$/;
my $table = $self->_multiselect_table($args);
$self->_do_operator_function($args);
my $term = $args->{term};
$term .= $args->{_extra_where} || '';
my $select = $args->{_select_field} || 'bug_id';
- return build_subselect("bugs.bug_id", $select, $table, $term, $not);
+ return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term, $not);
+}
+
+# We can't use the normal operator_functions to build isempty queries which
+# join to different tables.
+sub _multiselect_isempty {
+ my ($self, $args, $not) = @_;
+ my ($field, $operator, $joins, $chart_id) = @$args{qw(field operator joins chart_id)};
+ my $dbh = Bugzilla->dbh;
+ $operator = $self->_reverseoperator($operator) if $not;
+ $not = $operator eq 'isnotempty' ? 'NOT' : '';
+
+ if ($field eq 'keywords') {
+ push @$joins, {
+ table => 'keywords',
+ as => "keywords_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "keywords_$chart_id.bug_id IS $not NULL";
+ }
+ elsif ($field eq 'bug_group') {
+ push @$joins, {
+ table => 'bug_group_map',
+ as => "bug_group_map_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "bug_group_map_$chart_id.bug_id IS $not NULL";
+ }
+ elsif ($field eq 'flagtypes.name') {
+ push @$joins, {
+ table => 'flags',
+ as => "flags_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "flags_$chart_id.bug_id IS $not NULL";
+ }
+ elsif ($field eq 'blocked' or $field eq 'dependson') {
+ my $to = $field eq 'blocked' ? 'dependson' : 'blocked';
+ push @$joins, {
+ table => 'dependencies',
+ as => "dependencies_$chart_id",
+ from => 'bug_id',
+ to => $to,
+ };
+ return "dependencies_$chart_id.$to IS $not NULL";
+ }
+ elsif ($field eq 'longdesc') {
+ my @extra = ( "longdescs_$chart_id.type != " . CMT_HAS_DUPE );
+ push @extra, "longdescs_$chart_id.isprivate = 0"
+ unless $self->_user->is_insider;
+ push @$joins, {
+ table => 'longdescs',
+ as => "longdescs_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ extra => \@extra,
+ };
+ return $not
+ ? "longdescs_$chart_id.thetext != ''"
+ : "longdescs_$chart_id.thetext = ''";
+ }
+ elsif ($field eq 'longdescs.isprivate') {
+ ThrowUserError('search_field_operator_invalid', { field => $field,
+ operator => $operator });
+ }
+ elsif ($field =~ /^attachments\.(.+)/) {
+ my $sub_field = $1;
+ if ($sub_field eq 'description' || $sub_field eq 'filename' || $sub_field eq 'mimetype') {
+ # can't be null/empty
+ return $not ? '1=1' : '1=2';
+ } else {
+ # all other fields which get here are boolean
+ ThrowUserError('search_field_operator_invalid', { field => $field,
+ operator => $operator });
+ }
+ }
+ elsif ($field eq 'attach_data.thedata') {
+ push @$joins, {
+ table => 'attachments',
+ as => "attachments_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ extra => [ $self->_user->is_insider ? '' : "attachments_$chart_id.isprivate = 0" ],
+ };
+ push @$joins, {
+ table => 'attach_data',
+ as => "attach_data_$chart_id",
+ from => "attachments_$chart_id.attach_id",
+ to => 'id',
+ };
+ return "attach_data_$chart_id.thedata IS $not NULL";
+ }
+ elsif ($field eq 'tag') {
+ push @$joins, {
+ table => 'bug_tag',
+ as => "bug_tag_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ push @$joins, {
+ table => 'tag',
+ as => "tag_$chart_id",
+ from => "bug_tag_$chart_id.tag_id",
+ to => 'id',
+ extra => [ "tag_$chart_id.user_id = " . ($self->_sharer_id || $self->_user->id) ],
+ };
+ return "tag_$chart_id.id IS $not NULL";
+ }
}
###############################
@@ -2829,14 +3236,14 @@ sub _anywordsubstr {
my ($self, $args) = @_;
my @terms = $self->_substring_terms($args);
- $args->{term} = join("\n\tOR ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
}
sub _allwordssubstr {
my ($self, $args) = @_;
my @terms = $self->_substring_terms($args);
- $args->{term} = join("\n\tAND ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
}
sub _nowordssubstr {
@@ -2848,19 +3255,19 @@ sub _nowordssubstr {
sub _anywords {
my ($self, $args) = @_;
-
+
my @terms = $self->_word_terms($args);
# Because _word_terms uses AND, we need to parenthesize its terms
# if there are more than one.
@terms = map("($_)", @terms) if scalar(@terms) > 1;
- $args->{term} = join("\n\tOR ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
}
sub _allwords {
my ($self, $args) = @_;
-
+
my @terms = $self->_word_terms($args);
- $args->{term} = join("\n\tAND ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
}
sub _nowords {
@@ -2971,6 +3378,27 @@ sub _changed_security_check {
}
}
+sub _isempty {
+ my ($self, $args) = @_;
+ my $full_field = $args->{full_field};
+ $args->{term} = "$full_field IS NULL OR $full_field = " . $self->_empty_value($args->{field});
+}
+
+sub _isnotempty {
+ my ($self, $args) = @_;
+ my $full_field = $args->{full_field};
+ $args->{term} = "$full_field IS NOT NULL AND $full_field != " . $self->_empty_value($args->{field});
+}
+
+sub _empty_value {
+ my ($self, $field) = @_;
+ my $field_obj = $self->_chart_fields->{$field};
+ return "0" if $field_obj->type == FIELD_TYPE_BUG_ID;
+ return Bugzilla->dbh->quote(EMPTY_DATETIME) if $field_obj->type == FIELD_TYPE_DATETIME;
+ return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE;
+ return "''";
+}
+
######################
# Public Subroutines #
######################
@@ -2979,7 +3407,8 @@ sub _changed_security_check {
sub IsValidQueryType
{
my ($queryType) = @_;
- if (grep { $_ eq $queryType } qw(specific advanced)) {
+ # BMO: Added google and instant
+ if (grep { $_ eq $queryType } qw(specific advanced google instant)) {
return 1;
}
return 0;
@@ -2995,8 +3424,8 @@ sub split_order_term {
# Used to translate old SQL fragments from buglist.cgi's "order" argument
# into our modern field IDs.
-sub translate_old_column {
- my ($column) = @_;
+sub _translate_old_column {
+ my ($self, $column) = @_;
# All old SQL fragments have a period in them somewhere.
return $column if $column !~ /\./;
@@ -3010,12 +3439,124 @@ sub translate_old_column {
# If it doesn't match the regexps above, check to see if the old
# SQL fragment matches the SQL of an existing column
- foreach my $key (%{ COLUMNS() }) {
- next unless exists COLUMNS->{$key}->{name};
- return $key if COLUMNS->{$key}->{name} eq $column;
+ foreach my $key (%{ $self->COLUMNS }) {
+ next unless exists $self->COLUMNS->{$key}->{name};
+ return $key if $self->COLUMNS->{$key}->{name} eq $column;
}
return $column;
}
+# BMO - make product aliases lowercase
+foreach my $name (keys %{ PRODUCT_ALIASES() }) {
+ PRODUCT_ALIASES->{lc($name)} = PRODUCT_ALIASES->{$name};
+ delete PRODUCT_ALIASES->{$name};
+}
+
1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Search - Provides methods to run queries against bugs.
+
+=head1 SYNOPSIS
+
+ use Bugzilla::Search;
+
+ my $search = new Bugzilla::Search({'fields' => \@fields,
+ 'params' => \%search_criteria,
+ 'sharer' => $sharer_id,
+ 'user' => $user_obj,
+ 'allow_unlimited' => 1});
+
+ my $data = $search->data;
+ my ($data, $extra_data) = $search->data;
+
+=head1 DESCRIPTION
+
+Search.pm represents a search object. It's the single way to collect
+data about bugs in a secure way. The list of bugs matching criteria
+defined by the caller are filtered based on the user privileges.
+
+=head1 METHODS
+
+=head2 new
+
+=over
+
+=item B<Description>
+
+Create a Bugzilla::Search object.
+
+=item B<Params>
+
+=over
+
+=item C<fields>
+
+An arrayref representing the bug attributes for which data is desired.
+Legal attributes are listed in the fielddefs DB table. At least one field
+must be defined, typically the 'bug_id' field.
+
+=item C<params>
+
+A hashref representing search criteria. Each key => value pair represents
+a search criteria, where the key is the search field and the value is the
+value for this field. At least one search criteria must be defined if the
+'search_allow_no_criteria' parameter is turned off, else an error is thrown.
+
+=item C<sharer>
+
+When a saved search is shared by a user, this is his user ID.
+
+=item C<user>
+
+A L<Bugzilla::User> object representing the user to whom the data is addressed.
+All security checks are done based on this user object, so it's not safe
+to share results of the query with other users as not all users have the
+same privileges or have the same role for all bugs in the list. If this
+parameter is not defined, then the currently logged in user is taken into
+account. If no user is logged in, then only public bugs will be returned.
+
+=item C<allow_unlimited>
+
+If set to a true value, the number of bugs retrieved by the query is not
+limited.
+
+=back
+
+=item B<Returns>
+
+A L<Bugzilla::Search> object.
+
+=back
+
+=head2 data
+
+=over
+
+=item B<Description>
+
+Returns bugs matching search criteria passed to C<new()>.
+
+=item B<Params>
+
+None
+
+=item B<Returns>
+
+In scalar context, this method returns a reference to a list of bugs.
+Each item of the list represents a bug, which is itself a reference to
+a list where each item represents a bug attribute, in the same order as
+specified in the C<fields> parameter of C<new()>.
+
+In list context, this methods also returns a reference to a list containing
+references to hashes. For each hash, two keys are defined: C<sql> contains
+the SQL query which has been executed, and C<time> contains the time spent
+to execute the SQL query, in seconds. There can be either a single hash, or
+two hashes if two SQL queries have been executed sequentially to get all the
+required data.
+
+=back