aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/Search.pm')
-rw-r--r--Bugzilla/Search.pm5176
1 files changed, 2596 insertions, 2580 deletions
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 0694dd98c..a6e85f35b 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -13,8 +13,8 @@ use warnings;
use parent qw(Exporter);
@Bugzilla::Search::EXPORT = qw(
- IsValidQueryType
- split_order_term
+ IsValidQueryType
+ split_order_term
);
use Bugzilla::Error;
@@ -135,311 +135,275 @@ use constant NUMBER_REGEX => qr/
# If you specify a search type in the boolean charts, this describes
# which operator maps to which internal function here.
use constant OPERATORS => {
- equals => \&_simple_operator,
- notequals => \&_simple_operator,
- casesubstring => \&_casesubstring,
- substring => \&_substring,
- substr => \&_substring,
- notsubstring => \&_notsubstring,
- regexp => \&_regexp,
- notregexp => \&_notregexp,
- lessthan => \&_simple_operator,
- lessthaneq => \&_simple_operator,
- matches => sub { ThrowUserError("search_content_without_matches"); },
- notmatches => sub { ThrowUserError("search_content_without_matches"); },
- greaterthan => \&_simple_operator,
- greaterthaneq => \&_simple_operator,
- anyexact => \&_anyexact,
- anywordssubstr => \&_anywordsubstr,
- allwordssubstr => \&_allwordssubstr,
- nowordssubstr => \&_nowordssubstr,
- anywords => \&_anywords,
- allwords => \&_allwords,
- nowords => \&_nowords,
- changedbefore => \&_changedbefore_changedafter,
- changedafter => \&_changedbefore_changedafter,
- changedfrom => \&_changedfrom_changedto,
- changedto => \&_changedfrom_changedto,
- changedby => \&_changedby,
- isempty => \&_isempty,
- isnotempty => \&_isnotempty,
+ equals => \&_simple_operator,
+ notequals => \&_simple_operator,
+ casesubstring => \&_casesubstring,
+ substring => \&_substring,
+ substr => \&_substring,
+ notsubstring => \&_notsubstring,
+ regexp => \&_regexp,
+ notregexp => \&_notregexp,
+ lessthan => \&_simple_operator,
+ lessthaneq => \&_simple_operator,
+ matches => sub { ThrowUserError("search_content_without_matches"); },
+ notmatches => sub { ThrowUserError("search_content_without_matches"); },
+ greaterthan => \&_simple_operator,
+ greaterthaneq => \&_simple_operator,
+ anyexact => \&_anyexact,
+ anywordssubstr => \&_anywordsubstr,
+ allwordssubstr => \&_allwordssubstr,
+ nowordssubstr => \&_nowordssubstr,
+ anywords => \&_anywords,
+ allwords => \&_allwords,
+ nowords => \&_nowords,
+ changedbefore => \&_changedbefore_changedafter,
+ changedafter => \&_changedbefore_changedafter,
+ changedfrom => \&_changedfrom_changedto,
+ changedto => \&_changedfrom_changedto,
+ changedby => \&_changedby,
+ isempty => \&_isempty,
+ isnotempty => \&_isnotempty,
};
# Some operators are really just standard SQL operators, and are
# all implemented by the _simple_operator function, which uses this
# constant.
use constant SIMPLE_OPERATORS => {
- equals => '=',
- notequals => '!=',
- greaterthan => '>',
- greaterthaneq => '>=',
- lessthan => '<',
- lessthaneq => "<=",
+ equals => '=',
+ notequals => '!=',
+ greaterthan => '>',
+ greaterthaneq => '>=',
+ lessthan => '<',
+ lessthaneq => "<=",
};
# Most operators just reverse by removing or adding "not" from/to them.
# However, some operators reverse in a different way, so those are listed
# here.
use constant OPERATOR_REVERSE => {
- nowords => 'anywords',
- nowordssubstr => 'anywordssubstr',
- anywords => 'nowords',
- anywordssubstr => 'nowordssubstr',
- lessthan => 'greaterthaneq',
- lessthaneq => 'greaterthan',
- greaterthan => 'lessthaneq',
- greaterthaneq => 'lessthan',
- isempty => 'isnotempty',
- isnotempty => 'isempty',
- # The following don't currently have reversals:
- # casesubstring, anyexact, allwords, allwordssubstr
+ nowords => 'anywords',
+ nowordssubstr => 'anywordssubstr',
+ anywords => 'nowords',
+ anywordssubstr => 'nowordssubstr',
+ lessthan => 'greaterthaneq',
+ lessthaneq => 'greaterthan',
+ greaterthan => 'lessthaneq',
+ greaterthaneq => 'lessthan',
+ isempty => 'isnotempty',
+ isnotempty => 'isempty',
+
+ # The following don't currently have reversals:
+ # casesubstring, anyexact, allwords, allwordssubstr
};
# For these operators, even if a field is numeric (is_numeric returns true),
# we won't treat the input like a number.
use constant NON_NUMERIC_OPERATORS => qw(
- changedafter
- changedbefore
- changedfrom
- changedto
- regexp
- notregexp
+ changedafter
+ changedbefore
+ changedfrom
+ changedto
+ regexp
+ notregexp
);
# These operators ignore the entered value
use constant NO_VALUE_OPERATORS => qw(
- isempty
- isnotempty
+ isempty
+ isnotempty
);
use constant MULTI_SELECT_OVERRIDE => {
- notequals => \&_multiselect_negative,
- notregexp => \&_multiselect_negative,
- notsubstring => \&_multiselect_negative,
- nowords => \&_multiselect_negative,
- nowordssubstr => \&_multiselect_negative,
-
- allwords => \&_multiselect_multiple,
- allwordssubstr => \&_multiselect_multiple,
- anyexact => \&_multiselect_multiple,
- anywords => \&_multiselect_multiple,
- anywordssubstr => \&_multiselect_multiple,
-
- _non_changed => \&_multiselect_nonchanged,
+ notequals => \&_multiselect_negative,
+ notregexp => \&_multiselect_negative,
+ notsubstring => \&_multiselect_negative,
+ nowords => \&_multiselect_negative,
+ nowordssubstr => \&_multiselect_negative,
+
+ allwords => \&_multiselect_multiple,
+ allwordssubstr => \&_multiselect_multiple,
+ anyexact => \&_multiselect_multiple,
+ anywords => \&_multiselect_multiple,
+ anywordssubstr => \&_multiselect_multiple,
+
+ _non_changed => \&_multiselect_nonchanged,
};
use constant OPERATOR_FIELD_OVERRIDE => {
- # User fields
- 'attachments.submitter' => {
- _non_changed => \&_user_nonchanged,
- },
- assigned_to => {
- _non_changed => \&_user_nonchanged,
- },
- assigned_to_realname => {
- _non_changed => \&_user_nonchanged,
- },
- cc => {
- _non_changed => \&_user_nonchanged,
- },
- commenter => {
- _non_changed => \&_user_nonchanged,
- },
- reporter => {
- _non_changed => \&_user_nonchanged,
- },
- reporter_realname => {
- _non_changed => \&_user_nonchanged,
- },
- 'requestees.login_name' => {
- _non_changed => \&_user_nonchanged,
- },
- 'setters.login_name' => {
- _non_changed => \&_user_nonchanged,
- },
- qa_contact => {
- _non_changed => \&_user_nonchanged,
- },
- qa_contact_realname => {
- _non_changed => \&_user_nonchanged,
- },
- # General Bug Fields
- alias => { _non_changed => \&_alias_nonchanged },
- 'attach_data.thedata' => MULTI_SELECT_OVERRIDE,
- # We check all attachment fields against this.
- attachments => MULTI_SELECT_OVERRIDE,
- blocked => MULTI_SELECT_OVERRIDE,
- bug_file_loc => { _non_changed => \&_nullable },
- bug_group => MULTI_SELECT_OVERRIDE,
- classification => {
- _non_changed => \&_classification_nonchanged,
- },
- component => {
- _non_changed => \&_component_nonchanged,
- },
- content => {
- matches => \&_content_matches,
- notmatches => \&_content_matches,
- _default => sub { ThrowUserError("search_content_without_matches"); },
- },
- days_elapsed => {
- _default => \&_days_elapsed,
- },
- dependson => MULTI_SELECT_OVERRIDE,
- keywords => MULTI_SELECT_OVERRIDE,
- 'flagtypes.name' => {
- _non_changed => \&_flagtypes_nonchanged,
- },
- longdesc => {
- changedby => \&_long_desc_changedby,
- changedbefore => \&_long_desc_changedbefore_after,
- changedafter => \&_long_desc_changedbefore_after,
- _non_changed => \&_long_desc_nonchanged,
- },
- 'longdescs.count' => {
- changedby => \&_long_desc_changedby,
- changedbefore => \&_long_desc_changedbefore_after,
- changedafter => \&_long_desc_changedbefore_after,
- changedfrom => \&_invalid_combination,
- changedto => \&_invalid_combination,
- _default => \&_long_descs_count,
- },
- 'longdescs.isprivate' => MULTI_SELECT_OVERRIDE,
- owner_idle_time => {
- greaterthan => \&_owner_idle_time_greater_less,
- greaterthaneq => \&_owner_idle_time_greater_less,
- lessthan => \&_owner_idle_time_greater_less,
- lessthaneq => \&_owner_idle_time_greater_less,
- _default => \&_invalid_combination,
- },
- product => {
- _non_changed => \&_product_nonchanged,
- },
- tag => MULTI_SELECT_OVERRIDE,
- comment_tag => MULTI_SELECT_OVERRIDE,
-
- # Timetracking Fields
- deadline => { _non_changed => \&_deadline },
- percentage_complete => {
- _non_changed => \&_percentage_complete,
- },
- work_time => {
- changedby => \&_work_time_changedby,
- changedbefore => \&_work_time_changedbefore_after,
- changedafter => \&_work_time_changedbefore_after,
- _default => \&_work_time,
- },
- last_visit_ts => {
- _non_changed => \&_last_visit_ts,
- _default => \&_last_visit_ts_invalid_operator,
- },
-
- # Custom Fields
- 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,
+ # User fields
+ 'attachments.submitter' => {_non_changed => \&_user_nonchanged,},
+ assigned_to => {_non_changed => \&_user_nonchanged,},
+ assigned_to_realname => {_non_changed => \&_user_nonchanged,},
+ cc => {_non_changed => \&_user_nonchanged,},
+ commenter => {_non_changed => \&_user_nonchanged,},
+ reporter => {_non_changed => \&_user_nonchanged,},
+ reporter_realname => {_non_changed => \&_user_nonchanged,},
+ 'requestees.login_name' => {_non_changed => \&_user_nonchanged,},
+ 'setters.login_name' => {_non_changed => \&_user_nonchanged,},
+ qa_contact => {_non_changed => \&_user_nonchanged,},
+ qa_contact_realname => {_non_changed => \&_user_nonchanged,},
+
+ # General Bug Fields
+ alias => {_non_changed => \&_alias_nonchanged},
+ 'attach_data.thedata' => MULTI_SELECT_OVERRIDE,
+
+ # We check all attachment fields against this.
+ attachments => MULTI_SELECT_OVERRIDE,
+ blocked => MULTI_SELECT_OVERRIDE,
+ bug_file_loc => {_non_changed => \&_nullable},
+ bug_group => MULTI_SELECT_OVERRIDE,
+ classification => {_non_changed => \&_classification_nonchanged,},
+ component => {_non_changed => \&_component_nonchanged,},
+ content => {
+ matches => \&_content_matches,
+ notmatches => \&_content_matches,
+ _default => sub { ThrowUserError("search_content_without_matches"); },
+ },
+ days_elapsed => {_default => \&_days_elapsed,},
+ dependson => MULTI_SELECT_OVERRIDE,
+ keywords => MULTI_SELECT_OVERRIDE,
+ 'flagtypes.name' => {_non_changed => \&_flagtypes_nonchanged,},
+ longdesc => {
+ changedby => \&_long_desc_changedby,
+ changedbefore => \&_long_desc_changedbefore_after,
+ changedafter => \&_long_desc_changedbefore_after,
+ _non_changed => \&_long_desc_nonchanged,
+ },
+ 'longdescs.count' => {
+ changedby => \&_long_desc_changedby,
+ changedbefore => \&_long_desc_changedbefore_after,
+ changedafter => \&_long_desc_changedbefore_after,
+ changedfrom => \&_invalid_combination,
+ changedto => \&_invalid_combination,
+ _default => \&_long_descs_count,
+ },
+ 'longdescs.isprivate' => MULTI_SELECT_OVERRIDE,
+ owner_idle_time => {
+ greaterthan => \&_owner_idle_time_greater_less,
+ greaterthaneq => \&_owner_idle_time_greater_less,
+ lessthan => \&_owner_idle_time_greater_less,
+ lessthaneq => \&_owner_idle_time_greater_less,
+ _default => \&_invalid_combination,
+ },
+ product => {_non_changed => \&_product_nonchanged,},
+ tag => MULTI_SELECT_OVERRIDE,
+ comment_tag => MULTI_SELECT_OVERRIDE,
+
+ # Timetracking Fields
+ deadline => {_non_changed => \&_deadline},
+ percentage_complete => {_non_changed => \&_percentage_complete,},
+ work_time => {
+ changedby => \&_work_time_changedby,
+ changedbefore => \&_work_time_changedbefore_after,
+ changedafter => \&_work_time_changedbefore_after,
+ _default => \&_work_time,
+ },
+ last_visit_ts => {
+ _non_changed => \&_last_visit_ts,
+ _default => \&_last_visit_ts_invalid_operator,
+ },
+
+ # Custom Fields
+ 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,
};
# These are fields where special action is taken depending on the
# *value* passed in to the chart, sometimes.
# 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 => \&_contact_pronoun,
- commenter => \&_contact_pronoun,
- qa_contact => \&_contact_pronoun,
- reporter => \&_contact_pronoun,
- 'setters.login_name' => \&_contact_pronoun,
- 'requestees.login_name' => \&_contact_pronoun,
-
- # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format.
- creation_ts => \&_datetime_translate,
- deadline => \&_date_translate,
- delta_ts => \&_datetime_translate,
-
- # last_visit field that accept both a 1d, 1w, 1m, 1y format and the
- # %last_changed% pronoun.
- last_visit_ts => \&_last_visit_datetime,
- };
- 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;
- }
+ my $map = {
+
+ # Pronoun Fields (Ones that can accept %user%, etc.)
+ assigned_to => \&_contact_pronoun,
+ cc => \&_contact_pronoun,
+ commenter => \&_contact_pronoun,
+ qa_contact => \&_contact_pronoun,
+ reporter => \&_contact_pronoun,
+ 'setters.login_name' => \&_contact_pronoun,
+ 'requestees.login_name' => \&_contact_pronoun,
+
+ # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format.
+ creation_ts => \&_datetime_translate,
+ deadline => \&_date_translate,
+ delta_ts => \&_datetime_translate,
+
+ # last_visit field that accept both a 1d, 1w, 1m, 1y format and the
+ # %last_changed% pronoun.
+ last_visit_ts => \&_last_visit_datetime,
+ };
+ foreach my $field (Bugzilla->active_custom_fields) {
+ if ($field->type == FIELD_TYPE_DATETIME) {
+ $map->{$field->name} = \&_datetime_translate;
}
- return $map;
-};
+ elsif ($field->type == FIELD_TYPE_DATE) {
+ $map->{$field->name} = \&_date_translate;
+ }
+ }
+ return $map;
+}
# Information about fields that represent "users", used by _user_nonchanged.
# There are other user fields than the ones listed here, but those use
# defaults in _user_nonchanged.
use constant USER_FIELDS => {
- 'attachments.submitter' => {
- field => 'submitter_id',
- join => { table => 'attachments' },
- isprivate => 1,
- },
- cc => {
- field => 'who',
- join => { table => 'cc' },
- },
- commenter => {
- field => 'who',
- join => { table => 'longdescs', join => 'INNER' },
- isprivate => 1,
- },
- qa_contact => {
- nullable => 1,
- },
- 'requestees.login_name' => {
- nullable => 1,
- field => 'requestee_id',
- join => { table => 'flags' },
- },
- 'setters.login_name' => {
- field => 'setter_id',
- join => { table => 'flags' },
- },
+ 'attachments.submitter' =>
+ {field => 'submitter_id', join => {table => 'attachments'}, isprivate => 1,},
+ cc => {field => 'who', join => {table => 'cc'},},
+ commenter => {
+ field => 'who',
+ join => {table => 'longdescs', join => 'INNER'},
+ isprivate => 1,
+ },
+ qa_contact => {nullable => 1,},
+ 'requestees.login_name' =>
+ {nullable => 1, field => 'requestee_id', join => {table => 'flags'},},
+ 'setters.login_name' => {field => 'setter_id', join => {table => 'flags'},},
};
# Backwards compatibility for times that we changed the names of fields
# or URL parameters.
use constant FIELD_MAP => {
- 'attachments.thedata' => 'attach_data.thedata',
- bugidtype => 'bug_id_type',
- changedin => 'days_elapsed',
- long_desc => 'longdesc',
- tags => 'tag',
+ 'attachments.thedata' => 'attach_data.thedata',
+ bugidtype => 'bug_id_type',
+ changedin => 'days_elapsed',
+ long_desc => 'longdesc',
+ tags => 'tag',
};
# Some fields are not sorted on themselves, but on other fields.
# We need to have a list of these fields and what they map to.
use constant SPECIAL_ORDER => {
- 'target_milestone' => {
- order => ['map_target_milestone.sortkey','map_target_milestone.value'],
- join => {
- table => 'milestones',
- from => 'target_milestone',
- to => 'value',
- extra => ['bugs.product_id = map_target_milestone.product_id'],
- join => 'INNER',
- }
- },
+ 'target_milestone' => {
+ order => ['map_target_milestone.sortkey', 'map_target_milestone.value'],
+ join => {
+ table => 'milestones',
+ from => 'target_milestone',
+ to => 'value',
+ extra => ['bugs.product_id = map_target_milestone.product_id'],
+ join => 'INNER',
+ }
+ },
};
# Certain columns require other columns to come before them
# in _select_columns, and should be put there if they're not there.
use constant COLUMN_DEPENDS => {
- classification => ['product'],
- percentage_complete => ['actual_time', 'remaining_time'],
+ classification => ['product'],
+ percentage_complete => ['actual_time', 'remaining_time'],
};
# This describes tables that must be joined when you want to display
@@ -447,109 +411,81 @@ use constant COLUMN_DEPENDS => {
# DB::Schema to figure out what needs to be joined, but for some
# fields it needs a little help.
sub COLUMN_JOINS {
- my $invocant = shift;
- my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user;
-
- my $joins = {
- actual_time => {
- table => '(SELECT bug_id, SUM(work_time) AS total'
- . ' FROM longdescs GROUP BY bug_id)',
- join => 'INNER',
- },
- alias => {
- table => 'bugs_aliases',
- as => 'map_alias',
- },
- assigned_to => {
- from => 'assigned_to',
- to => 'userid',
- table => 'profiles',
- join => 'INNER',
- },
- reporter => {
- from => 'reporter',
- to => 'userid',
- table => 'profiles',
- join => 'INNER',
- },
- qa_contact => {
- from => 'qa_contact',
- to => 'userid',
- table => 'profiles',
- },
- component => {
- from => 'component_id',
- to => 'id',
- table => 'components',
- join => 'INNER',
- },
- product => {
- from => 'product_id',
- to => 'id',
- table => 'products',
- join => 'INNER',
- },
- classification => {
- table => 'classifications',
- from => 'map_product.classification_id',
- to => 'id',
- join => 'INNER',
- },
- 'flagtypes.name' => {
- as => 'map_flags',
- table => 'flags',
- extra => ['map_flags.attach_id IS NULL'],
- then_to => {
- as => 'map_flagtypes',
- table => 'flagtypes',
- from => 'map_flags.type_id',
- to => 'id',
- },
- },
- keywords => {
- table => 'keywords',
- then_to => {
- as => 'map_keyworddefs',
- table => 'keyworddefs',
- from => 'map_keywords.keywordid',
- to => 'id',
- },
- },
- blocked => {
- table => 'dependencies',
- to => 'dependson',
- },
- dependson => {
- table => 'dependencies',
- to => 'blocked',
- },
- 'longdescs.count' => {
- table => 'longdescs',
- join => 'INNER',
- },
- tag => {
- as => 'map_bug_tag',
- table => 'bug_tag',
- then_to => {
- as => 'map_tag',
- table => 'tag',
- extra => ['map_tag.user_id = ' . $user->id],
- from => 'map_bug_tag.tag_id',
- to => 'id',
- },
- },
- last_visit_ts => {
- as => 'bug_user_last_visit',
- table => 'bug_user_last_visit',
- extra => ['bug_user_last_visit.user_id = ' . $user->id],
- from => 'bug_id',
- to => 'bug_id',
- },
- };
- return $joins;
-};
+ my $invocant = shift;
+ my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user;
+
+ my $joins = {
+ actual_time => {
+ table => '(SELECT bug_id, SUM(work_time) AS total'
+ . ' FROM longdescs GROUP BY bug_id)',
+ join => 'INNER',
+ },
+ alias => {table => 'bugs_aliases', as => 'map_alias',},
+ assigned_to => {
+ from => 'assigned_to',
+ to => 'userid',
+ table => 'profiles',
+ join => 'INNER',
+ },
+ reporter =>
+ {from => 'reporter', to => 'userid', table => 'profiles', join => 'INNER',},
+ qa_contact => {from => 'qa_contact', to => 'userid', table => 'profiles',},
+ component =>
+ {from => 'component_id', to => 'id', table => 'components', join => 'INNER',},
+ product =>
+ {from => 'product_id', to => 'id', table => 'products', join => 'INNER',},
+ classification => {
+ table => 'classifications',
+ from => 'map_product.classification_id',
+ to => 'id',
+ join => 'INNER',
+ },
+ 'flagtypes.name' => {
+ as => 'map_flags',
+ table => 'flags',
+ extra => ['map_flags.attach_id IS NULL'],
+ then_to => {
+ as => 'map_flagtypes',
+ table => 'flagtypes',
+ from => 'map_flags.type_id',
+ to => 'id',
+ },
+ },
+ keywords => {
+ table => 'keywords',
+ then_to => {
+ as => 'map_keyworddefs',
+ table => 'keyworddefs',
+ from => 'map_keywords.keywordid',
+ to => 'id',
+ },
+ },
+ blocked => {table => 'dependencies', to => 'dependson',},
+ dependson => {table => 'dependencies', to => 'blocked',},
+ 'longdescs.count' => {table => 'longdescs', join => 'INNER',},
+ tag => {
+ as => 'map_bug_tag',
+ table => 'bug_tag',
+ then_to => {
+ as => 'map_tag',
+ table => 'tag',
+ extra => ['map_tag.user_id = ' . $user->id],
+ from => 'map_bug_tag.tag_id',
+ to => 'id',
+ },
+ },
+ last_visit_ts => {
+ as => 'bug_user_last_visit',
+ table => 'bug_user_last_visit',
+ extra => ['bug_user_last_visit.user_id = ' . $user->id],
+ from => 'bug_id',
+ to => 'bug_id',
+ },
+ };
+ return $joins;
+}
-# This constant defines the columns that can be selected in a query
+# This constant defines the columns that can be selected in a query
# and/or displayed in a bug list. Column records include the following
# fields:
#
@@ -559,7 +495,7 @@ sub COLUMN_JOINS {
# that returns the value of the column);
#
# 3. title: The title of the column as displayed to users.
-#
+#
# Note: There are a few hacks in the code that deviate from these definitions.
# In particular, the redundant short_desc column is removed when the
# client requests "all" columns.
@@ -570,150 +506,149 @@ sub COLUMN_JOINS {
# and we don't want it to happen at compile time, so we have it as a
# subroutine.
sub COLUMNS {
- my $invocant = shift;
- my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user;
- my $dbh = Bugzilla->dbh;
- my $cache = Bugzilla->request_cache;
+ my $invocant = shift;
+ my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+ my $cache = Bugzilla->request_cache;
- if (defined $cache->{search_columns}->{$user->id}) {
- return $cache->{search_columns}->{$user->id};
- }
-
- # These are columns that don't exist in fielddefs, but are valid buglist
- # columns. (Also see near the bottom of this function for the definition
- # of short_short_desc.)
- my %columns = (
- relevance => { title => 'Relevance' },
- );
-
- # Next we define columns that have special SQL instead of just something
- # like "bugs.bug_id".
- my $total_time = "(map_actual_time.total + bugs.remaining_time)";
- my %special_sql = (
- alias => $dbh->sql_group_concat('DISTINCT map_alias.alias'),
- deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'),
- actual_time => 'map_actual_time.total',
-
- # "FLOOR" is in there to turn this into an integer, making searches
- # totally predictable. Otherwise you get floating-point numbers that
- # are rather hard to search reliably if you're asking for exact
- # numbers.
- percentage_complete =>
- "(CASE WHEN $total_time = 0"
- . " THEN 0"
- . " ELSE FLOOR(100 * (map_actual_time.total / $total_time))"
- . " END)",
-
- 'flagtypes.name' => $dbh->sql_group_concat('DISTINCT '
- . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status'),
- undef, undef, 'map_flagtypes.sortkey, map_flagtypes.name'),
-
- '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)',
-
- tag => $dbh->sql_group_concat('DISTINCT map_tag.name'),
- last_visit_ts => 'bug_user_last_visit.last_visit_ts',
- );
-
- # 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
- # 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).
- my %old_names = (
- creation_ts => 'opendate',
- delta_ts => 'changeddate',
- work_time => 'actual_time',
- );
-
- # Fields that are email addresses
- my @email_fields = qw(assigned_to reporter qa_contact);
- # Other fields that are stored in the bugs table as an id, but
- # should be displayed using their name.
- my @id_fields = qw(product component classification);
-
- foreach my $col (@email_fields) {
- my $sql = "map_${col}.login_name";
- if (!$user->id) {
- $sql = $dbh->sql_string_until($sql, $dbh->quote('@'));
- }
- $special_sql{$col} = $sql;
- $special_sql{"${col}_realname"} = "map_${col}.realname";
- }
-
- foreach my $col (@id_fields) {
- $special_sql{$col} = "map_${col}.name";
+ if (defined $cache->{search_columns}->{$user->id}) {
+ return $cache->{search_columns}->{$user->id};
+ }
+
+ # These are columns that don't exist in fielddefs, but are valid buglist
+ # columns. (Also see near the bottom of this function for the definition
+ # of short_short_desc.)
+ my %columns = (relevance => {title => 'Relevance'},);
+
+ # Next we define columns that have special SQL instead of just something
+ # like "bugs.bug_id".
+ my $total_time = "(map_actual_time.total + bugs.remaining_time)";
+ my %special_sql = (
+ alias => $dbh->sql_group_concat('DISTINCT map_alias.alias'),
+ deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'),
+ actual_time => 'map_actual_time.total',
+
+ # "FLOOR" is in there to turn this into an integer, making searches
+ # totally predictable. Otherwise you get floating-point numbers that
+ # are rather hard to search reliably if you're asking for exact
+ # numbers.
+ percentage_complete => "(CASE WHEN $total_time = 0"
+ . " THEN 0"
+ . " ELSE FLOOR(100 * (map_actual_time.total / $total_time))" . " END)",
+
+ 'flagtypes.name' => $dbh->sql_group_concat(
+ 'DISTINCT ' . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status'),
+ undef,
+ undef,
+ 'map_flagtypes.sortkey, map_flagtypes.name'
+ ),
+
+ '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)',
+
+ tag => $dbh->sql_group_concat('DISTINCT map_tag.name'),
+ last_visit_ts => 'bug_user_last_visit.last_visit_ts',
+ );
+
+ # 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
+ # 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).
+ my %old_names = (
+ creation_ts => 'opendate',
+ delta_ts => 'changeddate',
+ work_time => 'actual_time',
+ );
+
+ # Fields that are email addresses
+ my @email_fields = qw(assigned_to reporter qa_contact);
+
+ # Other fields that are stored in the bugs table as an id, but
+ # should be displayed using their name.
+ my @id_fields = qw(product component classification);
+
+ foreach my $col (@email_fields) {
+ my $sql = "map_${col}.login_name";
+ if (!$user->id) {
+ $sql = $dbh->sql_string_until($sql, $dbh->quote('@'));
+ }
+ $special_sql{$col} = $sql;
+ $special_sql{"${col}_realname"} = "map_${col}.realname";
+ }
+
+ foreach my $col (@id_fields) {
+ $special_sql{$col} = "map_${col}.name";
+ }
+
+ # Do the actual column-getting from fielddefs, now.
+ my @fields = @{Bugzilla->fields({obsolete => 0, buglist => 1})};
+ foreach my $field (@fields) {
+ my $id = $field->name;
+ $id = $old_names{$id} if exists $old_names{$id};
+ my $sql;
+ if (exists $special_sql{$id}) {
+ $sql = $special_sql{$id};
+ }
+ elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+ $sql = $dbh->sql_group_concat('DISTINCT map_' . $field->name . '.value');
}
-
- # Do the actual column-getting from fielddefs, now.
- my @fields = @{ Bugzilla->fields({ obsolete => 0, buglist => 1 }) };
- foreach my $field (@fields) {
- my $id = $field->name;
- $id = $old_names{$id} if exists $old_names{$id};
- my $sql;
- if (exists $special_sql{$id}) {
- $sql = $special_sql{$id};
- }
- elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
- $sql = $dbh->sql_group_concat(
- 'DISTINCT map_' . $field->name . '.value');
- }
- else {
- $sql = 'bugs.' . $field->name;
- }
- $columns{$id} = { name => $sql, title => $field->description };
+ else {
+ $sql = 'bugs.' . $field->name;
}
+ $columns{$id} = {name => $sql, title => $field->description};
+ }
- # The short_short_desc column is identical to short_desc
- $columns{'short_short_desc'} = $columns{'short_desc'};
+ # The short_short_desc column is identical to short_desc
+ $columns{'short_short_desc'} = $columns{'short_desc'};
- Bugzilla::Hook::process('buglist_columns', { columns => \%columns });
+ Bugzilla::Hook::process('buglist_columns', {columns => \%columns});
- $cache->{search_columns}->{$user->id} = \%columns;
- return $cache->{search_columns}->{$user->id};
+ $cache->{search_columns}->{$user->id} = \%columns;
+ return $cache->{search_columns}->{$user->id};
}
sub REPORT_COLUMNS {
- my $invocant = shift;
- my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user;
-
- my $columns = dclone(blessed($invocant) ? $invocant->COLUMNS : COLUMNS);
- # There's no reason to support reporting on unique fields.
- # Also, some other fields don't make very good reporting axises,
- # or simply don't work with the current reporting system.
- my @no_report_columns =
- qw(bug_id alias short_short_desc opendate changeddate
- flagtypes.name relevance);
-
- # If you're not a time-tracker, you can't use time-tracking
- # columns.
- if (!$user->is_timetracker) {
- push(@no_report_columns, TIMETRACKING_FIELDS);
- }
+ my $invocant = shift;
+ my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user;
- foreach my $name (@no_report_columns) {
- delete $columns->{$name};
- }
- return $columns;
+ my $columns = dclone(blessed($invocant) ? $invocant->COLUMNS : COLUMNS);
+
+ # There's no reason to support reporting on unique fields.
+ # Also, some other fields don't make very good reporting axises,
+ # or simply don't work with the current reporting system.
+ my @no_report_columns = qw(bug_id alias short_short_desc opendate changeddate
+ flagtypes.name relevance);
+
+ # If you're not a time-tracker, you can't use time-tracking
+ # columns.
+ if (!$user->is_timetracker) {
+ push(@no_report_columns, TIMETRACKING_FIELDS);
+ }
+
+ foreach my $name (@no_report_columns) {
+ delete $columns->{$name};
+ }
+ return $columns;
}
# These are fields that never go into the GROUP BY on any DB. bug_id
# 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(
- alias
- blocked
- bug_id
- dependson
- flagtypes.name
- keywords
- longdescs.count
- percentage_complete
- tag
+ alias
+ blocked
+ bug_id
+ dependson
+ flagtypes.name
+ keywords
+ longdescs.count
+ percentage_complete
+ tag
);
###############
@@ -722,27 +657,27 @@ use constant GROUP_BY_SKIP => qw(
# Note that the params argument may be modified by Bugzilla::Search
sub new {
- my $invocant = shift;
- my $class = ref($invocant) || $invocant;
-
- my $self = { @_ };
- bless($self, $class);
- $self->{'user'} ||= Bugzilla->user;
-
- # There are certain behaviors of the CGI "Vars" hash that we don't want.
- # In particular, if you put a single-value arrayref into it, later you
- # get back out a string, which breaks anyexact charts (because they
- # need arrays even for individual items, or we will re-trigger bug 67036).
- #
- # We can't just untie the hash--that would give us a hash with no values.
- # We have to manually copy the hash into a new one, and we have to always
- # do it, because there's no way to know if we were passed a tied hash
- # or not.
- my $params_in = $self->_params;
- my %params = map { $_ => $params_in->{$_} } keys %$params_in;
- $self->{params} = \%params;
-
- return $self;
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+
+ my $self = {@_};
+ bless($self, $class);
+ $self->{'user'} ||= Bugzilla->user;
+
+ # There are certain behaviors of the CGI "Vars" hash that we don't want.
+ # In particular, if you put a single-value arrayref into it, later you
+ # get back out a string, which breaks anyexact charts (because they
+ # need arrays even for individual items, or we will re-trigger bug 67036).
+ #
+ # We can't just untie the hash--that would give us a hash with no values.
+ # We have to manually copy the hash into a new one, and we have to always
+ # do it, because there's no way to know if we were passed a tied hash
+ # or not.
+ my $params_in = $self->_params;
+ my %params = map { $_ => $params_in->{$_} } keys %$params_in;
+ $self->{params} = \%params;
+
+ return $self;
}
@@ -751,148 +686,156 @@ sub new {
####################
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} // $field) =~ /^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];
+ 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} // $field) =~ /^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;
-
- my ($joins, $clause) = $self->_charts_to_conditions();
-
- if (!$clause->as_string
- && !Bugzilla->params->{'search_allow_no_criteria'}
- && !$self->{allow_unlimited})
- {
- ThrowUserError('buglist_parameters_required');
- }
-
- my $select = join(', ', $self->_sql_select);
- my $from = $self->_sql_from($joins);
- my $where = $self->_sql_where($clause);
- my $group_by = $dbh->sql_group_by($self->_sql_group_by);
- my $order_by = $self->_sql_order_by
- ? "\nORDER BY " . join(', ', $self->_sql_order_by) : '';
- my $limit = $self->_sql_limit;
- $limit = "\n$limit" if $limit;
-
- my $query = <<END;
+ my ($self) = @_;
+ return $self->{sql} if $self->{sql};
+ my $dbh = Bugzilla->dbh;
+
+ my ($joins, $clause) = $self->_charts_to_conditions();
+
+ if ( !$clause->as_string
+ && !Bugzilla->params->{'search_allow_no_criteria'}
+ && !$self->{allow_unlimited})
+ {
+ ThrowUserError('buglist_parameters_required');
+ }
+
+ my $select = join(', ', $self->_sql_select);
+ my $from = $self->_sql_from($joins);
+ my $where = $self->_sql_where($clause);
+ my $group_by = $dbh->sql_group_by($self->_sql_group_by);
+ my $order_by
+ = $self->_sql_order_by
+ ? "\nORDER BY " . join(', ', $self->_sql_order_by)
+ : '';
+ my $limit = $self->_sql_limit;
+ $limit = "\n$limit" if $limit;
+
+ my $query = <<END;
SELECT $select
FROM $from
WHERE $where
$group_by$order_by$limit
END
- $self->{sql} = $query;
- return $self->{sql};
+ $self->{sql} = $query;
+ return $self->{sql};
}
sub search_description {
- my ($self, $params) = @_;
- my $desc = $self->{'search_description'} ||= [];
- if ($params) {
- push(@$desc, $params);
- }
- # Make sure that the description has actually been generated if
- # people are asking for the whole thing.
- else {
- $self->_sql;
- }
- return $self->{'search_description'};
+ my ($self, $params) = @_;
+ my $desc = $self->{'search_description'} ||= [];
+ if ($params) {
+ push(@$desc, $params);
+ }
+
+ # Make sure that the description has actually been generated if
+ # people are asking for the whole thing.
+ else {
+ $self->_sql;
+ }
+ return $self->{'search_description'};
}
sub boolean_charts_to_custom_search {
- my ($self, $cgi_buffer) = @_;
- my $boolean_charts = $self->_boolean_charts;
- my @as_params = $boolean_charts ? $boolean_charts->as_params : ();
-
- # We need to start our new ids after the last custom search "f" id.
- # We can just pick the last id in the array because they are sorted
- # numerically.
- my $last_id = ($self->_field_ids)[-1];
- my $count = defined($last_id) ? $last_id + 1 : 0;
- foreach my $param_set (@as_params) {
- foreach my $name (keys %$param_set) {
- my $value = $param_set->{$name};
- next if !defined $value;
- $cgi_buffer->param($name . $count, $value);
- }
- $count++;
+ my ($self, $cgi_buffer) = @_;
+ my $boolean_charts = $self->_boolean_charts;
+ my @as_params = $boolean_charts ? $boolean_charts->as_params : ();
+
+ # We need to start our new ids after the last custom search "f" id.
+ # We can just pick the last id in the array because they are sorted
+ # numerically.
+ my $last_id = ($self->_field_ids)[-1];
+ my $count = defined($last_id) ? $last_id + 1 : 0;
+ foreach my $param_set (@as_params) {
+ foreach my $name (keys %$param_set) {
+ my $value = $param_set->{$name};
+ next if !defined $value;
+ $cgi_buffer->param($name . $count, $value);
}
+ $count++;
+ }
}
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;
+ 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;
+ my ($self) = @_;
+ return $self->_valid_order;
}
######################
@@ -901,49 +844,48 @@ sub order {
# Fields that are legal for boolean charts of any kind.
sub _chart_fields {
- my ($self) = @_;
+ my ($self) = @_;
- if (!$self->{chart_fields}) {
- my $chart_fields = Bugzilla->fields({ by_name => 1 });
+ if (!$self->{chart_fields}) {
+ my $chart_fields = Bugzilla->fields({by_name => 1});
- if (!$self->_user->is_timetracker) {
- foreach my $tt_field (TIMETRACKING_FIELDS) {
- delete $chart_fields->{$tt_field};
- }
- }
- $self->{chart_fields} = $chart_fields;
+ if (!$self->_user->is_timetracker) {
+ foreach my $tt_field (TIMETRACKING_FIELDS) {
+ delete $chart_fields->{$tt_field};
+ }
}
- return $self->{chart_fields};
+ $self->{chart_fields} = $chart_fields;
+ }
+ return $self->{chart_fields};
}
# There are various places in Search.pm that we need to know the list of
# valid multi-select fields--or really, fields that are stored like
# multi-selects, which includes BUG_URLS fields.
sub _multi_select_fields {
- my ($self) = @_;
- $self->{multi_select_fields} ||= Bugzilla->fields({
- by_name => 1,
- type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]});
- return $self->{multi_select_fields};
+ my ($self) = @_;
+ $self->{multi_select_fields} ||= Bugzilla->fields(
+ {by_name => 1, type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]});
+ return $self->{multi_select_fields};
}
# $self->{params} contains values that could be undef, could be a string,
# or could be an arrayref. Sometimes we want that value as an array,
# always.
sub _param_array {
- my ($self, $name) = @_;
- my $value = $self->_params->{$name};
- if (!defined $value) {
- return ();
- }
- if (ref($value) eq 'ARRAY') {
- return @$value;
- }
- return ($value);
-}
-
-sub _params { $_[0]->{params} }
-sub _user { return $_[0]->{user} }
+ my ($self, $name) = @_;
+ my $value = $self->_params->{$name};
+ if (!defined $value) {
+ return ();
+ }
+ if (ref($value) eq 'ARRAY') {
+ return @$value;
+ }
+ return ($value);
+}
+
+sub _params { $_[0]->{params} }
+sub _user { return $_[0]->{user} }
sub _sharer_id { $_[0]->{sharer} }
##############################
@@ -952,81 +894,84 @@ sub _sharer_id { $_[0]->{sharer} }
# These are the fields the user has chosen to display on the buglist,
# exactly as they were passed to new().
-sub _input_columns { @{ $_[0]->{'fields'} || [] } }
+sub _input_columns { @{$_[0]->{'fields'} || []} }
# These are columns that are also going to be in the SELECT for one reason
# or another, but weren't actually requested by the caller.
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->_valid_order_columns);
- return @{ $self->{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->_valid_order_columns);
+ return @{$self->{extra_columns}};
}
# For search functions to modify extra_columns. It doesn't matter if
# people push the same column onto this array multiple times, because
# _select_columns will call "uniq" on its final result.
sub _add_extra_column {
- my ($self, $column) = @_;
- push(@{ $self->{extra_columns} }, $column);
+ my ($self, $column) = @_;
+ push(@{$self->{extra_columns}}, $column);
}
# These are the columns that we're going to be actually SELECTing.
sub _display_columns {
- my ($self) = @_;
- return @{ $self->{display_columns} } if $self->{display_columns};
-
- # Do not alter the list from _input_columns at all, even if there are
- # duplicated columns. Those are passed by the caller, and the caller
- # expects to get them back in the exact same order.
- my @columns = $self->_input_columns;
-
- # Only add columns which are not already listed.
- my %list = map { $_ => 1 } @columns;
- foreach my $column ($self->_extra_columns) {
- push(@columns, $column) unless $list{$column}++;
- }
- $self->{display_columns} = \@columns;
- return @{ $self->{display_columns} };
+ my ($self) = @_;
+ return @{$self->{display_columns}} if $self->{display_columns};
+
+ # Do not alter the list from _input_columns at all, even if there are
+ # duplicated columns. Those are passed by the caller, and the caller
+ # expects to get them back in the exact same order.
+ my @columns = $self->_input_columns;
+
+ # Only add columns which are not already listed.
+ my %list = map { $_ => 1 } @columns;
+ foreach my $column ($self->_extra_columns) {
+ push(@columns, $column) unless $list{$column}++;
+ }
+ $self->{display_columns} = \@columns;
+ return @{$self->{display_columns}};
}
# These are the columns that are involved in the query.
sub _select_columns {
- my ($self) = @_;
- return @{ $self->{select_columns} } if $self->{select_columns};
-
- my @select_columns;
- foreach my $column ($self->_display_columns) {
- if (my $add_first = COLUMN_DEPENDS->{$column}) {
- push(@select_columns, @$add_first);
- }
- push(@select_columns, $column);
+ my ($self) = @_;
+ return @{$self->{select_columns}} if $self->{select_columns};
+
+ my @select_columns;
+ foreach my $column ($self->_display_columns) {
+ if (my $add_first = COLUMN_DEPENDS->{$column}) {
+ push(@select_columns, @$add_first);
}
- # Remove duplicated columns.
- $self->{select_columns} = [uniq @select_columns];
- return @{ $self->{select_columns} };
+ push(@select_columns, $column);
+ }
+
+ # Remove duplicated columns.
+ $self->{select_columns} = [uniq @select_columns];
+ return @{$self->{select_columns}};
}
# This takes _display_columns and translates it into the actual SQL that
# will go into the SELECT clause.
sub _sql_select {
- my ($self) = @_;
- my @sql_fields;
- foreach my $column ($self->_display_columns) {
- my $sql = $self->COLUMNS->{$column}->{name} // '';
- if ($sql) {
- my $alias = $column;
- # Aliases cannot contain dots in them. We convert them to underscores.
- $alias =~ tr/./_/;
- $sql .= " AS $alias";
- }
- else {
- $sql = $column;
- }
- push(@sql_fields, $sql);
+ my ($self) = @_;
+ my @sql_fields;
+ foreach my $column ($self->_display_columns) {
+ my $sql = $self->COLUMNS->{$column}->{name} // '';
+ if ($sql) {
+ my $alias = $column;
+
+ # Aliases cannot contain dots in them. We convert them to underscores.
+ $alias =~ tr/./_/;
+ $sql .= " AS $alias";
+ }
+ else {
+ $sql = $column;
}
- return @sql_fields;
+ push(@sql_fields, $sql);
+ }
+ return @sql_fields;
}
################################
@@ -1035,85 +980,83 @@ sub _sql_select {
# The "order" that was requested by the consumer, exactly as it was
# requested.
-sub _input_order { @{ $_[0]->{'order'} || [] } }
+sub _input_order { @{$_[0]->{'order'} || []} }
+
# Requested order with invalid values removed and old names translated
sub _valid_order {
- my ($self) = @_;
- return map { ($self->_validate_order_column($_)) } $self->_input_order;
+ my ($self) = @_;
+ 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;
+ my ($self) = @_;
+ return map { (split_order_term($_))[0] } $self->_valid_order;
}
sub _validate_order_column {
- my ($self, $order_item) = @_;
+ my ($self, $order_item) = @_;
- # Translate old column names
- my ($field, $direction) = split_order_term($order_item);
- $field = $self->_translate_old_column($field);
+ # 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});
+ # 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});
+ # 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";
+ $direction = " $direction" if $direction;
+ return "$field$direction";
}
# A hashref that describes all the special stuff that has to be done
# for various fields if they go into the ORDER BY clause.
sub _special_order {
- my ($self) = @_;
- return $self->{special_order} if $self->{special_order};
-
- my %special_order = %{ SPECIAL_ORDER() };
- my $select_fields = Bugzilla->fields({ type => FIELD_TYPE_SINGLE_SELECT });
- foreach my $field (@$select_fields) {
- next if $field->is_abnormal;
- my $name = $field->name;
- $special_order{$name} = {
- order => ["map_$name.sortkey", "map_$name.value"],
- join => {
- table => $name,
- from => "bugs.$name",
- to => "value",
- join => 'INNER',
- }
- };
- }
- $self->{special_order} = \%special_order;
- return $self->{special_order};
+ my ($self) = @_;
+ return $self->{special_order} if $self->{special_order};
+
+ my %special_order = %{SPECIAL_ORDER()};
+ my $select_fields = Bugzilla->fields({type => FIELD_TYPE_SINGLE_SELECT});
+ foreach my $field (@$select_fields) {
+ next if $field->is_abnormal;
+ my $name = $field->name;
+ $special_order{$name} = {
+ order => ["map_$name.sortkey", "map_$name.value"],
+ join => {table => $name, from => "bugs.$name", to => "value", join => 'INNER',}
+ };
+ }
+ $self->{special_order} = \%special_order;
+ return $self->{special_order};
}
sub _sql_order_by {
- my ($self) = @_;
- if (!$self->{sql_order_by}) {
- my @order_by = map { $self->_translate_order_by_column($_) }
- $self->_valid_order;
- $self->{sql_order_by} = \@order_by;
- }
- return @{ $self->{sql_order_by} };
+ my ($self) = @_;
+ if (!$self->{sql_order_by}) {
+ my @order_by
+ = map { $self->_translate_order_by_column($_) } $self->_valid_order;
+ $self->{sql_order_by} = \@order_by;
+ }
+ return @{$self->{sql_order_by}};
}
sub _translate_order_by_column {
- my ($self, $order_by_item) = @_;
-
- my ($field, $direction) = split_order_term($order_by_item);
-
- $direction = '' if lc($direction) eq 'asc';
- my $special_order = $self->_special_order->{$field}->{order};
- # Standard fields have underscores in their SELECT alias instead
- # of a period (because aliases can't have periods).
- $field =~ s/\./_/g;
- my @items = $special_order ? @$special_order : $field;
- if (lc($direction) eq 'desc') {
- @items = map { "$_ DESC" } @items;
- }
- return @items;
+ my ($self, $order_by_item) = @_;
+
+ my ($field, $direction) = split_order_term($order_by_item);
+
+ $direction = '' if lc($direction) eq 'asc';
+ my $special_order = $self->_special_order->{$field}->{order};
+
+ # Standard fields have underscores in their SELECT alias instead
+ # of a period (because aliases can't have periods).
+ $field =~ s/\./_/g;
+ my @items = $special_order ? @$special_order : $field;
+ if (lc($direction) eq 'desc') {
+ @items = map {"$_ DESC"} @items;
+ }
+ return @items;
}
#############################
@@ -1121,32 +1064,30 @@ sub _translate_order_by_column {
#############################
sub _sql_limit {
- my ($self) = @_;
- my $limit = $self->_params->{limit};
- my $offset = $self->_params->{offset};
-
- my $max_results = Bugzilla->params->{'max_search_results'};
- if (!$self->{allow_unlimited} && (!$limit || $limit > $max_results)) {
- $limit = $max_results;
- }
-
- if (defined($offset) && !$limit) {
- $limit = INT_MAX;
- }
- if (defined $limit) {
- detaint_natural($limit)
- || ThrowCodeError('param_must_be_numeric',
- { function => 'Bugzilla::Search::new',
- param => 'limit' });
- if (defined $offset) {
- detaint_natural($offset)
- || ThrowCodeError('param_must_be_numeric',
- { function => 'Bugzilla::Search::new',
- param => 'offset' });
- }
- return Bugzilla->dbh->sql_limit($limit, $offset);
- }
- return '';
+ my ($self) = @_;
+ my $limit = $self->_params->{limit};
+ my $offset = $self->_params->{offset};
+
+ my $max_results = Bugzilla->params->{'max_search_results'};
+ if (!$self->{allow_unlimited} && (!$limit || $limit > $max_results)) {
+ $limit = $max_results;
+ }
+
+ if (defined($offset) && !$limit) {
+ $limit = INT_MAX;
+ }
+ if (defined $limit) {
+ detaint_natural($limit)
+ || ThrowCodeError('param_must_be_numeric',
+ {function => 'Bugzilla::Search::new', param => 'limit'});
+ if (defined $offset) {
+ detaint_natural($offset)
+ || ThrowCodeError('param_must_be_numeric',
+ {function => 'Bugzilla::Search::new', param => 'offset'});
+ }
+ return Bugzilla->dbh->sql_limit($limit, $offset);
+ }
+ return '';
}
############################
@@ -1154,176 +1095,176 @@ sub _sql_limit {
############################
sub _column_join {
- my ($self, $field) = @_;
- # The _realname fields require the same join as the username fields.
- $field =~ s/_realname$//;
- my $column_joins = $self->_get_column_joins();
- my $join_info = $column_joins->{$field};
- if ($join_info) {
- # Don't allow callers to modify the constant.
- $join_info = dclone($join_info);
- }
- else {
- if ($self->_multi_select_fields->{$field}) {
- $join_info = { table => "bug_$field" };
- }
- }
- if ($join_info and !$join_info->{as}) {
- $join_info = dclone($join_info);
- $join_info->{as} = "map_$field";
+ my ($self, $field) = @_;
+
+ # The _realname fields require the same join as the username fields.
+ $field =~ s/_realname$//;
+ my $column_joins = $self->_get_column_joins();
+ my $join_info = $column_joins->{$field};
+ if ($join_info) {
+
+ # Don't allow callers to modify the constant.
+ $join_info = dclone($join_info);
+ }
+ else {
+ if ($self->_multi_select_fields->{$field}) {
+ $join_info = {table => "bug_$field"};
}
- return $join_info ? $join_info : ();
+ }
+ if ($join_info and !$join_info->{as}) {
+ $join_info = dclone($join_info);
+ $join_info->{as} = "map_$field";
+ }
+ return $join_info ? $join_info : ();
}
# Sometimes we join the same table more than once. In this case, we
# want to AND all the various critiera that were used in both joins.
sub _combine_joins {
- my ($self, $joins) = @_;
- my @result;
- while(my $join = shift @$joins) {
- my $name = $join->{as};
- my ($others_like_me, $the_rest) = part { $_->{as} eq $name ? 0 : 1 }
- @$joins;
- if ($others_like_me) {
- my $from = $join->{from};
- my $to = $join->{to};
- # Sanity check to make sure that we have the same from and to
- # for all the same-named joins.
- if ($from) {
- all { $_->{from} eq $from } @$others_like_me
- or die "Not all same-named joins have identical 'from': "
- . Dumper($join, $others_like_me);
- }
- if ($to) {
- all { $_->{to} eq $to } @$others_like_me
- or die "Not all same-named joins have identical 'to': "
- . Dumper($join, $others_like_me);
- }
-
- # We don't need to call uniq here--translate_join will do that
- # for us.
- my @conditions = map { @{ $_->{extra} || [] } }
- ($join, @$others_like_me);
- $join->{extra} = \@conditions;
- $joins = $the_rest;
- }
- push(@result, $join);
- }
-
- return @result;
+ my ($self, $joins) = @_;
+ my @result;
+ while (my $join = shift @$joins) {
+ my $name = $join->{as};
+ my ($others_like_me, $the_rest) = part { $_->{as} eq $name ? 0 : 1 }
+ @$joins;
+ if ($others_like_me) {
+ my $from = $join->{from};
+ my $to = $join->{to};
+
+ # Sanity check to make sure that we have the same from and to
+ # for all the same-named joins.
+ if ($from) {
+ all { $_->{from} eq $from } @$others_like_me
+ or die "Not all same-named joins have identical 'from': "
+ . Dumper($join, $others_like_me);
+ }
+ if ($to) {
+ all { $_->{to} eq $to } @$others_like_me
+ or die "Not all same-named joins have identical 'to': "
+ . Dumper($join, $others_like_me);
+ }
+
+ # We don't need to call uniq here--translate_join will do that
+ # for us.
+ my @conditions = map { @{$_->{extra} || []} } ($join, @$others_like_me);
+ $join->{extra} = \@conditions;
+ $joins = $the_rest;
+ }
+ push(@result, $join);
+ }
+
+ return @result;
}
# Takes all the "then_to" items and just puts them as the next item in
# the array. Right now this only does one level of "then_to", but we
# could re-write this to handle then_to recursively if we need more levels.
sub _extract_then_to {
- my ($self, $joins) = @_;
- my @result;
- foreach my $join (@$joins) {
- push(@result, $join);
- if (my $then_to = $join->{then_to}) {
- push(@result, $then_to);
- }
+ my ($self, $joins) = @_;
+ my @result;
+ foreach my $join (@$joins) {
+ push(@result, $join);
+ if (my $then_to = $join->{then_to}) {
+ push(@result, $then_to);
}
- return @result;
+ }
+ return @result;
}
# JOIN statements for the SELECT and ORDER BY columns. This should not be
# called until the moment it is needed, because _select_columns might be
# modified by the charts.
sub _select_order_joins {
- my ($self) = @_;
- my @joins;
- foreach my $field ($self->_select_columns) {
- my @column_join = $self->_column_join($field);
- push(@joins, @column_join);
- }
- 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.
- $join_info = dclone($join_info);
- if (!$join_info->{as}) {
- $join_info->{as} = "map_$field";
- }
- push(@joins, $join_info);
- }
+ my ($self) = @_;
+ my @joins;
+ foreach my $field ($self->_select_columns) {
+ my @column_join = $self->_column_join($field);
+ push(@joins, @column_join);
+ }
+ 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.
+ $join_info = dclone($join_info);
+ if (!$join_info->{as}) {
+ $join_info->{as} = "map_$field";
+ }
+ push(@joins, $join_info);
}
- return @joins;
+ }
+ return @joins;
}
# These are the joins that are *always* in the FROM clause.
sub _standard_joins {
- my ($self) = @_;
- my $user = $self->_user;
- my @joins;
- return () if $self->{_no_security_check};
-
- my $security_join = {
- table => 'bug_group_map',
- as => 'security_map',
- };
- push(@joins, $security_join);
+ my ($self) = @_;
+ my $user = $self->_user;
+ my @joins;
+ return () if $self->{_no_security_check};
- if ($user->id) {
- # See also _standard_joins for the other half of the below statement
- if (!Bugzilla->params->{'or_groups'}) {
- $security_join->{extra} =
- ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"];
- }
-
- my $security_cc_join = {
- table => 'cc',
- as => 'security_cc',
- extra => ['security_cc.who = ' . $user->id],
- };
- push(@joins, $security_cc_join);
+ my $security_join = {table => 'bug_group_map', as => 'security_map',};
+ push(@joins, $security_join);
+
+ if ($user->id) {
+
+ # See also _standard_joins for the other half of the below statement
+ if (!Bugzilla->params->{'or_groups'}) {
+ $security_join->{extra}
+ = ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"];
}
-
- return @joins;
+
+ my $security_cc_join = {
+ table => 'cc',
+ as => 'security_cc',
+ extra => ['security_cc.who = ' . $user->id],
+ };
+ push(@joins, $security_cc_join);
+ }
+
+ return @joins;
}
sub _sql_from {
- my ($self, $joins_input) = @_;
- my @joins = ($self->_standard_joins, $self->_select_order_joins,
- @$joins_input);
- @joins = $self->_extract_then_to(\@joins);
- @joins = $self->_combine_joins(\@joins);
- my @join_sql = map { $self->_translate_join($_) } @joins;
- return "bugs\n" . join("\n", @join_sql);
+ my ($self, $joins_input) = @_;
+ my @joins = ($self->_standard_joins, $self->_select_order_joins, @$joins_input);
+ @joins = $self->_extract_then_to(\@joins);
+ @joins = $self->_combine_joins(\@joins);
+ my @join_sql = map { $self->_translate_join($_) } @joins;
+ return "bugs\n" . join("\n", @join_sql);
}
# This takes a join data structure and turns it into actual JOIN SQL.
sub _translate_join {
- my ($self, $join_info) = @_;
-
- 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 = $join_info->{bugs_table} || "bugs";
- my $from = $join_info->{from} || "bug_id";
- if ($from =~ /^(\w+)\.(\w+)$/) {
- ($from_table, $from) = ($1, $2);
- }
- my $table = $join_info->{table};
- my $name = $join_info->{as};
- my $to = $join_info->{to} || "bug_id";
- my $join = $join_info->{join} || 'LEFT';
- my @extra = @{ $join_info->{extra} || [] };
- $name =~ s/\./_/g;
-
- # If a term contains ORs, we need to put parens around the condition.
- # This is a pretty weak test, but it's actually OK to put parens
- # around too many things.
- @extra = map { $_ =~ /\bOR\b/i ? "($_)" : $_ } @extra;
- my $extra_condition = join(' AND ', uniq @extra);
- if ($extra_condition) {
- $extra_condition = " AND $extra_condition";
- }
-
- my @join_sql = "$join JOIN $table AS $name"
- . " ON $from_table.$from = $name.$to$extra_condition";
- return @join_sql;
+ my ($self, $join_info) = @_;
+
+ 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 = $join_info->{bugs_table} || "bugs";
+ my $from = $join_info->{from} || "bug_id";
+ if ($from =~ /^(\w+)\.(\w+)$/) {
+ ($from_table, $from) = ($1, $2);
+ }
+ my $table = $join_info->{table};
+ my $name = $join_info->{as};
+ my $to = $join_info->{to} || "bug_id";
+ my $join = $join_info->{join} || 'LEFT';
+ my @extra = @{$join_info->{extra} || []};
+ $name =~ s/\./_/g;
+
+ # If a term contains ORs, we need to put parens around the condition.
+ # This is a pretty weak test, but it's actually OK to put parens
+ # around too many things.
+ @extra = map { $_ =~ /\bOR\b/i ? "($_)" : $_ } @extra;
+ my $extra_condition = join(' AND ', uniq @extra);
+ if ($extra_condition) {
+ $extra_condition = " AND $extra_condition";
+ }
+
+ my @join_sql = "$join JOIN $table AS $name"
+ . " ON $from_table.$from = $name.$to$extra_condition";
+ return @join_sql;
}
#############################
@@ -1336,54 +1277,60 @@ sub _translate_join {
# The terms that are always in the WHERE clause. These implement bug
# 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
- # bugs we set their creation_ts to NULL, and don't give them a creation_ts
- # until their group controls are set. So if a bug has a NULL creation_ts,
- # it shouldn't show up in searches at all.
- my @where = ('bugs.creation_ts IS NOT NULL');
-
- my $user = $self->_user;
- my $security_term = '';
- # See also _standard_joins for the other half of the below statement
- if (Bugzilla->params->{'or_groups'}) {
- $security_term .= " (security_map.group_id IS NULL OR security_map.group_id IN (" . $user->groups_as_string . "))";
- }
- else {
- $security_term = 'security_map.group_id IS NULL';
- }
-
- if ($user->id) {
- my $userid = $user->id;
- # This indentation makes the resulting SQL more readable.
- $security_term .= <<END;
+ 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
+ # bugs we set their creation_ts to NULL, and don't give them a creation_ts
+ # until their group controls are set. So if a bug has a NULL creation_ts,
+ # it shouldn't show up in searches at all.
+ my @where = ('bugs.creation_ts IS NOT NULL');
+
+ my $user = $self->_user;
+ my $security_term = '';
+
+ # See also _standard_joins for the other half of the below statement
+ if (Bugzilla->params->{'or_groups'}) {
+ $security_term
+ .= " (security_map.group_id IS NULL OR security_map.group_id IN ("
+ . $user->groups_as_string . "))";
+ }
+ else {
+ $security_term = 'security_map.group_id IS NULL';
+ }
+
+ if ($user->id) {
+ my $userid = $user->id;
+
+ # This indentation makes the resulting SQL more readable.
+ $security_term .= <<END;
OR (bugs.reporter_accessible = 1 AND bugs.reporter = $userid)
OR (bugs.cclist_accessible = 1 AND security_cc.who IS NOT NULL)
OR bugs.assigned_to = $userid
END
- if (Bugzilla->params->{'useqacontact'}) {
- $security_term.= " OR bugs.qa_contact = $userid";
- }
- $security_term = "($security_term)";
+ if (Bugzilla->params->{'useqacontact'}) {
+ $security_term .= " OR bugs.qa_contact = $userid";
}
+ $security_term = "($security_term)";
+ }
- push(@where, $security_term);
+ push(@where, $security_term);
- return @where;
+ return @where;
}
sub _sql_where {
- my ($self, $main_clause) = @_;
- # The newline and this particular spacing makes the resulting
- # SQL a bit more readable for debugging.
- my $where = join("\n AND ", $self->_standard_where);
- my $clause_sql = $main_clause->as_string;
- $where .= "\n AND " . $clause_sql if $clause_sql;
- return $where;
+ my ($self, $main_clause) = @_;
+
+ # The newline and this particular spacing makes the resulting
+ # SQL a bit more readable for debugging.
+ my $where = join("\n AND ", $self->_standard_where);
+ my $clause_sql = $main_clause->as_string;
+ $where .= "\n AND " . $clause_sql if $clause_sql;
+ return $where;
}
################################
@@ -1393,40 +1340,40 @@ sub _sql_where {
# And these are the fields that we have to do GROUP BY for in DBs
# that are more strict about putting everything into GROUP BY.
sub _sql_group_by {
- my ($self) = @_;
-
- # Strict DBs require every element from the SELECT to be in the GROUP BY,
- # unless that element is being used in an aggregate function.
- my @extra_group_by;
- foreach my $column ($self->_select_columns) {
- next if $self->_skip_group_by->{$column};
- my $sql = $self->COLUMNS->{$column}->{name} // $column;
- push(@extra_group_by, $sql);
- }
+ my ($self) = @_;
- # 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->_valid_order_columns) {
- my $special_order = $self->_special_order->{$column}->{order};
- next if !$special_order;
- push(@extra_group_by, @$special_order);
- }
-
- @extra_group_by = uniq @extra_group_by;
-
- # bug_id is the only field we actually group by.
- return ('bugs.bug_id', join(',', @extra_group_by));
+ # Strict DBs require every element from the SELECT to be in the GROUP BY,
+ # unless that element is being used in an aggregate function.
+ my @extra_group_by;
+ foreach my $column ($self->_select_columns) {
+ next if $self->_skip_group_by->{$column};
+ my $sql = $self->COLUMNS->{$column}->{name} // $column;
+ push(@extra_group_by, $sql);
+ }
+
+ # 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->_valid_order_columns) {
+ my $special_order = $self->_special_order->{$column}->{order};
+ next if !$special_order;
+ push(@extra_group_by, @$special_order);
+ }
+
+ @extra_group_by = uniq @extra_group_by;
+
+ # bug_id is the only field we actually group by.
+ return ('bugs.bug_id', join(',', @extra_group_by));
}
# A helper for _sql_group_by.
sub _skip_group_by {
- my ($self) = @_;
- return $self->{skip_group_by} if $self->{skip_group_by};
- my @skip_list = GROUP_BY_SKIP;
- push(@skip_list, keys %{ $self->_multi_select_fields });
- my %skip_hash = map { $_ => 1 } @skip_list;
- $self->{skip_group_by} = \%skip_hash;
- return $self->{skip_group_by};
+ my ($self) = @_;
+ return $self->{skip_group_by} if $self->{skip_group_by};
+ my @skip_list = GROUP_BY_SKIP;
+ push(@skip_list, keys %{$self->_multi_select_fields});
+ my %skip_hash = map { $_ => 1 } @skip_list;
+ $self->{skip_group_by} = \%skip_hash;
+ return $self->{skip_group_by};
}
##############################################
@@ -1435,244 +1382,255 @@ sub _skip_group_by {
# Backwards compatibility for old field names.
sub _convert_old_params {
- my ($self) = @_;
- my $params = $self->_params;
-
- # bugidtype has different values in modern Search.pm.
- if (defined $params->{'bugidtype'}) {
- my $value = $params->{'bugidtype'};
- $params->{'bugidtype'} = $value eq 'exclude' ? 'nowords' : 'anyexact';
- }
-
- foreach my $old_name (keys %{ FIELD_MAP() }) {
- if (defined $params->{$old_name}) {
- my $new_name = FIELD_MAP->{$old_name};
- $params->{$new_name} = delete $params->{$old_name};
- }
+ my ($self) = @_;
+ my $params = $self->_params;
+
+ # bugidtype has different values in modern Search.pm.
+ if (defined $params->{'bugidtype'}) {
+ my $value = $params->{'bugidtype'};
+ $params->{'bugidtype'} = $value eq 'exclude' ? 'nowords' : 'anyexact';
+ }
+
+ foreach my $old_name (keys %{FIELD_MAP()}) {
+ if (defined $params->{$old_name}) {
+ my $new_name = FIELD_MAP->{$old_name};
+ $params->{$new_name} = delete $params->{$old_name};
}
+ }
}
# This parses all the standard search parameters except for the boolean
# charts.
sub _special_charts {
- my ($self) = @_;
- $self->_convert_old_params();
- $self->_special_parse_bug_status();
- $self->_special_parse_resolution();
- my $clause = new Bugzilla::Search::Clause();
- $clause->add( $self->_parse_basic_fields() );
- $clause->add( $self->_special_parse_email() );
- $clause->add( $self->_special_parse_chfield() );
- $clause->add( $self->_special_parse_deadline() );
- return $clause;
+ my ($self) = @_;
+ $self->_convert_old_params();
+ $self->_special_parse_bug_status();
+ $self->_special_parse_resolution();
+ my $clause = new Bugzilla::Search::Clause();
+ $clause->add($self->_parse_basic_fields());
+ $clause->add($self->_special_parse_email());
+ $clause->add($self->_special_parse_chfield());
+ $clause->add($self->_special_parse_deadline());
+ return $clause;
}
sub _parse_basic_fields {
- my ($self) = @_;
- my $params = $self->_params;
- my $chart_fields = $self->_chart_fields;
-
- my $clause = new Bugzilla::Search::Clause();
- foreach my $field_name (keys %$chart_fields) {
- # CGI params shouldn't have periods in them, so we only accept
- # period-separated fields with underscores where the periods go.
- my $param_name = $field_name;
- $param_name =~ s/\./_/g;
- my @values = $self->_param_array($param_name);
- next if !@values;
- my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact';
- my $operator = $params->{"${param_name}_type"} || $default_op;
- # Fields that are displayed as multi-selects are passed as arrays,
- # so that they can properly search values that contain commas.
- # However, other fields are sent as strings, so that they are properly
- # split on commas if required.
- my $field = $chart_fields->{$field_name};
- my $pass_value;
- if ($field->is_select or $field->name eq 'version'
- or $field->name eq 'target_milestone')
- {
- $pass_value = \@values;
- }
- else {
- $pass_value = join(',', @values);
- }
- $clause->add($field_name, $operator, $pass_value);
+ my ($self) = @_;
+ my $params = $self->_params;
+ my $chart_fields = $self->_chart_fields;
+
+ my $clause = new Bugzilla::Search::Clause();
+ foreach my $field_name (keys %$chart_fields) {
+
+ # CGI params shouldn't have periods in them, so we only accept
+ # period-separated fields with underscores where the periods go.
+ my $param_name = $field_name;
+ $param_name =~ s/\./_/g;
+ my @values = $self->_param_array($param_name);
+ next if !@values;
+ my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact';
+ my $operator = $params->{"${param_name}_type"} || $default_op;
+
+ # Fields that are displayed as multi-selects are passed as arrays,
+ # so that they can properly search values that contain commas.
+ # However, other fields are sent as strings, so that they are properly
+ # split on commas if required.
+ my $field = $chart_fields->{$field_name};
+ my $pass_value;
+ if ( $field->is_select
+ or $field->name eq 'version'
+ or $field->name eq 'target_milestone')
+ {
+ $pass_value = \@values;
+ }
+ else {
+ $pass_value = join(',', @values);
}
- return @{$clause->children} ? $clause : undef;
+ $clause->add($field_name, $operator, $pass_value);
+ }
+ return @{$clause->children} ? $clause : undef;
}
sub _special_parse_bug_status {
- my ($self) = @_;
- my $params = $self->_params;
- return if !defined $params->{'bug_status'};
- # We want to allow the bug_status_type parameter to work normally,
- # meaning that this special code should only be activated if we are
- # doing the normal "anyexact" search on bug_status.
- return if (defined $params->{'bug_status_type'}
- and $params->{'bug_status_type'} ne 'anyexact');
-
- my @bug_status = $self->_param_array('bug_status');
- # Also include inactive bug statuses, as you can query them.
- my $legal_statuses = $self->_chart_fields->{'bug_status'}->legal_values;
-
- # If the status contains __open__ or __closed__, translate those
- # into their equivalent lists of open and closed statuses.
- if (grep { $_ eq '__open__' } @bug_status) {
- my @open = grep { $_->is_open } @$legal_statuses;
- @open = map { $_->name } @open;
- push(@bug_status, @open);
- }
- if (grep { $_ eq '__closed__' } @bug_status) {
- my @closed = grep { not $_->is_open } @$legal_statuses;
- @closed = map { $_->name } @closed;
- push(@bug_status, @closed);
- }
-
- @bug_status = uniq @bug_status;
- my $all = grep { $_ eq "__all__" } @bug_status;
- # This will also handle removing __open__ and __closed__ for us
- # (__all__ too, which is why we check for it above, first).
- @bug_status = _valid_values(\@bug_status, $legal_statuses);
-
- # If the user has selected every status, change to selecting none.
- # This is functionally equivalent, but quite a lot faster.
- if ($all or scalar(@bug_status) == scalar(@$legal_statuses)) {
- delete $params->{'bug_status'};
- }
- else {
- $params->{'bug_status'} = \@bug_status;
- }
+ my ($self) = @_;
+ my $params = $self->_params;
+ return if !defined $params->{'bug_status'};
+
+ # We want to allow the bug_status_type parameter to work normally,
+ # meaning that this special code should only be activated if we are
+ # doing the normal "anyexact" search on bug_status.
+ return
+ if (defined $params->{'bug_status_type'}
+ and $params->{'bug_status_type'} ne 'anyexact');
+
+ my @bug_status = $self->_param_array('bug_status');
+
+ # Also include inactive bug statuses, as you can query them.
+ my $legal_statuses = $self->_chart_fields->{'bug_status'}->legal_values;
+
+ # If the status contains __open__ or __closed__, translate those
+ # into their equivalent lists of open and closed statuses.
+ if (grep { $_ eq '__open__' } @bug_status) {
+ my @open = grep { $_->is_open } @$legal_statuses;
+ @open = map { $_->name } @open;
+ push(@bug_status, @open);
+ }
+ if (grep { $_ eq '__closed__' } @bug_status) {
+ my @closed = grep { not $_->is_open } @$legal_statuses;
+ @closed = map { $_->name } @closed;
+ push(@bug_status, @closed);
+ }
+
+ @bug_status = uniq @bug_status;
+ my $all = grep { $_ eq "__all__" } @bug_status;
+
+ # This will also handle removing __open__ and __closed__ for us
+ # (__all__ too, which is why we check for it above, first).
+ @bug_status = _valid_values(\@bug_status, $legal_statuses);
+
+ # If the user has selected every status, change to selecting none.
+ # This is functionally equivalent, but quite a lot faster.
+ if ($all or scalar(@bug_status) == scalar(@$legal_statuses)) {
+ delete $params->{'bug_status'};
+ }
+ else {
+ $params->{'bug_status'} = \@bug_status;
+ }
}
sub _special_parse_chfield {
- my ($self) = @_;
- my $params = $self->_params;
-
- my $date_from = trim(lc($params->{'chfieldfrom'} || ''));
- my $date_to = trim(lc($params->{'chfieldto'} || ''));
- $date_from = '' if $date_from eq 'now';
- $date_to = '' if $date_to eq 'now';
- my @fields = $self->_param_array('chfield');
- my $value_to = $params->{'chfieldvalue'};
- $value_to = '' if !defined $value_to;
-
- @fields = map { $_ eq '[Bug creation]' ? 'creation_ts' : $_ } @fields;
-
- return undef unless ($date_from ne '' || $date_to ne '' || $value_to ne '');
-
- my $clause = new Bugzilla::Search::Clause();
-
- # It is always safe and useful to push delta_ts into the charts
- # if there is a "from" date specified. It doesn't conflict with
- # searching [Bug creation], because a bug's delta_ts is set to
- # its creation_ts when it is created. So this just gives the
- # database an additional index to possibly choose, on a table that
- # is smaller than bugs_activity.
- if ($date_from ne '') {
- $clause->add('delta_ts', 'greaterthaneq', $date_from);
- }
- # It's not normally safe to do it for "to" dates, though--"chfieldto" means
- # "a field that changed before this date", and delta_ts could be either
- # later or earlier than that, if we're searching for the time that a field
- # changed. However, chfieldto all by itself, without any chfieldvalue or
- # chfield, means "just search delta_ts", and so we still want that to
- # work.
- if ($date_to ne '' and !@fields and $value_to eq '') {
- $clause->add('delta_ts', 'lessthaneq', $date_to);
- }
-
- # chfieldto is supposed to be a relative date or a date of the form
- # YYYY-MM-DD, i.e. without the time appended to it. We append the
- # time ourselves so that the end date is correctly taken into account.
- $date_to .= ' 23:59:59' if $date_to =~ /^\d{4}-\d{1,2}-\d{1,2}$/;
-
- my $join_clause = new Bugzilla::Search::Clause('OR');
-
- foreach my $field (@fields) {
- my $sub_clause = new Bugzilla::Search::ClauseGroup();
- $sub_clause->add(condition($field, 'changedto', $value_to)) if $value_to ne '';
- $sub_clause->add(condition($field, 'changedafter', $date_from)) if $date_from ne '';
- $sub_clause->add(condition($field, 'changedbefore', $date_to)) if $date_to ne '';
- $join_clause->add($sub_clause);
- }
- $clause->add($join_clause);
-
- return @{$clause->children} ? $clause : undef;
+ my ($self) = @_;
+ my $params = $self->_params;
+
+ my $date_from = trim(lc($params->{'chfieldfrom'} || ''));
+ my $date_to = trim(lc($params->{'chfieldto'} || ''));
+ $date_from = '' if $date_from eq 'now';
+ $date_to = '' if $date_to eq 'now';
+ my @fields = $self->_param_array('chfield');
+ my $value_to = $params->{'chfieldvalue'};
+ $value_to = '' if !defined $value_to;
+
+ @fields = map { $_ eq '[Bug creation]' ? 'creation_ts' : $_ } @fields;
+
+ return undef unless ($date_from ne '' || $date_to ne '' || $value_to ne '');
+
+ my $clause = new Bugzilla::Search::Clause();
+
+ # It is always safe and useful to push delta_ts into the charts
+ # if there is a "from" date specified. It doesn't conflict with
+ # searching [Bug creation], because a bug's delta_ts is set to
+ # its creation_ts when it is created. So this just gives the
+ # database an additional index to possibly choose, on a table that
+ # is smaller than bugs_activity.
+ if ($date_from ne '') {
+ $clause->add('delta_ts', 'greaterthaneq', $date_from);
+ }
+
+ # It's not normally safe to do it for "to" dates, though--"chfieldto" means
+ # "a field that changed before this date", and delta_ts could be either
+ # later or earlier than that, if we're searching for the time that a field
+ # changed. However, chfieldto all by itself, without any chfieldvalue or
+ # chfield, means "just search delta_ts", and so we still want that to
+ # work.
+ if ($date_to ne '' and !@fields and $value_to eq '') {
+ $clause->add('delta_ts', 'lessthaneq', $date_to);
+ }
+
+ # chfieldto is supposed to be a relative date or a date of the form
+ # YYYY-MM-DD, i.e. without the time appended to it. We append the
+ # time ourselves so that the end date is correctly taken into account.
+ $date_to .= ' 23:59:59' if $date_to =~ /^\d{4}-\d{1,2}-\d{1,2}$/;
+
+ my $join_clause = new Bugzilla::Search::Clause('OR');
+
+ foreach my $field (@fields) {
+ my $sub_clause = new Bugzilla::Search::ClauseGroup();
+ $sub_clause->add(condition($field, 'changedto', $value_to)) if $value_to ne '';
+ $sub_clause->add(condition($field, 'changedafter', $date_from))
+ if $date_from ne '';
+ $sub_clause->add(condition($field, 'changedbefore', $date_to))
+ if $date_to ne '';
+ $join_clause->add($sub_clause);
+ }
+ $clause->add($join_clause);
+
+ return @{$clause->children} ? $clause : undef;
}
sub _special_parse_deadline {
- my ($self) = @_;
- my $params = $self->_params;
+ my ($self) = @_;
+ my $params = $self->_params;
- my $clause = new Bugzilla::Search::Clause();
- if (my $from = $params->{'deadlinefrom'}) {
- $clause->add('deadline', 'greaterthaneq', $from);
- }
- if (my $to = $params->{'deadlineto'}) {
- $clause->add('deadline', 'lessthaneq', $to);
- }
+ my $clause = new Bugzilla::Search::Clause();
+ if (my $from = $params->{'deadlinefrom'}) {
+ $clause->add('deadline', 'greaterthaneq', $from);
+ }
+ if (my $to = $params->{'deadlineto'}) {
+ $clause->add('deadline', 'lessthaneq', $to);
+ }
- return @{$clause->children} ? $clause : undef;
+ return @{$clause->children} ? $clause : undef;
}
sub _special_parse_email {
- my ($self) = @_;
- my $params = $self->_params;
-
- my @email_params = grep { $_ =~ /^email\d+$/ } keys %$params;
-
- my $clause = new Bugzilla::Search::Clause();
- foreach my $param (@email_params) {
- $param =~ /(\d+)$/;
- my $id = $1;
- my $email = trim($params->{"email$id"});
- next if !$email;
- my $type = $params->{"emailtype$id"} || 'anyexact';
- # for backward compatibility
- $type = "equals" if $type eq "exact";
-
- my $or_clause = new Bugzilla::Search::Clause('OR');
- foreach my $field (qw(assigned_to reporter cc qa_contact)) {
- if ($params->{"email$field$id"}) {
- $or_clause->add($field, $type, $email);
- }
- }
- if ($params->{"emaillongdesc$id"}) {
- $or_clause->add("commenter", $type, $email);
- }
-
- $clause->add($or_clause);
+ my ($self) = @_;
+ my $params = $self->_params;
+
+ my @email_params = grep { $_ =~ /^email\d+$/ } keys %$params;
+
+ my $clause = new Bugzilla::Search::Clause();
+ foreach my $param (@email_params) {
+ $param =~ /(\d+)$/;
+ my $id = $1;
+ my $email = trim($params->{"email$id"});
+ next if !$email;
+ my $type = $params->{"emailtype$id"} || 'anyexact';
+
+ # for backward compatibility
+ $type = "equals" if $type eq "exact";
+
+ my $or_clause = new Bugzilla::Search::Clause('OR');
+ foreach my $field (qw(assigned_to reporter cc qa_contact)) {
+ if ($params->{"email$field$id"}) {
+ $or_clause->add($field, $type, $email);
+ }
+ }
+ if ($params->{"emaillongdesc$id"}) {
+ $or_clause->add("commenter", $type, $email);
}
- return @{$clause->children} ? $clause : undef;
+ $clause->add($or_clause);
+ }
+
+ return @{$clause->children} ? $clause : undef;
}
sub _special_parse_resolution {
- my ($self) = @_;
- my $params = $self->_params;
- return if !defined $params->{'resolution'};
-
- my @resolution = $self->_param_array('resolution');
- my $legal_resolutions = $self->_chart_fields->{resolution}->legal_values;
- @resolution = _valid_values(\@resolution, $legal_resolutions, '---');
- if (scalar(@resolution) == scalar(@$legal_resolutions)) {
- delete $params->{'resolution'};
- }
+ my ($self) = @_;
+ my $params = $self->_params;
+ return if !defined $params->{'resolution'};
+
+ my @resolution = $self->_param_array('resolution');
+ my $legal_resolutions = $self->_chart_fields->{resolution}->legal_values;
+ @resolution = _valid_values(\@resolution, $legal_resolutions, '---');
+ if (scalar(@resolution) == scalar(@$legal_resolutions)) {
+ delete $params->{'resolution'};
+ }
}
sub _valid_values {
- my ($input, $valid, $extra_value) = @_;
- my @result;
- foreach my $item (@$input) {
- $item = trim($item);
- if (defined $extra_value and $item eq $extra_value) {
- push(@result, $item);
- }
- elsif (grep { $_->name eq $item } @$valid) {
- push(@result, $item);
- }
+ my ($input, $valid, $extra_value) = @_;
+ my @result;
+ foreach my $item (@$input) {
+ $item = trim($item);
+ if (defined $extra_value and $item eq $extra_value) {
+ push(@result, $item);
}
- return @result;
+ elsif (grep { $_->name eq $item } @$valid) {
+ push(@result, $item);
+ }
+ }
+ return @result;
}
######################################
@@ -1680,239 +1638,247 @@ sub _valid_values {
######################################
sub _charts_to_conditions {
- my ($self) = @_;
-
- my $clause = $self->_charts;
- my @joins;
- $clause->walk_conditions(sub {
- my ($clause, $condition) = @_;
- return if !$condition->translated;
- push(@joins, @{ $condition->translated->{joins} });
- });
- return (\@joins, $clause);
+ my ($self) = @_;
+
+ my $clause = $self->_charts;
+ my @joins;
+ $clause->walk_conditions(sub {
+ my ($clause, $condition) = @_;
+ return if !$condition->translated;
+ push(@joins, @{$condition->translated->{joins}});
+ });
+ return (\@joins, $clause);
}
sub _charts {
- my ($self) = @_;
-
- my $clause = $self->_params_to_data_structure();
- my $chart_id = 0;
- $clause->walk_conditions(sub { $self->_handle_chart($chart_id++, @_) });
- return $clause;
+ my ($self) = @_;
+
+ my $clause = $self->_params_to_data_structure();
+ my $chart_id = 0;
+ $clause->walk_conditions(sub { $self->_handle_chart($chart_id++, @_) });
+ return $clause;
}
sub _params_to_data_structure {
- my ($self) = @_;
-
- # First we get the "special" charts, representing all the normal
- # fields on the search page. This may modify _params, so it needs to
- # happen first.
- my $clause = $self->_special_charts;
-
- # Then we process the old Boolean Charts input format.
- $clause->add( $self->_boolean_charts );
-
- # And then process the modern "custom search" format.
- $clause->add( $self->_custom_search );
-
- return $clause;
+ my ($self) = @_;
+
+ # First we get the "special" charts, representing all the normal
+ # fields on the search page. This may modify _params, so it needs to
+ # happen first.
+ my $clause = $self->_special_charts;
+
+ # Then we process the old Boolean Charts input format.
+ $clause->add($self->_boolean_charts);
+
+ # And then process the modern "custom search" format.
+ $clause->add($self->_custom_search);
+
+ return $clause;
}
sub _boolean_charts {
- my ($self) = @_;
-
- my $params = $self->_params;
- my @param_list = keys %$params;
-
- my @all_field_params = grep { /^field-?\d+/ } @param_list;
- my @chart_ids = map { /^field(-?\d+)/; $1 } @all_field_params;
- @chart_ids = sort { $a <=> $b } uniq @chart_ids;
-
- my $clause = new Bugzilla::Search::Clause();
- foreach my $chart_id (@chart_ids) {
- my @all_and = grep { /^field$chart_id-\d+/ } @param_list;
- my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and;
- @and_ids = sort { $a <=> $b } uniq @and_ids;
-
- my $and_clause = new Bugzilla::Search::Clause();
- foreach my $and_id (@and_ids) {
- my @all_or = grep { /^field$chart_id-$and_id-\d+/ } @param_list;
- my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or;
- @or_ids = sort { $a <=> $b } uniq @or_ids;
-
- my $or_clause = new Bugzilla::Search::Clause('OR');
- foreach my $or_id (@or_ids) {
- my $identifier = "$chart_id-$and_id-$or_id";
- my $field = $params->{"field$identifier"};
- my $operator = $params->{"type$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);
- $and_clause->negate(1) if $params->{"negate$chart_id"};
- }
- $clause->add($and_clause);
+ my ($self) = @_;
+
+ my $params = $self->_params;
+ my @param_list = keys %$params;
+
+ my @all_field_params = grep {/^field-?\d+/} @param_list;
+ my @chart_ids = map { /^field(-?\d+)/; $1 } @all_field_params;
+ @chart_ids = sort { $a <=> $b } uniq @chart_ids;
+
+ my $clause = new Bugzilla::Search::Clause();
+ foreach my $chart_id (@chart_ids) {
+ my @all_and = grep {/^field$chart_id-\d+/} @param_list;
+ my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and;
+ @and_ids = sort { $a <=> $b } uniq @and_ids;
+
+ my $and_clause = new Bugzilla::Search::Clause();
+ foreach my $and_id (@and_ids) {
+ my @all_or = grep {/^field$chart_id-$and_id-\d+/} @param_list;
+ my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or;
+ @or_ids = sort { $a <=> $b } uniq @or_ids;
+
+ my $or_clause = new Bugzilla::Search::Clause('OR');
+ foreach my $or_id (@or_ids) {
+ my $identifier = "$chart_id-$and_id-$or_id";
+ my $field = $params->{"field$identifier"};
+ my $operator = $params->{"type$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);
+ $and_clause->negate(1) if $params->{"negate$chart_id"};
}
+ $clause->add($and_clause);
+ }
- return @{$clause->children} ? $clause : undef;
+ return @{$clause->children} ? $clause : undef;
}
sub _custom_search {
- my ($self) = @_;
- my $params = $self->_params;
-
- my @field_ids = $self->_field_ids;
- return unless scalar @field_ids;
-
- my $joiner = $params->{j_top} || '';
- my $current_clause = $joiner eq 'AND_G'
+ my ($self) = @_;
+ my $params = $self->_params;
+
+ my @field_ids = $self->_field_ids;
+ return unless scalar @field_ids;
+
+ 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 (@field_ids) {
+ my $field = $params->{"f$id"};
+ if ($field eq 'OP') {
+ my $joiner = $params->{"j$id"} || '';
+ my $new_clause
+ = $joiner eq 'AND_G'
? new Bugzilla::Search::ClauseGroup()
: new Bugzilla::Search::Clause($joiner);
-
- my @clause_stack;
- foreach my $id (@field_ids) {
- my $field = $params->{"f$id"};
- if ($field eq 'OP') {
- 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);
- $current_clause = $new_clause;
- next;
- }
- if ($field eq 'CP') {
- $current_clause = pop @clause_stack;
- ThrowCodeError('search_cp_without_op', { id => $id })
- if !$current_clause;
- next;
- }
-
- 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);
+ $new_clause->negate($params->{"n$id"});
+ $current_clause->add($new_clause);
+ push(@clause_stack, $current_clause);
+ $current_clause = $new_clause;
+ next;
}
-
- # We allow people to specify more OPs than CPs, so at the end of the
- # loop our top clause may be still in the stack instead of being
- # $current_clause.
- return $clause_stack[0] || $current_clause;
+ if ($field eq 'CP') {
+ $current_clause = pop @clause_stack;
+ ThrowCodeError('search_cp_without_op', {id => $id}) if !$current_clause;
+ next;
+ }
+
+ 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);
+ }
+
+ # We allow people to specify more OPs than CPs, so at the end of the
+ # loop our top clause may be still in the stack instead of being
+ # $current_clause.
+ return $clause_stack[0] || $current_clause;
}
sub _field_ids {
- my ($self) = @_;
- my $params = $self->_params;
- my @param_list = keys %$params;
-
- my @field_params = grep { /^f\d+$/ } @param_list;
- my @field_ids = map { /(\d+)/; $1 } @field_params;
- @field_ids = sort { $a <=> $b } @field_ids;
- return @field_ids;
+ my ($self) = @_;
+ my $params = $self->_params;
+ my @param_list = keys %$params;
+
+ my @field_params = grep {/^f\d+$/} @param_list;
+ my @field_ids = map { /(\d+)/; $1 } @field_params;
+ @field_ids = sort { $a <=> $b } @field_ids;
+ return @field_ids;
}
sub _handle_chart {
- my ($self, $chart_id, $clause, $condition) = @_;
- my $dbh = Bugzilla->dbh;
- my $params = $self->_params;
- my ($field, $operator, $value) = $condition->fov;
- return if (!defined $field or !defined $operator or !defined $value);
- $field = FIELD_MAP->{$field} || $field;
-
- my ($string_value, $orig_value);
- state $is_mysql = $dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
-
- if (ref $value eq 'ARRAY') {
- # Trim input and ignore blank values.
- @$value = map { trim($_) } @$value;
- @$value = grep { defined $_ and $_ ne '' } @$value;
- return if !@$value;
- $orig_value = join(',', @$value);
- if ($field eq 'longdesc' && $is_mysql) {
- @$value = map { _convert_unicode_characters($_) } @$value;
- }
- $string_value = join(',', @$value);
- }
- else {
- return if $value eq '';
- $orig_value = $value;
- if ($field eq 'longdesc' && $is_mysql) {
- $value = _convert_unicode_characters($value);
- }
- $string_value = $value;
- }
-
- $self->_chart_fields->{$field}
- or ThrowCodeError("invalid_field_name", { field => $field });
- trick_taint($field);
-
- # This is the field as you'd reference it in a SQL statement.
- my $full_field = $field =~ /\./ ? $field : "bugs.$field";
-
- # "value" and "quoted" are for search functions that always operate
- # on a scalar string and never care if they were passed multiple
- # parameters. If the user does pass multiple parameters, they will
- # become a space-separated string for those search functions.
- #
- # all_values is for search functions that do operate
- # 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 => [],
- 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);
-
- # If term is left empty, then this means the criteria
- # has no effect and can be ignored.
- return unless $search_args{term};
-
- # All the things here that don't get pulled out of
- # %search_args are their original values before
- # do_search_function modified them.
- $self->search_description({
- field => $field, type => $operator,
- value => $orig_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);
+ my ($self, $chart_id, $clause, $condition) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $params = $self->_params;
+ my ($field, $operator, $value) = $condition->fov;
+ return if (!defined $field or !defined $operator or !defined $value);
+ $field = FIELD_MAP->{$field} || $field;
+
+ my ($string_value, $orig_value);
+ state $is_mysql = $dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
+
+ if (ref $value eq 'ARRAY') {
+
+ # Trim input and ignore blank values.
+ @$value = map { trim($_) } @$value;
+ @$value = grep { defined $_ and $_ ne '' } @$value;
+ return if !@$value;
+ $orig_value = join(',', @$value);
+ if ($field eq 'longdesc' && $is_mysql) {
+ @$value = map { _convert_unicode_characters($_) } @$value;
+ }
+ $string_value = join(',', @$value);
+ }
+ else {
+ return if $value eq '';
+ $orig_value = $value;
+ if ($field eq 'longdesc' && $is_mysql) {
+ $value = _convert_unicode_characters($value);
+ }
+ $string_value = $value;
+ }
+
+ $self->_chart_fields->{$field}
+ or ThrowCodeError("invalid_field_name", {field => $field});
+ trick_taint($field);
+
+ # This is the field as you'd reference it in a SQL statement.
+ my $full_field = $field =~ /\./ ? $field : "bugs.$field";
+
+ # "value" and "quoted" are for search functions that always operate
+ # on a scalar string and never care if they were passed multiple
+ # parameters. If the user does pass multiple parameters, they will
+ # become a space-separated string for those search functions.
+ #
+ # all_values is for search functions that do operate
+ # 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 => [],
+ 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);
+
+ # If term is left empty, then this means the criteria
+ # has no effect and can be ignored.
+ return unless $search_args{term};
+
+ # All the things here that don't get pulled out of
+ # %search_args are their original values before
+ # do_search_function modified them.
+ $self->search_description({
+ field => $field,
+ type => $operator,
+ value => $orig_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);
}
# XXX - This is a hack for MySQL which doesn't understand Unicode characters
# above U+FFFF, see Bugzilla::Comment::_check_thetext(). This hack can go away
# once we require MySQL 5.5.3 and use utf8mb4.
sub _convert_unicode_characters {
- my $string = shift;
+ my $string = shift;
- # Perl 5.13.8 and older complain about non-characters.
- no warnings 'utf8';
- $string =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
- return $string;
+ # Perl 5.13.8 and older complain about non-characters.
+ no warnings 'utf8';
+ $string
+ =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
+ return $string;
}
##################################
@@ -1922,121 +1888,126 @@ sub _convert_unicode_characters {
# This takes information about the current boolean chart and translates
# it into SQL, using the constants at the top of this file.
sub do_search_function {
- my ($self, $args) = @_;
- my ($field, $operator) = @$args{qw(field operator)};
-
- if (my $parse_func = SPECIAL_PARSING->{$field}) {
- $self->$parse_func($args);
- # Some parsing functions set $term, though most do not.
- # For the ones that set $term, we don't need to do any further
- # parsing.
- return if $args->{term};
- }
-
- my $operator_field_override = $self->_get_operator_field_override();
- my $override = $operator_field_override->{$field};
- # Attachment fields get special handling, if they don't have a specific
- # individual override.
- if (!$override and $field =~ /^attachments\./) {
- $override = $operator_field_override->{attachments};
- }
- # If there's still no override, check for an override on the field's type.
- if (!$override) {
- my $field_obj = $self->_chart_fields->{$field};
- $override = $operator_field_override->{$field_obj->type};
- }
-
- if ($override) {
- my $search_func = $self->_pick_override_function($override, $operator);
- $self->$search_func($args) if $search_func;
- }
+ my ($self, $args) = @_;
+ my ($field, $operator) = @$args{qw(field operator)};
- # Some search functions set $term, and some don't. For the ones that
- # don't (or for fields that don't have overrides) we now call the
- # direct operator function from OPERATORS.
- if (!defined $args->{term}) {
- $self->_do_operator_function($args);
- }
-
- if (!defined $args->{term}) {
- # This field and this type don't work together. Generally,
- # this should never be reached, because it should be handled
- # explicitly by OPERATOR_FIELD_OVERRIDE.
- ThrowUserError("search_field_operator_invalid",
- { field => $field, operator => $operator });
- }
+ if (my $parse_func = SPECIAL_PARSING->{$field}) {
+ $self->$parse_func($args);
+
+ # Some parsing functions set $term, though most do not.
+ # For the ones that set $term, we don't need to do any further
+ # parsing.
+ return if $args->{term};
+ }
+
+ my $operator_field_override = $self->_get_operator_field_override();
+ my $override = $operator_field_override->{$field};
+
+ # Attachment fields get special handling, if they don't have a specific
+ # individual override.
+ if (!$override and $field =~ /^attachments\./) {
+ $override = $operator_field_override->{attachments};
+ }
+
+ # If there's still no override, check for an override on the field's type.
+ if (!$override) {
+ my $field_obj = $self->_chart_fields->{$field};
+ $override = $operator_field_override->{$field_obj->type};
+ }
+
+ if ($override) {
+ my $search_func = $self->_pick_override_function($override, $operator);
+ $self->$search_func($args) if $search_func;
+ }
+
+ # Some search functions set $term, and some don't. For the ones that
+ # don't (or for fields that don't have overrides) we now call the
+ # direct operator function from OPERATORS.
+ if (!defined $args->{term}) {
+ $self->_do_operator_function($args);
+ }
+
+ if (!defined $args->{term}) {
+
+ # This field and this type don't work together. Generally,
+ # this should never be reached, because it should be handled
+ # explicitly by OPERATOR_FIELD_OVERRIDE.
+ ThrowUserError("search_field_operator_invalid",
+ {field => $field, operator => $operator});
+ }
}
# A helper for various search functions that need to run operator
# functions directly.
sub _do_operator_function {
- my ($self, $func_args) = @_;
- my $operator = $func_args->{operator};
- my $operator_func = OPERATORS->{$operator}
- || ThrowCodeError("search_field_operator_unsupported",
- { operator => $operator });
- $self->$operator_func($func_args);
+ my ($self, $func_args) = @_;
+ my $operator = $func_args->{operator};
+ my $operator_func
+ = OPERATORS->{$operator}
+ || ThrowCodeError("search_field_operator_unsupported",
+ {operator => $operator});
+ $self->$operator_func($func_args);
}
sub _reverse_operator {
- my ($self, $operator) = @_;
- my $reverse = OPERATOR_REVERSE->{$operator};
- return $reverse if $reverse;
- if ($operator =~ s/^not//) {
- return $operator;
- }
- return "not$operator";
+ my ($self, $operator) = @_;
+ my $reverse = OPERATOR_REVERSE->{$operator};
+ return $reverse if $reverse;
+ if ($operator =~ s/^not//) {
+ return $operator;
+ }
+ return "not$operator";
}
sub _pick_override_function {
- my ($self, $override, $operator) = @_;
- my $search_func = $override->{$operator};
-
- if (!$search_func) {
- # If we don't find an override for one specific operator,
- # then there are some special override types:
- # _non_changed: For any operator that doesn't have the word
- # "changed" in it
- # _default: Overrides all operators that aren't explicitly specified.
- if ($override->{_non_changed} and $operator !~ /changed/) {
- $search_func = $override->{_non_changed};
- }
- elsif ($override->{_default}) {
- $search_func = $override->{_default};
- }
+ my ($self, $override, $operator) = @_;
+ my $search_func = $override->{$operator};
+
+ if (!$search_func) {
+
+ # If we don't find an override for one specific operator,
+ # then there are some special override types:
+ # _non_changed: For any operator that doesn't have the word
+ # "changed" in it
+ # _default: Overrides all operators that aren't explicitly specified.
+ if ($override->{_non_changed} and $operator !~ /changed/) {
+ $search_func = $override->{_non_changed};
}
+ elsif ($override->{_default}) {
+ $search_func = $override->{_default};
+ }
+ }
- return $search_func;
+ return $search_func;
}
sub _get_operator_field_override {
- my $self = shift;
- my $cache = Bugzilla->request_cache;
+ my $self = shift;
+ my $cache = Bugzilla->request_cache;
- return $cache->{operator_field_override}
- if defined $cache->{operator_field_override};
+ return $cache->{operator_field_override}
+ if defined $cache->{operator_field_override};
- my %operator_field_override = %{ OPERATOR_FIELD_OVERRIDE() };
- Bugzilla::Hook::process('search_operator_field_override',
- { search => $self,
- operators => \%operator_field_override });
+ my %operator_field_override = %{OPERATOR_FIELD_OVERRIDE()};
+ Bugzilla::Hook::process('search_operator_field_override',
+ {search => $self, operators => \%operator_field_override});
- $cache->{operator_field_override} = \%operator_field_override;
- return $cache->{operator_field_override};
+ $cache->{operator_field_override} = \%operator_field_override;
+ return $cache->{operator_field_override};
}
sub _get_column_joins {
- my $self = shift;
- my $cache = Bugzilla->request_cache;
+ my $self = shift;
+ my $cache = Bugzilla->request_cache;
- return $cache->{column_joins} if defined $cache->{column_joins};
+ return $cache->{column_joins} if defined $cache->{column_joins};
- my %column_joins = %{ $self->COLUMN_JOINS() };
- Bugzilla::Hook::process('buglist_column_joins',
- { column_joins => \%column_joins });
+ my %column_joins = %{$self->COLUMN_JOINS()};
+ Bugzilla::Hook::process('buglist_column_joins',
+ {column_joins => \%column_joins});
- $cache->{column_joins} = \%column_joins;
- return $cache->{column_joins};
+ $cache->{column_joins} = \%column_joins;
+ return $cache->{column_joins};
}
###########################
@@ -2048,47 +2019,49 @@ sub _get_column_joins {
# is just a performance optimization, but on SQLite it actually changes
# the behavior of some searches.
sub _quote_unless_numeric {
- my ($self, $args, $value) = @_;
- if (!defined $value) {
- $value = $args->{value};
- }
- my ($field, $operator) = @$args{qw(field operator)};
-
- my $numeric_operator = !grep { $_ eq $operator } NON_NUMERIC_OPERATORS;
- my $numeric_field = $self->_chart_fields->{$field}->is_numeric;
- my $numeric_value = ($value =~ NUMBER_REGEX) ? 1 : 0;
- my $is_numeric = $numeric_operator && $numeric_field && $numeric_value;
-
- # These operators are really numeric operators with numeric fields.
- $numeric_operator = grep { $_ eq $operator } keys %{ SIMPLE_OPERATORS() };
-
- if ($is_numeric) {
- my $quoted = $value;
- trick_taint($quoted);
- return $quoted;
- }
- elsif ($numeric_field && !$numeric_value && $numeric_operator) {
- ThrowUserError('number_not_numeric', { field => $field, num => $value });
- }
- return Bugzilla->dbh->quote($value);
+ my ($self, $args, $value) = @_;
+ if (!defined $value) {
+ $value = $args->{value};
+ }
+ my ($field, $operator) = @$args{qw(field operator)};
+
+ my $numeric_operator = !grep { $_ eq $operator } NON_NUMERIC_OPERATORS;
+ my $numeric_field = $self->_chart_fields->{$field}->is_numeric;
+ my $numeric_value = ($value =~ NUMBER_REGEX) ? 1 : 0;
+ my $is_numeric = $numeric_operator && $numeric_field && $numeric_value;
+
+ # These operators are really numeric operators with numeric fields.
+ $numeric_operator = grep { $_ eq $operator } keys %{SIMPLE_OPERATORS()};
+
+ if ($is_numeric) {
+ my $quoted = $value;
+ trick_taint($quoted);
+ return $quoted;
+ }
+ elsif ($numeric_field && !$numeric_value && $numeric_operator) {
+ ThrowUserError('number_not_numeric', {field => $field, num => $value});
+ }
+ return Bugzilla->dbh->quote($value);
}
sub build_subselect {
- my ($outer, $inner, $table, $cond, $negate) = @_;
- if ($table =~ /\battach_data\b/) {
- # It takes a long time to scan the whole attach_data table
- # unconditionally, so we return the subselect and let the DB optimizer
- # restrict the search based on other search criteria.
- my $not = $negate ? "NOT" : "";
- return "$outer $not IN (SELECT DISTINCT $inner FROM $table WHERE $cond)";
- }
- # Execute subselects immediately to avoid dependent subqueries, which are
- # large performance hits on MySql
- my $q = "SELECT DISTINCT $inner FROM $table WHERE $cond";
- my $dbh = Bugzilla->dbh;
- my $list = $dbh->selectcol_arrayref($q);
- return $negate ? "1=1" : "1=2" unless @$list;
- return $dbh->sql_in($outer, $list, $negate);
+ my ($outer, $inner, $table, $cond, $negate) = @_;
+ if ($table =~ /\battach_data\b/) {
+
+ # It takes a long time to scan the whole attach_data table
+ # unconditionally, so we return the subselect and let the DB optimizer
+ # restrict the search based on other search criteria.
+ my $not = $negate ? "NOT" : "";
+ return "$outer $not IN (SELECT DISTINCT $inner FROM $table WHERE $cond)";
+ }
+
+ # Execute subselects immediately to avoid dependent subqueries, which are
+ # large performance hits on MySql
+ my $q = "SELECT DISTINCT $inner FROM $table WHERE $cond";
+ my $dbh = Bugzilla->dbh;
+ my $list = $dbh->selectcol_arrayref($q);
+ return $negate ? "1=1" : "1=2" unless @$list;
+ return $dbh->sql_in($outer, $list, $negate);
}
# Used by anyexact to get the list of input values. This allows us to
@@ -2096,68 +2069,69 @@ sub build_subselect {
# still accept string values for the boolean charts (and split them on
# commas).
sub _all_values {
- my ($self, $args, $split_on) = @_;
- $split_on ||= qr/[\s,]+/;
- my $dbh = Bugzilla->dbh;
- my $all_values = $args->{all_values};
-
- my @array;
- if (ref $all_values eq 'ARRAY') {
- @array = @$all_values;
- }
- else {
- @array = split($split_on, $all_values);
- @array = map { trim($_) } @array;
- @array = grep { defined $_ and $_ ne '' } @array;
- }
-
- if ($args->{field} eq 'resolution') {
- @array = map { $_ eq '---' ? '' : $_ } @array;
- }
-
- return @array;
+ my ($self, $args, $split_on) = @_;
+ $split_on ||= qr/[\s,]+/;
+ my $dbh = Bugzilla->dbh;
+ my $all_values = $args->{all_values};
+
+ my @array;
+ if (ref $all_values eq 'ARRAY') {
+ @array = @$all_values;
+ }
+ else {
+ @array = split($split_on, $all_values);
+ @array = map { trim($_) } @array;
+ @array = grep { defined $_ and $_ ne '' } @array;
+ }
+
+ if ($args->{field} eq 'resolution') {
+ @array = map { $_ eq '---' ? '' : $_ } @array;
+ }
+
+ return @array;
}
# Support for "any/all/nowordssubstr" comparison type ("words as substrings")
sub _substring_terms {
- my ($self, $args) = @_;
- my $dbh = Bugzilla->dbh;
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
- # We don't have to (or want to) use _all_values, because we'd just
- # split each term on spaces and commas anyway.
- my @words = split(/[\s,]+/, $args->{value});
- @words = grep { defined $_ and $_ ne '' } @words;
- my @terms = map { $dbh->sql_ilike($_, $args->{full_field}) } @words;
- return @terms;
+ # We don't have to (or want to) use _all_values, because we'd just
+ # split each term on spaces and commas anyway.
+ my @words = split(/[\s,]+/, $args->{value});
+ @words = grep { defined $_ and $_ ne '' } @words;
+ my @terms = map { $dbh->sql_ilike($_, $args->{full_field}) } @words;
+ return @terms;
}
sub _word_terms {
- my ($self, $args) = @_;
- my $dbh = Bugzilla->dbh;
-
- my @values = split(/[\s,]+/, $args->{value});
- @values = grep { defined $_ and $_ ne '' } @values;
- my @substring_terms = $self->_substring_terms($args);
-
- my @terms;
- my $start = $dbh->WORD_START;
- my $end = $dbh->WORD_END;
- foreach my $word (@values) {
- my $regex = $start . quotemeta($word) . $end;
- my $quoted = $dbh->quote($regex);
- # We don't have to check the regexp, because we escaped it, so we're
- # sure it's valid.
- my $regex_term = $dbh->sql_regexp($args->{full_field}, $quoted,
- 'no check');
- # Regular expressions are slow--substring searches are faster.
- # If we're searching for a word, we're also certain that the
- # substring will appear in the value. So we limit first by
- # substring and then by a regex that will match just words.
- my $substring_term = shift @substring_terms;
- push(@terms, "$substring_term AND $regex_term");
- }
-
- return @terms;
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my @values = split(/[\s,]+/, $args->{value});
+ @values = grep { defined $_ and $_ ne '' } @values;
+ my @substring_terms = $self->_substring_terms($args);
+
+ my @terms;
+ my $start = $dbh->WORD_START;
+ my $end = $dbh->WORD_END;
+ foreach my $word (@values) {
+ my $regex = $start . quotemeta($word) . $end;
+ my $quoted = $dbh->quote($regex);
+
+ # We don't have to check the regexp, because we escaped it, so we're
+ # sure it's valid.
+ my $regex_term = $dbh->sql_regexp($args->{full_field}, $quoted, 'no check');
+
+ # Regular expressions are slow--substring searches are faster.
+ # If we're searching for a word, we're also certain that the
+ # substring will appear in the value. So we limit first by
+ # substring and then by a regex that will match just words.
+ my $substring_term = shift @substring_terms;
+ push(@terms, "$substring_term AND $regex_term");
+ }
+
+ return @terms;
}
#####################################
@@ -2165,109 +2139,118 @@ sub _word_terms {
#####################################
sub _timestamp_translate {
- my ($self, $ignore_time, $args) = @_;
- my $value = $args->{value};
- my $dbh = Bugzilla->dbh;
-
- # Force parsing of all dates & times, so that we filter weird values out
- # from users.
- #return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i;
-
- $value = SqlifyDate($value);
- # 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);
+ my ($self, $ignore_time, $args) = @_;
+ my $value = $args->{value};
+ my $dbh = Bugzilla->dbh;
+
+ # Force parsing of all dates & times, so that we filter weird values out
+ # from users.
+ #return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i;
+
+ $value = SqlifyDate($value);
+
+ # 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, @_);
+ return shift->_timestamp_translate(0, @_);
}
sub _last_visit_datetime {
- my ($self, $args) = @_;
- my $value = $args->{value};
-
- $self->_datetime_translate($args);
- if ($value eq $args->{value}) {
- # Failed to translate a datetime. let's try the pronoun expando.
- if ($value eq '%last_changed%') {
- $self->_add_extra_column('changeddate');
- $args->{value} = $args->{quoted} = 'bugs.delta_ts';
- }
+ my ($self, $args) = @_;
+ my $value = $args->{value};
+
+ $self->_datetime_translate($args);
+ if ($value eq $args->{value}) {
+
+ # Failed to translate a datetime. let's try the pronoun expando.
+ if ($value eq '%last_changed%') {
+ $self->_add_extra_column('changeddate');
+ $args->{value} = $args->{quoted} = 'bugs.delta_ts';
}
+ }
}
sub _date_translate {
- return shift->_timestamp_translate(1, @_);
+ return shift->_timestamp_translate(1, @_);
}
sub SqlifyDate {
- my ($str) = @_;
- my $fmt = "%Y-%m-%d %H:%M:%S";
- $str = "" if (!defined $str || lc($str) eq 'now');
- if ($str eq "") {
- my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time());
- return sprintf("%4d-%02d-%02d 00:00:00", $year+1900, $month+1, $mday);
- }
-
- if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/i) { # relative date
- my ($sign, $amount, $unit, $startof, $date) = ($1, $2, lc $3, lc $4, time);
- my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date);
- if ($sign && $sign eq '+') { $amount = -$amount; }
- $startof = 1 if $amount == 0;
- if ($unit eq 'w') { # convert weeks to days
- $amount = 7*$amount;
- $amount += $wday if $startof;
- $unit = 'd';
- }
- if ($unit eq 'd') {
- if ($startof) {
- $fmt = "%Y-%m-%d 00:00:00";
- $date -= $sec + 60*$min + 3600*$hour;
- }
- $date -= 24*3600*$amount;
- return time2str($fmt, $date);
- }
- elsif ($unit eq 'y') {
- if ($startof) {
- return sprintf("%4d-01-01 00:00:00", $year+1900-$amount);
- }
- else {
- return sprintf("%4d-%02d-%02d %02d:%02d:%02d",
- $year+1900-$amount, $month+1, $mday, $hour, $min, $sec);
- }
- }
- elsif ($unit eq 'm') {
- $month -= $amount;
- $year += floor($month/12);
- $month %= 12;
- if ($startof) {
- return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1);
- }
- else {
- return sprintf("%4d-%02d-%02d %02d:%02d:%02d",
- $year+1900, $month+1, $mday, $hour, $min, $sec);
- }
- }
- elsif ($unit eq 'h') {
- # Special case for 'beginning of an hour'
- if ($startof) {
- $fmt = "%Y-%m-%d %H:00:00";
- }
- $date -= 3600*$amount;
- return time2str($fmt, $date);
- }
- return undef; # should not happen due to regexp at top
- }
- my $date = str2time($str);
- if (!defined($date)) {
- ThrowUserError("illegal_date", { date => $str });
- }
- return time2str($fmt, $date);
+ my ($str) = @_;
+ my $fmt = "%Y-%m-%d %H:%M:%S";
+ $str = "" if (!defined $str || lc($str) eq 'now');
+ if ($str eq "") {
+ my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time());
+ return sprintf("%4d-%02d-%02d 00:00:00", $year + 1900, $month + 1, $mday);
+ }
+
+ if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/i) { # relative date
+ my ($sign, $amount, $unit, $startof, $date) = ($1, $2, lc $3, lc $4, time);
+ my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date);
+ if ($sign && $sign eq '+') { $amount = -$amount; }
+ $startof = 1 if $amount == 0;
+ if ($unit eq 'w') { # convert weeks to days
+ $amount = 7 * $amount;
+ $amount += $wday if $startof;
+ $unit = 'd';
+ }
+ if ($unit eq 'd') {
+ if ($startof) {
+ $fmt = "%Y-%m-%d 00:00:00";
+ $date -= $sec + 60 * $min + 3600 * $hour;
+ }
+ $date -= 24 * 3600 * $amount;
+ return time2str($fmt, $date);
+ }
+ elsif ($unit eq 'y') {
+ if ($startof) {
+ return sprintf("%4d-01-01 00:00:00", $year + 1900 - $amount);
+ }
+ else {
+ return sprintf(
+ "%4d-%02d-%02d %02d:%02d:%02d",
+ $year + 1900 - $amount,
+ $month + 1, $mday, $hour, $min, $sec
+ );
+ }
+ }
+ elsif ($unit eq 'm') {
+ $month -= $amount;
+ $year += floor($month / 12);
+ $month %= 12;
+ if ($startof) {
+ return sprintf("%4d-%02d-01 00:00:00", $year + 1900, $month + 1);
+ }
+ else {
+ return sprintf(
+ "%4d-%02d-%02d %02d:%02d:%02d",
+ $year + 1900,
+ $month + 1, $mday, $hour, $min, $sec
+ );
+ }
+ }
+ elsif ($unit eq 'h') {
+
+ # Special case for 'beginning of an hour'
+ if ($startof) {
+ $fmt = "%Y-%m-%d %H:00:00";
+ }
+ $date -= 3600 * $amount;
+ return time2str($fmt, $date);
+ }
+ return undef; # should not happen due to regexp at top
+ }
+ my $date = str2time($str);
+ if (!defined($date)) {
+ ThrowUserError("illegal_date", {date => $str});
+ }
+ return time2str($fmt, $date);
}
######################################
@@ -2275,104 +2258,109 @@ sub SqlifyDate {
######################################
sub pronoun {
- my ($noun, $user) = (@_);
- if ($noun eq "%user%") {
- if ($user->id) {
- return $user->id;
- } else {
- ThrowUserError('login_required_for_pronoun');
- }
- }
- if ($noun eq "%reporter%") {
- return "bugs.reporter";
- }
- if ($noun eq "%assignee%") {
- return "bugs.assigned_to";
+ my ($noun, $user) = (@_);
+ if ($noun eq "%user%") {
+ if ($user->id) {
+ return $user->id;
}
- if ($noun eq "%qacontact%") {
- return "COALESCE(bugs.qa_contact,0)";
+ else {
+ ThrowUserError('login_required_for_pronoun');
}
+ }
+ if ($noun eq "%reporter%") {
+ return "bugs.reporter";
+ }
+ if ($noun eq "%assignee%") {
+ return "bugs.assigned_to";
+ }
+ if ($noun eq "%qacontact%") {
+ return "COALESCE(bugs.qa_contact,0)";
+ }
- ThrowUserError('illegal_pronoun', { pronoun => $noun });
+ ThrowUserError('illegal_pronoun', {pronoun => $noun});
}
sub _contact_pronoun {
- my ($self, $args) = @_;
- my $value = $args->{value};
- my $user = $self->_user;
+ my ($self, $args) = @_;
+ my $value = $args->{value};
+ my $user = $self->_user;
- if ($value =~ /^\%group\.[^%]+%$/) {
- $self->_contact_exact_group($args);
- }
- elsif ($value =~ /^(%\w+%)$/) {
- $args->{value} = pronoun($1, $user);
- $args->{quoted} = $args->{value};
- $args->{value_is_id} = 1;
- }
+ if ($value =~ /^\%group\.[^%]+%$/) {
+ $self->_contact_exact_group($args);
+ }
+ elsif ($value =~ /^(%\w+%)$/) {
+ $args->{value} = pronoun($1, $user);
+ $args->{quoted} = $args->{value};
+ $args->{value_is_id} = 1;
+ }
}
sub _contact_exact_group {
- my ($self, $args) = @_;
- my ($value, $operator, $field, $chart_id, $joins, $sequence) =
- @$args{qw(value operator field chart_id joins sequence)};
- my $dbh = Bugzilla->dbh;
- my $user = $self->_user;
-
- # We already know $value will match this regexp, else we wouldn't be here.
- $value =~ /\%group\.([^%]+)%/;
- my $group_name = $1;
- my $group = Bugzilla::Group->check({ name => $group_name, _error => 'invalid_group_name' });
- # Pass $group_name instead of $group->name to the error message
- # to not leak the existence of the group.
- $user->in_group($group)
- || ThrowUserError('invalid_group_name', { name => $group_name });
- # Now that we know the user belongs to this group, it's safe
- # to disclose more information.
- $group->check_members_are_visible();
-
- my $group_ids = Bugzilla::Group->flatten_group_membership($group->id);
-
- if ($field eq 'cc' && $chart_id eq '') {
- # This is for the email1, email2, email3 fields from query.cgi.
- $chart_id = "CC$$sequence";
- $args->{sequence}++;
- }
-
- my $from = $field;
- # These fields need an additional table.
- if ($field =~ /^(commenter|cc)$/) {
- my $join_table = $field;
- $join_table = 'longdescs' if $field eq 'commenter';
- my $join_table_alias = "${field}_$chart_id";
- push(@$joins, { table => $join_table, as => $join_table_alias });
- $from = "$join_table_alias.who";
- }
-
- my $table = "user_group_map_$chart_id";
- my $join = {
- table => 'user_group_map',
- as => $table,
- from => $from,
- to => 'user_id',
- extra => [$dbh->sql_in("$table.group_id", $group_ids),
- "$table.isbless = 0"],
- };
- push(@$joins, $join);
- if ($operator =~ /^not/) {
- $args->{term} = "$table.group_id IS NULL";
- }
- else {
- $args->{term} = "$table.group_id IS NOT NULL";
- }
+ my ($self, $args) = @_;
+ my ($value, $operator, $field, $chart_id, $joins, $sequence)
+ = @$args{qw(value operator field chart_id joins sequence)};
+ my $dbh = Bugzilla->dbh;
+ my $user = $self->_user;
+
+ # We already know $value will match this regexp, else we wouldn't be here.
+ $value =~ /\%group\.([^%]+)%/;
+ my $group_name = $1;
+ my $group = Bugzilla::Group->check(
+ {name => $group_name, _error => 'invalid_group_name'});
+
+ # Pass $group_name instead of $group->name to the error message
+ # to not leak the existence of the group.
+ $user->in_group($group)
+ || ThrowUserError('invalid_group_name', {name => $group_name});
+
+ # Now that we know the user belongs to this group, it's safe
+ # to disclose more information.
+ $group->check_members_are_visible();
+
+ my $group_ids = Bugzilla::Group->flatten_group_membership($group->id);
+
+ if ($field eq 'cc' && $chart_id eq '') {
+
+ # This is for the email1, email2, email3 fields from query.cgi.
+ $chart_id = "CC$$sequence";
+ $args->{sequence}++;
+ }
+
+ my $from = $field;
+
+ # These fields need an additional table.
+ if ($field =~ /^(commenter|cc)$/) {
+ my $join_table = $field;
+ $join_table = 'longdescs' if $field eq 'commenter';
+ my $join_table_alias = "${field}_$chart_id";
+ push(@$joins, {table => $join_table, as => $join_table_alias});
+ $from = "$join_table_alias.who";
+ }
+
+ my $table = "user_group_map_$chart_id";
+ my $join = {
+ table => 'user_group_map',
+ as => $table,
+ from => $from,
+ to => 'user_id',
+ extra => [$dbh->sql_in("$table.group_id", $group_ids), "$table.isbless = 0"],
+ };
+ push(@$joins, $join);
+ if ($operator =~ /^not/) {
+ $args->{term} = "$table.group_id IS NULL";
+ }
+ else {
+ $args->{term} = "$table.group_id IS NOT NULL";
+ }
}
sub _get_user_id {
- my ($self, $value) = @_;
+ my ($self, $value) = @_;
- if ($value =~ /^%\w+%$/) {
- return pronoun($value, $self->_user);
- }
- return login_to_id($value, THROW_ERROR);
+ if ($value =~ /^%\w+%$/) {
+ return pronoun($value, $self->_user);
+ }
+ return login_to_id($value, THROW_ERROR);
}
#####################################################################
@@ -2380,546 +2368,556 @@ sub _get_user_id {
#####################################################################
sub _invalid_combination {
- my ($self, $args) = @_;
- my ($field, $operator) = @$args{qw(field operator)};
- ThrowUserError('search_field_operator_invalid',
- { field => $field, operator => $operator });
+ my ($self, $args) = @_;
+ my ($field, $operator) = @$args{qw(field operator)};
+ ThrowUserError('search_field_operator_invalid',
+ {field => $field, operator => $operator});
}
# For all the "user" fields--assigned_to, reporter, qa_contact,
# cc, commenter, requestee, etc.
sub _user_nonchanged {
- my ($self, $args) = @_;
- my ($field, $operator, $chart_id, $sequence, $joins) =
- @$args{qw(field operator chart_id sequence joins)};
-
- my $is_in_other_table;
- if (my $join = USER_FIELDS->{$field}->{join}) {
- $is_in_other_table = 1;
- my $as = "${field}_$chart_id";
- # Needed for setters.login_name and requestees.login_name.
- # Otherwise when we try to join "profiles" below, we'd get
- # something like "setters.login_name.login_name" in the "from".
- $as =~ s/\./_/g;
- # This helps implement the email1, email2, etc. parameters.
- if ($chart_id =~ /default/) {
- $as .= "_$sequence";
- }
- my $isprivate = USER_FIELDS->{$field}->{isprivate};
- my $extra = ($isprivate and !$self->_user->is_insider)
- ? ["$as.isprivate = 0"] : [];
- # We want to copy $join so as not to modify USER_FIELDS.
- push(@$joins, { %$join, as => $as, extra => $extra });
- my $search_field = USER_FIELDS->{$field}->{field};
- $args->{full_field} = "$as.$search_field";
- }
+ my ($self, $args) = @_;
+ my ($field, $operator, $chart_id, $sequence, $joins)
+ = @$args{qw(field operator chart_id sequence joins)};
+
+ my $is_in_other_table;
+ if (my $join = USER_FIELDS->{$field}->{join}) {
+ $is_in_other_table = 1;
+ my $as = "${field}_$chart_id";
+
+ # Needed for setters.login_name and requestees.login_name.
+ # Otherwise when we try to join "profiles" below, we'd get
+ # something like "setters.login_name.login_name" in the "from".
+ $as =~ s/\./_/g;
+
+ # This helps implement the email1, email2, etc. parameters.
+ if ($chart_id =~ /default/) {
+ $as .= "_$sequence";
+ }
+ my $isprivate = USER_FIELDS->{$field}->{isprivate};
+ my $extra
+ = ($isprivate and !$self->_user->is_insider) ? ["$as.isprivate = 0"] : [];
+
+ # We want to copy $join so as not to modify USER_FIELDS.
+ push(@$joins, {%$join, as => $as, extra => $extra});
+ my $search_field = USER_FIELDS->{$field}->{field};
+ $args->{full_field} = "$as.$search_field";
+ }
+
+ my $is_nullable = USER_FIELDS->{$field}->{nullable};
+ my $null_alternate = "''";
+
+ # When using a pronoun, we use the userid, and we don't have to
+ # join the profiles table.
+ if ($args->{value_is_id}) {
+ $null_alternate = 0;
+ }
+ elsif (substr($field, -9) eq '_realname') {
+ my $as = "name_${field}_$chart_id";
+
+ # For fields with periods in their name.
+ $as =~ s/\./_/;
+ my $join = {
+ table => 'profiles',
+ as => $as,
+ from => substr($args->{full_field}, 0, -9),
+ to => 'userid',
+ join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef,
+ };
+ push(@$joins, $join);
+ $args->{full_field} = "$as.realname";
+ }
+ else {
+ my $as = "name_${field}_$chart_id";
- my $is_nullable = USER_FIELDS->{$field}->{nullable};
- my $null_alternate = "''";
- # When using a pronoun, we use the userid, and we don't have to
- # join the profiles table.
- if ($args->{value_is_id}) {
- $null_alternate = 0;
+ # For fields with periods in their name.
+ $as =~ s/\./_/;
+ my $join = {
+ table => 'profiles',
+ as => $as,
+ from => $args->{full_field},
+ to => 'userid',
+ join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef,
+ };
+ push(@$joins, $join);
+ $args->{full_field} = "$as.login_name";
+ }
+
+ # We COALESCE fields that can be NULL, to make "not"-style operators
+ # continue to work properly. For example, "qa_contact is not equal to bob"
+ # should also show bugs where the qa_contact is NULL. With COALESCE,
+ # it does.
+ if ($is_nullable) {
+ $args->{full_field} = "COALESCE($args->{full_field}, $null_alternate)";
+ }
+
+ # For fields whose values are stored in other tables, negation (NOT)
+ # only works properly if we put the condition into the JOIN instead
+ # of the WHERE.
+ if ($is_in_other_table) {
+
+ # Using the last join works properly whether we're searching based
+ # on userid or login_name.
+ my $last_join = $joins->[-1];
+
+ # 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|isempty)/ ? 1 : 0;
+ if ($is_negative) {
+ $args->{operator} = $self->_reverse_operator($operator);
}
- elsif (substr($field, -9) eq '_realname') {
- my $as = "name_${field}_$chart_id";
- # For fields with periods in their name.
- $as =~ s/\./_/;
- my $join = {
- table => 'profiles',
- as => $as,
- from => substr($args->{full_field}, 0, -9),
- to => 'userid',
- join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef,
- };
- push(@$joins, $join);
- $args->{full_field} = "$as.realname";
+ $self->_do_operator_function($args);
+ push(@{$last_join->{extra}}, $args->{term});
+
+ # For login_name searches, we only want a single join.
+ # So we create a subselect table out of our two joins. This makes
+ # negation (NOT) work properly for values that are in other
+ # tables.
+ if ($last_join->{table} eq 'profiles') {
+ pop @$joins;
+ $last_join->{join} = 'INNER';
+ my ($join_sql) = $self->_translate_join($last_join);
+ my $first_join = $joins->[-1];
+ my $as = $first_join->{as};
+ my $table = $first_join->{table};
+ my $columns = "bug_id";
+ $columns .= ",isprivate" if @{$first_join->{extra}};
+ my $new_table = "SELECT DISTINCT $columns FROM $table AS $as $join_sql";
+ $first_join->{table} = "($new_table)";
+
+ # We always want to LEFT JOIN the generated table.
+ delete $first_join->{join};
+
+ # To support OR charts, we need multiple tables.
+ my $new_as = $first_join->{as} . "_$sequence";
+ $_ =~ s/\Q$as\E/$new_as/ foreach @{$first_join->{extra}};
+ $first_join->{as} = $new_as;
+ $last_join = $first_join;
+ }
+
+ # If we're joining the first table (we're using a pronoun and
+ # searching by user id) then we need to check $other_table->{field}.
+ my $check_field = $last_join->{as} . '.bug_id';
+ if ($is_negative) {
+ $args->{term} = "$check_field IS NULL";
}
else {
- my $as = "name_${field}_$chart_id";
- # For fields with periods in their name.
- $as =~ s/\./_/;
- my $join = {
- table => 'profiles',
- as => $as,
- from => $args->{full_field},
- to => 'userid',
- join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef,
- };
- push(@$joins, $join);
- $args->{full_field} = "$as.login_name";
- }
-
- # We COALESCE fields that can be NULL, to make "not"-style operators
- # continue to work properly. For example, "qa_contact is not equal to bob"
- # should also show bugs where the qa_contact is NULL. With COALESCE,
- # it does.
- if ($is_nullable) {
- $args->{full_field} = "COALESCE($args->{full_field}, $null_alternate)";
- }
-
- # For fields whose values are stored in other tables, negation (NOT)
- # only works properly if we put the condition into the JOIN instead
- # of the WHERE.
- if ($is_in_other_table) {
- # Using the last join works properly whether we're searching based
- # on userid or login_name.
- my $last_join = $joins->[-1];
-
- # 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|isempty)/ ? 1 : 0;
- if ($is_negative) {
- $args->{operator} = $self->_reverse_operator($operator);
- }
- $self->_do_operator_function($args);
- push(@{ $last_join->{extra} }, $args->{term});
-
- # For login_name searches, we only want a single join.
- # So we create a subselect table out of our two joins. This makes
- # negation (NOT) work properly for values that are in other
- # tables.
- if ($last_join->{table} eq 'profiles') {
- pop @$joins;
- $last_join->{join} = 'INNER';
- my ($join_sql) = $self->_translate_join($last_join);
- my $first_join = $joins->[-1];
- my $as = $first_join->{as};
- my $table = $first_join->{table};
- my $columns = "bug_id";
- $columns .= ",isprivate" if @{ $first_join->{extra} };
- my $new_table = "SELECT DISTINCT $columns FROM $table AS $as $join_sql";
- $first_join->{table} = "($new_table)";
- # We always want to LEFT JOIN the generated table.
- delete $first_join->{join};
- # To support OR charts, we need multiple tables.
- my $new_as = $first_join->{as} . "_$sequence";
- $_ =~ s/\Q$as\E/$new_as/ foreach @{ $first_join->{extra} };
- $first_join->{as} = $new_as;
- $last_join = $first_join;
- }
-
- # If we're joining the first table (we're using a pronoun and
- # searching by user id) then we need to check $other_table->{field}.
- my $check_field = $last_join->{as} . '.bug_id';
- if ($is_negative) {
- $args->{term} = "$check_field IS NULL";
- }
- else {
- $args->{term} = "$check_field IS NOT NULL";
- }
+ $args->{term} = "$check_field IS NOT NULL";
}
+ }
}
# XXX This duplicates having Commenter as a search field.
sub _long_desc_changedby {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)};
-
- my $table = "longdescs_$chart_id";
- push(@$joins, { table => 'longdescs', as => $table });
- my $user_id = $self->_get_user_id($value);
- $args->{term} = "$table.who = $user_id";
-
- # If the user is not part of the insiders group, they cannot see
- # private comments
- if (!$self->_user->is_insider) {
- $args->{term} .= " AND $table.isprivate = 0";
- }
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)};
+
+ my $table = "longdescs_$chart_id";
+ push(@$joins, {table => 'longdescs', as => $table});
+ my $user_id = $self->_get_user_id($value);
+ $args->{term} = "$table.who = $user_id";
+
+ # If the user is not part of the insiders group, they cannot see
+ # private comments
+ if (!$self->_user->is_insider) {
+ $args->{term} .= " AND $table.isprivate = 0";
+ }
}
sub _long_desc_changedbefore_after {
- my ($self, $args) = @_;
- my ($chart_id, $operator, $value, $joins) =
- @$args{qw(chart_id operator value joins)};
- my $dbh = Bugzilla->dbh;
-
- my $sql_operator = ($operator =~ /before/) ? '<=' : '>=';
- my $table = "longdescs_$chart_id";
- my $sql_date = $dbh->quote(SqlifyDate($value));
- my $join = {
- table => 'longdescs',
- as => $table,
- extra => ["$table.bug_when $sql_operator $sql_date"],
- };
- push(@$joins, $join);
- $args->{term} = "$table.bug_when IS NOT NULL";
-
- # If the user is not part of the insiders group, they cannot see
- # private comments
- if (!$self->_user->is_insider) {
- $args->{term} .= " AND $table.isprivate = 0";
- }
+ my ($self, $args) = @_;
+ my ($chart_id, $operator, $value, $joins)
+ = @$args{qw(chart_id operator value joins)};
+ my $dbh = Bugzilla->dbh;
+
+ my $sql_operator = ($operator =~ /before/) ? '<=' : '>=';
+ my $table = "longdescs_$chart_id";
+ my $sql_date = $dbh->quote(SqlifyDate($value));
+ my $join = {
+ table => 'longdescs',
+ as => $table,
+ extra => ["$table.bug_when $sql_operator $sql_date"],
+ };
+ push(@$joins, $join);
+ $args->{term} = "$table.bug_when IS NOT NULL";
+
+ # If the user is not part of the insiders group, they cannot see
+ # private comments
+ if (!$self->_user->is_insider) {
+ $args->{term} .= " AND $table.isprivate = 0";
+ }
}
sub _long_desc_nonchanged {
- my ($self, $args) = @_;
- 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";
- my $join_args = {
- chart_id => $chart_id,
- sequence => $chart_id,
- field => 'longdesc',
- full_field => "$table.thetext",
- operator => $operator,
- value => $value,
- all_values => $value,
- quoted => $dbh->quote($value),
- joins => [],
- bugs_table => $bugs_table,
- };
- $self->_do_operator_function($join_args);
-
- # If the user is not part of the insiders group, they cannot see
- # private comments
- if (!$self->_user->is_insider) {
- $join_args->{term} .= " AND $table.isprivate = 0";
- }
-
- my $join = {
- table => 'longdescs',
- as => $table,
- extra => [ $join_args->{term} ],
- };
- push(@$joins, $join);
-
- $args->{term} = "$table.comment_id IS NOT NULL";
+ my ($self, $args) = @_;
+ 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";
+ my $join_args = {
+ chart_id => $chart_id,
+ sequence => $chart_id,
+ field => 'longdesc',
+ full_field => "$table.thetext",
+ operator => $operator,
+ value => $value,
+ all_values => $value,
+ quoted => $dbh->quote($value),
+ joins => [],
+ bugs_table => $bugs_table,
+ };
+ $self->_do_operator_function($join_args);
+
+ # If the user is not part of the insiders group, they cannot see
+ # private comments
+ if (!$self->_user->is_insider) {
+ $join_args->{term} .= " AND $table.isprivate = 0";
+ }
+
+ my $join = {table => 'longdescs', as => $table, extra => [$join_args->{term}],};
+ push(@$joins, $join);
+
+ $args->{term} = "$table.comment_id IS NOT NULL";
}
sub _content_matches {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $fields, $operator, $value) =
- @$args{qw(chart_id joins fields operator value)};
- my $dbh = Bugzilla->dbh;
-
- # "content" is an alias for columns containing text for which we
- # can search a full-text index and retrieve results by relevance,
- # currently just bug comments (and summaries to some degree).
- # There's only one way to search a full-text index, so we only
- # accept the "matches" operator, which is specific to full-text
- # index searches.
-
- # Add the fulltext table to the query so we can search on it.
- my $table = "bugs_fulltext_$chart_id";
- my $comments_col = "comments";
- $comments_col = "comments_noprivate" unless $self->_user->is_insider;
- push(@$joins, { table => 'bugs_fulltext', as => $table });
-
- # Create search terms to add to the SELECT and WHERE clauses.
- my ($term1, $rterm1) =
- $dbh->sql_fulltext_search("$table.$comments_col", $value);
- my ($term2, $rterm2) =
- $dbh->sql_fulltext_search("$table.short_desc", $value);
- $rterm1 = $term1 if !$rterm1;
- $rterm2 = $term2 if !$rterm2;
-
- # The term to use in the WHERE clause.
- my $term = "$term1 OR $term2";
- if ($operator =~ /not/i) {
- $term = "NOT($term)";
- }
- $args->{term} = $term;
-
- # In order to sort by relevance (in case the user requests it),
- # we SELECT the relevance value so we can add it to the ORDER BY
- # clause. Every time a new fulltext chart isadded, this adds more
- # terms to the relevance sql.
- #
- # We build the relevance SQL by modifying the COLUMNS list directly,
- # which is kind of a hack but works.
- my $current = $self->COLUMNS->{'relevance'}->{name};
- $current = $current ? "$current + " : '';
- # For NOT searches, we just add 0 to the relevance.
- my $select_term = $operator =~ /not/ ? 0 : "($current$rterm1 + $rterm2)";
- $self->COLUMNS->{'relevance'}->{name} = $select_term;
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $fields, $operator, $value)
+ = @$args{qw(chart_id joins fields operator value)};
+ my $dbh = Bugzilla->dbh;
+
+ # "content" is an alias for columns containing text for which we
+ # can search a full-text index and retrieve results by relevance,
+ # currently just bug comments (and summaries to some degree).
+ # There's only one way to search a full-text index, so we only
+ # accept the "matches" operator, which is specific to full-text
+ # index searches.
+
+ # Add the fulltext table to the query so we can search on it.
+ my $table = "bugs_fulltext_$chart_id";
+ my $comments_col = "comments";
+ $comments_col = "comments_noprivate" unless $self->_user->is_insider;
+ push(@$joins, {table => 'bugs_fulltext', as => $table});
+
+ # Create search terms to add to the SELECT and WHERE clauses.
+ my ($term1, $rterm1)
+ = $dbh->sql_fulltext_search("$table.$comments_col", $value);
+ my ($term2, $rterm2) = $dbh->sql_fulltext_search("$table.short_desc", $value);
+ $rterm1 = $term1 if !$rterm1;
+ $rterm2 = $term2 if !$rterm2;
+
+ # The term to use in the WHERE clause.
+ my $term = "$term1 OR $term2";
+ if ($operator =~ /not/i) {
+ $term = "NOT($term)";
+ }
+ $args->{term} = $term;
+
+ # In order to sort by relevance (in case the user requests it),
+ # we SELECT the relevance value so we can add it to the ORDER BY
+ # clause. Every time a new fulltext chart isadded, this adds more
+ # terms to the relevance sql.
+ #
+ # We build the relevance SQL by modifying the COLUMNS list directly,
+ # which is kind of a hack but works.
+ my $current = $self->COLUMNS->{'relevance'}->{name};
+ $current = $current ? "$current + " : '';
+
+ # For NOT searches, we just add 0 to the relevance.
+ my $select_term = $operator =~ /not/ ? 0 : "($current$rterm1 + $rterm2)";
+ $self->COLUMNS->{'relevance'}->{name} = $select_term;
}
sub _long_descs_count {
- my ($self, $args) = @_;
- my ($chart_id, $joins) = @$args{qw(chart_id joins)};
- my $table = "longdescs_count_$chart_id";
- my $extra = $self->_user->is_insider ? "" : "WHERE isprivate = 0";
- my $join = {
- table => "(SELECT bug_id, COUNT(*) AS num"
- . " FROM longdescs $extra GROUP BY bug_id)",
- as => $table,
- };
- push(@$joins, $join);
- $args->{full_field} = "${table}.num";
+ my ($self, $args) = @_;
+ my ($chart_id, $joins) = @$args{qw(chart_id joins)};
+ my $table = "longdescs_count_$chart_id";
+ my $extra = $self->_user->is_insider ? "" : "WHERE isprivate = 0";
+ my $join = {
+ table => "(SELECT bug_id, COUNT(*) AS num"
+ . " FROM longdescs $extra GROUP BY bug_id)",
+ as => $table,
+ };
+ push(@$joins, $join);
+ $args->{full_field} = "${table}.num";
}
sub _work_time_changedby {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)};
-
- my $table = "longdescs_$chart_id";
- push(@$joins, { table => 'longdescs', as => $table });
- my $user_id = $self->_get_user_id($value);
- $args->{term} = "$table.who = $user_id AND $table.work_time != 0";
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)};
+
+ my $table = "longdescs_$chart_id";
+ push(@$joins, {table => 'longdescs', as => $table});
+ my $user_id = $self->_get_user_id($value);
+ $args->{term} = "$table.who = $user_id AND $table.work_time != 0";
}
sub _work_time_changedbefore_after {
- my ($self, $args) = @_;
- my ($chart_id, $operator, $value, $joins) =
- @$args{qw(chart_id operator value joins)};
- my $dbh = Bugzilla->dbh;
-
- my $table = "longdescs_$chart_id";
- my $sql_operator = ($operator =~ /before/) ? '<=' : '>=';
- my $sql_date = $dbh->quote(SqlifyDate($value));
- my $join = {
- table => 'longdescs',
- as => $table,
- extra => ["$table.work_time != 0",
- "$table.bug_when $sql_operator $sql_date"],
- };
- push(@$joins, $join);
-
- $args->{term} = "$table.bug_when IS NOT NULL";
+ my ($self, $args) = @_;
+ my ($chart_id, $operator, $value, $joins)
+ = @$args{qw(chart_id operator value joins)};
+ my $dbh = Bugzilla->dbh;
+
+ my $table = "longdescs_$chart_id";
+ my $sql_operator = ($operator =~ /before/) ? '<=' : '>=';
+ my $sql_date = $dbh->quote(SqlifyDate($value));
+ my $join = {
+ table => 'longdescs',
+ as => $table,
+ extra => ["$table.work_time != 0", "$table.bug_when $sql_operator $sql_date"],
+ };
+ push(@$joins, $join);
+
+ $args->{term} = "$table.bug_when IS NOT NULL";
}
sub _work_time {
- my ($self, $args) = @_;
- $self->_add_extra_column('actual_time');
- $args->{full_field} = $self->COLUMNS->{actual_time}->{name};
+ my ($self, $args) = @_;
+ $self->_add_extra_column('actual_time');
+ $args->{full_field} = $self->COLUMNS->{actual_time}->{name};
}
sub _percentage_complete {
- my ($self, $args) = @_;
-
- $args->{full_field} = $self->COLUMNS->{percentage_complete}->{name};
+ my ($self, $args) = @_;
- # We need actual_time in _select_columns, otherwise we can't use
- # it in the expression for searching percentage_complete.
- $self->_add_extra_column('actual_time');
+ $args->{full_field} = $self->COLUMNS->{percentage_complete}->{name};
+
+ # We need actual_time in _select_columns, otherwise we can't use
+ # it in the expression for searching percentage_complete.
+ $self->_add_extra_column('actual_time');
}
sub _last_visit_ts {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
- $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name};
- $self->_add_extra_column('last_visit_ts');
+ $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name};
+ $self->_add_extra_column('last_visit_ts');
}
sub _last_visit_ts_invalid_operator {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
- ThrowUserError('search_field_operator_invalid',
- { field => $args->{field},
- operator => $args->{operator} });
+ ThrowUserError('search_field_operator_invalid',
+ {field => $args->{field}, operator => $args->{operator}});
}
sub _days_elapsed {
- my ($self, $args) = @_;
- my $dbh = Bugzilla->dbh;
-
- $args->{full_field} = "(" . $dbh->sql_to_days('NOW()') . " - " .
- $dbh->sql_to_days('bugs.delta_ts') . ")";
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $args->{full_field}
+ = "("
+ . $dbh->sql_to_days('NOW()') . " - "
+ . $dbh->sql_to_days('bugs.delta_ts') . ")";
}
sub _component_nonchanged {
- my ($self, $args) = @_;
-
- $args->{full_field} = "components.name";
- $self->_do_operator_function($args);
- my $term = $args->{term};
- $args->{term} = build_subselect("bugs.component_id",
- "components.id", "components", $args->{term});
+ my ($self, $args) = @_;
+
+ $args->{full_field} = "components.name";
+ $self->_do_operator_function($args);
+ my $term = $args->{term};
+ $args->{term}
+ = build_subselect("bugs.component_id", "components.id", "components",
+ $args->{term});
}
sub _product_nonchanged {
- my ($self, $args) = @_;
-
- # Generate the restriction condition
- $args->{full_field} = "products.name";
- $self->_do_operator_function($args);
- my $term = $args->{term};
- $args->{term} = build_subselect("bugs.product_id",
- "products.id", "products", $term);
+ my ($self, $args) = @_;
+
+ # Generate the restriction condition
+ $args->{full_field} = "products.name";
+ $self->_do_operator_function($args);
+ my $term = $args->{term};
+ $args->{term}
+ = build_subselect("bugs.product_id", "products.id", "products", $term);
}
sub _alias_nonchanged {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
- $args->{full_field} = "bugs_aliases.alias";
- $self->_do_operator_function($args);
- $args->{term} = build_subselect("bugs.bug_id",
- "bugs_aliases.bug_id", "bugs_aliases", $args->{term});
+ $args->{full_field} = "bugs_aliases.alias";
+ $self->_do_operator_function($args);
+ $args->{term}
+ = build_subselect("bugs.bug_id", "bugs_aliases.bug_id", "bugs_aliases",
+ $args->{term});
}
sub _classification_nonchanged {
- my ($self, $args) = @_;
- my $joins = $args->{joins};
-
- # This joins the right tables for us.
- $self->_add_extra_column('product');
-
- # Generate the restriction condition
- $args->{full_field} = "classifications.name";
- $self->_do_operator_function($args);
- my $term = $args->{term};
- $args->{term} = build_subselect("map_product.classification_id",
- "classifications.id", "classifications", $term);
+ my ($self, $args) = @_;
+ my $joins = $args->{joins};
+
+ # This joins the right tables for us.
+ $self->_add_extra_column('product');
+
+ # Generate the restriction condition
+ $args->{full_field} = "classifications.name";
+ $self->_do_operator_function($args);
+ my $term = $args->{term};
+ $args->{term} = build_subselect("map_product.classification_id",
+ "classifications.id", "classifications", $term);
}
sub _nullable {
- my ($self, $args) = @_;
- my $field = $args->{full_field};
- $args->{full_field} = "COALESCE($field, '')";
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+ $args->{full_field} = "COALESCE($field, '')";
}
sub _nullable_int {
- my ($self, $args) = @_;
- my $field = $args->{full_field};
- $args->{full_field} = "COALESCE($field, 0)";
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+ $args->{full_field} = "COALESCE($field, 0)";
}
sub _nullable_datetime {
- my ($self, $args) = @_;
- my $field = $args->{full_field};
- my $empty = Bugzilla->dbh->quote(EMPTY_DATETIME);
- $args->{full_field} = "COALESCE($field, $empty)";
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+ my $empty = Bugzilla->dbh->quote(EMPTY_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)";
+ 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};
- # This makes "equals" searches work on all DBs (even on MySQL, which
- # has a bug: http://bugs.mysql.com/bug.php?id=60324).
- $args->{full_field} = Bugzilla->dbh->sql_date_format($field, '%Y-%m-%d');
- $self->_nullable_datetime($args);
+ my ($self, $args) = @_;
+ my $field = $args->{full_field};
+
+ # This makes "equals" searches work on all DBs (even on MySQL, which
+ # has a bug: http://bugs.mysql.com/bug.php?id=60324).
+ $args->{full_field} = Bugzilla->dbh->sql_date_format($field, '%Y-%m-%d');
+ $self->_nullable_datetime($args);
}
sub _owner_idle_time_greater_less {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $value, $operator) =
- @$args{qw(chart_id joins value operator)};
- my $dbh = Bugzilla->dbh;
-
- my $table = "idle_$chart_id";
- my $quoted = $dbh->quote(SqlifyDate($value));
-
- my $ld_table = "comment_$table";
- my $act_table = "activity_$table";
- my $comments_join = {
- table => 'longdescs',
- as => $ld_table,
- from => 'assigned_to',
- to => 'who',
- extra => ["$ld_table.bug_when > $quoted"],
- };
- my $activity_join = {
- table => 'bugs_activity',
- as => $act_table,
- from => 'assigned_to',
- to => 'who',
- extra => ["$act_table.bug_when > $quoted"]
- };
-
- push(@$joins, $comments_join, $activity_join);
-
- if ($operator =~ /greater/) {
- $args->{term} =
- "$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)";
- }
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $value, $operator)
+ = @$args{qw(chart_id joins value operator)};
+ my $dbh = Bugzilla->dbh;
+
+ my $table = "idle_$chart_id";
+ my $quoted = $dbh->quote(SqlifyDate($value));
+
+ my $ld_table = "comment_$table";
+ my $act_table = "activity_$table";
+ my $comments_join = {
+ table => 'longdescs',
+ as => $ld_table,
+ from => 'assigned_to',
+ to => 'who',
+ extra => ["$ld_table.bug_when > $quoted"],
+ };
+ my $activity_join = {
+ table => 'bugs_activity',
+ as => $act_table,
+ from => 'assigned_to',
+ to => 'who',
+ extra => ["$act_table.bug_when > $quoted"]
+ };
+
+ push(@$joins, $comments_join, $activity_join);
+
+ if ($operator =~ /greater/) {
+ $args->{term} = "$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)";
+ }
}
sub _multiselect_negative {
- my ($self, $args) = @_;
- my ($field, $operator) = @$args{qw(field operator)};
+ my ($self, $args) = @_;
+ my ($field, $operator) = @$args{qw(field operator)};
- $args->{operator} = $self->_reverse_operator($operator);
- $args->{term} = $self->_multiselect_term($args, 1);
+ $args->{operator} = $self->_reverse_operator($operator);
+ $args->{term} = $self->_multiselect_term($args, 1);
}
sub _multiselect_multiple {
- my ($self, $args) = @_;
- my ($chart_id, $field, $operator, $value)
- = @$args{qw(chart_id field operator value)};
- my $dbh = Bugzilla->dbh;
-
- # We want things like "cf_multi_select=two+words" to still be
- # considered a search for two separate words, unless we're using
- # anyexact. (_all_values would consider that to be one "word" with a
- # space in it, because it's not in the Boolean Charts).
- my @words = $operator eq 'anyexact' ? $self->_all_values($args)
- : split(/[\s,]+/, $value);
-
- my @terms;
- foreach my $word (@words) {
- next if $word eq '';
- $args->{value} = $word;
- $args->{quoted} = $dbh->quote($word);
- push(@terms, $self->_multiselect_term($args));
- }
-
- # The spacing in the joins helps make the resulting SQL more readable.
- if ($operator =~ /^any/) {
- $args->{term} = join("\n OR ", @terms);
- }
- else {
- $args->{term} = join("\n AND ", @terms);
- }
+ my ($self, $args) = @_;
+ my ($chart_id, $field, $operator, $value)
+ = @$args{qw(chart_id field operator value)};
+ my $dbh = Bugzilla->dbh;
+
+ # We want things like "cf_multi_select=two+words" to still be
+ # considered a search for two separate words, unless we're using
+ # anyexact. (_all_values would consider that to be one "word" with a
+ # space in it, because it's not in the Boolean Charts).
+ my @words
+ = $operator eq 'anyexact'
+ ? $self->_all_values($args)
+ : split(/[\s,]+/, $value);
+
+ my @terms;
+ foreach my $word (@words) {
+ next if $word eq '';
+ $args->{value} = $word;
+ $args->{quoted} = $dbh->quote($word);
+ push(@terms, $self->_multiselect_term($args));
+ }
+
+ # The spacing in the joins helps make the resulting SQL more readable.
+ if ($operator =~ /^any/) {
+ $args->{term} = join("\n OR ", @terms);
+ }
+ else {
+ $args->{term} = join("\n AND ", @terms);
+ }
}
sub _flagtypes_nonchanged {
- my ($self, $args) = @_;
- 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.
- # If you search for "Flags" (does not contain) "approval+" we actually want
- # to return *bugs* that don't contain an approval+ flag. Without rewriting
- # the negation we'll search for *flags* which don't contain approval+.
- if ($operator =~ s/^not//) {
- $args->{operator} = $operator;
- $condition->operator($operator);
- $condition->negate(1);
- }
-
- my $subselect_args = {
- chart_id => $chart_id,
- sequence => $chart_id,
- field => 'flagtypes.name',
- full_field => $dbh->sql_string_concat("flagtypes_$chart_id.name", "flags_$chart_id.status"),
- operator => $operator,
- value => $value,
- 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};
-
- # don't call build_subselect as this must run as a true sub-select
- $args->{term} = "EXISTS (
+ my ($self, $args) = @_;
+ 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.
+ # If you search for "Flags" (does not contain) "approval+" we actually want
+ # to return *bugs* that don't contain an approval+ flag. Without rewriting
+ # the negation we'll search for *flags* which don't contain approval+.
+ if ($operator =~ s/^not//) {
+ $args->{operator} = $operator;
+ $condition->operator($operator);
+ $condition->negate(1);
+ }
+
+ my $subselect_args = {
+ chart_id => $chart_id,
+ sequence => $chart_id,
+ field => 'flagtypes.name',
+ full_field =>
+ $dbh->sql_string_concat("flagtypes_$chart_id.name", "flags_$chart_id.status"),
+ operator => $operator,
+ value => $value,
+ 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};
+
+ # don't call build_subselect as this must run as a true sub-select
+ $args->{term} = "EXISTS (
SELECT 1
FROM $bugs_table bugs_$chart_id
LEFT JOIN attachments AS attachments_$chart_id
@@ -2936,209 +2934,224 @@ sub _flagtypes_nonchanged {
}
sub _multiselect_nonchanged {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $field, $operator) =
- @$args{qw(chart_id joins field operator)};
- $args->{term} = $self->_multiselect_term($args)
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $field, $operator)
+ = @$args{qw(chart_id joins field operator)};
+ $args->{term} = $self->_multiselect_term($args);
}
sub _multiselect_table {
- my ($self, $args) = @_;
- my ($field, $chart_id) = @$args{qw(field chart_id)};
- my $dbh = Bugzilla->dbh;
-
- if ($field eq 'keywords') {
- $args->{full_field} = 'keyworddefs.name';
- return "keywords INNER JOIN keyworddefs".
- " ON keywords.keywordid = keyworddefs.id";
- }
- elsif ($field eq 'tag') {
- $args->{full_field} = 'tag.name';
- return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = "
- . ($self->_sharer_id || $self->_user->id);
- }
- elsif ($field eq 'bug_group') {
- $args->{full_field} = 'groups.name';
- return "bug_group_map INNER JOIN groups
+ my ($self, $args) = @_;
+ my ($field, $chart_id) = @$args{qw(field chart_id)};
+ my $dbh = Bugzilla->dbh;
+
+ if ($field eq 'keywords') {
+ $args->{full_field} = 'keyworddefs.name';
+ return "keywords INNER JOIN keyworddefs"
+ . " ON keywords.keywordid = keyworddefs.id";
+ }
+ elsif ($field eq 'tag') {
+ $args->{full_field} = 'tag.name';
+ return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = "
+ . ($self->_sharer_id || $self->_user->id);
+ }
+ elsif ($field eq 'bug_group') {
+ $args->{full_field} = 'groups.name';
+ return "bug_group_map INNER JOIN groups
ON bug_group_map.group_id = groups.id";
- }
- elsif ($field eq 'blocked' or $field eq 'dependson') {
- my $select = $field eq 'blocked' ? 'dependson' : 'blocked';
- $args->{_select_field} = $select;
- $args->{full_field} = $field;
- return "dependencies";
- }
- elsif ($field eq 'longdesc') {
- $args->{_extra_where} = " AND isprivate = 0"
- if !$self->_user->is_insider;
- $args->{full_field} = 'thetext';
- return "longdescs";
- }
- elsif ($field eq 'longdescs.isprivate') {
- ThrowUserError('auth_failure', { action => 'search',
- object => 'bug_fields',
- field => 'longdescs.isprivate' })
- if !$self->_user->is_insider;
- $args->{full_field} = 'isprivate';
- return "longdescs";
- }
- elsif ($field =~ /^attachments/) {
- $args->{_extra_where} = " AND isprivate = 0"
- if !$self->_user->is_insider;
- $field =~ /^attachments\.(.+)$/;
- $args->{full_field} = $1;
- return "attachments";
- }
- elsif ($field eq 'attach_data.thedata') {
- $args->{_extra_where} = " AND attachments.isprivate = 0"
- if !$self->_user->is_insider;
- return "attachments INNER JOIN attach_data "
- . " ON attachments.attach_id = attach_data.id"
- }
- elsif ($field eq 'comment_tag') {
- $args->{_extra_where} = " AND longdescs.isprivate = 0"
- if !$self->_user->is_insider;
- $args->{full_field} = 'longdescs_tags.tag';
- return "longdescs INNER JOIN longdescs_tags".
- " ON longdescs.comment_id = longdescs_tags.comment_id";
- }
- my $table = "bug_$field";
- $args->{full_field} = "bug_$field.value";
- return $table;
+ }
+ elsif ($field eq 'blocked' or $field eq 'dependson') {
+ my $select = $field eq 'blocked' ? 'dependson' : 'blocked';
+ $args->{_select_field} = $select;
+ $args->{full_field} = $field;
+ return "dependencies";
+ }
+ elsif ($field eq 'longdesc') {
+ $args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider;
+ $args->{full_field} = 'thetext';
+ return "longdescs";
+ }
+ elsif ($field eq 'longdescs.isprivate') {
+ ThrowUserError('auth_failure',
+ {action => 'search', object => 'bug_fields', field => 'longdescs.isprivate'})
+ if !$self->_user->is_insider;
+ $args->{full_field} = 'isprivate';
+ return "longdescs";
+ }
+ elsif ($field =~ /^attachments/) {
+ $args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider;
+ $field =~ /^attachments\.(.+)$/;
+ $args->{full_field} = $1;
+ return "attachments";
+ }
+ elsif ($field eq 'attach_data.thedata') {
+ $args->{_extra_where} = " AND attachments.isprivate = 0"
+ if !$self->_user->is_insider;
+ return "attachments INNER JOIN attach_data "
+ . " ON attachments.attach_id = attach_data.id";
+ }
+ elsif ($field eq 'comment_tag') {
+ $args->{_extra_where} = " AND longdescs.isprivate = 0"
+ if !$self->_user->is_insider;
+ $args->{full_field} = 'longdescs_tags.tag';
+ return "longdescs INNER JOIN longdescs_tags"
+ . " ON longdescs.comment_id = longdescs_tags.comment_id";
+ }
+ my $table = "bug_$field";
+ $args->{full_field} = "bug_$field.value";
+ return $table;
}
sub _multiselect_term {
- my ($self, $args, $not) = @_;
- my ($operator) = $args->{operator};
- my $value = $args->{value} || '';
- # 'empty' operators require special handling
- return $self->_multiselect_isempty($args, $not)
- if ($operator =~ /^is(not)?empty$/ || $value eq '---');
- 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("$args->{bugs_table}.bug_id", $select, $table, $term, $not);
+ my ($self, $args, $not) = @_;
+ my ($operator) = $args->{operator};
+ my $value = $args->{value} || '';
+
+ # 'empty' operators require special handling
+ return $self->_multiselect_isempty($args, $not)
+ if ($operator =~ /^is(not)?empty$/ || $value eq '---');
+ 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("$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->_reverse_operator($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";
- }
- elsif ($self->_multi_select_fields->{$field}) {
- push @$joins, {
- table => "bug_$field",
- as => "bug_${field}_$chart_id",
- from => 'bug_id',
- to => 'bug_id',
- };
- return "bug_${field}_$chart_id.bug_id IS $not NULL";
+ my ($self, $args, $not) = @_;
+ my ($field, $operator, $joins, $chart_id)
+ = @$args{qw(field operator joins chart_id)};
+ my $dbh = Bugzilla->dbh;
+ $operator = $self->_reverse_operator($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";
+ }
+ elsif ($self->_multi_select_fields->{$field}) {
+ push @$joins,
+ {
+ table => "bug_$field",
+ as => "bug_${field}_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "bug_${field}_$chart_id.bug_id IS $not NULL";
+ }
}
###############################
@@ -3146,234 +3159,236 @@ sub _multiselect_isempty {
###############################
sub _simple_operator {
- my ($self, $args) = @_;
- my ($full_field, $quoted, $operator) =
- @$args{qw(full_field quoted operator)};
- my $sql_operator = SIMPLE_OPERATORS->{$operator};
- $args->{term} = "$full_field $sql_operator $quoted";
+ my ($self, $args) = @_;
+ my ($full_field, $quoted, $operator) = @$args{qw(full_field quoted operator)};
+ my $sql_operator = SIMPLE_OPERATORS->{$operator};
+ $args->{term} = "$full_field $sql_operator $quoted";
}
sub _casesubstring {
- my ($self, $args) = @_;
- my ($full_field, $value) = @$args{qw(full_field value)};
- my $dbh = Bugzilla->dbh;
+ my ($self, $args) = @_;
+ my ($full_field, $value) = @$args{qw(full_field value)};
+ my $dbh = Bugzilla->dbh;
- $args->{term} = $dbh->sql_like($value, $full_field);
+ $args->{term} = $dbh->sql_like($value, $full_field);
}
sub _substring {
- my ($self, $args) = @_;
- my ($full_field, $value) = @$args{qw(full_field value)};
- my $dbh = Bugzilla->dbh;
+ my ($self, $args) = @_;
+ my ($full_field, $value) = @$args{qw(full_field value)};
+ my $dbh = Bugzilla->dbh;
- $args->{term} = $dbh->sql_ilike($value, $full_field);
+ $args->{term} = $dbh->sql_ilike($value, $full_field);
}
sub _notsubstring {
- my ($self, $args) = @_;
- my ($full_field, $value) = @$args{qw(full_field value)};
- my $dbh = Bugzilla->dbh;
+ my ($self, $args) = @_;
+ my ($full_field, $value) = @$args{qw(full_field value)};
+ my $dbh = Bugzilla->dbh;
- $args->{term} = $dbh->sql_not_ilike($value, $full_field);
+ $args->{term} = $dbh->sql_not_ilike($value, $full_field);
}
sub _regexp {
- my ($self, $args) = @_;
- my ($full_field, $quoted) = @$args{qw(full_field quoted)};
- my $dbh = Bugzilla->dbh;
-
- $args->{term} = $dbh->sql_regexp($full_field, $quoted);
+ my ($self, $args) = @_;
+ my ($full_field, $quoted) = @$args{qw(full_field quoted)};
+ my $dbh = Bugzilla->dbh;
+
+ $args->{term} = $dbh->sql_regexp($full_field, $quoted);
}
sub _notregexp {
- my ($self, $args) = @_;
- my ($full_field, $quoted) = @$args{qw(full_field quoted)};
- my $dbh = Bugzilla->dbh;
-
- $args->{term} = $dbh->sql_not_regexp($full_field, $quoted);
+ my ($self, $args) = @_;
+ my ($full_field, $quoted) = @$args{qw(full_field quoted)};
+ my $dbh = Bugzilla->dbh;
+
+ $args->{term} = $dbh->sql_not_regexp($full_field, $quoted);
}
sub _anyexact {
- my ($self, $args) = @_;
- my ($field, $full_field) = @$args{qw(field full_field)};
- my $dbh = Bugzilla->dbh;
-
- my @list = $self->_all_values($args, ',');
- @list = map { $self->_quote_unless_numeric($args, $_) } @list;
-
- if (@list) {
- $args->{term} = $dbh->sql_in($full_field, \@list);
- }
- else {
- $args->{term} = '';
- }
+ my ($self, $args) = @_;
+ my ($field, $full_field) = @$args{qw(field full_field)};
+ my $dbh = Bugzilla->dbh;
+
+ my @list = $self->_all_values($args, ',');
+ @list = map { $self->_quote_unless_numeric($args, $_) } @list;
+
+ if (@list) {
+ $args->{term} = $dbh->sql_in($full_field, \@list);
+ }
+ else {
+ $args->{term} = '';
+ }
}
sub _anywordsubstr {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
- my @terms = $self->_substring_terms($args);
- $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
+ my @terms = $self->_substring_terms($args);
+ $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
}
sub _allwordssubstr {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
- my @terms = $self->_substring_terms($args);
- $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
+ my @terms = $self->_substring_terms($args);
+ $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
}
sub _nowordssubstr {
- my ($self, $args) = @_;
- $self->_anywordsubstr($args);
- my $term = $args->{term};
- $args->{term} = "NOT($term)";
+ my ($self, $args) = @_;
+ $self->_anywordsubstr($args);
+ my $term = $args->{term};
+ $args->{term} = "NOT($term)";
}
sub _anywords {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
+
+ my @terms = $self->_word_terms($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} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
+ # 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} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
}
sub _allwords {
- my ($self, $args) = @_;
+ my ($self, $args) = @_;
- my @terms = $self->_word_terms($args);
- $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
+ my @terms = $self->_word_terms($args);
+ $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
}
sub _nowords {
- my ($self, $args) = @_;
- $self->_anywords($args);
- my $term = $args->{term};
- $args->{term} = "NOT($term)";
+ my ($self, $args) = @_;
+ $self->_anywords($args);
+ my $term = $args->{term};
+ $args->{term} = "NOT($term)";
}
sub _changedbefore_changedafter {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $field, $operator, $value) =
- @$args{qw(chart_id joins field operator value)};
- my $dbh = Bugzilla->dbh;
-
- my $field_object = $self->_chart_fields->{$field}
- || ThrowCodeError("invalid_field_name", { field => $field });
-
- # Asking when creation_ts changed is just asking when the bug was created.
- if ($field_object->name eq 'creation_ts') {
- $args->{operator} =
- $operator eq 'changedbefore' ? 'lessthaneq' : 'greaterthaneq';
- return $self->_do_operator_function($args);
- }
-
- my $sql_operator = ($operator =~ /before/) ? '<=' : '>=';
- my $field_id = $field_object->id;
- # Charts on changed* fields need to be field-specific. Otherwise,
- # OR chart rows make no sense if they contain multiple fields.
- my $table = "act_${field_id}_$chart_id";
-
- my $sql_date = $dbh->quote(SqlifyDate($value));
- my $join = {
- table => 'bugs_activity',
- as => $table,
- extra => ["$table.fieldid = $field_id",
- "$table.bug_when $sql_operator $sql_date"],
- };
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $field, $operator, $value)
+ = @$args{qw(chart_id joins field operator value)};
+ my $dbh = Bugzilla->dbh;
- $args->{term} = "$table.bug_when IS NOT NULL";
- $self->_changed_security_check($args, $join);
- push(@$joins, $join);
+ my $field_object = $self->_chart_fields->{$field}
+ || ThrowCodeError("invalid_field_name", {field => $field});
+
+ # Asking when creation_ts changed is just asking when the bug was created.
+ if ($field_object->name eq 'creation_ts') {
+ $args->{operator}
+ = $operator eq 'changedbefore' ? 'lessthaneq' : 'greaterthaneq';
+ return $self->_do_operator_function($args);
+ }
+
+ my $sql_operator = ($operator =~ /before/) ? '<=' : '>=';
+ my $field_id = $field_object->id;
+
+ # Charts on changed* fields need to be field-specific. Otherwise,
+ # OR chart rows make no sense if they contain multiple fields.
+ my $table = "act_${field_id}_$chart_id";
+
+ my $sql_date = $dbh->quote(SqlifyDate($value));
+ my $join = {
+ table => 'bugs_activity',
+ as => $table,
+ extra =>
+ ["$table.fieldid = $field_id", "$table.bug_when $sql_operator $sql_date"],
+ };
+
+ $args->{term} = "$table.bug_when IS NOT NULL";
+ $self->_changed_security_check($args, $join);
+ push(@$joins, $join);
}
sub _changedfrom_changedto {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $field, $operator, $quoted) =
- @$args{qw(chart_id joins field operator quoted)};
-
- my $column = ($operator =~ /from/) ? 'removed' : 'added';
- my $field_object = $self->_chart_fields->{$field}
- || ThrowCodeError("invalid_field_name", { field => $field });
- my $field_id = $field_object->id;
- my $table = "act_${field_id}_$chart_id";
- my $join = {
- table => 'bugs_activity',
- as => $table,
- extra => ["$table.fieldid = $field_id",
- "$table.$column = $quoted"],
- };
-
- $args->{term} = "$table.bug_when IS NOT NULL";
- $self->_changed_security_check($args, $join);
- push(@$joins, $join);
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $field, $operator, $quoted)
+ = @$args{qw(chart_id joins field operator quoted)};
+
+ my $column = ($operator =~ /from/) ? 'removed' : 'added';
+ my $field_object = $self->_chart_fields->{$field}
+ || ThrowCodeError("invalid_field_name", {field => $field});
+ my $field_id = $field_object->id;
+ my $table = "act_${field_id}_$chart_id";
+ my $join = {
+ table => 'bugs_activity',
+ as => $table,
+ extra => ["$table.fieldid = $field_id", "$table.$column = $quoted"],
+ };
+
+ $args->{term} = "$table.bug_when IS NOT NULL";
+ $self->_changed_security_check($args, $join);
+ push(@$joins, $join);
}
sub _changedby {
- my ($self, $args) = @_;
- my ($chart_id, $joins, $field, $operator, $value) =
- @$args{qw(chart_id joins field operator value)};
-
- my $field_object = $self->_chart_fields->{$field}
- || ThrowCodeError("invalid_field_name", { field => $field });
- my $field_id = $field_object->id;
- my $table = "act_${field_id}_$chart_id";
- my $user_id = $self->_get_user_id($value);
- my $join = {
- table => 'bugs_activity',
- as => $table,
- extra => ["$table.fieldid = $field_id",
- "$table.who = $user_id"],
- };
-
- $args->{term} = "$table.bug_when IS NOT NULL";
- $self->_changed_security_check($args, $join);
- push(@$joins, $join);
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $field, $operator, $value)
+ = @$args{qw(chart_id joins field operator value)};
+
+ my $field_object = $self->_chart_fields->{$field}
+ || ThrowCodeError("invalid_field_name", {field => $field});
+ my $field_id = $field_object->id;
+ my $table = "act_${field_id}_$chart_id";
+ my $user_id = $self->_get_user_id($value);
+ my $join = {
+ table => 'bugs_activity',
+ as => $table,
+ extra => ["$table.fieldid = $field_id", "$table.who = $user_id"],
+ };
+
+ $args->{term} = "$table.bug_when IS NOT NULL";
+ $self->_changed_security_check($args, $join);
+ push(@$joins, $join);
}
sub _changed_security_check {
- my ($self, $args, $join) = @_;
- my ($chart_id, $field) = @$args{qw(chart_id field)};
-
- my $field_object = $self->_chart_fields->{$field}
- || ThrowCodeError("invalid_field_name", { field => $field });
- my $field_id = $field_object->id;
-
- # If the user is not part of the insiders group, they cannot see
- # changes to attachments (including attachment flags) that are private
- if ($field =~ /^(?:flagtypes\.name$|attach)/ and !$self->_user->is_insider) {
- $join->{then_to} = {
- as => "attach_${field_id}_$chart_id",
- table => 'attachments',
- from => "act_${field_id}_$chart_id.attach_id",
- to => 'attach_id',
- };
-
- $args->{term} .= " AND COALESCE(attach_${field_id}_$chart_id.isprivate, 0) = 0";
- }
+ my ($self, $args, $join) = @_;
+ my ($chart_id, $field) = @$args{qw(chart_id field)};
+
+ my $field_object = $self->_chart_fields->{$field}
+ || ThrowCodeError("invalid_field_name", {field => $field});
+ my $field_id = $field_object->id;
+
+ # If the user is not part of the insiders group, they cannot see
+ # changes to attachments (including attachment flags) that are private
+ if ($field =~ /^(?:flagtypes\.name$|attach)/ and !$self->_user->is_insider) {
+ $join->{then_to} = {
+ as => "attach_${field_id}_$chart_id",
+ table => 'attachments',
+ from => "act_${field_id}_$chart_id.attach_id",
+ to => 'attach_id',
+ };
+
+ $args->{term} .= " AND COALESCE(attach_${field_id}_$chart_id.isprivate, 0) = 0";
+ }
}
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});
+ 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});
+ 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 "''";
+ 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 "''";
}
######################
@@ -3381,46 +3396,47 @@ sub _empty_value {
######################
# Validate that the query type is one we can deal with
-sub IsValidQueryType
-{
- my ($queryType) = @_;
- if (grep { $_ eq $queryType } qw(specific advanced)) {
- return 1;
- }
- return 0;
+sub IsValidQueryType {
+ my ($queryType) = @_;
+ if (grep { $_ eq $queryType } qw(specific advanced)) {
+ return 1;
+ }
+ return 0;
}
# Splits out "asc|desc" from a sort order item.
sub split_order_term {
- my $fragment = shift;
- $fragment =~ /^(.+?)(?:\s+(ASC|DESC))?$/i;
- my ($column_name, $direction) = (lc($1), uc($2 || ''));
- return wantarray ? ($column_name, $direction) : $column_name;
+ my $fragment = shift;
+ $fragment =~ /^(.+?)(?:\s+(ASC|DESC))?$/i;
+ my ($column_name, $direction) = (lc($1), uc($2 || ''));
+ return wantarray ? ($column_name, $direction) : $column_name;
}
# Used to translate old SQL fragments from buglist.cgi's "order" argument
# into our modern field IDs.
sub _translate_old_column {
- my ($self, $column) = @_;
- # All old SQL fragments have a period in them somewhere.
- return $column if $column !~ /\./;
+ my ($self, $column) = @_;
- if ($column =~ /\bAS\s+(\w+)$/i) {
- return $1;
- }
- # product, component, classification, assigned_to, qa_contact, reporter
- elsif ($column =~ /map_(\w+?)s?\.(login_)?name/i) {
- return $1;
- }
-
- # 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 (%{ $self->COLUMNS }) {
- next unless exists $self->COLUMNS->{$key}->{name};
- return $key if $self->COLUMNS->{$key}->{name} eq $column;
- }
+ # All old SQL fragments have a period in them somewhere.
+ return $column if $column !~ /\./;
+
+ if ($column =~ /\bAS\s+(\w+)$/i) {
+ return $1;
+ }
+
+ # product, component, classification, assigned_to, qa_contact, reporter
+ elsif ($column =~ /map_(\w+?)s?\.(login_)?name/i) {
+ return $1;
+ }
+
+ # 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 (%{$self->COLUMNS}) {
+ next unless exists $self->COLUMNS->{$key}->{name};
+ return $key if $self->COLUMNS->{$key}->{name} eq $column;
+ }
- return $column;
+ return $column;
}
1;