diff options
author | Perl Tidy <perltidy@bugzilla.org> | 2018-12-05 21:38:52 +0100 |
---|---|---|
committer | Dylan William Hardison <dylan@hardison.net> | 2018-12-05 23:49:08 +0100 |
commit | 8ec8da0491ad89604700b3e29a227966f6d84ba1 (patch) | |
tree | 9d270f173330ca19700e0ba9f2ee931300646de1 /Bugzilla/Search | |
parent | a7bb5a65b71644d9efce5fed783ed545b9336548 (diff) | |
download | bugzilla-8ec8da0491ad89604700b3e29a227966f6d84ba1.tar.gz bugzilla-8ec8da0491ad89604700b3e29a227966f6d84ba1.tar.xz |
no bug - reformat all the code using the new perltidy rules
Diffstat (limited to 'Bugzilla/Search')
-rw-r--r-- | Bugzilla/Search/Clause.pm | 170 | ||||
-rw-r--r-- | Bugzilla/Search/ClauseGroup.pm | 121 | ||||
-rw-r--r-- | Bugzilla/Search/Condition.pm | 70 | ||||
-rw-r--r-- | Bugzilla/Search/Quicksearch.pm | 1148 | ||||
-rw-r--r-- | Bugzilla/Search/Recent.pm | 116 | ||||
-rw-r--r-- | Bugzilla/Search/Saved.pm | 369 |
6 files changed, 1021 insertions, 973 deletions
diff --git a/Bugzilla/Search/Clause.pm b/Bugzilla/Search/Clause.pm index 4426ea576..b0eaddeb0 100644 --- a/Bugzilla/Search/Clause.pm +++ b/Bugzilla/Search/Clause.pm @@ -16,121 +16,123 @@ use Bugzilla::Search::Condition qw(condition); use Bugzilla::Util qw(trick_taint); sub new { - my ($class, $joiner) = @_; - if ($joiner and $joiner ne 'OR' and $joiner ne 'AND') { - ThrowCodeError('search_invalid_joiner', { joiner => $joiner }); - } - # This will go into SQL directly so needs to be untainted. - trick_taint($joiner) if $joiner; - bless { joiner => $joiner || 'AND' }, $class; + my ($class, $joiner) = @_; + if ($joiner and $joiner ne 'OR' and $joiner ne 'AND') { + ThrowCodeError('search_invalid_joiner', {joiner => $joiner}); + } + + # This will go into SQL directly so needs to be untainted. + trick_taint($joiner) if $joiner; + bless {joiner => $joiner || 'AND'}, $class; } sub children { - my ($self) = @_; - $self->{children} ||= []; - return $self->{children}; + my ($self) = @_; + $self->{children} ||= []; + return $self->{children}; } sub update_search_args { - my ($self, $search_args) = @_; - # abstract + my ($self, $search_args) = @_; + + # abstract } sub joiner { return $_[0]->{joiner} } sub has_translated_conditions { - my ($self) = @_; - my $children = $self->children; - return 1 if grep { $_->isa('Bugzilla::Search::Condition') - && $_->translated } @$children; - foreach my $child (@$children) { - next if $child->isa('Bugzilla::Search::Condition'); - return 1 if $child->has_translated_conditions; - } - return 0; + my ($self) = @_; + my $children = $self->children; + return 1 + if grep { $_->isa('Bugzilla::Search::Condition') && $_->translated } + @$children; + foreach my $child (@$children) { + next if $child->isa('Bugzilla::Search::Condition'); + return 1 if $child->has_translated_conditions; + } + return 0; } sub add { - my $self = shift; - my $children = $self->children; - if (@_ == 3) { - push(@$children, condition(@_)); - return; - } - - my ($child) = @_; - return if !defined $child; - $child->isa(__PACKAGE__) || $child->isa('Bugzilla::Search::Condition') - || die 'child not the right type: ' . $child; - push(@{ $self->children }, $child); + my $self = shift; + my $children = $self->children; + if (@_ == 3) { + push(@$children, condition(@_)); + return; + } + + my ($child) = @_; + return if !defined $child; + $child->isa(__PACKAGE__) + || $child->isa('Bugzilla::Search::Condition') + || die 'child not the right type: ' . $child; + push(@{$self->children}, $child); } sub negate { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{negate} = $value ? 1 : 0; - } - return $self->{negate}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{negate} = $value ? 1 : 0; + } + return $self->{negate}; } sub walk_conditions { - my ($self, $callback) = @_; - foreach my $child (@{ $self->children }) { - if ($child->isa('Bugzilla::Search::Condition')) { - $callback->($self, $child); - } - else { - $child->walk_conditions($callback); - } + my ($self, $callback) = @_; + foreach my $child (@{$self->children}) { + if ($child->isa('Bugzilla::Search::Condition')) { + $callback->($self, $child); + } + else { + $child->walk_conditions($callback); } + } } sub as_string { - my ($self) = @_; - if (!$self->{sql}) { - my @strings; - foreach my $child (@{ $self->children }) { - next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions; - next if $child->isa('Bugzilla::Search::Condition') - && !$child->translated; - - my $string = $child->as_string; - next unless $string; - if ($self->joiner eq 'AND') { - $string = "( $string )" if $string =~ /OR/; - } - else { - $string = "( $string )" if $string =~ /AND/; - } - push(@strings, $string); - } - - my $sql = join(' ' . $self->joiner . ' ', @strings); - $sql = "NOT( $sql )" if $sql && $self->negate; - $self->{sql} = $sql; + my ($self) = @_; + if (!$self->{sql}) { + my @strings; + foreach my $child (@{$self->children}) { + next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions; + next if $child->isa('Bugzilla::Search::Condition') && !$child->translated; + + my $string = $child->as_string; + next unless $string; + if ($self->joiner eq 'AND') { + $string = "( $string )" if $string =~ /OR/; + } + else { + $string = "( $string )" if $string =~ /AND/; + } + push(@strings, $string); } - return $self->{sql}; + + my $sql = join(' ' . $self->joiner . ' ', @strings); + $sql = "NOT( $sql )" if $sql && $self->negate; + $self->{sql} = $sql; + } + return $self->{sql}; } # Search.pm converts URL parameters to Clause objects. This helps do the # reverse. sub as_params { - my ($self) = @_; - my @params; - foreach my $child (@{ $self->children }) { - if ($child->isa(__PACKAGE__)) { - my %open_paren = (f => 'OP', n => scalar $child->negate, - j => $child->joiner); - push(@params, \%open_paren); - push(@params, $child->as_params); - my %close_paren = (f => 'CP'); - push(@params, \%close_paren); - } - else { - push(@params, $child->as_params); - } + my ($self) = @_; + my @params; + foreach my $child (@{$self->children}) { + if ($child->isa(__PACKAGE__)) { + my %open_paren = (f => 'OP', n => scalar $child->negate, j => $child->joiner); + push(@params, \%open_paren); + push(@params, $child->as_params); + my %close_paren = (f => 'CP'); + push(@params, \%close_paren); + } + else { + push(@params, $child->as_params); } - return @params; + } + return @params; } 1; diff --git a/Bugzilla/Search/ClauseGroup.pm b/Bugzilla/Search/ClauseGroup.pm index c7d3e6ae7..5c063a803 100644 --- a/Bugzilla/Search/ClauseGroup.pm +++ b/Bugzilla/Search/ClauseGroup.pm @@ -19,79 +19,82 @@ use Bugzilla::Util qw(trick_taint); use List::MoreUtils qw(uniq); use constant UNSUPPORTED_FIELDS => qw( - classification - commenter - component - longdescs.count - product - owner_idle_time + classification + commenter + component + longdescs.count + product + owner_idle_time ); sub new { - my ($class) = @_; - my $self = bless({ joiner => 'AND' }, $class); - # Add a join back to the bugs table which will be used to group conditions - # for this clause - my $condition = Bugzilla::Search::Condition->new({}); - $condition->translated({ - joins => [{ - table => 'bugs', - as => 'bugs_g0', - from => 'bug_id', - to => 'bug_id', - extra => [], - }], - term => '1 = 1', - }); - $self->SUPER::add($condition); - $self->{group_condition} = $condition; - return $self; + my ($class) = @_; + my $self = bless({joiner => 'AND'}, $class); + + # Add a join back to the bugs table which will be used to group conditions + # for this clause + my $condition = Bugzilla::Search::Condition->new({}); + $condition->translated({ + joins => [{ + table => 'bugs', + as => 'bugs_g0', + from => 'bug_id', + to => 'bug_id', + extra => [], + }], + term => '1 = 1', + }); + $self->SUPER::add($condition); + $self->{group_condition} = $condition; + return $self; } sub add { - my ($self, @args) = @_; - my $field = scalar(@args) == 3 ? $args[0] : $args[0]->{field}; - - # We don't support nesting of conditions under this clause - if (scalar(@args) == 1 && !$args[0]->isa('Bugzilla::Search::Condition')) { - ThrowUserError('search_grouped_invalid_nesting'); - } - - # Ensure all conditions use the same field - if (!$self->{_field}) { - $self->{_field} = $field; - } elsif ($field ne $self->{_field}) { - ThrowUserError('search_grouped_field_mismatch'); - } - - # Unsupported fields - if (grep { $_ eq $field } UNSUPPORTED_FIELDS ) { - ThrowUserError('search_grouped_field_invalid', { field => $field }); - } - - $self->SUPER::add(@args); + my ($self, @args) = @_; + my $field = scalar(@args) == 3 ? $args[0] : $args[0]->{field}; + + # We don't support nesting of conditions under this clause + if (scalar(@args) == 1 && !$args[0]->isa('Bugzilla::Search::Condition')) { + ThrowUserError('search_grouped_invalid_nesting'); + } + + # Ensure all conditions use the same field + if (!$self->{_field}) { + $self->{_field} = $field; + } + elsif ($field ne $self->{_field}) { + ThrowUserError('search_grouped_field_mismatch'); + } + + # Unsupported fields + if (grep { $_ eq $field } UNSUPPORTED_FIELDS) { + ThrowUserError('search_grouped_field_invalid', {field => $field}); + } + + $self->SUPER::add(@args); } sub update_search_args { - my ($self, $search_args) = @_; + my ($self, $search_args) = @_; - # No need to change things if there's only one child condition - return unless scalar(@{ $self->children }) > 1; + # No need to change things if there's only one child condition + return unless scalar(@{$self->children}) > 1; - # we want all the terms to use the same join table - if (!exists $self->{_first_chart_id}) { - $self->{_first_chart_id} = $search_args->{chart_id}; - } else { - $search_args->{chart_id} = $self->{_first_chart_id}; - } + # we want all the terms to use the same join table + if (!exists $self->{_first_chart_id}) { + $self->{_first_chart_id} = $search_args->{chart_id}; + } + else { + $search_args->{chart_id} = $self->{_first_chart_id}; + } - my $suffix = '_g' . $self->{_first_chart_id}; - $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix"; + my $suffix = '_g' . $self->{_first_chart_id}; + $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix"; - $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./; + $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./; - $search_args->{table_suffix} = $suffix; - $search_args->{bugs_table} = "bugs$suffix"; + $search_args->{table_suffix} = $suffix; + $search_args->{bugs_table} = "bugs$suffix"; } 1; diff --git a/Bugzilla/Search/Condition.pm b/Bugzilla/Search/Condition.pm index 1c38c1f3e..33b0bfd8b 100644 --- a/Bugzilla/Search/Condition.pm +++ b/Bugzilla/Search/Condition.pm @@ -15,55 +15,59 @@ use base qw(Exporter); our @EXPORT_OK = qw(condition); sub new { - my ($class, $params) = @_; - my %self = %$params; - bless \%self, $class; - return \%self; + my ($class, $params) = @_; + my %self = %$params; + bless \%self, $class; + return \%self; } -sub field { return $_[0]->{field} } -sub value { return $_[0]->{value} } +sub field { return $_[0]->{field} } +sub value { return $_[0]->{value} } sub operator { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{operator} = $value; - } - return $self->{operator}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{operator} = $value; + } + return $self->{operator}; } sub fov { - my ($self) = @_; - return ($self->field, $self->operator, $self->value); + my ($self) = @_; + return ($self->field, $self->operator, $self->value); } sub translated { - my ($self, $params) = @_; - if (@_ == 2) { - $self->{translated} = $params; - } - return $self->{translated}; + my ($self, $params) = @_; + if (@_ == 2) { + $self->{translated} = $params; + } + return $self->{translated}; } sub as_string { - my ($self) = @_; - my $term = $self->translated->{term}; - $term = "NOT( $term )" if $term && $self->negate; - return $term; + my ($self) = @_; + my $term = $self->translated->{term}; + $term = "NOT( $term )" if $term && $self->negate; + return $term; } sub as_params { - my ($self) = @_; - return { f => $self->field, o => $self->operator, v => $self->value, - n => scalar $self->negate }; + my ($self) = @_; + return { + f => $self->field, + o => $self->operator, + v => $self->value, + n => scalar $self->negate + }; } sub negate { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{negate} = $value ? 1 : 0; - } - return $self->{negate}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{negate} = $value ? 1 : 0; + } + return $self->{negate}; } ########################### @@ -71,9 +75,9 @@ sub negate { ########################### sub condition { - my ($field, $operator, $value) = @_; - return __PACKAGE__->new({ field => $field, operator => $operator, - value => $value }); + my ($field, $operator, $value) = @_; + return __PACKAGE__->new( + {field => $field, operator => $operator, value => $value}); } 1; diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm index 6c0253e27..f2bb83e2e 100644 --- a/Bugzilla/Search/Quicksearch.pm +++ b/Bugzilla/Search/Quicksearch.pm @@ -27,256 +27,265 @@ use base qw(Exporter); # Custom mappings for some fields. use constant MAPPINGS => { - # Status, Resolution, Platform, OS, Priority, Severity - "status" => "bug_status", - "platform" => "rep_platform", - "os" => "op_sys", - "severity" => "bug_severity", - - # People: AssignedTo, Reporter, QA Contact, CC, etc. - "assignee" => "assigned_to", - "owner" => "assigned_to", - "mentor" => "bug_mentor", - - # Product, Version, Component, Target Milestone - "milestone" => "target_milestone", - - # Summary, Description, URL, Status whiteboard, Keywords - "summary" => "short_desc", - "description" => "longdesc", - "comment" => "longdesc", - "url" => "bug_file_loc", - "whiteboard" => "status_whiteboard", - "sw" => "status_whiteboard", - "kw" => "keywords", - "group" => "bug_group", - - # Flags - "flag" => "flagtypes.name", - "requestee" => "requestees.login_name", - "setter" => "setters.login_name", - - # Attachments - "attachment" => "attachments.description", - "attachmentdesc" => "attachments.description", - "attachdesc" => "attachments.description", - "attachmentmimetype" => "attachments.mimetype", - "attachmimetype" => "attachments.mimetype" + + # Status, Resolution, Platform, OS, Priority, Severity + "status" => "bug_status", + "platform" => "rep_platform", + "os" => "op_sys", + "severity" => "bug_severity", + + # People: AssignedTo, Reporter, QA Contact, CC, etc. + "assignee" => "assigned_to", + "owner" => "assigned_to", + "mentor" => "bug_mentor", + + # Product, Version, Component, Target Milestone + "milestone" => "target_milestone", + + # Summary, Description, URL, Status whiteboard, Keywords + "summary" => "short_desc", + "description" => "longdesc", + "comment" => "longdesc", + "url" => "bug_file_loc", + "whiteboard" => "status_whiteboard", + "sw" => "status_whiteboard", + "kw" => "keywords", + "group" => "bug_group", + + # Flags + "flag" => "flagtypes.name", + "requestee" => "requestees.login_name", + "setter" => "setters.login_name", + + # Attachments + "attachment" => "attachments.description", + "attachmentdesc" => "attachments.description", + "attachdesc" => "attachments.description", + "attachmentmimetype" => "attachments.mimetype", + "attachmimetype" => "attachments.mimetype" }; sub FIELD_MAP { - my $cache = Bugzilla->request_cache; - return $cache->{quicksearch_fields} if $cache->{quicksearch_fields}; - - # Get all the fields whose names don't contain periods. (Fields that - # contain periods are always handled in MAPPINGS.) - my @db_fields = grep { $_->name !~ /\./ } - @{ Bugzilla->fields({ obsolete => 0 }) }; - my %full_map = (%{ MAPPINGS() }, map { $_->name => $_->name } @db_fields); - - # Eliminate the fields that start with bug_ or rep_, because those are - # handled by the MAPPINGS instead, and we don't want too many names - # for them. (Also, otherwise "rep" doesn't match "reporter".) - # - # Remove "status_whiteboard" because we have "whiteboard" for it in - # the mappings, and otherwise "stat" can't match "status". - # - # Also, don't allow searching the _accessible stuff via quicksearch - # (both because it's unnecessary and because otherwise - # "reporter_accessible" and "reporter" both match "rep". - delete @full_map{qw(rep_platform bug_status bug_file_loc bug_group - bug_severity bug_status - status_whiteboard - cclist_accessible reporter_accessible)}; - - Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map} ); - - $cache->{quicksearch_fields} = \%full_map; - - return $cache->{quicksearch_fields}; + my $cache = Bugzilla->request_cache; + return $cache->{quicksearch_fields} if $cache->{quicksearch_fields}; + + # Get all the fields whose names don't contain periods. (Fields that + # contain periods are always handled in MAPPINGS.) + my @db_fields = grep { $_->name !~ /\./ } @{Bugzilla->fields({obsolete => 0})}; + my %full_map = (%{MAPPINGS()}, map { $_->name => $_->name } @db_fields); + + # Eliminate the fields that start with bug_ or rep_, because those are + # handled by the MAPPINGS instead, and we don't want too many names + # for them. (Also, otherwise "rep" doesn't match "reporter".) + # + # Remove "status_whiteboard" because we have "whiteboard" for it in + # the mappings, and otherwise "stat" can't match "status". + # + # Also, don't allow searching the _accessible stuff via quicksearch + # (both because it's unnecessary and because otherwise + # "reporter_accessible" and "reporter" both match "rep". + delete @full_map{ + qw(rep_platform bug_status bug_file_loc bug_group + bug_severity bug_status + status_whiteboard + cclist_accessible reporter_accessible) + }; + + Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map}); + + $cache->{quicksearch_fields} = \%full_map; + + return $cache->{quicksearch_fields}; } # Certain fields, when specified like "field:value" get an operator other # than "substring" -use constant FIELD_OPERATOR => { - content => 'matches', - owner_idle_time => 'greaterthan', -}; +use constant FIELD_OPERATOR => + {content => 'matches', owner_idle_time => 'greaterthan',}; # Mappings for operators symbols to support operators other than "substring" use constant OPERATOR_SYMBOLS => { - ':' => 'substring', - '=' => 'equals', - '!=' => 'notequals', - '>=' => 'greaterthaneq', - '<=' => 'lessthaneq', - '>' => 'greaterthan', - '<' => 'lessthan', + ':' => 'substring', + '=' => 'equals', + '!=' => 'notequals', + '>=' => 'greaterthaneq', + '<=' => 'lessthaneq', + '>' => 'greaterthan', + '<' => 'lessthan', }; # We might want to put this into localconfig or somewhere use constant PRODUCT_EXCEPTIONS => ( - 'row', # [Browser] - # ^^^ - 'new', # [MailNews] - # ^^^ + 'row', # [Browser] + # ^^^ + 'new', # [MailNews] + # ^^^ ); use constant COMPONENT_EXCEPTIONS => ( - 'hang' # [Bugzilla: Component/Keyword Changes] - # ^^^^ + 'hang' # [Bugzilla: Component/Keyword Changes] + # ^^^^ ); # Quicksearch-wide globals for boolean charts. our ($chart, $and, $or, $fulltext, $bug_status_set, $ELASTIC); sub quicksearch { - my ($searchstring) = (@_); - my $cgi = Bugzilla->cgi; + my ($searchstring) = (@_); + my $cgi = Bugzilla->cgi; - $chart = 0; - $and = 0; - $or = 0; + $chart = 0; + $and = 0; + $or = 0; - # Remove leading and trailing commas and whitespace. - $searchstring =~ s/(^[\s,]+|[\s,]+$)//g; - ThrowUserError('buglist_parameters_required') unless ($searchstring); + # Remove leading and trailing commas and whitespace. + $searchstring =~ s/(^[\s,]+|[\s,]+$)//g; + ThrowUserError('buglist_parameters_required') unless ($searchstring); - if ($searchstring =~ m/^[0-9,\s]*$/) { - _bug_numbers_only($searchstring); - } - else { - _handle_alias($searchstring); - - # Retain backslashes and quotes, to know which strings are quoted, - # and which ones are not. - my @words = _parse_line('\s+', 1, $searchstring); - # If parse_line() returns no data, this means strings are badly quoted. - # Rather than trying to guess what the user wanted to do, we throw an error. - scalar(@words) - || ThrowUserError('quicksearch_unbalanced_quotes', - { string => $searchstring, quicksearch => $searchstring }); - - # A query cannot start with AND or OR, nor can it end with AND, OR or NOT. + if ($searchstring =~ m/^[0-9,\s]*$/) { + _bug_numbers_only($searchstring); + } + else { + _handle_alias($searchstring); + + # Retain backslashes and quotes, to know which strings are quoted, + # and which ones are not. + my @words = _parse_line('\s+', 1, $searchstring); + + # If parse_line() returns no data, this means strings are badly quoted. + # Rather than trying to guess what the user wanted to do, we throw an error. + scalar(@words) || ThrowUserError('quicksearch_unbalanced_quotes', + {string => $searchstring, quicksearch => $searchstring}); + + # A query cannot start with AND or OR, nor can it end with AND, OR or NOT. + ThrowUserError('quicksearch_invalid_query', {quicksearch => $searchstring}) + if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/); + + $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0; + + my (@qswords, @or_group); + while (scalar @words) { + my $word = shift @words; + + # AND is the default word separator, similar to a whitespace, + # but |a AND OR b| is not a valid combination. + if ($word eq 'AND') { ThrowUserError('quicksearch_invalid_query', - { quicksearch => $searchstring }) - if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/); - - $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0; - - my (@qswords, @or_group); - while (scalar @words) { - my $word = shift @words; - # AND is the default word separator, similar to a whitespace, - # but |a AND OR b| is not a valid combination. - if ($word eq 'AND') { - ThrowUserError('quicksearch_invalid_query', - { operators => ['AND', 'OR'], quicksearch => $searchstring }) - if $words[0] eq 'OR'; - } - # |a OR AND b| is not a valid combination. - # |a OR OR b| is equivalent to |a OR b| and so is harmless. - elsif ($word eq 'OR') { - ThrowUserError('quicksearch_invalid_query', - { operators => ['OR', 'AND'], quicksearch => $searchstring }) - if $words[0] eq 'AND'; - } - # NOT negates the following word. - # |NOT AND| and |NOT OR| are not valid combinations. - # |NOT NOT| is fine but has no effect as they cancel themselves. - elsif ($word eq 'NOT') { - $word = shift @words; - next if $word eq 'NOT'; - if ($word eq 'AND' || $word eq 'OR') { - ThrowUserError('quicksearch_invalid_query', - { operators => ['NOT', $word], quicksearch => $searchstring }); - } - unshift(@words, "-$word"); - } - # --comment and ++comment disable or enable fulltext searching - elsif ($word =~ /^(--|\+\+)comments?$/i) { - $fulltext = $1 eq '--' ? 0 : 1; - } - else { - # OR groups words together, as OR has higher precedence than AND. - push(@or_group, $word); - # If the next word is not OR, then we are not in a OR group, - # or we are leaving it. - if (!defined $words[0] || $words[0] ne 'OR') { - push(@qswords, join('|', @or_group)); - @or_group = (); - } - } - } + {operators => ['AND', 'OR'], quicksearch => $searchstring}) + if $words[0] eq 'OR'; + } - _handle_status_and_resolution($qswords[0]); - shift(@qswords) if $bug_status_set; - - my (@unknownFields, %ambiguous_fields); - - # Loop over all main-level QuickSearch words. - foreach my $qsword (@qswords) { - my @or_operand = _parse_line('\|', 1, $qsword); - foreach my $term (@or_operand) { - next unless defined $term; - my $negate = substr($term, 0, 1) eq '-'; - if ($negate) { - $term = substr($term, 1); - } - - next if _handle_special_first_chars($term, $negate); - next if _handle_field_names($term, $negate, \@unknownFields, - \%ambiguous_fields); - - # Having ruled out the special cases, we may now split - # by comma, which is another legal boolean OR indicator. - # Remove quotes from quoted words, if any. - @words = _parse_line(',', 0, $term); - foreach my $word (@words) { - if (!_special_field_syntax($word, $negate)) { - _default_quicksearch_word($word, $negate); - } - _handle_urls($word, $negate); - } - } - $chart++; - $and = 0; - $or = 0; + # |a OR AND b| is not a valid combination. + # |a OR OR b| is equivalent to |a OR b| and so is harmless. + elsif ($word eq 'OR') { + ThrowUserError('quicksearch_invalid_query', + {operators => ['OR', 'AND'], quicksearch => $searchstring}) + if $words[0] eq 'AND'; + } + + # NOT negates the following word. + # |NOT AND| and |NOT OR| are not valid combinations. + # |NOT NOT| is fine but has no effect as they cancel themselves. + elsif ($word eq 'NOT') { + $word = shift @words; + next if $word eq 'NOT'; + if ($word eq 'AND' || $word eq 'OR') { + ThrowUserError('quicksearch_invalid_query', + {operators => ['NOT', $word], quicksearch => $searchstring}); } - - # If there is no mention of a bug status, we restrict the query - # to open bugs by default. - unless ($bug_status_set) { - $cgi->param('bug_status', BUG_STATE_OPEN); + unshift(@words, "-$word"); + } + + # --comment and ++comment disable or enable fulltext searching + elsif ($word =~ /^(--|\+\+)comments?$/i) { + $fulltext = $1 eq '--' ? 0 : 1; + } + else { + # OR groups words together, as OR has higher precedence than AND. + push(@or_group, $word); + + # If the next word is not OR, then we are not in a OR group, + # or we are leaving it. + if (!defined $words[0] || $words[0] ne 'OR') { + push(@qswords, join('|', @or_group)); + @or_group = (); } + } + } + + _handle_status_and_resolution($qswords[0]); + shift(@qswords) if $bug_status_set; + + my (@unknownFields, %ambiguous_fields); - # Inform user about any unknown fields - if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) { - ThrowUserError("quicksearch_unknown_field", - { unknown => \@unknownFields, - ambiguous => \%ambiguous_fields, - quicksearch => $searchstring }); + # Loop over all main-level QuickSearch words. + foreach my $qsword (@qswords) { + my @or_operand = _parse_line('\|', 1, $qsword); + foreach my $term (@or_operand) { + next unless defined $term; + my $negate = substr($term, 0, 1) eq '-'; + if ($negate) { + $term = substr($term, 1); } - # Make sure we have some query terms left - scalar($cgi->param())>0 || ThrowUserError("buglist_parameters_required"); + next if _handle_special_first_chars($term, $negate); + next + if _handle_field_names($term, $negate, \@unknownFields, \%ambiguous_fields); + + # Having ruled out the special cases, we may now split + # by comma, which is another legal boolean OR indicator. + # Remove quotes from quoted words, if any. + @words = _parse_line(',', 0, $term); + foreach my $word (@words) { + if (!_special_field_syntax($word, $negate)) { + _default_quicksearch_word($word, $negate); + } + _handle_urls($word, $negate); + } + } + $chart++; + $and = 0; + $or = 0; + } + + # If there is no mention of a bug status, we restrict the query + # to open bugs by default. + unless ($bug_status_set) { + $cgi->param('bug_status', BUG_STATE_OPEN); + } + + # Inform user about any unknown fields + if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) { + ThrowUserError( + "quicksearch_unknown_field", + { + unknown => \@unknownFields, + ambiguous => \%ambiguous_fields, + quicksearch => $searchstring + } + ); } - # List of quicksearch-specific CGI parameters to get rid of. - my @params_to_strip = ('quicksearch', 'load', 'run'); - my $modified_query_string = $cgi->canonicalise_query(@params_to_strip); + # Make sure we have some query terms left + scalar($cgi->param()) > 0 || ThrowUserError("buglist_parameters_required"); + } - if ($cgi->param('load')) { - my $urlbase = Bugzilla->localconfig->{urlbase}; - # Param 'load' asks us to display the query in the advanced search form. - print $cgi->redirect(-uri => "${urlbase}query.cgi?format=advanced&" - . $modified_query_string); - } + # List of quicksearch-specific CGI parameters to get rid of. + my @params_to_strip = ('quicksearch', 'load', 'run'); + my $modified_query_string = $cgi->canonicalise_query(@params_to_strip); + + if ($cgi->param('load')) { + my $urlbase = Bugzilla->localconfig->{urlbase}; - # Otherwise, pass the modified query string to the caller. - # We modified $cgi->params, so the caller can choose to look at that, too, - # and disregard the return value. - $cgi->delete(@params_to_strip); - return $modified_query_string; + # Param 'load' asks us to display the query in the advanced search form. + print $cgi->redirect( + -uri => "${urlbase}query.cgi?format=advanced&" . $modified_query_string); + } + + # Otherwise, pass the modified query string to the caller. + # We modified $cgi->params, so the caller can choose to look at that, too, + # and disregard the return value. + $cgi->delete(@params_to_strip); + return $modified_query_string; } ########################## @@ -284,336 +293,353 @@ sub quicksearch { ########################## sub _parse_line { - my ($delim, $keep, $line) = @_; - return () unless defined $line; - - # parse_line always treats ' as a quote character, making it impossible - # to sanely search for contractions. As this behavour isn't - # configurable, we replace ' with a placeholder to hide it from the - # parser. - - # only treat ' at the start or end of words as quotes - # it's easier to do this in reverse with regexes - $line =~ s/(^|\s|:)'/$1\001/g; - $line =~ s/'($|\s)/\001$1/g; - $line =~ s/\\?'/\000/g; - $line =~ tr/\001/'/; - - my @words = parse_line($delim, $keep, $line); - foreach my $word (@words) { - $word =~ tr/\000/'/ if defined $word; - } - return @words; + my ($delim, $keep, $line) = @_; + return () unless defined $line; + + # parse_line always treats ' as a quote character, making it impossible + # to sanely search for contractions. As this behavour isn't + # configurable, we replace ' with a placeholder to hide it from the + # parser. + + # only treat ' at the start or end of words as quotes + # it's easier to do this in reverse with regexes + $line =~ s/(^|\s|:)'/$1\001/g; + $line =~ s/'($|\s)/\001$1/g; + $line =~ s/\\?'/\000/g; + $line =~ tr/\001/'/; + + my @words = parse_line($delim, $keep, $line); + foreach my $word (@words) { + $word =~ tr/\000/'/ if defined $word; + } + return @words; } sub _bug_numbers_only { - my $searchstring = shift; - my $cgi = Bugzilla->cgi; - # Allow separation by comma or whitespace. - $searchstring =~ s/[,\s]+/,/g; - - if ($searchstring !~ /,/ && !i_am_webservice()) { - # Single bug number; shortcut to show_bug.cgi. - print $cgi->redirect( - -uri => Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$searchstring"); - exit; - } - else { - # List of bug numbers. - $cgi->param('bug_id', $searchstring); - $cgi->param('order', 'bugs.bug_id'); - $cgi->param('bug_id_type', 'anyexact'); - } + my $searchstring = shift; + my $cgi = Bugzilla->cgi; + + # Allow separation by comma or whitespace. + $searchstring =~ s/[,\s]+/,/g; + + if ($searchstring !~ /,/ && !i_am_webservice()) { + + # Single bug number; shortcut to show_bug.cgi. + print $cgi->redirect( + -uri => Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$searchstring"); + exit; + } + else { + # List of bug numbers. + $cgi->param('bug_id', $searchstring); + $cgi->param('order', 'bugs.bug_id'); + $cgi->param('bug_id_type', 'anyexact'); + } } sub _handle_alias { - my $searchstring = shift; - if ($searchstring =~ /^([^,\s]+)$/) { - my $alias = $1; - # We use this direct SQL because we want quicksearch to be VERY fast. - my $bug_id = Bugzilla->dbh->selectrow_array( - q{SELECT bug_id FROM bugs WHERE alias = ?}, undef, $alias); - # If the user cannot see the bug or if we are using a webservice, - # do not resolve its alias. - if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) { - $alias = url_quote($alias); - print Bugzilla->cgi->redirect( - -uri => Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$alias"); - exit; - } - } + my $searchstring = shift; + if ($searchstring =~ /^([^,\s]+)$/) { + my $alias = $1; + + # We use this direct SQL because we want quicksearch to be VERY fast. + my $bug_id + = Bugzilla->dbh->selectrow_array(q{SELECT bug_id FROM bugs WHERE alias = ?}, + undef, $alias); + + # If the user cannot see the bug or if we are using a webservice, + # do not resolve its alias. + if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) { + $alias = url_quote($alias); + print Bugzilla->cgi->redirect( + -uri => Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$alias"); + exit; + } + } } sub _handle_status_and_resolution { - my $word = shift; - my $legal_statuses = get_legal_field_values('bug_status'); - my (%states, %resolutions); - $bug_status_set = 1; - - if ($word =~ s/^(ALL|OPEN)\+$/$1/) { - Bugzilla->cgi->param('limit' => 0); - } - - if ($word eq 'OPEN') { - $states{$_} = 1 foreach BUG_STATE_OPEN; - } - # If we want all bugs, then there is nothing to do. - elsif ($word ne 'ALL' - && !matchPrefixes(\%states, \%resolutions, $word, $legal_statuses)) - { - $bug_status_set = 0; - } - - # If we have wanted resolutions, allow closed states - if (keys(%resolutions)) { - foreach my $status (@$legal_statuses) { - $states{$status} = 1 unless is_open_state($status); - } - } - - Bugzilla->cgi->param('bug_status', keys(%states)); - Bugzilla->cgi->param('resolution', keys(%resolutions)); + my $word = shift; + my $legal_statuses = get_legal_field_values('bug_status'); + my (%states, %resolutions); + $bug_status_set = 1; + + if ($word =~ s/^(ALL|OPEN)\+$/$1/) { + Bugzilla->cgi->param('limit' => 0); + } + + if ($word eq 'OPEN') { + $states{$_} = 1 foreach BUG_STATE_OPEN; + } + + # If we want all bugs, then there is nothing to do. + elsif ($word ne 'ALL' + && !matchPrefixes(\%states, \%resolutions, $word, $legal_statuses)) + { + $bug_status_set = 0; + } + + # If we have wanted resolutions, allow closed states + if (keys(%resolutions)) { + foreach my $status (@$legal_statuses) { + $states{$status} = 1 unless is_open_state($status); + } + } + + Bugzilla->cgi->param('bug_status', keys(%states)); + Bugzilla->cgi->param('resolution', keys(%resolutions)); } sub _handle_special_first_chars { - my ($qsword, $negate) = @_; - return 0 if !defined $qsword || length($qsword) <= 1; - - my $firstChar = substr($qsword, 0, 1); - my $baseWord = substr($qsword, 1); - my @subWords = split(/,/, $baseWord); - - if ($firstChar eq '#') { - addChart('short_desc', 'substring', $baseWord, $negate); - addChart('content', 'matches', _matches_phrase($baseWord), $negate) if $fulltext; - return 1; - } - if ($firstChar eq ':') { - foreach (@subWords) { - addChart('product', 'substring', $_, $negate); - addChart('component', 'substring', $_, $negate); - } - return 1; - } - if ($firstChar eq '@') { - addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords); - return 1; - } - if ($firstChar eq '[') { - addChart('short_desc', 'substring', $baseWord, $negate); - addChart('status_whiteboard', 'substring', $baseWord, $negate); - return 1; - } - if ($firstChar eq '!') { - addChart('keywords', 'anywords', $baseWord, $negate); - return 1; - } - return 0; + my ($qsword, $negate) = @_; + return 0 if !defined $qsword || length($qsword) <= 1; + + my $firstChar = substr($qsword, 0, 1); + my $baseWord = substr($qsword, 1); + my @subWords = split(/,/, $baseWord); + + if ($firstChar eq '#') { + addChart('short_desc', 'substring', $baseWord, $negate); + addChart('content', 'matches', _matches_phrase($baseWord), $negate) + if $fulltext; + return 1; + } + if ($firstChar eq ':') { + foreach (@subWords) { + addChart('product', 'substring', $_, $negate); + addChart('component', 'substring', $_, $negate); + } + return 1; + } + if ($firstChar eq '@') { + addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords); + return 1; + } + if ($firstChar eq '[') { + addChart('short_desc', 'substring', $baseWord, $negate); + addChart('status_whiteboard', 'substring', $baseWord, $negate); + return 1; + } + if ($firstChar eq '!') { + addChart('keywords', 'anywords', $baseWord, $negate); + return 1; + } + return 0; } sub _handle_field_names { - my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_; - - # Flag and requestee shortcut - if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) { - # BMO: Do not treat custom fields as flags if value is ? - if ($1 !~ /^cf_/) { - my ($flagtype, $requestee) = ($1, $2); - addChart('flagtypes.name', 'substring', $flagtype, $negate); - if ($requestee) { - # AND - $chart++; - $and = $or = 0; - addChart('requestees.login_name', 'substring', $requestee, $negate); - } - return 1; + my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_; + + # Flag and requestee shortcut + if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) { + + # BMO: Do not treat custom fields as flags if value is ? + if ($1 !~ /^cf_/) { + my ($flagtype, $requestee) = ($1, $2); + addChart('flagtypes.name', 'substring', $flagtype, $negate); + if ($requestee) { + + # AND + $chart++; + $and = $or = 0; + addChart('requestees.login_name', 'substring', $requestee, $negate); + } + return 1; + } + } + + # Generic field1,field2,field3:value1,value2 notation. + # We have to correctly ignore commas and colons in quotes. + # Longer operators must be tested first as we don't want single character + # operators such as <, > and = to be tested before <=, >= and !=. + my @operators = sort { length($b) <=> length($a) } keys %{OPERATOR_SYMBOLS()}; + + foreach my $symbol (@operators) { + my @field_values = _parse_line($symbol, 1, $or_operand); + next unless scalar @field_values == 2; + my @fields = _parse_line(',', 1, $field_values[0]); + my @values = _parse_line(',', 1, $field_values[1]); + foreach my $field (@fields) { + my $translated = _translate_field_name($field); + + # Skip and record any unknown fields + if (!defined $translated) { + push(@$unknownFields, $field); + } + + # If we got back an array, that means the substring is + # ambiguous and could match more than field name + elsif (ref $translated) { + $ambiguous_fields->{$field} = $translated; + } + else { + if ($translated eq 'bug_status' || $translated eq 'resolution') { + $bug_status_set = 1; } - } - - # Generic field1,field2,field3:value1,value2 notation. - # We have to correctly ignore commas and colons in quotes. - # Longer operators must be tested first as we don't want single character - # operators such as <, > and = to be tested before <=, >= and !=. - my @operators = sort { length($b) <=> length($a) } keys %{ OPERATOR_SYMBOLS() }; - - foreach my $symbol (@operators) { - my @field_values = _parse_line($symbol, 1, $or_operand); - next unless scalar @field_values == 2; - my @fields = _parse_line(',', 1, $field_values[0]); - my @values = _parse_line(',', 1, $field_values[1]); - foreach my $field (@fields) { - my $translated = _translate_field_name($field); - # Skip and record any unknown fields - if (!defined $translated) { - push(@$unknownFields, $field); - } - # If we got back an array, that means the substring is - # ambiguous and could match more than field name - elsif (ref $translated) { - $ambiguous_fields->{$field} = $translated; - } - else { - if ($translated eq 'bug_status' || $translated eq 'resolution') { - $bug_status_set = 1; - } - foreach my $value (@values) { - next unless defined $value; - my $operator = FIELD_OPERATOR->{$translated} - || OPERATOR_SYMBOLS->{$symbol} - || 'substring'; - # If the string was quoted to protect some special - # characters such as commas and colons, we need - # to remove quotes. - if ($value =~ /^(["'])(.+)\1$/) { - $value = $2; - $value =~ s/\\(["'])/$1/g; - } - # If the value is a pair of matching quotes, the person wanted the empty string - elsif ($value =~ /^(["'])\1$/ || $translated eq 'resolution' && $value eq '---') { - $value = ""; - $operator = "isempty"; - } - addChart($translated, $operator, $value, $negate); - } - } + foreach my $value (@values) { + next unless defined $value; + my $operator + = FIELD_OPERATOR->{$translated} || OPERATOR_SYMBOLS->{$symbol} || 'substring'; + + # If the string was quoted to protect some special + # characters such as commas and colons, we need + # to remove quotes. + if ($value =~ /^(["'])(.+)\1$/) { + $value = $2; + $value =~ s/\\(["'])/$1/g; + } + + # If the value is a pair of matching quotes, the person wanted the empty string + elsif ($value =~ /^(["'])\1$/ || $translated eq 'resolution' && $value eq '---') + { + $value = ""; + $operator = "isempty"; + } + addChart($translated, $operator, $value, $negate); } - return 1; + } } - return 0; + return 1; + } + return 0; } sub _translate_field_name { - my $field = shift; - $field = lc($field); - my $field_map = FIELD_MAP; - - # If the field exactly matches a mapping, just return right now. - return $field_map->{$field} if exists $field_map->{$field}; - - # Check if we match, as a starting substring, exactly one field. - my @field_names = keys %$field_map; - my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names; - # Eliminate duplicates that are actually the same field - # (otherwise "assi" matches both "assignee" and "assigned_to", and - # the lines below fail when they shouldn't.) - my %match_unique = map { $field_map->{$_} => $_ } @matches; - @matches = values %match_unique; - - if (scalar(@matches) == 1) { - return $field_map->{$matches[0]}; - } - elsif (scalar(@matches) > 1) { - return \@matches; - } - - # Check if we match exactly one custom field, ignoring the cf_ on the - # custom fields (to allow people to type things like "build" for - # "cf_build"). - my %cfless; - foreach my $name (@field_names) { - my $no_cf = $name; - if ($no_cf =~ s/^cf_//) { - if ($field eq $no_cf) { - return $field_map->{$name}; - } - $cfless{$no_cf} = $name; - } - } - - # See if we match exactly one substring of any of the cf_-less fields. - my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless); - - if (scalar(@cfless_matches) == 1) { - my $match = $cfless_matches[0]; - my $actual_field = $cfless{$match}; - return $field_map->{$actual_field}; - } - elsif (scalar(@matches) > 1) { - return \@matches; - } - - return undef; + my $field = shift; + $field = lc($field); + my $field_map = FIELD_MAP; + + # If the field exactly matches a mapping, just return right now. + return $field_map->{$field} if exists $field_map->{$field}; + + # Check if we match, as a starting substring, exactly one field. + my @field_names = keys %$field_map; + my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names; + + # Eliminate duplicates that are actually the same field + # (otherwise "assi" matches both "assignee" and "assigned_to", and + # the lines below fail when they shouldn't.) + my %match_unique = map { $field_map->{$_} => $_ } @matches; + @matches = values %match_unique; + + if (scalar(@matches) == 1) { + return $field_map->{$matches[0]}; + } + elsif (scalar(@matches) > 1) { + return \@matches; + } + + # Check if we match exactly one custom field, ignoring the cf_ on the + # custom fields (to allow people to type things like "build" for + # "cf_build"). + my %cfless; + foreach my $name (@field_names) { + my $no_cf = $name; + if ($no_cf =~ s/^cf_//) { + if ($field eq $no_cf) { + return $field_map->{$name}; + } + $cfless{$no_cf} = $name; + } + } + + # See if we match exactly one substring of any of the cf_-less fields. + my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless); + + if (scalar(@cfless_matches) == 1) { + my $match = $cfless_matches[0]; + my $actual_field = $cfless{$match}; + return $field_map->{$actual_field}; + } + elsif (scalar(@matches) > 1) { + return \@matches; + } + + return undef; } sub _special_field_syntax { - my ($word, $negate) = @_; - return unless defined($word); - - # P1-5 Syntax - if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) { - my ($p_start, $p_end) = ($1, $2); - my $legal_priorities = get_legal_field_values('priority'); - - # If Pn exists explicitly, use it. - my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities; - my $end; - $end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end; - - # If Pn doesn't exist explicitly, then we mean the nth priority. - if ($start == -1) { - $start = max(0, $p_start - 1); - } - my $prios = $legal_priorities->[$start]; - - if (defined $end) { - # If Pn doesn't exist explicitly, then we mean the nth priority. - if ($end == -1) { - $end = min(scalar(@$legal_priorities), $p_end) - 1; - $end = max(0, $end); # Just in case the user typed P0. - } - ($start, $end) = ($end, $start) if $end < $start; - $prios = join(',', @$legal_priorities[$start..$end]) - } + my ($word, $negate) = @_; + return unless defined($word); - addChart('priority', 'anyexact', $prios, $negate); - return 1; - } - return 0; -} + # P1-5 Syntax + if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) { + my ($p_start, $p_end) = ($1, $2); + my $legal_priorities = get_legal_field_values('priority'); -sub _default_quicksearch_word { - my ($word, $negate) = @_; - return unless defined($word); + # If Pn exists explicitly, use it. + my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities; + my $end; + $end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end; - if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) { - addChart('product', 'substring', $word, $negate); + # If Pn doesn't exist explicitly, then we mean the nth priority. + if ($start == -1) { + $start = max(0, $p_start - 1); } + my $prios = $legal_priorities->[$start]; - if (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) { - addChart('component', 'substring', $word, $negate); - } + if (defined $end) { - my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all); - if (grep { lc($word) eq lc($_) } @legal_keywords) { - addChart('keywords', 'substring', $word, $negate); + # If Pn doesn't exist explicitly, then we mean the nth priority. + if ($end == -1) { + $end = min(scalar(@$legal_priorities), $p_end) - 1; + $end = max(0, $end); # Just in case the user typed P0. + } + ($start, $end) = ($end, $start) if $end < $start; + $prios = join(',', @$legal_priorities[$start .. $end]); } - addChart('alias', 'substring', $word, $negate); - addChart('short_desc', 'substring', $word, $negate); - addChart('status_whiteboard', 'substring', $word, $negate); - addChart('longdesc', 'substring', $word, $negate) if $ELASTIC; - addChart('content', 'matches', _matches_phrase($word), $negate) if $fulltext && !$ELASTIC; + addChart('priority', 'anyexact', $prios, $negate); + return 1; + } + return 0; +} - # BMO Bug 664124 - Include the crash signature (sig:) field in default quicksearches - addChart('cf_crash_signature', 'substring', $word, $negate); +sub _default_quicksearch_word { + my ($word, $negate) = @_; + return unless defined($word); + + if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) { + addChart('product', 'substring', $word, $negate); + } + + if (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) { + addChart('component', 'substring', $word, $negate); + } + + my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all); + if (grep { lc($word) eq lc($_) } @legal_keywords) { + addChart('keywords', 'substring', $word, $negate); + } + + addChart('alias', 'substring', $word, $negate); + addChart('short_desc', 'substring', $word, $negate); + addChart('status_whiteboard', 'substring', $word, $negate); + addChart('longdesc', 'substring', $word, $negate) if $ELASTIC; + addChart('content', 'matches', _matches_phrase($word), $negate) + if $fulltext && !$ELASTIC; + +# BMO Bug 664124 - Include the crash signature (sig:) field in default quicksearches + addChart('cf_crash_signature', 'substring', $word, $negate); } sub _handle_urls { - my ($word, $negate) = @_; - return unless defined($word); - - # URL field (for IP addrs, host.names, - # scheme://urls) - if ($word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ - || $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/ - || $word =~ /:[\\\/][\\\/]/ - || $word =~ /localhost/ - || $word =~ /mailto[:]?/) - # || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port - { - addChart('bug_file_loc', 'substring', $word, $negate); - } + my ($word, $negate) = @_; + return unless defined($word); + + # URL field (for IP addrs, host.names, + # scheme://urls) + if ( $word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ + || $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/ + || $word =~ /:[\\\/][\\\/]/ + || $word =~ /localhost/ + || $word =~ /mailto[:]?/) + + # || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port + { + addChart('bug_file_loc', 'substring', $word, $negate); + } } ########################################################################### @@ -622,77 +648,77 @@ sub _handle_urls { # Quote and escape a phrase appropriately for a "content matches" search. sub _matches_phrase { - my ($phrase) = @_; - return $phrase if $ELASTIC; - $phrase =~ s/"/\\"/g; - return "\"$phrase\""; + my ($phrase) = @_; + return $phrase if $ELASTIC; + $phrase =~ s/"/\\"/g; + return "\"$phrase\""; } # Expand found prefixes to states or resolutions sub matchPrefixes { - my ($hr_states, $hr_resolutions, $word, $ar_check_states) = @_; - return unless $word =~ /^[A-Z_]+(,[A-Z_]+)*\+?$/; - - my @ar_prefixes = split(/,/, $word); - if ($ar_prefixes[-1] =~ s/\+$//) { - Bugzilla->cgi->param(limit => 0); - } - my $ar_check_resolutions = get_legal_field_values('resolution'); - my $foundMatch = 0; - - foreach my $prefix (@ar_prefixes) { - foreach (@$ar_check_states) { - if (/^$prefix/) { - $$hr_states{$_} = 1; - $foundMatch = 1; - } - } - foreach (@$ar_check_resolutions) { - if (/^$prefix/) { - $$hr_resolutions{$_} = 1; - $foundMatch = 1; - } - } - } - return $foundMatch; + my ($hr_states, $hr_resolutions, $word, $ar_check_states) = @_; + return unless $word =~ /^[A-Z_]+(,[A-Z_]+)*\+?$/; + + my @ar_prefixes = split(/,/, $word); + if ($ar_prefixes[-1] =~ s/\+$//) { + Bugzilla->cgi->param(limit => 0); + } + my $ar_check_resolutions = get_legal_field_values('resolution'); + my $foundMatch = 0; + + foreach my $prefix (@ar_prefixes) { + foreach (@$ar_check_states) { + if (/^$prefix/) { + $$hr_states{$_} = 1; + $foundMatch = 1; + } + } + foreach (@$ar_check_resolutions) { + if (/^$prefix/) { + $$hr_resolutions{$_} = 1; + $foundMatch = 1; + } + } + } + return $foundMatch; } # Negate comparison type sub negateComparisonType { - my $comparisonType = shift; - - if ($comparisonType eq 'anywords') { - return 'nowords'; - } - elsif ($comparisonType eq 'isempty') { - return 'isnotempty'; - } - return "not$comparisonType"; + my $comparisonType = shift; + + if ($comparisonType eq 'anywords') { + return 'nowords'; + } + elsif ($comparisonType eq 'isempty') { + return 'isnotempty'; + } + return "not$comparisonType"; } # Add a boolean chart sub addChart { - my ($field, $comparisonType, $value, $negate) = @_; - - $negate && ($comparisonType = negateComparisonType($comparisonType)); - makeChart("$chart-$and-$or", $field, $comparisonType, $value); - if ($negate) { - $and++; - $or = 0; - } - else { - $or++; - } + my ($field, $comparisonType, $value, $negate) = @_; + + $negate && ($comparisonType = negateComparisonType($comparisonType)); + makeChart("$chart-$and-$or", $field, $comparisonType, $value); + if ($negate) { + $and++; + $or = 0; + } + else { + $or++; + } } # Create the CGI parameters for a boolean chart sub makeChart { - my ($expr, $field, $type, $value) = @_; + my ($expr, $field, $type, $value) = @_; - my $cgi = Bugzilla->cgi; - $cgi->param("field$expr", $field); - $cgi->param("type$expr", $type); - $cgi->param("value$expr", $value); + my $cgi = Bugzilla->cgi; + $cgi->param("field$expr", $field); + $cgi->param("type$expr", $type); + $cgi->param("value$expr", $value); } 1; diff --git a/Bugzilla/Search/Recent.pm b/Bugzilla/Search/Recent.pm index a5d9e2417..5738dc93f 100644 --- a/Bugzilla/Search/Recent.pm +++ b/Bugzilla/Search/Recent.pm @@ -21,24 +21,25 @@ use Bugzilla::Util; # Constants # ############# -use constant DB_TABLE => 'profile_search'; +use constant DB_TABLE => 'profile_search'; use constant LIST_ORDER => 'id DESC'; + # Do not track buglists viewed by users. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - user_id - bug_list - list_order + id + user_id + bug_list + list_order ); use constant VALIDATORS => { - user_id => \&_check_user_id, - bug_list => \&_check_bug_list, - list_order => \&_check_list_order, + user_id => \&_check_user_id, + bug_list => \&_check_bug_list, + list_order => \&_check_list_order, }; use constant UPDATE_COLUMNS => qw(bug_list list_order); @@ -51,29 +52,30 @@ use constant USE_MEMCACHED => 0; ################### sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $search = $class->SUPER::create(@_); - my $user_id = $search->user_id; - - # Enforce there only being SAVE_NUM_SEARCHES per user. - my @ids = @{ $dbh->selectcol_arrayref( - "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id", - undef, $user_id) }; - if (scalar(@ids) > SAVE_NUM_SEARCHES) { - splice(@ids, - SAVE_NUM_SEARCHES); - $dbh->do( - "DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")"); - } - $dbh->bz_commit_transaction(); - return $search; + my $class = shift; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my $search = $class->SUPER::create(@_); + my $user_id = $search->user_id; + + # Enforce there only being SAVE_NUM_SEARCHES per user. + my @ids = @{ + $dbh->selectcol_arrayref( + "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id", undef, + $user_id + ) + }; + if (scalar(@ids) > SAVE_NUM_SEARCHES) { + splice(@ids, - SAVE_NUM_SEARCHES); + $dbh->do("DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")"); + } + $dbh->bz_commit_transaction(); + return $search; } sub create_placeholder { - my $class = shift; - return $class->create({ user_id => Bugzilla->user->id, - bug_list => '' }); + my $class = shift; + return $class->create({user_id => Bugzilla->user->id, bug_list => ''}); } ############### @@ -81,41 +83,43 @@ sub create_placeholder { ############### sub check { - my $class = shift; - my $search = $class->SUPER::check(@_); - my $user = Bugzilla->user; - if ($search->user_id != $user->id) { - ThrowUserError('object_does_not_exist', { id => $search->id }); - } - return $search; + my $class = shift; + my $search = $class->SUPER::check(@_); + my $user = Bugzilla->user; + if ($search->user_id != $user->id) { + ThrowUserError('object_does_not_exist', {id => $search->id}); + } + return $search; } sub check_quietly { - my $class = shift; - my $error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $search = eval { $class->check(@_) }; - Bugzilla->error_mode($error_mode); - return $search; + my $class = shift; + my $error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $search = eval { $class->check(@_) }; + Bugzilla->error_mode($error_mode); + return $search; } sub new_from_cookie { - my ($invocant, $bug_ids) = @_; - my $class = ref($invocant) || $invocant; + my ($invocant, $bug_ids) = @_; + my $class = ref($invocant) || $invocant; - my $search = { id => 'cookie', - user_id => Bugzilla->user->id, - bug_list => join(',', @$bug_ids) }; + my $search = { + id => 'cookie', + user_id => Bugzilla->user->id, + bug_list => join(',', @$bug_ids) + }; - bless $search, $class; - return $search; + bless $search, $class; + return $search; } #################### # Simple Accessors # #################### -sub bug_list { return [split(',', $_[0]->{'bug_list'})]; } +sub bug_list { return [split(',', $_[0]->{'bug_list'})]; } sub list_order { return $_[0]->{'list_order'}; } sub user_id { return $_[0]->{'user_id'}; } @@ -131,17 +135,17 @@ sub set_list_order { $_[0]->set('list_order', $_[1]); } ############## sub _check_user_id { - my ($invocant, $id) = @_; - require Bugzilla::User; - return Bugzilla::User->check({ id => $id })->id; + my ($invocant, $id) = @_; + require Bugzilla::User; + return Bugzilla::User->check({id => $id})->id; } sub _check_bug_list { - my ($invocant, $list) = @_; + my ($invocant, $list) = @_; - my @bug_ids = ref($list) ? @$list : split(',', $list || ''); - detaint_natural($_) foreach @bug_ids; - return join(',', @bug_ids); + my @bug_ids = ref($list) ? @$list : split(',', $list || ''); + detaint_natural($_) foreach @bug_ids; + return join(',', @bug_ids); } sub _check_list_order { defined $_[1] ? trim($_[1]) : '' } diff --git a/Bugzilla/Search/Saved.pm b/Bugzilla/Search/Saved.pm index 1511cd87b..c24d333a8 100644 --- a/Bugzilla/Search/Saved.pm +++ b/Bugzilla/Search/Saved.pm @@ -28,22 +28,23 @@ use Scalar::Util qw(blessed); ############# use constant DB_TABLE => 'namedqueries'; + # Do not track buglists saved by users. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - userid - name - query + id + userid + name + query ); use constant VALIDATORS => { - name => \&_check_name, - query => \&_check_query, - link_in_footer => \&_check_link_in_footer, + name => \&_check_name, + query => \&_check_query, + link_in_footer => \&_check_link_in_footer, }; use constant UPDATE_COLUMNS => qw(name query); @@ -53,56 +54,53 @@ use constant UPDATE_COLUMNS => qw(name query); ############### sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $user; - if (ref $param) { - $user = $param->{user} || Bugzilla->user; - my $name = $param->{name}; - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - my $condition = 'userid = ? AND name = ?'; - my $user_id = blessed $user ? $user->id : $user; - detaint_natural($user_id) - || ThrowCodeError('param_must_be_numeric', - {function => $class . '::_init', param => 'user'}); - my @values = ($user_id, $name); - $param = { condition => $condition, values => \@values }; - } - - unshift @_, $param; - my $self = $class->SUPER::new(@_); - if ($self) { - $self->{user} = $user if blessed $user; - - # Some DBs (read: Oracle) incorrectly mark the query string as UTF-8 - # when it's coming out of the database, even though it has no UTF-8 - # characters in it, which prevents Bugzilla::CGI from later reading - # it correctly. - utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query}); + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $user; + if (ref $param) { + $user = $param->{user} || Bugzilla->user; + my $name = $param->{name}; + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); } - return $self; + my $condition = 'userid = ? AND name = ?'; + my $user_id = blessed $user ? $user->id : $user; + detaint_natural($user_id) + || ThrowCodeError('param_must_be_numeric', + {function => $class . '::_init', param => 'user'}); + my @values = ($user_id, $name); + $param = {condition => $condition, values => \@values}; + } + + unshift @_, $param; + my $self = $class->SUPER::new(@_); + if ($self) { + $self->{user} = $user if blessed $user; + + # Some DBs (read: Oracle) incorrectly mark the query string as UTF-8 + # when it's coming out of the database, even though it has no UTF-8 + # characters in it, which prevents Bugzilla::CGI from later reading + # it correctly. + utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query}); + } + return $self; } sub check { - my $class = shift; - my $search = $class->SUPER::check(@_); - my $user = Bugzilla->user; - return $search if $search->user->id == $user->id; - - if (!$search->shared_with_group - or !$user->in_group($search->shared_with_group)) - { - ThrowUserError('missing_query', { name => $search->name, - sharer_id => $search->user->id }); - } - - return $search; + my $class = shift; + my $search = $class->SUPER::check(@_); + my $user = Bugzilla->user; + return $search if $search->user->id == $user->id; + + if (!$search->shared_with_group or !$user->in_group($search->shared_with_group)) + { + ThrowUserError('missing_query', + {name => $search->name, sharer_id => $search->user->id}); + } + + return $search; } ############## @@ -112,24 +110,25 @@ sub check { sub _check_link_in_footer { return $_[1] ? 1 : 0; } sub _check_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError("query_name_missing"); - $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); - if (length($name) > MAX_LEN_QUERY_NAME) { - ThrowUserError("query_name_too_long"); - } - return $name; + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowUserError("query_name_missing"); + $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); + if (length($name) > MAX_LEN_QUERY_NAME) { + ThrowUserError("query_name_too_long"); + } + return $name; } sub _check_query { - my ($invocant, $query) = @_; - $query || ThrowUserError("buglist_parameters_required"); - my $cgi = new Bugzilla::CGI($query); - $cgi->clean_search_url; - # Don't store the query name as a parameter. - $cgi->delete('known_name'); - return $cgi->query_string; + my ($invocant, $query) = @_; + $query || ThrowUserError("buglist_parameters_required"); + my $cgi = new Bugzilla::CGI($query); + $cgi->clean_search_url; + + # Don't store the query name as a parameter. + $cgi->delete('known_name'); + return $cgi->query_string; } ######################### @@ -137,170 +136,180 @@ sub _check_query { ######################### sub create { - my $class = shift; - Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; - $class->check_required_create_fields(@_); - $dbh->bz_start_transaction(); - my $params = $class->run_create_validators(@_); - - # Right now you can only create a Saved Search for the current user. - $params->{userid} = Bugzilla->user->id; - - my $lif = delete $params->{link_in_footer}; - my $obj = $class->insert_create_data($params); - if ($lif) { - $dbh->do('INSERT INTO namedqueries_link_in_footer - (user_id, namedquery_id) VALUES (?,?)', - undef, $params->{userid}, $obj->id); - } - $dbh->bz_commit_transaction(); - - return $obj; + my $class = shift; + Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + $class->check_required_create_fields(@_); + $dbh->bz_start_transaction(); + my $params = $class->run_create_validators(@_); + + # Right now you can only create a Saved Search for the current user. + $params->{userid} = Bugzilla->user->id; + + my $lif = delete $params->{link_in_footer}; + my $obj = $class->insert_create_data($params); + if ($lif) { + $dbh->do( + 'INSERT INTO namedqueries_link_in_footer + (user_id, namedquery_id) VALUES (?,?)', undef, $params->{userid}, + $obj->id + ); + } + $dbh->bz_commit_transaction(); + + return $obj; } sub rename_field_value { - my ($class, $field, $old_value, $new_value) = @_; - - my $old = url_quote($old_value); - my $new = url_quote($new_value); - my $old_sql = $old; - $old_sql =~ s/([_\%])/\\$1/g; - - my $table = $class->DB_TABLE; - my $id_field = $class->ID_FIELD; - - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - - my %queries = @{ $dbh->selectcol_arrayref( - "SELECT $id_field, query FROM $table WHERE query LIKE ?", - {Columns=>[1,2]}, "\%$old_sql\%") }; - foreach my $id (keys %queries) { - my $query = $queries{$id}; - $query =~ s/\b$field=\Q$old\E\b/$field=$new/gi; - # Fix boolean charts. - while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) { - my $chart_id = $1; - # Note that this won't handle lists or substrings inside of - # boolean charts. Users will have to fix those themselves. - $query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i; - } - $dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?", - undef, $query, $id); - Bugzilla->memcached->clear({ table => $table, id => $id }); + my ($class, $field, $old_value, $new_value) = @_; + + my $old = url_quote($old_value); + my $new = url_quote($new_value); + my $old_sql = $old; + $old_sql =~ s/([_\%])/\\$1/g; + + my $table = $class->DB_TABLE; + my $id_field = $class->ID_FIELD; + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + + my %queries = @{ + $dbh->selectcol_arrayref( + "SELECT $id_field, query FROM $table WHERE query LIKE ?", + {Columns => [1, 2]}, + "\%$old_sql\%" + ) + }; + foreach my $id (keys %queries) { + my $query = $queries{$id}; + $query =~ s/\b$field=\Q$old\E\b/$field=$new/gi; + + # Fix boolean charts. + while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) { + my $chart_id = $1; + + # Note that this won't handle lists or substrings inside of + # boolean charts. Users will have to fix those themselves. + $query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i; } + $dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?", undef, $query, $id); + Bugzilla->memcached->clear({table => $table, id => $id}); + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub preload { - my ($searches) = @_; - my $dbh = Bugzilla->dbh; + my ($searches) = @_; + my $dbh = Bugzilla->dbh; - return unless scalar @$searches; + return unless scalar @$searches; - my @query_ids = map { $_->id } @$searches; - my $queries_in_footer = $dbh->selectcol_arrayref( - 'SELECT namedquery_id + my @query_ids = map { $_->id } @$searches; + my $queries_in_footer = $dbh->selectcol_arrayref( + 'SELECT namedquery_id FROM namedqueries_link_in_footer WHERE ' . $dbh->sql_in('namedquery_id', \@query_ids) . ' AND user_id = ?', - undef, Bugzilla->user->id); + undef, Bugzilla->user->id + ); - my %links_in_footer = map { $_ => 1 } @$queries_in_footer; - foreach my $query (@$searches) { - $query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0; - } + my %links_in_footer = map { $_ => 1 } @$queries_in_footer; + foreach my $query (@$searches) { + $query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0; + } } ##################### # Complex Accessors # ##################### sub edit_link { - my ($self) = @_; - return $self->{edit_link} if defined $self->{edit_link}; - my $cgi = new Bugzilla::CGI($self->url); - if (!$cgi->param('query_type') - || !IsValidQueryType($cgi->param('query_type'))) - { - $cgi->param('query_type', 'advanced'); - } - $self->{edit_link} = $cgi->canonicalise_query; - return $self->{edit_link}; + my ($self) = @_; + return $self->{edit_link} if defined $self->{edit_link}; + my $cgi = new Bugzilla::CGI($self->url); + if (!$cgi->param('query_type') || !IsValidQueryType($cgi->param('query_type'))) + { + $cgi->param('query_type', 'advanced'); + } + $self->{edit_link} = $cgi->canonicalise_query; + return $self->{edit_link}; } sub used_in_whine { - my ($self) = @_; - return $self->{used_in_whine} if exists $self->{used_in_whine}; - ($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array( - 'SELECT 1 FROM whine_events INNER JOIN whine_queries + my ($self) = @_; + return $self->{used_in_whine} if exists $self->{used_in_whine}; + ($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array( + 'SELECT 1 FROM whine_events INNER JOIN whine_queries ON whine_events.id = whine_queries.eventid WHERE whine_events.owner_userid = ? AND query_name = ?', undef, - $self->{userid}, $self->name) || 0; - return $self->{used_in_whine}; + $self->{userid}, $self->name + ) || 0; + return $self->{used_in_whine}; } sub link_in_footer { - my ($self, $user) = @_; - # We only cache link_in_footer for the current Bugzilla->user. - return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user; - my $user_id = $user ? $user->id : Bugzilla->user->id; - my $link_in_footer = Bugzilla->dbh->selectrow_array( - 'SELECT 1 FROM namedqueries_link_in_footer - WHERE namedquery_id = ? AND user_id = ?', - undef, $self->id, $user_id) || 0; - $self->{link_in_footer} = $link_in_footer if !$user; - return $link_in_footer; + my ($self, $user) = @_; + + # We only cache link_in_footer for the current Bugzilla->user. + return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user; + my $user_id = $user ? $user->id : Bugzilla->user->id; + my $link_in_footer = Bugzilla->dbh->selectrow_array( + 'SELECT 1 FROM namedqueries_link_in_footer + WHERE namedquery_id = ? AND user_id = ?', undef, $self->id, $user_id + ) || 0; + $self->{link_in_footer} = $link_in_footer if !$user; + return $link_in_footer; } sub shared_with_group { - my ($self) = @_; - return $self->{shared_with_group} if exists $self->{shared_with_group}; - # Bugzilla only currently supports sharing with one group, even - # though the database backend allows for an infinite number. - my ($group_id) = Bugzilla->dbh->selectrow_array( - 'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?', - undef, $self->id); - $self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id) - : undef; - return $self->{shared_with_group}; + my ($self) = @_; + return $self->{shared_with_group} if exists $self->{shared_with_group}; + + # Bugzilla only currently supports sharing with one group, even + # though the database backend allows for an infinite number. + my ($group_id) + = Bugzilla->dbh->selectrow_array( + 'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?', + undef, $self->id); + $self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id) : undef; + return $self->{shared_with_group}; } sub shared_with_users { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!exists $self->{shared_with_users}) { - $self->{shared_with_users} = - $dbh->selectrow_array('SELECT COUNT(*) + if (!exists $self->{shared_with_users}) { + $self->{shared_with_users} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM namedqueries_link_in_footer INNER JOIN namedqueries ON namedquery_id = id WHERE namedquery_id = ? - AND user_id != userid', - undef, $self->id); - } - return $self->{shared_with_users}; + AND user_id != userid', undef, $self->id + ); + } + return $self->{shared_with_users}; } #################### # Simple Accessors # #################### -sub url { return $_[0]->{'query'}; } +sub url { return $_[0]->{'query'}; } sub user { - my ($self) = @_; - return $self->{user} ||= - Bugzilla::User->new({ id => $self->{userid}, cache => 1 }); + my ($self) = @_; + return $self->{user} + ||= Bugzilla::User->new({id => $self->{userid}, cache => 1}); } ############ # Mutators # ############ -sub set_name { $_[0]->set('name', $_[1]); } -sub set_url { $_[0]->set('query', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_url { $_[0]->set('query', $_[1]); } 1; |