aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CGI.pl49
-rw-r--r--CHANGES19
-rw-r--r--README30
-rw-r--r--bug_form.pl32
-rwxr-xr-xbuglist.cgi32
-rw-r--r--defparams.pl9
-rwxr-xr-xdoeditowners.cgi10
-rwxr-xr-xdoeditparams.cgi9
-rwxr-xr-xeditowners.cgi9
-rwxr-xr-xeditparams.cgi9
-rw-r--r--globals.pl29
-rwxr-xr-xlong_list.cgi7
-rwxr-xr-xmakebugtable.sh1
-rwxr-xr-xmakegroupstable.sh69
-rwxr-xr-xmakeprofilestable.sh1
-rwxr-xr-xprocess_bug.cgi16
-rwxr-xr-xquery.cgi16
-rwxr-xr-xsanitycheck.cgi16
18 files changed, 279 insertions, 84 deletions
diff --git a/CGI.pl b/CGI.pl
index f16640bc1..3f3ed82b4 100644
--- a/CGI.pl
+++ b/CGI.pl
@@ -231,6 +231,35 @@ sub PasswordForLogin {
return $result;
}
+
+sub quietly_check_login() {
+ $::usergroupset = '0';
+ my $loginok = 0;
+ if (defined $::COOKIE{"Bugzilla_login"} &&
+ defined $::COOKIE{"Bugzilla_logincookie"}) {
+ ConnectToDatabase();
+ SendSQL("select profiles.groupset, profiles.login_name = " .
+ SqlQuote($::COOKIE{"Bugzilla_login"}) .
+ " and profiles.cryptpassword = logincookies.cryptpassword " .
+ "and logincookies.hostname = " .
+ SqlQuote($ENV{"REMOTE_HOST"}) .
+ " from profiles,logincookies where logincookies.cookie = " .
+ $::COOKIE{"Bugzilla_logincookie"} .
+ " and profiles.userid = logincookies.userid");
+ my @row;
+ if (@row = FetchSQLData()) {
+ $loginok = $row[1];
+ if ($loginok) {
+ $::usergroupset = $row[0];
+ }
+ }
+ }
+ return $loginok;
+}
+
+
+
+
sub confirm_login {
my ($nexturl) = (@_);
@@ -324,25 +353,9 @@ To use the wonders of bugzilla, you can use the following:
}
- my $loginok = 0;
-
- if (defined $::COOKIE{"Bugzilla_login"} &&
- defined $::COOKIE{"Bugzilla_logincookie"}) {
- SendSQL("select profiles.login_name = " .
- SqlQuote($::COOKIE{"Bugzilla_login"}) .
- " and profiles.cryptpassword = logincookies.cryptpassword " .
- "and logincookies.hostname = " .
- SqlQuote($ENV{"REMOTE_HOST"}) .
- " from profiles,logincookies where logincookies.cookie = " .
- $::COOKIE{"Bugzilla_logincookie"} .
- " and profiles.userid = logincookies.userid");
- $loginok = FetchOneColumn();
- if (!defined $loginok) {
- $loginok = 0;
- }
- }
+ my $loginok = quietly_check_login();
- if ($loginok ne "1") {
+ if ($loginok != 1) {
print "Content-type: text/html\n\n";
print "<H1>Please log in.</H1>\n";
print "I need a legitimate e-mail address and password to continue.\n";
diff --git a/CHANGES b/CHANGES
index 06514c038..d152a9ba1 100644
--- a/CHANGES
+++ b/CHANGES
@@ -10,6 +10,25 @@ query the CVS tree. For example,
will tell you what has been changed in the last week.
+3/10/99 Added 'groups' stuff, where we have different group bits that we can
+put on a person or on a bug. Some of the group bits control access to bugzilla
+features. And a person can't access a bug unless he has every group bit set
+that is also set on the bug. See the comments in makegroupstable.sh for a bit
+more info.
+
+The 'maintainer' param is now used only as an email address for people to send
+complaints to. The groups table is what is now used to determine permissions.
+
+You will need to run the new script "makegroupstable.sh". And then you need to
+feed the following lines to MySQL (replace XXX with the login name of the
+maintainer, the person you wish to be all-powerful).
+
+ alter table bugs add column groupset bigint not null;
+ alter table profiles add column groupset bigint not null;
+ update profiles set groupset=0x7fffffffffffffff where login_name = XXX;
+
+
+
3/8/99 Added params to control how priorities are set in a new bug. You can
now choose whether to let submitters of new bugs choose a priority, or whether
they should just accept the default priority (which is now no longer hardcoded
diff --git a/README b/README
index cc98889c0..8aab09394 100644
--- a/README
+++ b/README
@@ -301,27 +301,23 @@ takes four parameters which are (with appropriate values):
Just fill in those values and close up global.pl
-5. Setting the Maintainer Information
+5. Setting up yourself as Maintainer
- Before the last file level configuration can be done you'll have to create
-a data/params file. This file is created when the first bugzilla page is
-accessed that needs it. The easiest way is to go visit the "query.cgi"
-bugzilla page. After that, the data subdirectory should have been created, and
-the data/params file should have appeared.
-
- Within that directory you'll find a file called 'params'. params contains
-all sorts of juicy things that you'll be tempted to change, but don't bother --
-there's a nice web form to change all except the maintainer's email address.
-Find the line that begins with "$::param{'maintainer'}" and set the
-maintainer's email address to your own.
-
- Now, you can create your own bugzilla account. To do so, just try to "add
+ Start by creating your own bugzilla account. To do so, just try to "add
a bug" from the main bugzilla menu (now available from your system through your
web browser!). You'll be prompted for logon info, and you should enter your
email address and then select 'mail me my password'. When you get the password
-mail, log in with it. Don't finish entering that new bug; instead, go to the
-query page (off of the bugzilla main menu) where you'll now find a 'edit
-parameters' option which is filled with editable treats.
+mail, log in with it. Don't finish entering that new bug.
+
+ Now, bring up MySQL, and add yourself to every group. This will
+effectively make you 'superuser'. The SQL to type is:
+
+ update profiles set groupset=0x7fffffffffffffff where login_name = XXX;
+
+replacing XXX with your email address in quotes.
+
+Now, if you go to the query page (off of the bugzilla main menu) where you'll
+now find a 'edit parameters' option which is filled with editable treats.
6. Setting Up the Whining Cron Job (Optional)
diff --git a/bug_form.pl b/bug_form.pl
index d56d6b42a..cd2ff0e33 100644
--- a/bug_form.pl
+++ b/bug_form.pl
@@ -21,6 +21,8 @@
use diagnostics;
use strict;
+quietly_check_login();
+
my $query = "
select
bug_id,
@@ -40,9 +42,11 @@ select
target_milestone,
qa_contact,
status_whiteboard,
- date_format(creation_ts,'Y-m-d')
+ date_format(creation_ts,'Y-m-d'),
+ groupset
from bugs
-where bug_id = $::FORM{'id'}";
+where bug_id = $::FORM{'id'}
+and bugs.groupset & $::usergroupset = bugs.groupset";
SendSQL($query);
my %bug;
@@ -53,7 +57,8 @@ if (@row = FetchSQLData()) {
"op_sys", "bug_status", "resolution", "priority",
"bug_severity", "component", "assigned_to", "reporter",
"bug_file_loc", "short_desc", "target_milestone",
- "qa_contact", "status_whiteboard", "creation_ts") {
+ "qa_contact", "status_whiteboard", "creation_ts",
+ "groupset") {
$bug{$field} = shift @row;
if (!defined $bug{$field}) {
$bug{$field} = "";
@@ -212,11 +217,28 @@ print "
<br>
<B>Additional Comments:</B>
<BR>
-<TEXTAREA WRAP=HARD NAME=comment ROWS=5 COLS=80></TEXTAREA><BR>
-<br>
+<TEXTAREA WRAP=HARD NAME=comment ROWS=5 COLS=80></TEXTAREA><BR>";
+
+
+if ($::usergroupset ne '0') {
+ SendSQL("select bit, description, (bit & $bug{'groupset'} != 0) from groups where bit & $::usergroupset != 0 and isbuggroup != 0 order by bit");
+ while (MoreSQLData()) {
+ my ($bit, $description, $ison) = (FetchSQLData());
+ my $check0 = !$ison ? " SELECTED" : "";
+ my $check1 = $ison ? " SELECTED" : "";
+ print "<select name=bit-$bit><option value=0$check0>\n";
+ print "People not in the \"$description\" group can see this bug\n";
+ print "<option value=1$check1>\n";
+ print "Only people in the \"$description\" group can see this bug\n";
+ print "</select><br>\n";
+ }
+}
+
+print "<br>
<INPUT TYPE=radio NAME=knob VALUE=none CHECKED>
Leave as <b>$bug{'bug_status'} $bug{'resolution'}</b><br>";
+
# knum is which knob number we're generating, in javascript terms.
my $knum = 1;
diff --git a/buglist.cgi b/buglist.cgi
index 8308bb940..b5f5e1608 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -177,12 +177,14 @@ my $dotweak = defined $::FORM{'tweak'};
if ($dotweak) {
confirm_login();
+} else {
+ quietly_check_login();
}
print "Content-type: text/html\n\n";
-my $query = "select bugs.bug_id";
+my $query = "select bugs.bug_id, bugs.groupset";
foreach my $c (@collist) {
@@ -210,6 +212,7 @@ where bugs.assigned_to = assign.userid
and bugs.reporter = report.userid
and bugs.product = projector.program
and bugs.version = projector.value
+and bugs.groupset & $::usergroupset = bugs.groupset
";
if ((defined $::FORM{'emailcc1'} && $::FORM{'emailcc1'}) ||
@@ -439,9 +442,19 @@ my %seen;
my @bugarray;
my %prodhash;
my %statushash;
+my $buggroupset = "";
while (@row = FetchSQLData()) {
my $bug_id = shift @row;
+ my $g = shift @row; # Bug's group set.
+ if ($buggroupset eq "") {
+ $buggroupset = $g;
+ } elsif ($buggroupset ne $g) {
+ $buggroupset = "x"; # We only play games with tweaking the
+ # buggroupset if all the bugs have exactly
+ # the same group. If they don't, we leave
+ # it alone.
+ }
if (!defined $seen{$bug_id}) {
$seen{$bug_id} = 1;
$count++;
@@ -627,6 +640,23 @@ document.write(\" <input type=button value=\\\"Uncheck All\\\" onclick=\\\"SetCh
<BR>
<TEXTAREA WRAP=HARD NAME=comment ROWS=5 COLS=80></TEXTAREA><BR>";
+if ($::usergroupset ne '0' && $buggroupset =~ /^\d*$/) {
+ SendSQL("select bit, description, (bit & $buggroupset != 0) from groups where bit & $::usergroupset != 0 and isbuggroup != 0 order by bit");
+ while (MoreSQLData()) {
+ my ($bit, $description, $ison) = (FetchSQLData());
+ my $check0 = !$ison ? " SELECTED" : "";
+ my $check1 = $ison ? " SELECTED" : "";
+ print "<select name=bit-$bit><option value=0$check0>\n";
+ print "People not in the \"$description\" group can see these bugs\n";
+ print "<option value=1$check1>\n";
+ print "Only people in the \"$description\" group can see these bugs\n";
+ print "</select><br>\n";
+ }
+}
+
+
+
+
# knum is which knob number we're generating, in javascript terms.
my $knum = 0;
diff --git a/defparams.pl b/defparams.pl
index d7b2e5a46..df1a06396 100644
--- a/defparams.pl
+++ b/defparams.pl
@@ -90,17 +90,10 @@ sub check_numeric {
# the database tables. The name of the parameter is of the form
# "tablename.columnname".
-# This very first one is silly. At some point, "superuserness" should be an
-# attribute of the person's profile entry, and not a single name like this.
-#
-# When first installing bugzilla, you need to either change this line to be
-# you, or (better) edit the initial "params" file and change the entry for
-# param(maintainer).
-
DefParam("maintainer",
"The email address of the person who maintains this installation of Bugzilla.",
"t",
- 'terry@mozilla.org');
+ 'THE MAINTAINER HAS NOT YET BEEN SET');
DefParam("urlbase",
"The URL that is the common initial leading part of all Bugzilla URLs.",
diff --git a/doeditowners.cgi b/doeditowners.cgi
index b09d7298b..540b4bfb0 100755
--- a/doeditowners.cgi
+++ b/doeditowners.cgi
@@ -24,17 +24,13 @@ use strict;
require "CGI.pl";
-# Shut up misguided -w warnings about "used only once":
-use vars %::COOKIE;
-
-
confirm_login();
print "Content-type: text/html\n\n";
-if (Param("maintainer") ne $::COOKIE{'Bugzilla_login'}) {
- print "<H1>Sorry, you aren't the maintainer of this system.</H1>\n";
- print "And so, you aren't allowed to edit the parameters of it.\n";
+if (!UserInGroup("editcomponents")) {
+ print "<H1>Sorry, you aren't a member of the 'editcomponents' group.</H1>\n";
+ print "And so, you aren't allowed to edit the owners.\n";
exit;
}
diff --git a/doeditparams.cgi b/doeditparams.cgi
index dd6214982..1df99f077 100755
--- a/doeditparams.cgi
+++ b/doeditparams.cgi
@@ -28,17 +28,16 @@ require "defparams.pl";
# Shut up misguided -w warnings about "used only once":
use vars %::param,
%::param_default,
- @::param_list,
- %::COOKIE;
+ @::param_list;
confirm_login();
print "Content-type: text/html\n\n";
-if (Param("maintainer") ne $::COOKIE{'Bugzilla_login'}) {
- print "<H1>Sorry, you aren't the maintainer of this system.</H1>\n";
- print "And so, you aren't allowed to edit the parameters of it.\n";
+if (!UserInGroup("tweakparams")) {
+ print "<H1>Sorry, you aren't a member of the 'tweakparams' group.</H1>\n";
+ print "And so, you aren't allowed to edit the parameters.\n";
exit;
}
diff --git a/editowners.cgi b/editowners.cgi
index 1bfb6ac13..1f4a7742a 100755
--- a/editowners.cgi
+++ b/editowners.cgi
@@ -26,16 +26,13 @@ use strict;
require "CGI.pl";
-# Shut up misguided -w warnings about "used only once":
-use vars %::COOKIE;
-
confirm_login();
print "Content-type: text/html\n\n";
-if (Param("maintainer") ne $::COOKIE{Bugzilla_login}) {
- print "<H1>Sorry, you aren't the maintainer of this system.</H1>\n";
- print "And so, you aren't allowed to edit the parameters of it.\n";
+if (!UserInGroup("editcomponents")) {
+ print "<H1>Sorry, you aren't a member of the 'editcomponents' group.</H1>\n";
+ print "And so, you aren't allowed to edit the owners.\n";
exit;
}
diff --git a/editparams.cgi b/editparams.cgi
index 75c7500d7..0171860da 100755
--- a/editparams.cgi
+++ b/editparams.cgi
@@ -28,16 +28,15 @@ require "defparams.pl";
# Shut up misguided -w warnings about "used only once":
use vars @::param_desc,
- @::param_list,
- %::COOKIE;
+ @::param_list;
confirm_login();
print "Content-type: text/html\n\n";
-if (Param("maintainer") ne $::COOKIE{Bugzilla_login}) {
- print "<H1>Sorry, you aren't the maintainer of this system.</H1>\n";
- print "And so, you aren't allowed to edit the parameters of it.\n";
+if (!UserInGroup("tweakparams")) {
+ print "<H1>Sorry, you aren't a member of the 'tweakparams' group.</H1>\n";
+ print "And so, you aren't allowed to edit the parameters.\n";
exit;
}
diff --git a/globals.pl b/globals.pl
index f3288e8c8..409d12b63 100644
--- a/globals.pl
+++ b/globals.pl
@@ -356,7 +356,19 @@ sub InsertNewUser {
for (my $i=0 ; $i<8 ; $i++) {
$password .= substr("abcdefghijklmnopqrstuvwxyz", int(rand(26)), 1);
}
- SendSQL("insert into profiles (login_name, password, cryptpassword) values (@{[SqlQuote($username)]}, '$password', encrypt('$password'))");
+ SendSQL("select bit, userregexp from groups where userregexp != ''");
+ my $groupset = "0";
+ while (MoreSQLData()) {
+ my @row = FetchSQLData();
+ if ($username =~ m/$row[1]/) {
+ $groupset .= "+ $row[0]"; # Silly hack to let MySQL do the math,
+ # not Perl, since we're dealing with 64
+ # bit ints here, and I don't *think* Perl
+ # does that.
+ }
+ }
+
+ SendSQL("insert into profiles (login_name, password, cryptpassword, groupset) values (@{[SqlQuote($username)]}, '$password', encrypt('$password'), $groupset)");
return $password;
}
@@ -484,6 +496,21 @@ sub SqlQuote {
+sub UserInGroup {
+ my ($groupname) = (@_);
+ if ($::usergroupset eq "0") {
+ return 0;
+ }
+ ConnectToDatabase();
+ SendSQL("select (bit & $::usergroupset) != 0 from groups where name = " . SqlQuote($groupname));
+ my $bit = FetchOneColumn();
+ if ($bit) {
+ return 1;
+ }
+ return 0;
+}
+
+
sub Param {
my ($value) = (@_);
if (defined $::param{$value}) {
diff --git a/long_list.cgi b/long_list.cgi
index 18f3c0e27..723aa2cf3 100755
--- a/long_list.cgi
+++ b/long_list.cgi
@@ -31,6 +31,9 @@ use vars %::FORM;
print "Content-type: text/html\n\n";
print "<TITLE>Full Text Bug Listing</TITLE>\n";
+ConnectToDatabase();
+quietly_check_login();
+
my $generic_query = "
select
bugs.bug_id,
@@ -52,9 +55,7 @@ select
bugs.status_whiteboard
from bugs,profiles assign,profiles report
where assign.userid = bugs.assigned_to and report.userid = bugs.reporter and
-";
-
-ConnectToDatabase();
+bugs.groupset & $::usergroupset = bugs.groupset and";
foreach my $bug (split(/:/, $::FORM{'buglist'})) {
SendSQL("$generic_query bugs.bug_id = $bug");
diff --git a/makebugtable.sh b/makebugtable.sh
index b6ca5473d..bad74010e 100755
--- a/makebugtable.sh
+++ b/makebugtable.sh
@@ -29,6 +29,7 @@ mysql << OK_ALL_DONE
use bugs;
create table bugs (
bug_id mediumint not null auto_increment primary key,
+groupset bigint not null,
assigned_to mediumint not null, # This is a comment.
bug_file_loc text,
bug_severity enum("critical", "major", "normal", "minor", "trivial", "enhancement") not null,
diff --git a/makegroupstable.sh b/makegroupstable.sh
new file mode 100755
index 000000000..561823878
--- /dev/null
+++ b/makegroupstable.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+#
+# The contents of this file are subject to the Mozilla Public License
+# Version 1.0 (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.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are Copyright (C) 1998
+# Netscape Communications Corporation. All Rights Reserved.
+#
+# Contributor(s): Terry Weissman <terry@mozilla.org>
+
+mysql > /dev/null 2>/dev/null << OK_ALL_DONE
+
+use bugs;
+
+drop table groups
+OK_ALL_DONE
+
+mysql << OK_ALL_DONE
+use bugs;
+create table groups (
+ # This must be a power of two. Groups are identified by a bit; sets of
+ # groups are indicated by or-ing these values together.
+ bit bigint not null,
+ name varchar(255) not null,
+ description text not null,
+
+ # isbuggroup is nonzero if this is a group that controls access to a set
+ # of bugs. In otherword, the groupset field in the bugs table should only
+ # have this group's bit set if isbuggroup is nonzero.
+ isbuggroup tinyint not null,
+
+ # User regexp is which email addresses are initially put into this group.
+ # This is only used when an email account is created; otherwise, profiles
+ # may be individually tweaked to add them in and out of groups.
+ userregexp tinytext not null,
+
+
+ unique(bit),
+ unique(name)
+
+);
+
+
+insert into groups (bit, name, description, userregexp) values (pow(2,0), "tweakparams", "Can tweak operating parameters", "");
+insert into groups (bit, name, description, userregexp) values (pow(2,1), "editgroupmembers", "Can put people in and out of groups that they are members of.", "");
+insert into groups (bit, name, description, userregexp) values (pow(2,2), "creategroups", "Can create and destroy groups.", "");
+insert into groups (bit, name, description, userregexp) values (pow(2,3), "editcomponents", "Can create, destroy, and edit components.", "");
+
+
+
+show columns from groups;
+show index from groups;
+
+
+# Check for bad bit values.
+
+select "*** Bad bit value", bit from groups where bit != pow(2, round(log(bit) / log(2)));
+
+OK_ALL_DONE
diff --git a/makeprofilestable.sh b/makeprofilestable.sh
index 76ce65c31..be4c03e4b 100755
--- a/makeprofilestable.sh
+++ b/makeprofilestable.sh
@@ -33,6 +33,7 @@ login_name varchar(255) not null,
password varchar(16),
cryptpassword varchar(64),
realname varchar(255),
+groupset bigint not null,
index(login_name)
);
diff --git a/process_bug.cgi b/process_bug.cgi
index f68b7e186..fc425a199 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -120,6 +120,22 @@ sub ChangeResolution {
}
+my $foundbit = 0;
+foreach my $b (grep(/^bit-\d*$/, keys %::FORM)) {
+ if (!$foundbit) {
+ $foundbit = 1;
+ DoComma();
+ $::query .= "groupset = 0";
+ }
+ if ($::FORM{$b}) {
+ my $v = substr($b, 4);
+ $::query .= "+ $v"; # Carefully written so that the math is
+ # done by MySQL, which can handle 64-bit math,
+ # and not by Perl, which I *think* can not.
+ }
+}
+
+
foreach my $field ("rep_platform", "priority", "bug_severity", "url",
"summary", "component", "bug_file_loc", "short_desc",
"product", "version", "component", "op_sys",
diff --git a/query.cgi b/query.cgi
index f782bd5c1..2f8e1bef4 100755
--- a/query.cgi
+++ b/query.cgi
@@ -377,17 +377,17 @@ print "
";
+quietly_check_login();
+
+if (UserInGroup("tweakparams")) {
+ print "<a href=editparams.cgi>Edit Bugzilla operating parameters</a><br>\n";
+}
+if (UserInGroup("editcomponents")) {
+ print "<a href=editowners.cgi>Edit Bugzilla component owners</a><br>\n";
+}
if (defined $::COOKIE{"Bugzilla_login"}) {
- if ($::COOKIE{"Bugzilla_login"} eq Param("maintainer")) {
- print "<a href=editparams.cgi>Edit Bugzilla operating parameters</a><br>\n";
- print "<a href=editowners.cgi>Edit Bugzilla component owners</a><br>\n";
- }
print "<a href=relogin.cgi>Log in as someone besides <b>$::COOKIE{'Bugzilla_login'}</b></a><br>\n";
}
print "<a href=changepassword.cgi>Change your password.</a><br>\n";
print "<a href=\"enter_bug.cgi\">Create a new bug.</a><br>\n";
print "<a href=\"reports.cgi\">Bug reports</a><br>\n";
-
-
-
-
diff --git a/sanitycheck.cgi b/sanitycheck.cgi
index 0d2f4072f..969cfd3c4 100755
--- a/sanitycheck.cgi
+++ b/sanitycheck.cgi
@@ -53,6 +53,22 @@ print "OK, now running sanity checks.<P>\n";
my @row;
my @checklist;
+Status("Checking groups");
+SendSQL("select bit from groups where bit != pow(2, round(log(bit) / log(2)))");
+while (my $bit = FetchOneColumn()) {
+ Alert("Illegal bit number found in group table: $bit");
+}
+
+SendSQL("select sum(bit) from groups where isbuggroup != 0");
+my $buggroupset = FetchOneColumn();
+SendSQL("select bug_id, groupset from bugs where groupset & $buggroupset != groupset");
+while (@row = FetchSQLData()) {
+ Alert("Bad groupset $row[1] found in bug " . BugLink($row[0]));
+}
+
+
+
+
Status("Checking version/products");
SendSQL("select distinct product, version from bugs");