summaryrefslogtreecommitdiffstats
path: root/Bugzilla/Search
diff options
context:
space:
mode:
authorlpsolit%gmail.com <>2005-08-22 03:16:40 +0200
committerlpsolit%gmail.com <>2005-08-22 03:16:40 +0200
commitf4966aeb0e7a655c986aeb285c1a220274ddbfd9 (patch)
tree70fc81865b58b54e97da10c8cc824ae9fb641672 /Bugzilla/Search
parentd055246d2010e546bbad8c65d99496d53eee0bff (diff)
downloadbugzilla-f4966aeb0e7a655c986aeb285c1a220274ddbfd9.tar.gz
bugzilla-f4966aeb0e7a655c986aeb285c1a220274ddbfd9.tar.xz
Bug 70907: QuickSearch: port the JS code to perl (make it server-side) - Patch by Marc Schumann <wurblzap@gmail.com> r=wicked a=myk
Diffstat (limited to 'Bugzilla/Search')
-rw-r--r--Bugzilla/Search/Quicksearch.pm499
1 files changed, 499 insertions, 0 deletions
diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm
new file mode 100644
index 000000000..9f6724507
--- /dev/null
+++ b/Bugzilla/Search/Quicksearch.pm
@@ -0,0 +1,499 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# Contributor(s): C. Begle
+# Jesse Ruderman
+# Andreas Franke <afranke@mathweb.org>
+# Stephen Lee <slee@uk.bnsmc.com>
+# Marc Schumann <wurblzap@gmail.com>
+
+package Bugzilla::Search::Quicksearch;
+
+# Make it harder for us to do dangerous things in Perl.
+use strict;
+
+use Bugzilla;
+use Bugzilla::Config;
+use Bugzilla::Error;
+
+use base qw(Exporter);
+@Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch);
+
+my $cgi = Bugzilla->cgi;
+
+# Word renamings
+my %mappings = (# Status, Resolution, Platform, OS, Priority, Severity
+ "status" => "bug_status",
+ "resolution" => "resolution", # no change
+ "platform" => "rep_platform",
+ "os" => "op_sys",
+ "opsys" => "op_sys",
+ "priority" => "priority", # no change
+ "pri" => "priority",
+ "severity" => "bug_severity",
+ "sev" => "bug_severity",
+ # People: AssignedTo, Reporter, QA Contact, CC, Added comment (?)
+ "owner" => "assigned_to", # deprecated since bug 76507
+ "assignee" => "assigned_to",
+ "assignedto" => "assigned_to",
+ "reporter" => "reporter", # no change
+ "rep" => "reporter",
+ "qa" => "qa_contact",
+ "qacontact" => "qa_contact",
+ "cc" => "cc", # no change
+ # Product, Version, Component, Target Milestone
+ "product" => "product", # no change
+ "prod" => "product",
+ "version" => "version", # no change
+ "ver" => "version",
+ "component" => "component", # no change
+ "comp" => "component",
+ "milestone" => "target_milestone",
+ "target" => "target_milestone",
+ "targetmilestone" => "target_milestone",
+ # Summary, Description, URL, Status whiteboard, Keywords
+ "summary" => "short_desc",
+ "shortdesc" => "short_desc",
+ "desc" => "longdesc",
+ "description" => "longdesc",
+ #"comment" => "longdesc", # ???
+ # reserve "comment" for "added comment" email search?
+ "longdesc" => "longdesc",
+ "url" => "bug_file_loc",
+ "whiteboard" => "status_whiteboard",
+ "statuswhiteboard" => "status_whiteboard",
+ "sw" => "status_whiteboard",
+ "keywords" => "keywords", # no change
+ "kw" => "keywords",
+ # Attachments
+ "attachment" => "attachments.description",
+ "attachmentdesc" => "attachments.description",
+ "attachdesc" => "attachments.description",
+ "attachmentdata" => "attachments.thedata",
+ "attachdata" => "attachments.thedata",
+ "attachmentmimetype" => "attachments.mimetype",
+ "attachmimetype" => "attachments.mimetype");
+
+# We might want to put this into localconfig or somewhere
+my @platforms = ('pc', 'sun', 'macintosh', 'mac');
+my @productExceptions = ('row' # [Browser]
+ # ^^^
+ ,'new' # [MailNews]
+ # ^^^
+ );
+my @componentExceptions = ('hang' # [Bugzilla: Component/Keyword Changes]
+ # ^^^^
+ );
+
+# Quicksearch-wide globals for boolean charts.
+my $chart = 0;
+my $and = 0;
+my $or = 0;
+
+sub quicksearch {
+ my ($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 number(s) only.
+
+ # Allow separation by comma or whitespace.
+ $searchstring =~ s/[,\s]+/,/g;
+
+ if (index($searchstring, ',') < $[) {
+ # Single bug number; shortcut to show_bug.cgi.
+ print $cgi->redirect(-uri => Param('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('bugidtype', 'include');
+ }
+ }
+ else {
+ # It's not just a bug number or a list of bug numbers.
+ # Maybe it's an alias?
+ if ($searchstring =~ /^([^,\s]+)$/) {
+ if (Bugzilla->dbh->selectrow_array(q{SELECT COUNT(*)
+ FROM bugs
+ WHERE alias = ?},
+ undef,
+ $1)) {
+ print $cgi->redirect(-uri => Param('urlbase') .
+ "show_bug.cgi?id=$1");
+ exit;
+ }
+ }
+
+ # It's no alias either, so it's a more complex query.
+
+ &::GetVersionTable();
+
+ # Globally translate " AND ", " OR ", " NOT " to space, pipe, dash.
+ $searchstring =~ s/\s+AND\s+/ /g;
+ $searchstring =~ s/\s+OR\s+/|/g;
+ $searchstring =~ s/\s+NOT\s+/ -/g;
+
+ my @words = splitString($searchstring);
+ my $searchComments = $#words < Param('quicksearch_comment_cutoff');
+ my @openStates = &::OpenStates();
+ my @closedStates;
+ my (%states, %resolutions);
+
+ foreach (@::legal_bug_status) {
+ push(@closedStates, $_) unless &::IsOpenedState($_);
+ }
+ foreach (@openStates) { $states{$_} = 1 }
+ if ($words[0] eq 'ALL') {
+ foreach (@::legal_bug_status) { $states{$_} = 1 }
+ shift @words;
+ }
+ elsif ($words[0] eq 'OPEN') {
+ shift @words;
+ }
+ elsif ($words[0] =~ /^\+[A-Z]+(,[A-Z]+)*$/) {
+ # e.g. +DUP,FIX
+ if (matchPrefixes(\%states,
+ \%resolutions,
+ [split(/,/, substr($words[0], 1))],
+ \@closedStates,
+ \@::legal_resolution)) {
+ shift @words;
+ # Allowing additional resolutions means we need to keep
+ # the "no resolution" resolution.
+ $resolutions{'---'} = 1;
+ }
+ else {
+ # Carry on if no match found.
+ }
+ }
+ elsif ($words[0] =~ /^[A-Z]+(,[A-Z]+)*$/) {
+ # e.g. NEW,ASSI,REOP,FIX
+ undef %states;
+ if (matchPrefixes(\%states,
+ \%resolutions,
+ [split(/,/, $words[0])],
+ \@::legal_bug_status,
+ \@::legal_resolution)) {
+ shift @words;
+ }
+ else {
+ # Carry on if no match found
+ foreach (@openStates) { $states{$_} = 1 }
+ }
+ }
+ else {
+ # Default: search for unresolved bugs only.
+ # Put custom code here if you would like to change this behaviour.
+ }
+
+ # If we have wanted resolutions, allow closed states
+ if (keys(%resolutions)) {
+ foreach (@closedStates) { $states{$_} = 1 }
+ }
+
+ $cgi->param('bug_status', keys(%states));
+ $cgi->param('resolution', keys(%resolutions));
+
+ # Loop over all main-level QuickSearch words.
+ foreach my $qsword (@words) {
+ my $negate = substr($qsword, 0, 1) eq '-';
+ if ($negate) {
+ $qsword = substr($qsword, 1);
+ }
+
+ my $firstChar = substr($qsword, 0, 1);
+ my $baseWord = substr($qsword, 1);
+ my @subWords = split(/[\|,]/, $baseWord);
+ if ($firstChar eq '+') {
+ foreach (@subWords) {
+ addChart('short_desc', 'substring', $qsword, $negate);
+ }
+ }
+ elsif ($firstChar eq '#') {
+ addChart('short_desc', 'anywords', $baseWord, $negate);
+ if ($searchComments) {
+ addChart('longdesc', 'anywords', $baseWord, $negate);
+ }
+ }
+ elsif ($firstChar eq ':') {
+ foreach (@subWords) {
+ addChart('product', 'substring', $_, $negate);
+ addChart('component', 'substring', $_, $negate);
+ }
+ }
+ elsif ($firstChar eq '@') {
+ foreach (@subWords) {
+ addChart('assigned_to', 'substring', $_, $negate);
+ }
+ }
+ elsif ($firstChar eq '[') {
+ addChart('short_desc', 'substring', $baseWord, $negate);
+ addChart('status_whiteboard', 'substring', $baseWord, $negate);
+ }
+ elsif ($firstChar eq '!') {
+ addChart('keywords', 'anywords', $baseWord, $negate);
+
+ }
+ else { # No special first char
+
+ # Split by '|' to get all operands for a boolean OR.
+ foreach my $or_operand (split(/\|/, $qsword)) {
+ if ($or_operand =~ /^votes:([0-9]+)$/) {
+ # votes:xx ("at least xx votes")
+ addChart('votes', 'greaterthan', $1, $negate);
+ }
+ elsif ($or_operand =~ /^([^:]+):([^:]+)$/) {
+ # generic field1,field2,field3:value1,value2 notation
+ my @fields = split(/,/, $1);
+ my @values = split(/,/, $2);
+ foreach my $field (@fields) {
+ # Be tolerant about unknown fields
+ next unless defined($mappings{$field});
+ $field = $mappings{$field};
+ foreach (@values) {
+ addChart($field, 'substring', $_, $negate);
+ }
+ }
+
+ }
+ else {
+
+ # Having ruled out the special cases, we may now split
+ # by comma, which is another legal boolean OR indicator.
+ foreach my $word (split(/,/, $or_operand)) {
+ # Platform
+ if (grep({lc($word) eq $_} @platforms)) {
+ addChart('rep_platform', 'substring',
+ $word, $negate);
+ }
+ # Priority
+ elsif ($word =~ m/^[pP]([1-5](-[1-5])?)$/) {
+ addChart('priority', 'regexp',
+ "[$1]", $negate);
+ }
+ # Severity
+ elsif (grep({lc($word) eq substr($_, 0, 3)}
+ @::legal_severity)) {
+ addChart('bug_severity', 'substring',
+ $word, $negate);
+ }
+ # Votes (votes>xx)
+ elsif ($word =~ m/^votes>([0-9]+)$/) {
+ addChart('votes', 'greaterthan',
+ $1, $negate);
+ }
+ # Votes (votes>=xx, votes=>xx)
+ elsif ($word =~ m/^votes(>=|=>)([0-9]+)$/) {
+ addChart('votes', 'greaterthan',
+ $2-1, $negate);
+
+ }
+ else { # Default QuickSearch word
+
+ if (!grep({lc($word) eq $_}
+ @productExceptions) &&
+ length($word)>2
+ ) {
+ addChart('product', 'substring',
+ $word, $negate);
+ }
+ if (!grep({lc($word) eq $_}
+ @componentExceptions) &&
+ length($word)>2
+ ) {
+ addChart('component', 'substring',
+ $word, $negate);
+ }
+ if (grep({lc($word) eq $_}
+ @::legal_keywords)) {
+ addChart('keywords', 'substring',
+ $word, $negate);
+ if (length($word)>2) {
+ addChart('short_desc', 'substring',
+ $word, $negate);
+ addChart('status_whiteboard',
+ 'substring',
+ $word, $negate);
+ }
+
+ }
+ else {
+
+ addChart('short_desc', 'substring',
+ $word, $negate);
+ addChart('status_whiteboard', 'substring',
+ $word, $negate);
+ }
+ if ($searchComments) {
+ addChart('longdesc', 'substring',
+ $word, $negate);
+ }
+ }
+ # URL field (for IP addrs, host.names,
+ # scheme://urls)
+ if ($word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/
+ || $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/
+ || $word =~ /:[\\\/][\\\/]/
+ || $word =~ /localhost/
+ || $word =~ /mailto[:]?/
+ # || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port
+ ) {
+ addChart('bug_file_loc', 'substring',
+ $word, $negate);
+ }
+ } # foreach my $word (split(/,/, $qsword))
+ } # votes and generic field detection
+ } # foreach (split(/\|/, $_))
+ } # "switch" $firstChar
+ $chart++;
+ $and = 0;
+ $or = 0;
+ } # foreach (@words)
+
+ # We've been very tolerant about invalid queries, so all that's left
+ # may be an empty query.
+ scalar($cgi->param())>0 || ThrowUserError("buglist_parameters_required");
+ }
+
+ # List of quicksearch-specific CGI parameters to get rid of.
+ my @params_to_strip = ('quicksearch', 'load', 'run');
+ my $modified_query_string = $cgi->canonicalise_query(@params_to_strip);
+
+ if ($cgi->param('load')) {
+ # Param 'load' asks us to display the query in the advanced search form.
+ print $cgi->redirect(-uri => Param('urlbase') . "query.cgi?" .
+ "format=advanced&amp;" .
+ $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;
+}
+
+###########################################################################
+# Helpers
+###########################################################################
+
+# Split string on whitespace, retaining quoted strings as one
+sub splitString {
+ my $string = shift;
+ my @quoteparts;
+ my @parts;
+ my $i = 0;
+
+ # Escape backslashes
+ $string =~ s/\\/\\\//g;
+
+ # Now split on quote sign; be tolerant about unclosed quotes
+ @quoteparts = split(/"/, $string);
+ foreach (@quoteparts) {
+ # After every odd quote, escape whitespace
+ s/(\s)/\\$1/g if $i++ % 2;
+ }
+ # Join again
+ $string = join('"', @quoteparts);
+
+ # Now split on unescaped whitespace
+ @parts = split(/(?<!\\)\s+/, $string);
+ foreach (@parts) {
+ # Restore whitespace
+ s/\\(\s)/$1/g;
+ # Restore backslashes
+ s/\\\//\\/g;
+ # Remove quotes
+ s/"//g;
+ }
+ return @parts;
+}
+
+# Expand found prefixes to states or resolutions
+sub matchPrefixes {
+ my $hr_states = shift;
+ my $hr_resolutions = shift;
+ my $ar_prefixes = shift;
+ my $ar_check_states = shift;
+ my $ar_check_resolutions = shift;
+ 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 'substring') {
+ return 'notsubstring';
+ }
+ elsif ($comparisonType eq 'anywords') {
+ return 'nowords';
+ }
+ elsif ($comparisonType eq 'regexp') {
+ return 'notregexp';
+ }
+ else {
+ # Don't know how to negate that
+ ThrowCodeError('unknown_comparison_type');
+ }
+}
+
+# Add a boolean chart
+sub addChart {
+ my ($field, $comparisonType, $value, $negate) = @_;
+
+ $negate && ($comparisonType = negateComparisonType($comparisonType));
+ makeChart("$chart-$and-$or", $field, $comparisonType, $value);
+ if ($negate) {
+ $and++;
+ $or = 0;
+ }
+ else {
+ $or++;
+ }
+}
+
+# Create the CGI parameters for a boolean chart
+sub makeChart {
+ my ($expr, $field, $type, $value) = @_;
+
+ $cgi->param("field$expr", $field);
+ $cgi->param("type$expr", $type);
+ $cgi->param("value$expr", $value);
+}
+
+1;