summaryrefslogtreecommitdiffstats
path: root/extensions/ProdCompSearch
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/ProdCompSearch')
-rw-r--r--extensions/ProdCompSearch/Config.pm15
-rw-r--r--extensions/ProdCompSearch/Extension.pm21
-rw-r--r--extensions/ProdCompSearch/lib/WebService.pm120
-rw-r--r--extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl25
-rw-r--r--extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl36
-rw-r--r--extensions/ProdCompSearch/web/images/throbber.gifbin0 -> 723 bytes
-rw-r--r--extensions/ProdCompSearch/web/js/prod_comp_search.js139
-rw-r--r--extensions/ProdCompSearch/web/styles/prod_comp_search.css20
8 files changed, 376 insertions, 0 deletions
diff --git a/extensions/ProdCompSearch/Config.pm b/extensions/ProdCompSearch/Config.pm
new file mode 100644
index 000000000..c28b6d8f6
--- /dev/null
+++ b/extensions/ProdCompSearch/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProdCompSearch;
+use strict;
+
+use constant NAME => 'ProdCompSearch';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/ProdCompSearch/Extension.pm b/extensions/ProdCompSearch/Extension.pm
new file mode 100644
index 000000000..a5955fd8b
--- /dev/null
+++ b/extensions/ProdCompSearch/Extension.pm
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProdCompSearch;
+use strict;
+use base qw(Bugzilla::Extension);
+
+our $VERSION = '1';
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{PCS} = "Bugzilla::Extension::ProdCompSearch::WebService";
+}
+
+
+__PACKAGE__->NAME;
diff --git a/extensions/ProdCompSearch/lib/WebService.pm b/extensions/ProdCompSearch/lib/WebService.pm
new file mode 100644
index 000000000..3f8f0b75e
--- /dev/null
+++ b/extensions/ProdCompSearch/lib/WebService.pm
@@ -0,0 +1,120 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProdCompSearch::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(detaint_natural trick_taint trim);
+
+sub prod_comp_search {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->switch_to_shadow_db();
+
+ my $search = trim($params->{'search'} || '');
+ $search || ThrowCodeError('param_required',
+ { function => 'PCS.prod_comp_search', param => 'search' });
+
+ my $limit = detaint_natural($params->{'limit'})
+ ? $dbh->sql_limit($params->{'limit'})
+ : '';
+
+ # We do this in the DB directly as we want it to be fast and
+ # not have the overhead of loading full product objects
+
+ # All products which the user has "Entry" access to.
+ my $enterable_ids = $dbh->selectcol_arrayref(
+ 'SELECT products.id FROM products
+ LEFT JOIN group_control_map
+ ON group_control_map.product_id = products.id
+ AND group_control_map.entry != 0
+ AND group_id NOT IN (' . $user->groups_as_string . ')
+ WHERE group_id IS NULL
+ AND products.isactive = 1');
+
+ if (scalar @$enterable_ids) {
+ # And all of these products must have at least one component
+ # and one version.
+ $enterable_ids = $dbh->selectcol_arrayref(
+ 'SELECT DISTINCT products.id FROM products
+ WHERE ' . $dbh->sql_in('products.id', $enterable_ids) .
+ ' AND products.id IN (SELECT DISTINCT components.product_id
+ FROM components
+ WHERE components.isactive = 1)
+ AND products.id IN (SELECT DISTINCT versions.product_id
+ FROM versions
+ WHERE versions.isactive = 1)');
+ }
+
+ return { products => [] } if !scalar @$enterable_ids;
+
+ trick_taint($search);
+ my @terms;
+ my @order;
+
+ if ($search =~ /^(.*?)::(.*)$/) {
+ my ($product, $component) = (trim($1), trim($2));
+ push @terms, _build_terms($product, 1, 0);
+ push @terms, _build_terms($component, 0, 1);
+ push @order, "products.name != " . $dbh->quote($product) if $product ne '';
+ push @order, "components.name != " . $dbh->quote($component) if $component ne '';
+ push @order, "products.name";
+ push @order, "components.name";
+ } else {
+ push @terms, _build_terms($search, 1, 1);
+ push @order, "products.name != " . $dbh->quote($search);
+ push @order, "components.name != " . $dbh->quote($search);
+ push @order, "products.name";
+ push @order, "components.name";
+ }
+ return { products => [] } if !scalar @terms;
+
+ # To help mozilla staff file bmo administration bugs into the right
+ # component, sort bmo first when searching for 'bugzilla'
+ if ($search =~ /bugzilla/i && $search !~ /^bugzilla\s*::/i
+ && ($user->in_group('mozilla-corporation') || $user->in_group('mozilla-foundation')))
+ {
+ unshift @order, "products.name != 'bugzilla.mozilla.org'";
+ }
+
+ my $products = $dbh->selectall_arrayref("
+ SELECT products.name AS product,
+ components.name AS component
+ FROM products
+ INNER JOIN components ON products.id = components.product_id
+ WHERE (" . join(" AND ", @terms) . ")
+ AND products.id IN (" . join(",", @$enterable_ids) . ")
+ ORDER BY " . join(", ", @order) . " $limit",
+ { Slice => {} });
+
+ return { products => $products };
+}
+
+sub _build_terms {
+ my ($query, $product, $component) = @_;
+ my $dbh = Bugzilla->dbh();
+
+ my @fields;
+ push @fields, 'products.name', 'products.description' if $product;
+ push @fields, 'components.name', 'components.description' if $component;
+ # note: CONCAT_WS is MySQL specific
+ my $field = "CONCAT_WS(' ', ". join(',', @fields) . ")";
+
+ my @terms;
+ foreach my $word (split(/[\s,]+/, $query)) {
+ push(@terms, $dbh->sql_iposition($dbh->quote($word), $field) . " > 0")
+ if $word ne '';
+ }
+ return @terms;
+}
+
+1;
diff --git a/extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl b/extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl
new file mode 100644
index 000000000..5b39315b5
--- /dev/null
+++ b/extensions/ProdCompSearch/template/en/default/pages/prodcompsearch.html.tmpl
@@ -0,0 +1,25 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "File a $terms.Bug"
+ javascript_urls = [ "js/yui3/yui/yui-min.js",
+ "extensions/ProdCompSearch/web/js/prod_comp_search.js" ]
+ style_urls = [ "extensions/ProdCompSearch/web/styles/prod_comp_search.css" ]
+%]
+
+<div id="prod_comp_search_main">
+ [% PROCESS prodcompsearch/form.html.tmpl
+ query_header = "File a $terms.Bug:"
+ script_name = "enter_bug.cgi"
+ %]
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl b/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl
new file mode 100644
index 000000000..b3d1deab1
--- /dev/null
+++ b/extensions/ProdCompSearch/template/en/default/prodcompsearch/form.html.tmpl
@@ -0,0 +1,36 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script type="text/javascript">
+ [% IF script_name %]
+ ProdCompSearch.script_name = '[% script_name FILTER js %]';
+ [% END %]
+ [% IF format %]
+ ProdCompSearch.format = '[% format FILTER js %]';
+ [% END %]
+ [% IF cloned_bug_id %]
+ ProdCompSearch.cloned_bug_id = '[% cloned_bug_id FILTER js %]';
+ [% END %]
+ [% IF new_tab %]
+ ProdCompSearch.new_tab = true;
+ [% END %]
+</script>
+
+<div id="prod_comp_search_form" class="yui3-skin-sam">
+ <div id="prod_comp_search_header">
+ [% input_label FILTER none %]&nbsp;
+ <img id="prod_comp_throbber" src="extensions/ProdCompSearch/web/images/throbber.gif"
+ class="bz_default_hidden" width="16" height="11">
+ <span id="prod_comp_no_components" class="bz_default_hidden">
+ No components found</span>
+ <span id="prod_comp_error" class="bz_default_hidden">
+ An error occured</span>
+ </div>
+ <input id="prod_comp_search" type="text" size="50"
+ placeholder="Search by product and component keywords">
+</div>
diff --git a/extensions/ProdCompSearch/web/images/throbber.gif b/extensions/ProdCompSearch/web/images/throbber.gif
new file mode 100644
index 000000000..bc4fa6561
--- /dev/null
+++ b/extensions/ProdCompSearch/web/images/throbber.gif
Binary files differ
diff --git a/extensions/ProdCompSearch/web/js/prod_comp_search.js b/extensions/ProdCompSearch/web/js/prod_comp_search.js
new file mode 100644
index 000000000..cb4a50ccc
--- /dev/null
+++ b/extensions/ProdCompSearch/web/js/prod_comp_search.js
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+// Product and component search to file a new bug
+
+var ProdCompSearch = {
+ script_name: 'enter_bug.cgi',
+ script_choices: ['enter_bug.cgi', 'describecomponents.cgi'],
+ format: null,
+ cloned_bug_id: null,
+ new_tab: null
+};
+
+YUI({
+ base: 'js/yui3/',
+ combine: false
+}).use("node", "json-stringify", "autocomplete", "escape",
+ "datasource-io", "datasource-jsonschema", function(Y) {
+ Y.on("domready", function() {
+ var counter = 0,
+ dataSource = null,
+ autoComplete = null;
+
+ var resultListFormat = function(query, results) {
+ return Y.Array.map(results, function(result) {
+ var data = result.raw;
+ result.text = data.product + ' :: ' + data.component;
+ return Y.Escape.html(result.text);
+ });
+ };
+
+ var requestTemplate = function(query) {
+ counter = counter + 1;
+ var json_object = {
+ version: "1.1",
+ method : "PCS.prod_comp_search",
+ id : counter,
+ params : { search: query }
+ };
+ return Y.JSON.stringify(json_object);
+ };
+
+ var dataSource = new Y.DataSource.IO({
+ source: 'jsonrpc.cgi',
+ ioConfig: {
+ method: "POST",
+ headers: { 'Content-Type': 'application/json' }
+ },
+ on: {
+ error: function(e) {
+ if (console.error && e.response.meta.error) {
+ console.error(e.response.meta.error.message);
+ }
+ Y.one("#prod_comp_throbber").addClass('bz_default_hidden');
+ Y.one("#prod_comp_error").removeClass('bz_default_hidden');
+ }
+ }
+ });
+
+ dataSource.plug(Y.Plugin.DataSourceJSONSchema, {
+ schema: {
+ resultListLocator : "result.products",
+ resultFields : [ "product", "component" ],
+ metaFields : { error : 'error' }
+ }
+ });
+
+ var input = Y.one('#prod_comp_search');
+
+ input.plug(Y.Plugin.AutoComplete, {
+ activateFirstItem: false,
+ enableCache: true,
+ source: dataSource,
+ minQueryLength: 3,
+ queryDelay: 0.05,
+ resultFormatter: resultListFormat,
+ suppressInputUpdate: true,
+ maxResults: 25,
+ scrollIntoView: true,
+ requestTemplate: requestTemplate,
+ on: {
+ query: function(e) {
+ Y.one("#prod_comp_throbber").removeClass('bz_default_hidden');
+ Y.one("#prod_comp_no_components").addClass('bz_default_hidden');
+ Y.one("#prod_comp_error").addClass('bz_default_hidden');
+ },
+ results: function(e) {
+ Y.one("#prod_comp_throbber").addClass('bz_default_hidden');
+ input.ac.set('activateFirstItem', e.results.length == 1);
+ if (e.results.length == 0) {
+ Y.one("#prod_comp_no_components").removeClass('bz_default_hidden');
+ }
+ },
+ select: function(e) {
+ // Only redirect if the script_name is a valid choice
+ if (Y.Array.indexOf(ProdCompSearch.script_choices, ProdCompSearch.script_name) == -1)
+ return;
+
+ var data = e.result.raw;
+ var url = ProdCompSearch.script_name +
+ "?product=" + encodeURIComponent(data.product) +
+ "&component=" + encodeURIComponent(data.component);
+ if (ProdCompSearch.script_name == 'enter_bug.cgi') {
+ if (ProdCompSearch.format)
+ url += "&format=" + encodeURIComponent(ProdCompSearch.format);
+ if (ProdCompSearch.cloned_bug_id)
+ url += "&cloned_bug_id=" + encodeURIComponent(ProdCompSearch.cloned_bug_id);
+ }
+ if (ProdCompSearch.script_name == 'describecomponents.cgi') {
+ url += "#" + encodeURIComponent(data.component);
+ }
+ if (ProdCompSearch.new_tab) {
+ window.open(url, '_blank');
+ }
+ else {
+ window.location.href = url;
+ }
+ }
+ },
+ after: {
+ select: function(e) {
+ if (ProdCompSearch.new_tab) {
+ input.set('value','');
+ }
+ }
+ }
+ });
+
+ input.on('focus', function (e) {
+ if (e.target.value && e.target.value.length > 3) {
+ dataSource.load(e.target.value);
+ }
+ });
+ });
+});
diff --git a/extensions/ProdCompSearch/web/styles/prod_comp_search.css b/extensions/ProdCompSearch/web/styles/prod_comp_search.css
new file mode 100644
index 000000000..a89856e22
--- /dev/null
+++ b/extensions/ProdCompSearch/web/styles/prod_comp_search.css
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#prod_comp_search_main {
+ width: 400px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+#prod_comp_search_form .yui3-aclist-input {
+ width: 360px;
+}
+
+#prod_comp_no_components, #prod_comp_error {
+ color: red;
+}