diff options
Diffstat (limited to 'Bugzilla/Search.pm')
-rw-r--r-- | Bugzilla/Search.pm | 504 |
1 files changed, 446 insertions, 58 deletions
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index 8e419c0ee..aff189c43 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -48,6 +48,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 +57,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 # ----------------------------- @@ -132,6 +134,7 @@ use Storable qw(dclone); # 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 +185,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 +213,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 +230,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 +349,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 +357,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 +510,14 @@ use constant COLUMN_JOINS => { to => 'id', }, }, + blocked => { + table => 'dependencies', + to => 'dependson', + }, + dependson => { + table => 'dependencies', + to => 'blocked', + }, 'longdescs.count' => { table => 'longdescs', join => 'INNER', @@ -552,6 +585,9 @@ 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)', ); @@ -647,7 +683,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 +726,70 @@ 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; + } + + 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; + + # 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]; + return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; +} + +sub _sql { my ($self) = @_; return $self->{sql} if $self->{sql}; my $dbh = Bugzilla->dbh; @@ -730,7 +831,7 @@ sub search_description { # 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'}; } @@ -1088,6 +1189,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 +1228,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 +1266,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 @@ -1525,7 +1628,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 +1648,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 +1657,7 @@ sub _params_to_data_structure { # And then process the modern "custom search" format. $clause->add( $self->_custom_search ); - + return $clause; } @@ -1585,7 +1688,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 +1706,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 +1733,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 +1758,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 +1794,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 +1823,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); } @@ -1951,22 +2070,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 +2385,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 +2465,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 +2485,7 @@ sub _long_desc_nonchanged { all_values => $value, quoted => $dbh->quote($value), joins => [], + bugs_table => $bugs_table, }; $self->_do_operator_function($join_args); @@ -2544,6 +2676,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 +2725,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 +2769,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 +2799,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 +2807,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 +2816,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 +2893,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 +3089,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 +3108,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 +3231,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 +3260,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; @@ -3019,3 +3301,109 @@ sub translate_old_column { } 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 |