From c64d51111a5ae02d6fc45163a847d0b7e2004548 Mon Sep 17 00:00:00 2001
From: "bugreport%peshkin.net" <>
Date: Mon, 25 Nov 2002 03:56:17 +0000
Subject: [PATCH] Bug 147275 Rearchitect product groups Patch by joel
 r=bbaetz,justdave a=justdave

---
 Bugzilla/Config.pm                            |  10 +
 Bugzilla/Constants.pm                         |  76 +++
 bug_form.pl                                   |  56 +-
 buglist.cgi                                   |   1 -
 checksetup.pl                                 |  65 ++-
 defparams.pl                                  |   6 +-
 describecomponents.cgi                        |   9 +-
 editgroups.cgi                                |  57 +-
 editproducts.cgi                              | 539 +++++++++++++++++-
 enter_bug.cgi                                 |  60 +-
 globals.pl                                    | 113 +++-
 post_bug.cgi                                  |  35 +-
 process_bug.cgi                               | 213 +++++--
 query.cgi                                     |  11 +-
 queryhelp.cgi                                 |   2 +-
 reports.cgi                                   |  26 +-
 sanitycheck.cgi                               |  50 ++
 .../groupcontrol/confirm-edit.html.tmpl       |  55 ++
 .../products/groupcontrol/edit.html.tmpl      | 284 +++++++++
 template/en/default/bug/edit.html.tmpl        |  22 +-
 .../bug/process/verify-new-product.html.tmpl  |   4 +-
 .../en/default/global/user-error.html.tmpl    |  10 +
 22 files changed, 1507 insertions(+), 197 deletions(-)
 create mode 100644 Bugzilla/Constants.pm
 create mode 100644 template/en/default/admin/products/groupcontrol/confirm-edit.html.tmpl
 create mode 100644 template/en/default/admin/products/groupcontrol/edit.html.tmpl

diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 25792d476..f72004e3b 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -167,6 +167,16 @@ sub UpdateParams {
         delete $param{'usequip'};
     }
 
+    # Change from old product groups to controls for group_control_map
+    # 2002-10-14 bug 147275 bugreport@peshkin.net
+    if (exists $param{'usebuggroups'} && !exists $param{'makeproductgroups'}) {
+        $param{'makeproductgroups'} = $param{'usebuggroups'};
+    }
+    if (exists $param{'usebuggroupsentry'} 
+       && !exists $param{'useentrygroupdefault'}) {
+        $param{'useentrygroupdefault'} = $param{'usebuggroupsentry'};
+    }
+
     # --- DEFAULTS FOR NEW PARAMS ---
 
     foreach my $item (@param_list) {
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
new file mode 100644
index 000000000..70773e036
--- /dev/null
+++ b/Bugzilla/Constants.pm
@@ -0,0 +1,76 @@
+# -*- 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.
+#
+# 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>
+#                 Dawn Endico <endico@mozilla.org>
+#                 Dan Mosedale <dmose@mozilla.org>
+#                 Joe Robins <jmrobins@tgix.com>
+#                 Jake <jake@bugzilla.org>
+#                 J. Paul Reed <preed@sigkill.com>
+#                 Bradley Baetz <bbaetz@student.usyd.edu.au>
+#                 Christopher Aillon <christopher@aillon.com>
+
+
+package Bugzilla::Constants;
+use strict;
+use base qw(Exporter);
+
+@Bugzilla::Constants::EXPORT = qw(
+    CONTROLMAPNA
+    CONTROLMAPSHOWN
+    CONTROLMAPDEFAULT
+    CONTROLMAPMANDATORY
+    );
+   
+
+# CONSTANTS
+#
+# ControlMap constants for group_control_map.
+# membercontol:othercontrol => meaning
+# Na:Na               => Bugs in this product may not be restricted to this 
+#                        group.
+# Shown:Na            => Members of the group may restrict bugs 
+#                        in this product to this group.
+# Shown:Shown         => Members of the group may restrict bugs
+#                        in this product to this group.
+#                        Anyone who can enter bugs in this product may initially
+#                        restrict bugs in this product to this group.
+# Shown:Mandatory     => Members of the group may restrict bugs
+#                        in this product to this group.
+#                        Non-members who can enter bug in this product
+#                        will be forced to restrict it.
+# Default:Na          => Members of the group may restrict bugs in this
+#                        product to this group and do so by default.
+# Default:Default     => Members of the group may restrict bugs in this
+#                        product to this group and do so by default and
+#                        nonmembers have this option on entry.
+# Default:Mandatory   => Members of the group may restrict bugs in this
+#                        product to this group and do so by default.
+#                        Non-members who can enter bug in this product
+#                        will be forced to restrict it.
+# Mandatory:Mandatory => Bug will be forced into this group regardless.
+# All other combinations are illegal.
+
+use constant CONTROLMAPNA => 0;
+use constant CONTROLMAPSHOWN => 1;
+use constant CONTROLMAPDEFAULT => 2;
+use constant CONTROLMAPMANDATORY => 3;
+
+1;
+
diff --git a/bug_form.pl b/bug_form.pl
index c620f03fd..e390ad51e 100644
--- a/bug_form.pl
+++ b/bug_form.pl
@@ -24,7 +24,7 @@
 use strict;
 
 use RelationSet;
-
+use Bugzilla::Constants;
 # Use the Attachment module to display attachments for the bug.
 use Attachment;
 
@@ -144,12 +144,9 @@ sub show_bug {
             next;
         }
 
-        if (Param("usebuggroupsentry")
-          && GroupExists($product)
-          && !UserInGroup($product))
-        {
+        if (!CanEnterProduct($product)) {
             # If we're using bug groups to restrict entry on products, and
-            # this product has a bug group, and the user is not in that
+            # this product has an entry group, and the user is not in that
             # group, we don't want to include that product in this list.
             next;
         }
@@ -275,7 +272,7 @@ sub show_bug {
     SendSQL("SELECT DISTINCT groups.id, name, description," .
              " bug_group_map.group_id IS NOT NULL," .
              " user_group_map.group_id IS NOT NULL," .
-             " isactive" .
+             " isactive, membercontrol, othercontrol" .
              " FROM groups" . 
              " LEFT JOIN bug_group_map" .
              " ON bug_group_map.group_id = groups.id" .
@@ -284,33 +281,48 @@ sub show_bug {
              " ON user_group_map.group_id = groups.id" .
              " AND user_id = $::userid" .
              " AND NOT isbless" .
+             " LEFT JOIN group_control_map" .
+             " ON group_control_map.group_id = groups.id" .
+             " AND group_control_map.product_id = " . $bug{'product_id'} .
              " WHERE isbuggroup");
 
     $user{'inallgroups'} = 1;
 
     while (MoreSQLData()) {
-        my ($groupid, $name, $description, $ison, $ingroup, $isactive) 
-            = FetchSQLData();
+        my ($groupid, $name, $description, $ison, $ingroup, $isactive, 
+            $membercontrol, $othercontrol) = FetchSQLData();
 
         $bug{'inagroup'} = 1 if ($ison);
+        $membercontrol ||= 0;
 
+        if ($isactive && ($membercontrol == CONTROLMAPMANDATORY)) {
+            $bug{'inagroup'} = 1;
+        }
         # For product groups, we only want to display the checkbox if either
-        # (1) The bit is already set, or
-        # (2) The user is in the group, but either:
-        #     (a) The group is a product group for the current product, or
-        #     (b) The group name isn't a product name
-        # This means that all product groups will be skipped, but 
-        # non-product bug groups will still be displayed.
-        if($ison || 
-           ($isactive && ($ingroup && (!Param("usebuggroups") || ($name eq $bug{'product'}) ||
-                         (!defined $::proddesc{$name})))))
+        # (1) The bit is set and not required, or
+        # (2) The group is Shown or Default for members and
+        #     the user is a member of the group.
+        if ($ison || 
+           ($isactive && $ingroup 
+                       && (($membercontrol == CONTROLMAPDEFAULT)
+                       || ($membercontrol == CONTROLMAPSHOWN))
+            ))
         {
             $user{'inallgroups'} &= $ingroup;
 
-            push (@groups, { "bit" => $groupid,
-                             "ison" => $ison,
-                             "ingroup" => $ingroup,
-                             "description" => $description });            
+            my $mandatory;
+            if ($isactive && ($membercontrol == CONTROLMAPMANDATORY)) {
+                $mandatory = 1;
+            } else {
+                $mandatory = 0;
+            }
+            if (($ison) || ($ingroup)) {
+                push (@groups, { "bit" => $groupid,
+                                 "ison" => $ison,
+                                 "ingroup" => $ingroup,
+                                 "mandatory" => $mandatory,
+                                 "description" => $description });            
+            }
         }
     }
 
diff --git a/buglist.cgi b/buglist.cgi
index fdab9eb83..18d5b1b55 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -728,7 +728,6 @@ $vars->{'order'} = $order;
 my $login = $::COOKIE{'Bugzilla_login'};
 
 $vars->{'caneditbugs'} = UserInGroup('editbugs');
-$vars->{'usebuggroups'} = Param('usebuggroups');
 
 # Whether or not this user is authorized to move bugs to another installation.
 $vars->{'ismover'} = 1
diff --git a/checksetup.pl b/checksetup.pl
index f4a70c284..5f947dabe 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -112,6 +112,7 @@
 
 use strict;
 use vars qw( $db_name %answer );
+use Bugzilla::Constants;
 
 ###########################################################################
 # Non-interactive override
@@ -1716,6 +1717,17 @@ $table{quips} =
      userid mediumint not null default 0, 
      quip text not null';
 
+$table{group_control_map} =
+    'group_id mediumint not null,
+     product_id mediumint not null,
+     entry tinyint not null,
+     membercontrol tinyint not null,
+     othercontrol tinyint not null,
+     canedit tinyint not null,
+     
+     unique(product_id, group_id),
+     index(group_id)';
+
 ###########################################################################
 # Create tables
 ###########################################################################
@@ -2954,7 +2966,7 @@ if (GetFieldDef("logincookies", "hostname")) {
     AddField("logincookies", "ipaddr", "varchar(40) NOT NULL");
 }
 
-# 2002-05-10 - enhanchment bug 143826
+# 2002-08-19 - bugreport@peshkin.net bug 143826
 # Add private comments and private attachments on less-private bugs
 AddField('longdescs', 'isprivate', 'tinyint not null default 0');
 AddField('attachments', 'isprivate', 'tinyint not null default 0');
@@ -3121,7 +3133,7 @@ if (($fielddef = GetFieldDef("attachments", "creation_ts")) &&
     ChangeFieldType("attachments", "creation_ts", "datetime NOT NULL");
 }
 
-# 2002-08-XX - bugreport@peshkin.net - bug 157756
+# 2002-09-22 - bugreport@peshkin.net - bug 157756
 #
 # If the whole groups system is new, but the installation isn't, 
 # convert all the old groupset groups, etc...
@@ -3464,6 +3476,54 @@ if (TableExists("attachstatuses") && TableExists("attachstatusdefs")) {
     print "done.\n";
 }
 
+# 2002-11-24 - bugreport@peshkin.net - bug 147275 
+#
+if (Param('makeproductgroups')) {
+    # If makeproductgroups is enabled and group_control_map is empty,
+    # backward-compatbility usebuggroups-equivalent records should
+    # be created.
+    my $entry = Param('useentrygroupdefault');
+    $sth = $dbh->prepare("SELECT COUNT(*) FROM group_control_map");
+    $sth->execute();
+    my ($mapcnt) = $sth->fetchrow_array();
+    if ($mapcnt == 0) {
+        # Initially populate group_control_map.
+        # First, get all the existing products and their groups.
+        $sth = $dbh->prepare("SELECT groups.id, products.id, groups.name, " .
+                             "products.name FROM groups, products " .
+                             "WHERE isbuggroup != 0 AND isactive != 0");
+        $sth->execute();
+        while (my ($groupid, $productid, $groupname, $productname) 
+                = $sth->fetchrow_array()) {
+            if ($groupname eq $productname) {
+                # Product and group have same name.
+                $dbh->do("INSERT INTO group_control_map " .
+                         "(group_id, product_id, entry, membercontrol, " .
+                         "othercontrol, canedit) " .
+                         "VALUES ($groupid, $productid, $entry, " .
+                         CONTROLMAPDEFAULT . ", " .
+                         CONTROLMAPNA . ", 0)");
+            } else {
+                # See if this group is a product group at all.
+                my $sth2 = $dbh->prepare("SELECT id FROM products WHERE name = " .
+                                     $dbh->quote($groupname));
+                $sth2->execute();
+                my ($id) = $sth2->fetchrow_array();
+                if (!$id) {
+                    # If there is no product with the same name as this
+                    # group, then it is permitted for all products.
+                    $dbh->do("INSERT INTO group_control_map " .
+                             "(group_id, product_id, entry, membercontrol, " .
+                             "othercontrol, canedit) " .
+                             "VALUES ($groupid, $productid, 0, " .
+                             CONTROLMAPSHOWN . ", " .
+                             CONTROLMAPNA . ", 0)");
+                }
+            }
+        }
+    }
+}
+
 # If you had to change the --TABLE-- definition in any way, then add your
 # differential change code *** A B O V E *** this comment.
 #
@@ -3793,3 +3853,4 @@ $dbh->do("UPDATE components SET initialowner = $adminuid WHERE initialowner = 0"
 unlink "data/versioncache";
 
 print "Reminder: Bugzilla now requires version 8.7 or later of sendmail.\n" unless $silent;
+
diff --git a/defparams.pl b/defparams.pl
index f518a2de1..b0567c783 100644
--- a/defparams.pl
+++ b/defparams.pl
@@ -244,7 +244,7 @@ sub check_netmask {
   },
 
   {
-   name => 'usebuggroups',
+   name => 'makeproductgroups',
    desc => 'If this is on, Bugzilla will associate a bug group with each ' .
            'product in the database, and use it for querying bugs.',
    type => 'b',
@@ -252,9 +252,9 @@ sub check_netmask {
   },
 
   {
-   name => 'usebuggroupsentry',
+   name => 'useentrygroupdefault',
    desc => 'If this is on, Bugzilla will use product bug groups to restrict ' .
-           'who can enter bugs.  Requires usebuggroups to be on as well.',
+           'who can enter bugs.  Requires makeproductgroups to be on as well.',
    type => 'b',
    default => 0
   },
diff --git a/describecomponents.cgi b/describecomponents.cgi
index b4953ddc6..a1a6f0049 100755
--- a/describecomponents.cgi
+++ b/describecomponents.cgi
@@ -42,11 +42,11 @@ if (!defined $::FORM{'product'}) {
     # Reference to a subset of %::proddesc, which the user is allowed to see
     my %products;
 
-    if (Param("usebuggroups")) {
+    if (AnyDefaultGroups()) {
         # OK, now only add products the user can see
         confirm_login() unless $::userid;
         foreach my $p (@::legal_product) {
-            if (!GroupExists($p) || UserInGroup($p)) {
+            if (CanEnterProduct($p)) {
                 $products{$p} = $::proddesc{$p};
             }
         }
@@ -88,11 +88,8 @@ if (!$product_id) {
 }
 
 # Make sure the user is authorized to access this product.
-if (Param("usebuggroups") && GroupExists($product)) {
-    confirm_login() unless $::userid;
-    UserInGroup($product)
+CanEnterProduct($product)
       || ThrowUserError("product_access_denied");
-}
 
 ######################################################################
 # End Data/Security Validation
diff --git a/editgroups.cgi b/editgroups.cgi
index 5dd2395af..031a23c9e 100755
--- a/editgroups.cgi
+++ b/editgroups.cgi
@@ -27,6 +27,7 @@
 use strict;
 use lib ".";
 
+use Bugzilla::Constants;
 require "CGI.pl";
 
 ConnectToDatabase();
@@ -117,9 +118,9 @@ unless ($action) {
     while (MoreSQLData()) {
         my ($groupid, $name, $desc, $regexp, $isactive, $isbuggroup) = FetchSQLData();
         print "<tr>\n";
-        print "<td>$name</td>\n";
-        print "<td>$desc</td>\n";
-        print "<td>$regexp&nbsp</td>\n";
+        print "<td>" . html_quote($name) . "</td>\n";
+        print "<td>" . html_quote($desc) . "</td>\n";
+        print "<td>" . html_quote($regexp) . "&nbsp</td>\n";
         print "<td align=center>";
         print "X" if (($isactive != 0) && ($isbuggroup != 0));
         print "&nbsp</td>\n";
@@ -185,22 +186,27 @@ if ($action eq 'changeform') {
     print "<TABLE BORDER=1 CELLPADDING=4>";
     print "<TR><TH>Group:</TH><TD>";
     if ($isbuggroup == 0) {
-        print "$name";
+        print html_quote($name);
     } else {
-        print "<INPUT TYPE=HIDDEN NAME=\"oldname\" VALUE=$name>
-        <INPUT SIZE=60 NAME=\"name\" VALUE=\"$name\">";
+        print "<INPUT TYPE=HIDDEN NAME=\"oldname\" VALUE=" . 
+        html_quote($name) . ">
+        <INPUT SIZE=60 NAME=\"name\" VALUE=\"" . html_quote($name) . "\">";
     }
     print "</TD></TR><TR><TH>Description:</TH><TD>";
     if ($isbuggroup == 0) {
-        print "$description";
+        print html_quote($description);
     } else {
-        print "<INPUT TYPE=HIDDEN NAME=\"olddesc\" VALUE=\"$description\">
-        <INPUT SIZE=70 NAME=\"desc\" VALUE=\"$description\">";
+        print "<INPUT TYPE=HIDDEN NAME=\"olddesc\" VALUE=\"" .
+        html_quote($description) . "\">
+        <INPUT SIZE=70 NAME=\"desc\" VALUE=\"" . 
+            html_quote($description) . "\">";
     }
     print "</TD></TR><TR>
            <TH>User Regexp:</TH><TD>";
-    print "<INPUT TYPE=HIDDEN NAME=\"oldrexp\" VALUE=\"$rexp\">
-           <INPUT SIZE=40 NAME=\"rexp\" VALUE=\"$rexp\"></TD></TR>";
+    print "<INPUT TYPE=HIDDEN NAME=\"oldrexp\" VALUE=\"" . 
+           html_quote($rexp) . "\">
+           <INPUT SIZE=40 NAME=\"rexp\" VALUE=\"" . 
+           html_quote($rexp) . "\"></TD></TR>";
     if ($isbuggroup == 1) {
         print "<TR><TH>Use For Bugs:</TH><TD>
         <INPUT TYPE=checkbox NAME =\"isactive\" VALUE=1 " . (($isactive == 1) ? "CHECKED" : "") . ">
@@ -252,8 +258,8 @@ if ($action eq 'changeform') {
         print "<INPUT TYPE=HIDDEN NAME=\"oldbless-$grpid\" VALUE=$blessmember></TD>";
         print "<TD><INPUT TYPE=checkbox NAME=\"grp-$grpid\" $grpchecked VALUE=1>";
         print "<INPUT TYPE=HIDDEN NAME=\"oldgrp-$grpid\" VALUE=$grpmember></TD>";
-        print "<TD><B>$grpnam</B></TD>";
-        print "<TD>$grpdesc</TD>";
+        print "<TD><B>" . html_quote($grpnam) . "</B></TD>";
+        print "<TD>" . html_quote($grpdesc) . "</TD>";
         print "</TR>\n";
     }
 
@@ -290,6 +296,10 @@ if ($action eq 'add') {
     print "<td><input size=30 name=\"regexp\"></td>\n";
     print "<td><input type=\"checkbox\" name=\"isactive\" value=\"1\" checked></td>\n";
     print "</TR></TABLE>\n<HR>\n";
+    print "<input type=\"checkbox\" name=\"insertnew\" value=\"1\"";
+    print " checked" if Param("makeproductgroups");
+    print ">\n";
+    print "Insert new group into all existing products.<P>\n";
     print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"new\">\n";
     print "</FORM>";
@@ -308,9 +318,13 @@ to this group, although bugs already in the group will remain in the group.
 Doing so is a much less drastic way to stop a group from growing
 than deleting the group would be.  <b>Note: If you are creating a group, you
 probably want it to be usable for bugs, in which case you should leave this checked.</b><p>";
-    print "<b>User RegExp</b> is optional, and if filled in, will automatically
-grant membership to this group to anyone creating a new account with an
-email address that matches this regular expression.<p>";
+    print "<b>User RegExp</b> is optional, and if filled in, will ";
+    print "automatically grant membership to this group to anyone with an ";
+    print "email address that matches this regular expression.<p>\n";
+    print "By default, the new group will be associated with existing ";
+    print "products. Unchecking the \"Insert new group into all existing ";
+    print "products\" option will prevent this and make the group become ";
+    print "visible only when its controls have been added to a product.<P>\n";
 
     PutTrailer("<a href=editgroups.cgi>Back to the group list</a>");
     exit;
@@ -384,6 +398,16 @@ if ($action eq 'new') {
              VALUES ($admin, $gid, 0)");
     SendSQL("INSERT INTO group_group_map (member_id, grantor_id, isbless)
              VALUES ($admin, $gid, 1)");
+    # Permit all existing products to use the new group if makeproductgroups.
+    if ($::FORM{insertnew}) {
+        SendSQL("INSERT INTO group_control_map " .
+                "(group_id, product_id, entry, membercontrol, " .
+                "othercontrol, canedit) " .
+                "SELECT $gid, products.id, 0, " .
+                CONTROLMAPSHOWN . ", " .
+                CONTROLMAPNA . ", 0 " .
+                "FROM products");
+    }
     print "OK, done.<p>\n";
     PutTrailer("<a href=\"editgroups.cgi?action=add\">Add another group</a>",
                "<a href=\"editgroups.cgi\">Back to the group list</a>");
@@ -543,6 +567,7 @@ if ($action eq 'delete') {
     SendSQL("DELETE FROM user_group_map WHERE group_id = $gid");
     SendSQL("DELETE FROM group_group_map WHERE grantor_id = $gid");
     SendSQL("DELETE FROM bug_group_map WHERE group_id = $gid");
+    SendSQL("DELETE FROM group_control_map WHERE group_id = $gid");
     SendSQL("DELETE FROM groups WHERE id = $gid");
     print "<B>Group $gid has been deleted.</B><BR>";
 
diff --git a/editproducts.cgi b/editproducts.cgi
index 5649c4440..675a70d9e 100755
--- a/editproducts.cgi
+++ b/editproducts.cgi
@@ -29,7 +29,8 @@
 
 use strict;
 use lib ".";
-
+use vars qw ($template $vars);
+use Bugzilla::Constants;
 require "CGI.pl";
 require "globals.pl";
 
@@ -38,9 +39,16 @@ require "globals.pl";
 
 sub sillyness {
     my $zz;
+    $zz = %::MFORM;
     $zz = $::unconfirmedstate;
 }
 
+my %ctl = ( 
+    &::CONTROLMAPNA => 'NA',
+    &::CONTROLMAPSHOWN => 'Shown',
+    &::CONTROLMAPDEFAULT => 'Default',
+    &::CONTROLMAPMANDATORY => 'Mandatory'
+);
 
 # TestProduct:  just returns if the specified product does exists
 # CheckProduct: same check, optionally  emit an error text
@@ -187,10 +195,9 @@ unless (UserInGroup("editcomponents")) {
 #
 my $product = trim($::FORM{product} || '');
 my $action  = trim($::FORM{action}  || '');
+my $headerdone = 0;
 my $localtrailer = "<A HREF=\"editproducts.cgi\">edit</A> more products";
 
-
-
 #
 # action='' -> Show nice list of products
 #
@@ -342,7 +349,7 @@ if ($action eq 'new') {
 
     # If we're using bug groups, then we need to create a group for this
     # product as well.  -JMR, 2/16/00
-    if(Param("usebuggroups")) {
+    if(Param("makeproductgroups")) {
         # Next we insert into the groups table
         SendSQL("INSERT INTO groups " .
                 "(name, description, isbuggroup, last_changed) " .
@@ -356,6 +363,35 @@ if ($action eq 'new') {
                  VALUES ($admin, $gid, 0)");
         SendSQL("INSERT INTO group_group_map (member_id, grantor_id, isbless)
                  VALUES ($admin, $gid, 1)");
+
+        # Associate the new group and new product.
+        SendSQL("INSERT INTO group_control_map " .
+                "(group_id, product_id, entry, " .
+                "membercontrol, othercontrol, canedit) VALUES " .
+                "($gid, $product_id, " . Param("useentrygroupdefault") .
+                ", " . CONTROLMAPDEFAULT . ", " .
+                CONTROLMAPNA . ", 0)");
+        
+        # Permit the new product to use any non-product bug groups.
+        SendSQL("SELECT groups.id, products.id IS NULL FROM groups " .
+                "LEFT JOIN products " .
+                "ON groups.name = products.name " .
+                "WHERE isactive != 0 AND isbuggroup != 0 ");
+        while (MoreSQLData()) {
+            my ($grpid, $nonproductgroup) = FetchSQLData();
+            if ($nonproductgroup) {
+                PushGlobalSQLState();
+                SendSQL("INSERT INTO group_control_map " .
+                        "(group_id, product_id, entry, " .
+                        "membercontrol, othercontrol, canedit) VALUES " .
+                        "entry, control, canedit) VALUES " .
+                        "($grpid, $product_id, 0, " .
+                        CONTROLMAPSHOWN . ", " .
+                        CONTROLMAPNA . ", 0)");
+                PopGlobalSQLState();
+            }
+        }
+
         
     }
 
@@ -510,7 +546,7 @@ one.";
     print "<INPUT TYPE=SUBMIT VALUE=\"Yes, delete\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"delete\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"product\" VALUE=\"" .
-        value_quote($product) . "\">\n";
+        html_quote($product) . "\">\n";
     print "</FORM>";
 
     PutTrailer($localtrailer);
@@ -538,6 +574,7 @@ if ($action eq 'delete') {
                          versions WRITE,
                          products WRITE,
                          groups WRITE,
+                         group_control_map WRITE,
                          profiles WRITE,
                          milestones WRITE,
                          flaginclusions WRITE,
@@ -583,6 +620,10 @@ if ($action eq 'delete') {
              WHERE product_id=$product_id");
     print "Milestones deleted.<BR>\n";
 
+    SendSQL("DELETE FROM group_control_map
+             WHERE product_id=$product_id");
+    print "Group controls deleted.<BR>\n";
+
     SendSQL("DELETE FROM flaginclusions
              WHERE product_id=$product_id");
     SendSQL("DELETE FROM flagexclusions
@@ -593,7 +634,6 @@ if ($action eq 'delete') {
              WHERE id=$product_id");
     print "Product '$product' deleted.<BR>\n";
 
-
     SendSQL("UNLOCK TABLES");
 
     unlink "data/versioncache";
@@ -692,6 +732,27 @@ if ($action eq 'edit') {
         }
     }
 
+    print "</TD>\n</TR><TR>\n";
+    print "  <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=editgroupcontrols&product=", url_quote($product), "\">Edit Group Access Controls</A></TH>\n";
+    print "<TD>\n";
+    SendSQL("SELECT id, name, isactive, entry, membercontrol, othercontrol, canedit " .
+            "FROM groups, " .
+            "group_control_map " .
+            "WHERE group_control_map.group_id = id AND product_id = $product_id " .
+            "AND isbuggroup != 0 ORDER BY name");
+    while (MoreSQLData()) {
+        my ($id, $name, $isactive, $entry, $membercontrol, $othercontrol, $canedit) 
+            = FetchSQLData();
+        print "<B>" . html_quote($name) . ":</B> ";
+        if ($isactive) {
+            print $ctl{$membercontrol} . "/" . $ctl{$othercontrol}; 
+            print ", ENTRY" if $entry;
+            print ", CANEDIT" if $canedit;
+        } else {
+            print "DISABLED";
+        }
+        print "<BR>\n";
+    }
     print "</TD>\n</TR><TR>\n";
     print "  <TH ALIGN=\"right\">Bugs:</TH>\n";
     print "  <TD>";
@@ -706,11 +767,11 @@ if ($action eq 'edit') {
     print "</TD>\n</TR></TABLE>\n";
 
     print "<INPUT TYPE=HIDDEN NAME=\"productold\" VALUE=\"" .
-        value_quote($product) . "\">\n";
+        html_quote($product) . "\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"descriptionold\" VALUE=\"" .
-        value_quote($description) . "\">\n";
+        html_quote($description) . "\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"milestoneurlold\" VALUE=\"" .
-        value_quote($milestoneurl) . "\">\n";
+        html_quote($milestoneurl) . "\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"disallownewold\" VALUE=\"$disallownew\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"votesperuserold\" VALUE=\"$votesperuser\">\n";
     print "<INPUT TYPE=HIDDEN NAME=\"maxvotesperbugold\" VALUE=\"$maxvotesperbug\">\n";
@@ -729,6 +790,230 @@ if ($action eq 'edit') {
 }
 
 
+#
+# action='updategroupcontrols' -> update the product
+#
+
+if ($action eq 'updategroupcontrols') {
+    my $product_id = get_product_id($product);
+    my @now_na = ();
+    my @now_mandatory = ();
+    foreach my $f (keys %::FORM) {
+        if ($f =~ /^membercontrol_(\d+)$/) {
+            my $id = $1;
+            if ($::FORM{$f} == CONTROLMAPNA) {
+                push @now_na,$id;
+            } elsif ($::FORM{$f} == CONTROLMAPMANDATORY) {
+                push @now_mandatory,$id;
+            }
+        }
+    }
+    if (!($::FORM{'confirmed'})) {
+        $vars->{'form'} = \%::FORM;
+        $vars->{'mform'} = \%::MFORM;
+        my @na_groups = ();
+        if (@now_na) {
+            SendSQL("SELECT groups.name, COUNT(bugs.bug_id) 
+                     FROM bugs, bug_group_map, groups
+                     WHERE groups.id IN(" . join(',',@now_na) . ")
+                     AND bug_group_map.group_id = groups.id
+                     AND bug_group_map.bug_id = bugs.bug_id
+                     AND bugs.product_id = $product_id
+                     GROUP BY groups.name");
+            while (MoreSQLData()) {
+                my ($groupname, $bugcount) = FetchSQLData();
+                my %g = ();
+                $g{'name'} = $groupname;
+                $g{'count'} = $bugcount;
+                push @na_groups,\%g;
+            }
+        }
+
+        my @mandatory_groups = ();
+        if (@now_mandatory) {
+            SendSQL("SELECT groups.name, COUNT(bugs.bug_id) 
+                     FROM bugs, groups
+                     LEFT JOIN bug_group_map
+                     ON bug_group_map.group_id = groups.id
+                     AND bug_group_map.bug_id = bugs.bug_id
+                     WHERE groups.id IN(" . join(',',@now_mandatory) . ")
+                     AND bugs.product_id = $product_id
+                     AND bug_group_map.bug_id IS NULL
+                     GROUP BY groups.name");
+            while (MoreSQLData()) {
+                my ($groupname, $bugcount) = FetchSQLData();
+                my %g = ();
+                $g{'name'} = $groupname;
+                $g{'count'} = $bugcount;
+                push @mandatory_groups,\%g;
+            }
+        }
+        if ((@na_groups) || (@mandatory_groups)) {
+            $vars->{'product'} = $product;
+            $vars->{'na_groups'} = \@na_groups;
+            $vars->{'mandatory_groups'} = \@mandatory_groups;
+            $template->process("admin/products/groupcontrol/confirm-edit.html.tmpl", $vars)
+                || ThrowTemplateError($template->error());
+            exit;                
+        }
+    }
+    PutHeader("Update group access controls for product \"$product\"");
+    $headerdone = 1;
+    SendSQL("SELECT id, name FROM groups " .
+            "WHERE isbuggroup != 0 AND isactive != 0");
+    while (MoreSQLData()){
+        my ($groupid, $groupname) = FetchSQLData();
+        my $newmembercontrol = $::FORM{"membercontrol_$groupid"} || 0;
+        my $newothercontrol = $::FORM{"othercontrol_$groupid"} || 0;
+        #  Legality of control combination is a function of
+        #  membercontrol\othercontrol
+        #                 NA SH DE MA
+        #              NA  +  -  -  -
+        #              SH  +  +  +  +
+        #              DE  +  -  +  +
+        #              MA  -  -  -  +
+        unless (($newmembercontrol == $newothercontrol)
+              || ($newmembercontrol == CONTROLMAPSHOWN)
+              || (($newmembercontrol == CONTROLMAPDEFAULT)
+               && ($newothercontrol != CONTROLMAPSHOWN))) {
+            ThrowUserError('illegal_group_control_combination',
+                            {groupname => $groupname,
+                             header_done => 1});
+        }
+    }
+    SendSQL("LOCK TABLES groups READ,
+             group_control_map WRITE,
+             bugs WRITE,
+             bugs_activity WRITE,
+             bug_group_map WRITE,
+             fielddefs READ");
+    SendSQL("SELECT id, name, entry, membercontrol, othercontrol, canedit " .
+            "FROM groups " .
+            "LEFT JOIN group_control_map " .
+            "ON group_control_map.group_id = id AND product_id = $product_id " .
+            "WHERE isbuggroup != 0 AND isactive != 0");
+    while (MoreSQLData()) {
+        my ($groupid, $groupname, $entry, $membercontrol, 
+            $othercontrol, $canedit) = FetchSQLData();
+        my $newentry = $::FORM{"entry_$groupid"} || 0;
+        my $newmembercontrol = $::FORM{"membercontrol_$groupid"} || 0;
+        my $newothercontrol = $::FORM{"othercontrol_$groupid"} || 0;
+        my $newcanedit = $::FORM{"canedit_$groupid"} || 0;
+        my $oldentry = $entry;
+        $entry = $entry || 0;
+        $membercontrol = $membercontrol || 0;
+        $othercontrol = $othercontrol || 0;
+        $canedit = $canedit || 0;
+        detaint_natural($newentry);
+        detaint_natural($newothercontrol);
+        detaint_natural($newmembercontrol);
+        detaint_natural($newcanedit);
+        if ((!defined($oldentry)) && 
+             (($newentry) || ($newmembercontrol) || ($newcanedit))) {
+            PushGlobalSQLState();
+            SendSQL("INSERT INTO group_control_map " .
+                    "(group_id, product_id, entry, " .
+                    "membercontrol, othercontrol, canedit) " .
+                    "VALUES " .
+                    "($groupid, $product_id, $newentry, " .
+                    "$newmembercontrol, $newothercontrol, $newcanedit)");
+            PopGlobalSQLState();
+        } elsif (($newentry != $entry) 
+                  || ($newmembercontrol != $membercontrol) 
+                  || ($newothercontrol != $othercontrol) 
+                  || ($newcanedit != $canedit)) {
+            PushGlobalSQLState();
+            SendSQL("UPDATE group_control_map " .
+                    "SET entry = $newentry, " .
+                    "membercontrol = $newmembercontrol, " .
+                    "othercontrol = $newothercontrol, " .
+                    "canedit = $newcanedit " .
+                    "WHERE group_id = $groupid " .
+                    "AND product_id = $product_id");
+            PopGlobalSQLState();
+        }
+
+        if (($newentry == 0) && ($newmembercontrol == 0)
+          && ($newothercontrol == 0) && ($newcanedit == 0)) {
+            PushGlobalSQLState();
+            SendSQL("DELETE FROM group_control_map " .
+                    "WHERE group_id = $groupid " .
+                    "AND product_id = $product_id");
+            PopGlobalSQLState();
+        }
+    }
+
+    foreach my $groupid (@now_na) {
+        print "Removing bugs from NA group " 
+             . html_quote(GroupIdToName($groupid)) . "<P>\n";
+        my $count = 0;
+        SendSQL("SELECT bugs.bug_id, 
+                 (lastdiffed >= delta_ts)
+                 FROM bugs, bug_group_map
+                 WHERE group_id = $groupid
+                 AND bug_group_map.bug_id = bugs.bug_id
+                 AND bugs.product_id = $product_id
+                 ORDER BY bugs.bug_id");
+        while (MoreSQLData()) {
+            my ($bugid, $mailiscurrent) = FetchSQLData();
+            PushGlobalSQLState();
+            SendSQL("DELETE FROM bug_group_map WHERE
+                     bug_id = $bugid AND group_id = $groupid");
+            SendSQL("SELECT name, NOW() FROM groups WHERE id = $groupid");
+            my ($removed, $timestamp) = FetchSQLData();
+            LogActivityEntry($bugid, "bug_group", $removed, "",
+                             $::userid, $timestamp);
+            if ($mailiscurrent != 0) {
+                SendSQL("UPDATE bugs SET lastdiffed = " . SqlQuote($timestamp)
+                     . " WHERE bug_id = $bugid");
+            }
+            SendSQL("UPDATE bugs SET delta_ts = " . SqlQuote($timestamp)
+                 . " WHERE bug_id = $bugid");
+            PopGlobalSQLState();
+            $count++;
+        }
+        print "dropped $count bugs<p>\n";
+    }
+
+    foreach my $groupid (@now_mandatory) {
+        print "Adding bugs to Mandatory group " 
+             . html_quote(GroupIdToName($groupid)) . "<P>\n";
+        my $count = 0;
+        SendSQL("SELECT bugs.bug_id,
+                 (lastdiffed >= delta_ts)
+                 FROM bugs
+                 LEFT JOIN bug_group_map
+                 ON bug_group_map.bug_id = bugs.bug_id
+                 AND group_id = $groupid
+                 WHERE bugs.product_id = $product_id
+                 AND bug_group_map.bug_id IS NULL
+                 ORDER BY bugs.bug_id");
+        while (MoreSQLData()) {
+            my ($bugid, $mailiscurrent) = FetchSQLData();
+            PushGlobalSQLState();
+            SendSQL("INSERT INTO bug_group_map (bug_id, group_id)
+                     VALUES ($bugid, $groupid)");
+            SendSQL("SELECT name, NOW() FROM groups WHERE id = $groupid");
+            my ($added, $timestamp) = FetchSQLData();
+            LogActivityEntry($bugid, "bug_group", "", $added,
+                             $::userid, $timestamp);
+            if ($mailiscurrent != 0) {
+                SendSQL("UPDATE bugs SET lastdiffed = " . SqlQuote($timestamp)
+                     . " WHERE bug_id = $bugid");
+            }
+            SendSQL("UPDATE bugs SET delta_ts = " . SqlQuote($timestamp)
+                 . " WHERE bug_id = $bugid");
+            PopGlobalSQLState();
+            $count++;
+        }
+        print "added $count bugs<p>\n";
+    }
+    SendSQL("UNLOCK TABLES");
+    print "Group control updates done<P>\n";
+
+    PutTrailer($localtrailer);
+    exit;
+}
 
 #
 # action='update' -> update the product
@@ -770,6 +1055,7 @@ if ($action eq 'update') {
     SendSQL("LOCK TABLES products WRITE,
                          versions READ,
                          groups WRITE,
+                         group_control_map WRITE,
                          profiles WRITE,
                          milestones READ");
 
@@ -863,9 +1149,9 @@ if ($action eq 'update') {
         }
 
         SendSQL("UPDATE products SET name=$qp WHERE id=$product_id");
-        # Need to do an update to groups as well.  If there is a corresponding
-        # bug group, whether usebuggroups is currently set or not, we want to
-        # update it so it will match in the future.  If there is no group, this
+        # Need to do an update to groups as well.  If there is 
+        # a corresponding bug group, we want to update it so it will 
+        # match in the future.  If there is no group, this
         # update statement will do nothing, so no harm done.  -JMR, 3/8/00
         SendSQL("UPDATE groups " .
                 "SET name = $qp, " .
@@ -945,6 +1231,235 @@ if ($action eq 'update') {
     exit;
 }
 
+#
+# action='editgroupcontrols' -> update product group controls
+#
+
+if ($action eq 'editgroupcontrols') {
+    my $product_id = get_product_id($product);
+    # Display a group if it is either enabled or has bugs for this product.
+    SendSQL("SELECT id, name, entry, membercontrol, othercontrol, canedit, " .
+            "isactive, COUNT(bugs.bug_id) " .
+            "FROM groups " .
+            "LEFT JOIN group_control_map " .
+            "ON group_control_map.group_id = id " .
+            "AND group_control_map.product_id = $product_id " .
+            "LEFT JOIN bug_group_map " .
+            "ON bug_group_map.group_id = groups.id " .
+            "LEFT JOIN bugs " .
+            "ON bugs.bug_id = bug_group_map.bug_id " .
+            "AND bugs.product_id = $product_id " .
+            "WHERE isbuggroup != 0 " .
+            "AND (isactive != 0 OR entry IS NOT NULL " .
+            "OR bugs.bug_id IS NOT NULL) " .
+            "GROUP BY name");
+    my @groups = ();
+    while (MoreSQLData()) {
+        my %group = ();
+        my ($groupid, $groupname, $entry, $membercontrol, $othercontrol, 
+            $canedit, $isactive, $bugcount) = FetchSQLData();
+        $group{'id'} = $groupid;
+        $group{'name'} = $groupname;
+        $group{'entry'} = $entry;
+        $group{'membercontrol'} = $membercontrol;
+        $group{'othercontrol'} = $othercontrol;
+        $group{'canedit'} = $canedit;
+        $group{'isactive'} = $isactive;
+        $group{'bugcount'} = $bugcount;
+        push @groups,\%group;
+    }
+    $vars->{'header_done'} = $headerdone;
+    $vars->{'product'} = $product;
+    $vars->{'groups'} = \@groups;
+    $vars->{'const'} = {
+        'CONTROLMAPNA' => CONTROLMAPNA,
+        'CONTROLMAPSHOWN' => CONTROLMAPSHOWN,
+        'CONTROLMAPDEFAULT' => CONTROLMAPDEFAULT,
+        'CONTROLMAPMANDATORY' => CONTROLMAPMANDATORY,
+    };
+
+    $template->process("admin/products/groupcontrol/edit.html.tmpl", $vars)
+        || ThrowTemplateError($template->error());
+    exit;                
+
+    print "<!-- \n";
+    print "<script type=\"text/javascript\">\n";
+    print "function hide(id) {\n";
+    print "  id.visibility = 0\n";
+    print "  alert(id)\n";
+    print "}\n";
+    print "</script>";
+    print " -->\n";
+        print "<STYLE type=\"text/css\">\n";
+        print "  .hstyle { visibility: visible; color: red; }\n";
+        print "</STYLE>\n";
+    print "<FORM METHOD=POST ACTION=editproducts.cgi>\n";
+    print "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0><TR BGCOLOR=\"#6666FF\">\n";
+    print "  <TH ALIGN=\"left\">Group</TH>\n";
+    print "  <TH ALIGN=\"left\">Entry</TH>\n";
+    print "  <TH ALIGN=\"left\">MemberControl</TH>\n";
+    print "  <TH ALIGN=\"left\">OtherControl</TH>\n";
+    print "  <TH ALIGN=\"left\">Canedit</TH>\n";
+    while (MoreSQLData()) {
+        print "</TR>\n";
+        my ($groupid, $groupname, $entry, $membercontrol, $othercontrol, 
+            $canedit) = FetchSQLData();
+        print "<TR id=\"row_$groupname\" class=\"hstyle\" ";
+        print "onload=\"document.row.row_$groupname.color=green\">\n";
+        print "  <TD>\n";
+        print "    $groupname\n";
+        print "  </TD><TD>\n";
+        $entry |= 0;
+        print "    <INPUT TYPE=CHECKBOX NAME=\"entry_$groupid\" VALUE=1";
+        print " CHECKED " if $entry;
+        print ">\n";
+        print "  </TD><TD>\n";
+        $membercontrol |= 0;
+        $othercontrol |= 0;
+        print "    <SELECT NAME=\"membercontrol_$groupid\">\n";
+        print "      <OPTION VALUE=" . CONTROLMAPNA;
+        print " selected=\"selected\"" if ($membercontrol == CONTROLMAPNA);
+        print ">NA</OPTION>\n";
+        print "      <OPTION VALUE=" . CONTROLMAPSHOWN;
+        print " selected=\"selected\"" if ($membercontrol == CONTROLMAPSHOWN);
+        print ">Shown</OPTION>\n";
+        print "      <OPTION VALUE=" . CONTROLMAPDEFAULT;
+        print " selected=\"selected\"" if ($membercontrol == CONTROLMAPDEFAULT);
+        print ">Default</OPTION>\n";
+        print "      <OPTION VALUE=" . CONTROLMAPMANDATORY;
+        print " selected=\"selected\"" if ($membercontrol == CONTROLMAPMANDATORY);
+        print ">Mandatory</OPTION>\n";
+        print "</SELECT>\n";
+        print "  </TD><TD>\n";
+        print "    <SELECT NAME=\"othercontrol_$groupid\">\n";
+        print "      <OPTION VALUE=" . CONTROLMAPNA;
+        print " selected=\"selected\"" if ($othercontrol == CONTROLMAPNA);
+        print ">NA</OPTION>\n";
+        print "      <OPTION VALUE=" . CONTROLMAPSHOWN;
+        print " selected=\"selected\"" if ($othercontrol == CONTROLMAPSHOWN);
+        print ">Shown</OPTION>\n";
+        print "      <OPTION VALUE=" . CONTROLMAPDEFAULT;
+        print " selected=\"selected\"" if ($othercontrol == CONTROLMAPDEFAULT);
+        print ">Default</OPTION>\n";
+        print "      <OPTION VALUE=" . CONTROLMAPMANDATORY;
+        print " selected=\"selected\"" if ($othercontrol == CONTROLMAPMANDATORY);
+        print ">Mandatory</OPTION>\n";
+        print "</SELECT>\n";
+        print "  </TD><TD>\n";
+        $canedit |= 0;
+        print "    <INPUT TYPE=CHECKBOX NAME=\"canedit_$groupid\" VALUE=1";
+        print " CHECKED " if $canedit;
+        print ">\n";
+
+    }
+
+    print "</TR>\n";
+    print "</TABLE><BR>";
+    print "Add controls to the panel above:<BR>\n";
+    print "<SELECT NAME=\"newgroups\" SIZE=\"10\" MULTIPLE=\"MULTIPLE\">\n";
+    SendSQL("SELECT id, name " .
+            "FROM groups " .
+            "LEFT JOIN group_control_map " .
+            "ON group_control_map.group_id = id AND product_id = $product_id " .
+            "WHERE canedit IS NULL AND isbuggroup != 0 AND isactive != 0 " .
+            "ORDER BY name");
+    while (MoreSQLData()) {
+        my ($groupid, $groupname) = FetchSQLData();
+        print "<OPTION VALUE=\"$groupid\">$groupname</OPTION>\n";
+    }
+    print "</SELECT><BR><BR>\n";
+
+    print "<INPUT TYPE=SUBMIT VALUE=\"Update\">\n";
+    print "<INPUT TYPE=RESET>\n";
+    print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"updategroupcontrols\">\n";
+    print "<INPUT TYPE=HIDDEN NAME=\"product\" VALUE=\"$product\">\n";
+    print "</FORM>\n";
+    print "<P>note: Any group controls Set to NA/NA with no other checkboxes ";
+    print "will automatically be removed from the panel the next time ";
+    print "update is clicked.\n";
+    print "<P>These settings control the relationship of the groups to this ";
+    print "product.\n";
+    print "<P>If any group has <B>Entry</B> selected, then this product will ";
+    print "restrict bug entry to only those users who are members of all the ";
+    print "groups with entry selected.\n";
+    print "<P>If any group has <B>Canedit</B> selected, then this product ";
+    print "will be read-only for any users who are not members of all of ";
+    print "the groups with Canedit selected. ONLY users who are members of ";
+    print "all the canedit groups will be able to edit. This is an additional ";
+    print "restriction that further restricts what can be edited by a user.\n";
+    print "<P>The <B>MemberControl</B> and <B>OtherControl</B> fields ";
+    print "indicate which bugs will be placed in ";
+    print "this group according to the following definitions.\n";
+    print "<BR><TABLE BORDER=1>";
+    print "<TR>";
+    print "<TH>MemberControl</TH><TH>OtherControl</TH><TH>Interpretation</TH>";
+    print "</TR><TR>";
+    print "<TD>NA</TD>\n";
+    print "<TD>NA</TD>\n";
+    print "<TD>Bugs in this product are never associated with this group.</TD>\n";
+    print "</TR><TR>";
+    print "<TD>Shown</TD>\n";
+    print "<TD>NA</TD>\n";
+    print "<TD>Bugs in this product are permitted to be restricted to this ";
+    print "group.  Users who are a member of this group will be able ";
+    print "to place bugs in this group.</TD>\n";
+    print "</TR><TR>";
+    print "<TD>Shown</TD>\n";
+    print "<TD>Shown</TD>\n";
+    print "<TD>Bugs in this product can be placed in this group by anyone ";
+    print "with permission to edit the bug even if they are not a member ";
+    print "of this group.</TD>\n";
+    print "</TR><TR>";
+    print "<TD>Shown</TD>\n";
+    print "<TD>Default</TD>\n";
+    print "<TD>Bugs in this product can be placed in this group by anyone ";
+    print "with permission to edit the bug even if they are not a member ";
+    print "of this group. Non-members place bugs in this group by default.";
+    print "</TD>\n";
+    print "</TR><TR>";
+    print "<TD>Shown</TD>\n";
+    print "<TD>Mandatory</TD>\n";
+    print "<TD>Bugs in this product are permitted to be restricted to this ";
+    print "group.  Users who are a member of this group will be able ";
+    print "to place bugs in this group.";
+    print "Non-members will be forced to restrict bugs to this group ";
+    print "when they initially enter a bug in this product.";
+    print "</TD>\n";
+    print "</TR><TR>";
+    print "<TD>Default</TD>\n";
+    print "<TD>NA</TD>\n";
+    print "<TD>Bugs in this product are permitted to be restricted to this ";
+    print "group and are placed in this group by default.";
+    print "Users who are a member of this group will be able ";
+    print "to place bugs in this group.</TD>\n";
+    print "</TR><TR>";
+    print "<TD>Default</TD>\n";
+    print "<TD>Default</TD>\n";
+    print "<TD>Bugs in this product are permitted to be restricted to this ";
+    print "group and are placed in this group by default.";
+    print "Users who are a member of this group will be able ";
+    print "to place bugs in this group. Non-members will be able to ";
+    print "restrict bugs to this group on entry and will do so by default ";
+    print "</TD>\n";
+    print "</TR><TR>";
+    print "<TD>Default</TD>\n";
+    print "<TD>Mandatory</TD>\n";
+    print "<TD>Bugs in this product are permitted to be restricted to this ";
+    print "group and are placed in this group by default.";
+    print "Users who are a member of this group will be able ";
+    print "to place bugs in this group. Non-members will be forced ";
+    print "to place bugs in this group on entry.";
+    print "</TR><TR>";
+    print "<TD>Mandatory</TD>\n";
+    print "<TD>Mandatory</TD>\n";
+    print "<TD>Bugs in this product are required to be restricted to this ";
+    print "group.  Users are not given any option.</TD>\n";
+    print "</TABLE>";
+
+
+    PutTrailer($localtrailer);
+    exit;
+}
 
 
 #
diff --git a/enter_bug.cgi b/enter_bug.cgi
index 5492bb14a..162431ecd 100755
--- a/enter_bug.cgi
+++ b/enter_bug.cgi
@@ -36,6 +36,7 @@ use strict;
 
 use lib qw(.);
 
+use Bugzilla::Constants;
 require "CGI.pl";
 
 use vars qw(
@@ -51,6 +52,7 @@ use vars qw(
   $userid
   %MFORM
   %versions
+  $proddesc
 );
 
 # We have to connect to the database, even though we don't use it in this code,
@@ -60,7 +62,7 @@ ConnectToDatabase();
 
 # If we're using bug groups to restrict bug entry, we need to know who the 
 # user is right from the start. 
-confirm_login() if (Param("usebuggroupsentry"));
+confirm_login() if AnyEntryGroups();
 
 if (!defined $::FORM{'product'}) {
     GetVersionTable();
@@ -69,9 +71,7 @@ if (!defined $::FORM{'product'}) {
     my %products;
 
     foreach my $p (@enterable_products) {
-        if (!(Param("usebuggroupsentry") 
-              && GroupExists($p) 
-              && !UserInGroup($p)))
+        if (CanEnterProduct($p))
         {
             $products{$p} = $::proddesc{$p};
         }
@@ -215,13 +215,11 @@ sub pickos {
 # End of subroutines
 ##############################################################################
 
-confirm_login() if (!(Param("usebuggroupsentry")));
+confirm_login() if (!(AnyEntryGroups()));
 
-# If the usebuggroupsentry parameter is set, we need to check and make sure
+# We need to check and make sure
 # that the user has permission to enter a bug against this product.
-if(Param("usebuggroupsentry") 
-   && GroupExists($product) 
-   && !UserInGroup($product)) 
+if(!CanEnterProduct($product))
 {
     ThrowUserError("entry_access_denied", { product => $product});         
 }
@@ -309,30 +307,25 @@ if (UserInGroup("editbugs") || UserInGroup("canconfirm")) {
 $vars->{'bug_status'} = \@status; 
 $default{'bug_status'} = $status[0];
 
-# Select whether to restrict this bug to the product's bug group or not, 
-# if the usebuggroups parameter is set, and if this product has a bug group.
-# First we get the bit and description for the group.
-my $group_id = '0';
-
-if(Param("usebuggroups")) {
-    ($group_id) = GroupExists($product);
-}
-
-SendSQL("SELECT DISTINCT groups.id, groups.name, groups.description " .
-        "FROM groups, user_group_map " .
-        "WHERE user_group_map.group_id = groups.id " .
-        "AND user_group_map.user_id = $::userid " .
-        "AND isbless = 0 " .
-        "AND isbuggroup = 1 AND isactive = 1 ORDER BY description");
+SendSQL("SELECT DISTINCT groups.id, groups.name, groups.description, " .
+        "membercontrol, othercontrol " .
+        "FROM groups LEFT JOIN group_control_map " .
+        "ON group_id = id AND product_id = $product_id " .
+        "WHERE isbuggroup != 0 AND isactive != 0 ORDER BY description");
 
 my @groups;
 
 while (MoreSQLData()) {
-    my ($id, $prodname, $description) = FetchSQLData();
-    # Don't want to include product groups other than this product.
-    next unless(!Param("usebuggroups") || $prodname eq $product || 
-                !defined($::proddesc{$prodname}));
-
+    my ($id, $groupname, $description, $membercontrol, $othercontrol) 
+        = FetchSQLData();
+    # Only include groups if the entering user will have an option.
+    next if ((!$membercontrol) 
+               || ($membercontrol == CONTROLMAPNA) 
+               || ($membercontrol == CONTROLMAPMANDATORY)
+               || (($othercontrol != CONTROLMAPSHOWN) 
+                    && ($othercontrol != CONTROLMAPDEFAULT)
+                    && (!UserInGroup($groupname)))
+             );
     my $check;
 
     # If this is the group for this product, make it checked.
@@ -343,11 +336,10 @@ while (MoreSQLData()) {
         $check = formvalue("bit-$id", 0);
     }
     else {
-        # $group_bit will only have a non-zero value if we're using
-        # bug groups and have one for this product.
-        # If $group_bit is 0, it won't match the current group, so compare 
-        # it to the current bit instead of checking for non-zero.
-        $check = ($group_id == $id);
+        # Checkbox is checked by default if $control is a default state.
+        $check = (($membercontrol == CONTROLMAPDEFAULT)
+                 || (($othercontrol == CONTROLMAPDEFAULT)
+                      && (!UserInGroup($groupname))));
     }
 
     my $group = 
diff --git a/globals.pl b/globals.pl
index 7eef23115..9d4372d00 100644
--- a/globals.pl
+++ b/globals.pl
@@ -28,6 +28,7 @@
 
 use strict;
 
+use Bugzilla::Constants;
 use Bugzilla::Util;
 # Bring ChmodDataFile in until this is all moved to the module
 use Bugzilla::Config qw(:DEFAULT ChmodDataFile);
@@ -684,6 +685,116 @@ sub GenerateRandomPassword {
     return $password;
 }
 
+#
+# This function checks if there are any entry groups defined.
+# If called with no arguments, it identifies
+# entry groups for all products.  If called with a product
+# id argument, it checks for entry groups associated with 
+# one particular product.
+sub AnyEntryGroups {
+    my $product_id = shift;
+    $product_id = 0 unless ($product_id);
+    return $::CachedAnyEntryGroups{$product_id} 
+        if defined($::CachedAnyEntryGroups{$product_id});
+    PushGlobalSQLState();
+    my $query = "SELECT 1 FROM group_control_map WHERE entry != 0";
+    $query .= " AND product_id = $product_id" if ($product_id);
+    $query .= " LIMIT 1";
+    SendSQL($query);
+    $::CachedAnyEntryGroups{$product_id} = MoreSQLData();
+    FetchSQLData();
+    PopGlobalSQLState();
+    return $::CachedAnyEntryGroups{$product_id};
+}
+
+#
+# This function checks if there are any default groups defined.
+# If so, then groups may have to be changed when bugs move from
+# one bug to another.
+sub AnyDefaultGroups {
+    return $::CachedAnyDefaultGroups if defined($::CachedAnyDefaultGroups);
+    PushGlobalSQLState();
+    SendSQL("SELECT 1 FROM group_control_map, groups WHERE " .
+            "groups.id = group_control_map.group_id " .
+            "AND isactive != 0 AND " .
+            "(membercontrol = " . CONTROLMAPDEFAULT .
+            " OR othercontrol = " . CONTROLMAPDEFAULT .
+            ") LIMIT 1");
+    $::CachedAnyDefaultGroups = MoreSQLData();
+    FetchSQLData();
+    PopGlobalSQLState();
+    return $::CachedAnyDefaultGroups;
+}
+
+#
+# This function checks if, given a product id, the user can edit
+# bugs in this product at all.
+sub CanEditProductId {
+    my ($productid) = @_;
+    my $query = "SELECT group_id FROM group_control_map " .
+                "WHERE product_id = $productid " .
+                "AND canedit != 0 "; 
+    if ((defined @{$::vars->{user}{groupids}}) 
+        && (@{$::vars->{user}{groupids}} > 0)) {
+        $query .= "AND group_id NOT IN(" . 
+                   join(',',@{$::vars->{user}{groupids}}) . ") ";
+    }
+    $query .= "LIMIT 1";
+    PushGlobalSQLState();
+    SendSQL($query);
+    my ($result) = FetchSQLData();
+    PopGlobalSQLState();
+    return (!defined($result));
+}
+
+#
+# This function determines if a user can enter bugs in the named
+# product.
+sub CanEnterProduct {
+    my ($productname) = @_;
+    my $query = "SELECT group_id IS NULL " .
+                "FROM products " .
+                "LEFT JOIN group_control_map " .
+                "ON group_control_map.product_id = products.id " .
+                "AND group_control_map.entry != 0 ";
+    if ((defined @{$::vars->{user}{groupids}}) 
+        && (@{$::vars->{user}{groupids}} > 0)) {
+        $query .= "AND group_id NOT IN(" . 
+                   join(',',@{$::vars->{user}{groupids}}) . ") ";
+    }
+    $query .= "WHERE products.name = " . SqlQuote($productname) . " LIMIT 1";
+    PushGlobalSQLState();
+    SendSQL($query);
+    my ($ret) = FetchSQLData();
+    PopGlobalSQLState();
+    return ($ret);
+}
+
+#
+# This function returns an alphabetical list of product names to which
+# the user can enter bugs.
+sub GetEnterableProducts {
+    my $query = "SELECT name " .
+                "FROM products " .
+                "LEFT JOIN group_control_map " .
+                "ON group_control_map.product_id = products.id " .
+                "AND group_control_map.entry != 0 ";
+    if ((defined @{$::vars->{user}{groupids}}) 
+        && (@{$::vars->{user}{groupids}} > 0)) {
+        $query .= "AND group_id NOT IN(" . 
+                   join(',',@{$::vars->{user}{groupids}}) . ") ";
+    }
+    $query .= "WHERE group_id IS NULL ORDER BY name";
+    PushGlobalSQLState();
+    SendSQL($query);
+    my @products = ();
+    while (MoreSQLData()) {
+        push @products,FetchOneColumn();
+    }
+    PopGlobalSQLState();
+    return (@products);
+}
+
 sub CanSeeBug {
 
     my ($id, $userid) = @_;
@@ -1749,5 +1860,5 @@ $::vars =
     'VERSION' => $Bugzilla::Config::VERSION,
   };
 
-
 1;
+
diff --git a/post_bug.cgi b/post_bug.cgi
index 4cacb271c..3ddfbe689 100755
--- a/post_bug.cgi
+++ b/post_bug.cgi
@@ -26,6 +26,7 @@
 use strict;
 use lib qw(.);
 
+use Bugzilla::Constants;
 require "CGI.pl";
 require "bug_form.pl";
 
@@ -101,9 +102,8 @@ if (defined $::FORM{'maketemplate'}) {
 umask 0;
 
 # Some sanity checking
-if(Param("usebuggroupsentry") && GroupExists($product)) {
-    UserInGroup($product) || 
-      ThrowUserError("entry_access_denied", {product => $product});
+if (!CanEnterProduct($product)) {
+    ThrowUserError("entry_access_denied", {product => $product});
 }
 
 my $component_id = get_component_id($product_id, $::FORM{component});
@@ -363,13 +363,38 @@ foreach my $b (grep(/^bit-\d*$/, keys %::FORM)) {
                  WHERE user_id = $::userid
                  AND group_id = $v
                  AND isbless = 0");
-        my ($member) = FetchSQLData();
-        if ($member) {
+        my ($permit) = FetchSQLData();
+        if (!$permit) {
+            SendSQL("SELECT othercontrol FROM group_control_map
+                     WHERE group_id = $v AND product_id = $product_id");
+            my ($othercontrol) = FetchSQLData();
+            $permit = (($othercontrol == CONTROLMAPSHOWN)
+                       || ($othercontrol == CONTROLMAPDEFAULT));
+        }
+        if ($permit) {
             push(@groupstoadd, $v)
         }
     }
 }
 
+SendSQL("SELECT DISTINCT groups.id, groups.name, " .
+        "membercontrol, othercontrol " .
+        "FROM groups LEFT JOIN group_control_map " .
+        "ON group_id = id AND product_id = $product_id " .
+        " WHERE isbuggroup != 0 AND isactive != 0 ORDER BY description");
+while (MoreSQLData()) {
+    my ($id, $groupname, $membercontrol, $othercontrol ) = FetchSQLData();
+    $membercontrol ||= 0;
+    $othercontrol ||= 0;
+    # Add groups required
+    if (($membercontrol == CONTROLMAPMANDATORY)
+       || (($othercontrol == CONTROLMAPMANDATORY) 
+            && (!UserInGroup($groupname)))) {
+        # User had no option, bug needs to be in this group.
+        push(@groupstoadd, $id)
+    }
+}
+        
 
 # Lock tables before inserting records for the new bug into the database
 # if we are using a shadow database to prevent shadow database corruption
diff --git a/process_bug.cgi b/process_bug.cgi
index 31638b343..a59e439dc 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -31,6 +31,7 @@ my $UserInCanConfirmGroupSet = -1;
 
 use lib qw(.);
 
+use Bugzilla::Constants;
 require "CGI.pl";
 require "bug_form.pl";
 
@@ -236,7 +237,7 @@ if ((($::FORM{'id'} && $::FORM{'product'} ne $::oldproduct)
     # If the product-specific fields need to be verified, or we need to verify
     # whether or not to add the bugs to their new product's group, display
     # a verification form.
-    if (!$vok || !$cok || !$mok || (Param('usebuggroups') && !defined($::FORM{'addtonewgroup'}))) {
+    if (!$vok || !$cok || !$mok || (AnyDefaultGroups() && !defined($::FORM{'addtonewgroup'}))) {
         $vars->{'form'} = \%::FORM;
         $vars->{'mform'} = \%::MFORM;
         
@@ -276,7 +277,7 @@ if ((($::FORM{'id'} && $::FORM{'product'} ne $::oldproduct)
             $vars->{"verify_fields"} = 0;
         }
         
-        $vars->{'verify_bug_group'} = (Param('usebuggroups') 
+        $vars->{'verify_bug_group'} = (AnyDefaultGroups() 
                                        && !defined($::FORM{'addtonewgroup'}));
         
         $template->process("bug/process/verify-new-product.html.tmpl", $vars)
@@ -594,11 +595,9 @@ sub ChangeResolution {
 # select lists.  This means that instead of looking for the bit-X values in
 # the form, we need to loop through all the bug groups this user has access
 # to, and for each one, see if it's selected.
-# In order to make mass changes work correctly, keep a sum of bits for groups
-# added, and another one for groups removed, and then let mysql do the bit
-# operations
 # If the form element isn't present, or the user isn't in the group, leave
 # it as-is
+
 my @groupAdd = ();
 my @groupDel = ();
 
@@ -1070,7 +1069,10 @@ foreach my $id (@idlist) {
             "bug_group_map $write, flags $write, duplicates $write," .
             "user_group_map READ, flagtypes READ, " . 
             "flaginclusions AS i READ, flagexclusions AS e READ, " .
-            "keyworddefs READ, groups READ, attachments READ");
+            "keyworddefs READ, groups READ, attachments READ, " .
+            "group_control_map AS oldcontrolmap READ, " .
+            "group_control_map AS newcontrolmap READ, " .
+            "group_control_map READ");
     my @oldvalues = SnapShotBug($id);
     my %oldhash;
     # Fun hack.  @::log_columns only contains the component_id,
@@ -1104,6 +1106,18 @@ foreach my $id (@idlist) {
         $i++;
     }
     $oldhash{'product'} = get_product_name($oldhash{'product_id'});
+    if (!CanEditProductId($oldhash{'product_id'})) {
+        $vars->{'product'} = $oldhash{'product'};
+        ThrowUserError("product_edit_denied");
+    }
+
+    if (defined $::FORM{'product'} 
+        && $::FORM{'product'} ne $::FORM{'dontchange'} 
+        && $::FORM{'product'} ne $oldhash{'product'}
+        && !CanEnterProduct($::FORM{'product'})) {
+        $vars->{'product'} = $::FORM{'product'};
+        ThrowUserError("entry_access_denied");
+    }
     if ($requiremilestone) {
         my $value = $::FORM{'target_milestone'};
         if (!defined $value || $value eq $::FORM{'dontchange'}) {
@@ -1268,6 +1282,7 @@ foreach my $id (@idlist) {
     if ($::comma ne "") {
         SendSQL($query);
     }
+
     # Check for duplicates if the bug is [re]open
     SendSQL("SELECT resolution FROM bugs WHERE bug_id = $id");
     my $resolution = FetchOneColumn();
@@ -1275,8 +1290,34 @@ foreach my $id (@idlist) {
         SendSQL("DELETE FROM duplicates WHERE dupe = $id");
     }
     
+    my $newproduct_id = $oldhash{'product_id'};
+    if ((defined $::FORM{'product'})
+        && ($::FORM{'product'} ne $::FORM{'dontchange'})) {
+        my $newproduct_id = get_product_id($::FORM{'product'});
+    }
+
+    my %groupsrequired = ();
+    my %groupsforbidden = ();
+    SendSQL("SELECT id, membercontrol 
+             FROM groups LEFT JOIN group_control_map
+             ON id = group_id
+             AND product_id = $newproduct_id WHERE isactive != 0");
+    while (MoreSQLData()) {
+        my ($group, $control) = FetchSQLData();
+        $control ||= 0;
+        unless ($control > &::CONTROLMAPNA)  {
+            $groupsforbidden{$group} = 1;
+        }
+        if ($control == &::CONTROLMAPMANDATORY) {
+            $groupsrequired{$group} = 1;
+        }
+    }
+
     my @groupAddNames = ();
-    foreach my $grouptoadd (@groupAdd) {
+    my @groupAddNamesAll = ();
+    foreach my $grouptoadd (@groupAdd, keys %groupsrequired) {
+        next if $groupsforbidden{$grouptoadd};
+        push(@groupAddNamesAll, GroupIdToName($grouptoadd));
         if (!BugInGroupId($id, $grouptoadd)) {
             push(@groupAddNames, GroupIdToName($grouptoadd));
             SendSQL("INSERT INTO bug_group_map (bug_id, group_id) 
@@ -1284,7 +1325,10 @@ foreach my $id (@idlist) {
         }
     }
     my @groupDelNames = ();
-    foreach my $grouptodel (@groupDel) {
+    my @groupDelNamesAll = ();
+    foreach my $grouptodel (@groupDel, keys %groupsforbidden) {
+        push(@groupDelNamesAll, GroupIdToName($grouptodel));
+        next if $groupsrequired{$grouptodel};
         if (BugInGroupId($id, $grouptodel)) {
             push(@groupDelNames, GroupIdToName($grouptodel));
         }
@@ -1399,63 +1443,117 @@ foreach my $id (@idlist) {
     # group or add it to the new one.  There are a very specific series of
     # conditions under which these activities take place, more information
     # about which can be found in comments within the conditionals below.
+    # Check if the user has changed the product to which the bug belongs;
     if ( 
-      # the "usebuggroups" parameter is on, indicating that products
-      # are associated with groups of the same name;
-      Param('usebuggroups')
-
-      # the user has changed the product to which the bug belongs;
-      && defined $::FORM{'product'} 
+      defined $::FORM{'product'} 
         && $::FORM{'product'} ne $::FORM{'dontchange'} 
           && $::FORM{'product'} ne $oldhash{'product'} 
     ) {
-        if (
-          # the user wants to add the bug to the new product's group;
-          ($::FORM{'addtonewgroup'} eq 'yes' 
-            || ($::FORM{'addtonewgroup'} eq 'yesifinold' 
-                  && BugInGroup($id, $oldhash{'product'})))  
-
-          # the new product is associated with a group;
-          && GroupExists($::FORM{'product'})
-
-          # the bug is not already in the group; (This can happen when the user
-          # goes to the "edit multiple bugs" form with a list of bugs at least
-          # one of which is in the new group.  In this situation, the user can
-          # simultaneously change the bugs to a new product and move the bugs
-          # into that product's group, which happens earlier in this script
-          # and thus is already done.  If we didn't check for this, then this
-          # situation would cause us to add the bug to the group twice, which
-          # would result in the bug being added to a totally different group.)
-          && !BugInGroup($id, $::FORM{'product'})
-
-          # the user is a member of the associated group, indicating they
-          # are authorized to add bugs to that group, *or* the "usebuggroupsentry"
-          # parameter is off, indicating that users can add bugs to a product 
-          # regardless of whether or not they belong to its associated group;
-          && (UserInGroup($::FORM{'product'}) || !Param('usebuggroupsentry'))
-
-          # the associated group is active, indicating it can accept new bugs;
-          && GroupIsActive(GroupNameToId($::FORM{'product'}))
-        ) { 
-            # Add the bug to the group associated with its new product.
-            my $groupid = GroupNameToId($::FORM{'product'});
-            if (!BugInGroupId($id, $groupid)) {
-                SendSQL("INSERT INTO bug_group_map (bug_id, group_id) VALUES ($id, $groupid)");
+        my $newproduct_id = get_product_id($::FORM{'product'});
+        # Depending on the "addtonewgroup" variable, groups with
+        # defaults will change.
+        #
+        # For each group, determine
+        # - The group id and if it is active
+        # - The control map value for the old product and this group
+        # - The control map value for the new product and this group
+        # - Is the user in this group?
+        # - Is the bug in this group?
+        SendSQL("SELECT DISTINCT groups.id, isactive, " .
+                "oldcontrolmap.membercontrol, newcontrolmap.membercontrol, " .
+                "user_group_map.user_id IS NOT NULL, " .
+                "bug_group_map.group_id IS NOT NULL " .
+                "FROM groups " .
+                "LEFT JOIN group_control_map AS oldcontrolmap " .
+                "ON oldcontrolmap.group_id = groups.id " .
+                "AND oldcontrolmap.product_id = " . $oldhash{'product_id'} .
+                " LEFT JOIN group_control_map AS newcontrolmap " .
+                "ON newcontrolmap.group_id = groups.id " .
+                "AND newcontrolmap.product_id = $newproduct_id " .
+                "LEFT JOIN user_group_map " .
+                "ON user_group_map.group_id = groups.id " .
+                "AND user_group_map.user_id = $::userid " .
+                "AND user_group_map.isbless = 0 " .
+                "LEFT JOIN bug_group_map " .
+                "ON bug_group_map.group_id = groups.id " .
+                "AND bug_group_map.bug_id = $id "
+            );
+        my @groupstoremove = ();
+        my @groupstoadd = ();
+        my @defaultstoremove = ();
+        my @defaultstoadd = ();
+        my @allgroups = ();
+        my $buginanydefault = 0;
+        my $buginanychangingdefault = 0;
+        while (MoreSQLData()) {
+            my ($groupid, $isactive, $oldcontrol, $newcontrol, 
+            $useringroup, $bugingroup) = FetchSQLData();
+            # An undefined newcontrol is none.
+            $newcontrol = CONTROLMAPNA unless $newcontrol;
+            $oldcontrol = CONTROLMAPNA unless $oldcontrol;
+            push(@allgroups, $groupid);
+            if (($bugingroup) && ($isactive)
+                && ($oldcontrol == CONTROLMAPDEFAULT)) {
+                # Bug was in a default group.
+                $buginanydefault = 1;
+                if ($newcontrol != CONTROLMAPDEFAULT) {
+                    # Bug was in a default group that no longer is.
+                    $buginanychangingdefault = 1;
+                    push (@defaultstoremove, $groupid);
+                }
+            }
+            if (($isactive) && (!$bugingroup)
+                && ($newcontrol == CONTROLMAPDEFAULT)
+                && ($useringroup)) {
+                push (@defaultstoadd, $groupid);
+            }
+            if (($bugingroup) && ($isactive) && ($newcontrol == CONTROLMAPNA)) {
+                # Group is no longer permitted.
+                push(@groupstoremove, $groupid);
+            }
+            if ((!$bugingroup) && ($isactive) 
+                && ($newcontrol == CONTROLMAPMANDATORY)) {
+                # Group is now required.
+                push(@groupstoadd, $groupid);
             }
         }
-
-        if (
-          # the old product is associated with a group;
-          GroupExists($oldhash{'product'})
-
-          # the bug is a member of that group;
-          && BugInGroup($id, $oldhash{'product'}) 
-        ) { 
-            # Remove the bug from the group associated with its old product.
-            my $groupid = GroupNameToId($oldhash{'product'});
-            SendSQL("DELETE FROM bug_group_map WHERE bug_id = $id AND group_id = $groupid");
+        # If addtonewgroups = "yes", old default groups will be removed
+        # and new default groups will be added.
+        # If addtonewgroups = "yesifinold", old default groups will be removed
+        # and new default groups will be added only if the bug was in ANY
+        # of the old default groups.
+        # If addtonewgroups = "no", old default groups will be removed and not
+        # replaced.
+        push(@groupstoremove, @defaultstoremove);
+        if (AnyDefaultGroups()
+            && (($::FORM{'addtonewgroup'} eq 'yes')
+            || (($::FORM{'addtonewgroup'} eq 'yesifinold') 
+            && ($buginanydefault)))) {
+            push(@groupstoadd, @defaultstoadd);
         }
 
+        # Now actually update the bug_group_map.
+        my @DefGroupsAdded = ();
+        my @DefGroupsRemoved = ();
+        foreach my $groupid (@allgroups) {
+            my $thisadd = grep( ($_ == $groupid), @groupstoadd);
+            my $thisdel = grep( ($_ == $groupid), @groupstoremove);
+            if ($thisadd) {
+                push(@DefGroupsAdded, GroupIdToName($groupid));
+                SendSQL("INSERT INTO bug_group_map (bug_id, group_id) VALUES " .
+                        "($id, $groupid)");
+            } elsif ($thisdel) {
+                push(@DefGroupsRemoved, GroupIdToName($groupid));
+                SendSQL("DELETE FROM bug_group_map WHERE bug_id = $id " .
+                        "AND group_id = $groupid");
+            }
+        }
+        if ((@DefGroupsAdded) || (@DefGroupsRemoved)) {
+            LogActivityEntry($id, "bug_group",
+                join(', ', @DefGroupsRemoved),
+                join(', ', @DefGroupsAdded),
+                     $whoid, $timestamp); 
+        }
     }
   
     # get a snapshot of the newly set values out of the database, 
@@ -1471,7 +1569,6 @@ foreach my $id (@idlist) {
         $newhash{$col} = $newvalues[$i];
         $i++;
     }
-
     # for passing to processmail to ensure that when someone is removed
     # from one of these fields, they get notified of that fact (if desired)
     #
diff --git a/query.cgi b/query.cgi
index 0d9c37235..680ed0557 100755
--- a/query.cgi
+++ b/query.cgi
@@ -191,19 +191,14 @@ if ($default{'chfieldto'}->[0] eq "") {
 
 GetVersionTable();
 
-# if using usebuggroups, then we don't want people to see products they don't
-# have access to. Remove them from the list.
+# if using groups for entry, then we don't want people to see products they 
+# don't have access to. Remove them from the list.
 
 my @products = ();
 my %component_set;
 my %version_set;
 my %milestone_set;
-foreach my $p (@::legal_product) {
-    # If we're using bug groups to restrict entry on products, and
-    # this product has a bug group, and the user is not in that
-    # group, we don't want to include that product in this list.
-    next if (Param("usebuggroups") && GroupExists($p) && !UserInGroup($p));
-
+foreach my $p (GetEnterableProducts()) {
     # We build up boolean hashes in the "-set" hashes for each of these things 
     # before making a list because there may be duplicates names across products.
     push @products, $p;
diff --git a/queryhelp.cgi b/queryhelp.cgi
index bfd7c0f69..eb9893bd8 100755
--- a/queryhelp.cgi
+++ b/queryhelp.cgi
@@ -663,7 +663,7 @@ SendSQL("SELECT name, description FROM products ORDER BY name");
         while (MoreSQLData()) {
 
         my ($product, $productdesc) = FetchSQLData();
-        next if (Param("usebuggroups") && GroupExists($product) && !UserInGroup($product));
+        next if (!CanEnterProduct($product));  
         push (@products, $product);
 
         $line_count++;
diff --git a/reports.cgi b/reports.cgi
index f3a18f557..67b175a5e 100755
--- a/reports.cgi
+++ b/reports.cgi
@@ -23,8 +23,6 @@
 # Dawn Endico <endico@mozilla.org>
 # Bryce Nesbitt <bryce@nextbus.COM>,
 # Joe Robins <jmrobins@tgix.com>,
-#    If using the usebuggroups parameter, users shouldn't be able to see
-#    reports for products they don't have access to.
 # Gervase Markham <gerv@gerv.net> and Adam Spiers <adam@spiers.net>
 #    Added ability to chart any combination of resolutions/statuses.
 #    Derive the choice of resolutions/statuses from the -All- data file
@@ -60,20 +58,11 @@ quietly_check_login();
 
 GetVersionTable();
 
-# If the usebuggroups parameter is set, we don't want to list all products.
 # We only want those products that the user has permissions for.
 my @myproducts;
-if(Param("usebuggroups")) {
-    push( @myproducts, "-All-");
-    foreach my $this_product (@legal_product) {
-        if(GroupExists($this_product) && !UserInGroup($this_product)) {
-            next;
-        } else {
-            push( @myproducts, $this_product )
-        }
-    }
-} else {
-    push( @myproducts, "-All-", @legal_product );
+push( @myproducts, "-All-");
+foreach my $this_product (@legal_product) {
+    push(@myproducts, $this_product) if CanEnterProduct($this_product);
 }
 
 if (! defined $FORM{'product'}) {
@@ -91,12 +80,11 @@ if (! defined $FORM{'product'}) {
     grep($_ eq $FORM{'product'}, @myproducts)
       || ThrowUserError("invalid_product_name", {product => $FORM{'product'}});
 
-    # If usebuggroups is on, we don't want people to be able to view
+    # We don't want people to be able to view
     # reports for products they don't have permissions for...
-    Param("usebuggroups") 
-      && GroupExists($FORM{'product'}) 
-      && !UserInGroup($FORM{'product'})
-      && ThrowUserError("report_access_denied");
+    if (!CanEnterProduct($FORM{'product'})) {
+        ThrowUserError("report_access_denied");
+    }
           
     # We've checked that the product exists, and that the user can see it
     # This means that is OK to detaint
diff --git a/sanitycheck.cgi b/sanitycheck.cgi
index 8977ce3b5..49c007f7f 100755
--- a/sanitycheck.cgi
+++ b/sanitycheck.cgi
@@ -26,6 +26,7 @@ use strict;
 use lib qw(.);
 
 require "CGI.pl";
+use Bugzilla::Constants;
 
 use vars qw(%FORM $unconfirmedstate);
 
@@ -263,6 +264,7 @@ CrossCheck("groups", "id",
            ["bug_group_map", "group_id"],
            ["group_group_map", "grantor_id"],
            ["group_group_map", "member_id"],
+           ["group_control_map", "group_id"],
            ["user_group_map", "group_id"]);
 
 CrossCheck("profiles", "userid",
@@ -288,6 +290,7 @@ CrossCheck("products", "id",
            ["components", "product_id", "name"],
            ["milestones", "product_id", "value"],
            ["versions", "product_id", "value"],
+           ["group_control_map", "product_id"],
            ["flaginclusions", "product_id", "type_id"],
            ["flagexclusions", "product_id", "type_id"]);
 
@@ -612,6 +615,53 @@ sub DateCheck {
 DateCheck("groups", "last_changed");
 DateCheck("profiles", "refreshed_when");
 
+###########################################################################
+# Control Values
+###########################################################################
+
+# Checks for values that are invalid OR
+# not among the 9 valid combinations
+Status("Checking for bad values in group_control_map");
+SendSQL("SELECT COUNT(product_id) FROM group_control_map WHERE " .
+        "membercontrol NOT IN(" . CONTROLMAPNA . "," . CONTROLMAPSHOWN .
+        "," . CONTROLMAPDEFAULT . "," . CONTROLMAPMANDATORY . ")" .
+        " OR " .
+        "othercontrol NOT IN(" . CONTROLMAPNA . "," . CONTROLMAPSHOWN .
+        "," . CONTROLMAPDEFAULT . "," . CONTROLMAPMANDATORY . ")" .
+        " OR " .
+        "( (membercontrol != othercontrol) " .
+          "AND (membercontrol != " . CONTROLMAPSHOWN . ") " .
+          "AND ((membercontrol != " . CONTROLMAPDEFAULT . ") " .
+            "OR (othercontrol = " . CONTROLMAPSHOWN . ")))");
+my $c = FetchOneColumn();
+if ($c) {
+    Alert("Found $c bad group_control_map entries");
+}
+
+Status("Checking for bugs with groups violating their product's group controls");
+BugCheck("bugs, groups, bug_group_map
+         LEFT JOIN group_control_map
+         ON group_control_map.product_id = bugs.product_id
+         AND group_control_map.group_id = bug_group_map.group_id
+         WHERE bugs.bug_id = bug_group_map.bug_id
+         AND bug_group_map.group_id = groups.id
+         AND groups.isactive != 0
+         AND ((group_control_map.membercontrol = " . CONTROLMAPNA . ")
+         OR (group_control_map.membercontrol IS NULL))",
+         "Have groups not permitted for their products");
+
+BugCheck("bugs, groups, group_control_map
+         LEFT JOIN bug_group_map
+         ON group_control_map.group_id = bug_group_map.group_id
+         AND bugs.bug_id = bug_group_map.bug_id
+         WHERE group_control_map.product_id = bugs.product_id
+         AND bug_group_map.group_id = groups.id
+         AND groups.isactive != 0
+         AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY . "
+         AND bug_group_map.group_id IS NULL",
+         "Are missing groups required for their products");
+
+
 ###########################################################################
 # Unsent mail
 ###########################################################################
diff --git a/template/en/default/admin/products/groupcontrol/confirm-edit.html.tmpl b/template/en/default/admin/products/groupcontrol/confirm-edit.html.tmpl
new file mode 100644
index 000000000..85f89e6e8
--- /dev/null
+++ b/template/en/default/admin/products/groupcontrol/confirm-edit.html.tmpl
@@ -0,0 +1,55 @@
+<!-- 1.0@bugzilla.org -->
+[%# 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.
+  #
+  # 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): Joel Peshkin <bugreport@peshkin.net>
+  #                 
+  #%]
+
+
+
+[% PROCESS global/header.html.tmpl title="Confirm Group Control Change for product \'$product\'" %]
+
+[% FOREACH group = mandatory_groups %]
+<P>
+group '[% group.name FILTER html %]' impacts [% group.count %] bugs for which the group is
+newly mandatory and will be added.
+[% END %]
+
+[% FOREACH group = na_groups %]
+<P>
+group '[% group.name FILTER html %]' impacts [% group.count %] bugs for which the group is no longer applicable and will be removed.
+[% END %]
+<form method="post" >
+
+  [% PROCESS "global/hidden-fields.html.tmpl" exclude="^(Bugzilla|LDAP)_(login|password)$" %]
+
+  <br>
+     Click "Continue" to proceed with the change including the changes 
+     indicated above.  If you do not want these changes, use "back" to
+     return to the previous page.
+  <p>
+    <input type="hidden" name="confirmed" value="confirmed">
+    <input type="submit" value="Continue">
+  </p>
+
+</form>
+
+
+[% PROCESS global/footer.html.tmpl %]
+
+
diff --git a/template/en/default/admin/products/groupcontrol/edit.html.tmpl b/template/en/default/admin/products/groupcontrol/edit.html.tmpl
new file mode 100644
index 000000000..11bb99de5
--- /dev/null
+++ b/template/en/default/admin/products/groupcontrol/edit.html.tmpl
@@ -0,0 +1,284 @@
+<!-- 1.0@bugzilla.org -->
+[%# 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.
+  #
+  # 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): Joel Peshkin <bugreport@peshkin.net>
+  #%]
+
+
+[% filt_product = product FILTER html %]
+[% PROCESS global/header.html.tmpl 
+  title = "Edit Group Controls for '$filt_product'"
+%]
+
+<form method="post" action="editproducts.cgi">
+  <input type="hidden" name="action" value="updategroupcontrols">
+  <input type="hidden" name="product" value="[% filt_product %]">
+  
+  <table id="form" cellspacing="0" cellpadding="4" border="1">
+    <tr bgcolor="#6666ff">
+      <th>Group</th>
+      <th>Entry</th>
+      <th>MemberControl</th>
+      <th>OtherControl</th>
+      <th>Canedit</th>
+      <th>Bugs</th>
+    </tr>
+    [% FOREACH group = groups %]
+      [% IF group.isactive == 0 AND group.bugcount > 0 %]
+        <tr bgcolor="#bbbbbb">
+          <td>
+            [% group.name FILTER html %]
+          </td>
+          <td align="center" colspan=4>
+            Disabled
+          </td>
+          <td>
+            [% group.bugcount %]
+          </td>
+        <tr>
+      [% ELSIF group.isactive != 0 %]
+        <tr>
+          <td>
+            [% group.name FILTER html %]
+          </td>
+          <td>
+            <input type=checkbox value=1 name=entry_[% group.id %]
+            [% " checked=\"checked\"" IF group.entry %]>
+          </td>
+          <td>
+            <select name="membercontrol_[% group.id %]">
+              <option value=[% const.CONTROLMAPNA %]
+                [% " selected=\"selected\"" 
+                  IF group.membercontrol == const.CONTROLMAPNA %]
+                >NA
+              </option>
+              <option value=[% const.CONTROLMAPSHOWN %]
+                [% " selected=\"selected\"" 
+                  IF group.membercontrol == const.CONTROLMAPSHOWN %]
+                >Shown
+              </option>
+              <option value=[% const.CONTROLMAPDEFAULT %]
+                [% " selected=\"selected\"" 
+                  IF group.membercontrol == const.CONTROLMAPDEFAULT %]
+                >Default
+              </option>
+              <option value=[% const.CONTROLMAPMANDATORY %]
+                [% " selected=\"selected\"" 
+                  IF group.membercontrol == const.CONTROLMAPMANDATORY %]
+                >Mandatory
+              </option>
+            </select>
+          </td>
+          <td>
+            <select name="othercontrol_[% group.id %]">
+              <option value=[% const.CONTROLMAPNA %]
+                [% " selected=\"selected\"" 
+                  IF group.othercontrol == const.CONTROLMAPNA %]
+                >NA
+              </option>
+              <option value=[% const.CONTROLMAPSHOWN %]
+                [% " selected=\"selected\"" 
+                  IF group.othercontrol == const.CONTROLMAPSHOWN %]
+                >Shown
+              </option>
+              <option value=[% const.CONTROLMAPDEFAULT %]
+                [% " selected=\"selected\"" 
+                  IF group.othercontrol == const.CONTROLMAPDEFAULT %]
+                >Default
+              </option>
+              <option value=[% const.CONTROLMAPMANDATORY %]
+                [% " selected=\"selected\"" 
+                  IF group.othercontrol == const.CONTROLMAPMANDATORY %]
+                >Mandatory
+              </option>
+            </select>
+          </td>
+          <td>
+            <input type=checkbox value=1 name=canedit_[% group.id %]
+            [% " checked=\"checked\"" IF group.canedit %]>
+          </td>
+          <td>
+            [% group.bugcount %]
+          </td>
+        </tr>
+      [% END %]
+    [% END %]
+
+  </table>
+  <br>
+  <input type=submit name="submit" value="submit">
+  <br>
+</form>
+
+
+<p>
+These settings control the relationship of the groups to this
+product.
+<p>
+If any group has <b>Entry</b> selected, then this product will
+restrict bug entry to only those users who are members of all the
+groups with entry selected.
+<p>
+If any group has <b>Canedit</b> selected, then this product
+will be read-only for any users who are not members of all of
+the groups with Canedit selected. ONLY users who are members of
+all the canedit groups will be able to edit. This is an additional
+restriction that further restricts what can be edited by a user.
+<p>
+The <b>MemberControl</b> and <b>OtherControl</b> fields
+indicate which bugs will be placed in
+this group according to the following definitions.
+<br>
+<table border=1>
+  <tr>
+    <th>
+      MemberControl
+    </th>
+    <th>
+      OtherControl
+    </th>
+    <th>
+      Interpretation
+    </th>
+  </tr>
+  <tr>
+    <td>
+      NA
+    </td>
+    <td>
+      NA
+    </td>
+    <td>
+      Bugs in this product are never associated with this group.
+    </td>
+  </tr>
+  <tr>
+    <td>
+      Shown
+    </td>
+    <td>
+      NA
+    </td>
+    <td>
+      Bugs in this product are permitted to be restricted to this
+      group.  Users who are a member of this group will be able
+      to place bugs in this group.
+    </td>
+  </tr>
+  <tr>
+    <td>
+      Shown
+    </td>
+    <td>
+      Shown
+    </td>
+    <td>
+      Bugs in this product can be placed in this group by anyone
+      with permission to edit the bug even if they are not a member
+      of this group.
+    </td>
+  </tr>
+  <tr>
+    <td>
+      Shown
+    </td>
+    <td>
+      Default
+    </td>
+    <td>
+      Bugs in this product can be placed in this group by anyone
+      with permission to edit the bug even if they are not a member
+      of this group. Non-members place bugs in this group by default.
+    </td>
+  </tr>
+  <tr>
+    <td>
+      Shown
+    </td>
+    <td>
+      Mandatory
+    </td>
+    <td>
+      Bugs in this product are permitted to be restricted to this
+      group.  Users who are a member of this group will be able
+      to place bugs in this group.
+      Non-members will be forced to restrict bugs to this group
+      when they initially enter a bug in this product.
+    </td>
+  </tr>
+  <tr>
+    <td>
+      Default
+    </td>
+    <td>
+      NA
+    </td>
+    <td>
+      Bugs in this product are permitted to be restricted to this
+      group and are placed in this group by default.
+      Users who are a member of this group will be able
+      to place bugs in this group.
+    </td>
+  </tr>
+  <tr>
+    <td>
+      Default
+    </td>
+    <td>
+      Default
+    </td>
+    <td>
+      Bugs in this product are permitted to be restricted to this
+      group and are placed in this group by default.
+      Users who are a member of this group will be able
+      to place bugs in this group. Non-members will be able to
+      restrict bugs to this group on entry and will do so by default
+    </td>
+  </tr>
+  <tr>
+    <td>
+      Default
+    </td>
+    <td>
+      Mandatory
+    </td>
+    <td>
+      Bugs in this product are permitted to be restricted to this
+      group and are placed in this group by default.
+      Users who are a member of this group will be able
+      to place bugs in this group. Non-members will be forced
+      to place bugs in this group on entry.
+    </td>
+  </tr>
+  <tr>
+    <td>
+      Mandatory
+    </td>
+    <td>
+      Mandatory
+    </td>
+    <td>
+      Bugs in this product are required to be restricted to this
+      group.  Users are not given any option.
+    </td>
+  </tr>
+</table>
+
+
+[% PROCESS global/footer.html.tmpl %]
+
diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl
index 575c0ea07..4af65af1c 100644
--- a/template/en/default/bug/edit.html.tmpl
+++ b/template/en/default/bug/edit.html.tmpl
@@ -405,20 +405,28 @@
   <br>
 
   [% IF groups.size > 0 %]
-    <br>
-    <b>Only users in all of the selected groups can view this bug:</b>
-    <br>
-    <font size="-1">(Unchecking all boxes makes this a public bug.)</font>
-    <br>
-    <br>
 
     [% FOREACH group = groups %]
+      [% IF NOT group.mandatory %]
+        [% IF NOT emitted_description %]
+          [% emitted_description = 1 %]
+          <br>
+          <b>Only users in all of the selected groups can view this bug:</b>
+          <br>
+          <font size="-1">
+            (Unchecking all boxes makes this a more public bug.)
+          </font>
+          <br>
+          <br>
+        [% END %]
+
       &nbsp;&nbsp;&nbsp;&nbsp;
       <input type="checkbox" name="bit-[% group.bit %]" value="1" 
         [% " checked=\"checked\"" IF group.ison %]
         [% " disabled=\"disabled\"" IF NOT group.ingroup %]>
       [% group.description %]
       <br>
+      [% END %]
     [% END %]
 
     [% IF NOT user.inallgroups %]
@@ -431,7 +439,7 @@
 
     [% IF bug.inagroup %]
       <p>
-        <b>But users in the roles selected below can always view this bug:</b>
+        <b>Users in the roles selected below can always view this bug:</b>
         <br>
         <small>
           (The assignee                                                
diff --git a/template/en/default/bug/process/verify-new-product.html.tmpl b/template/en/default/bug/process/verify-new-product.html.tmpl
index 77a2ab762..bba85d637 100644
--- a/template/en/default/bug/process/verify-new-product.html.tmpl
+++ b/template/en/default/bug/process/verify-new-product.html.tmpl
@@ -81,14 +81,14 @@
   <h3>Verify Bug Group</h3>
 
   <p>
-    Do you want to add the bug to its new product's group (if any)?
+    Do you want to add the bug to its new product's default groups (if any)?
   </p>
 
   <p>
     <input type="radio" name="addtonewgroup" value="no"><b>no</b><br>
     <input type="radio" name="addtonewgroup" value="yes"><b>yes</b><br>
     <input type="radio" name="addtonewgroup" value="yesifinold" checked="checked">
-      <b>yes, but only if the bug was in its old product's group</b><br>
+      <b>yes, but only if the bug was in any of its old product's default groups</b><br>
   </p>
 [% END %]
 
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index b3b50f68a..885cf24cd 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -230,6 +230,12 @@
     It must also not contain any of these special characters:
     <tt>\ ( ) &amp; &lt; &gt; , ; : &quot; [ ]</tt>, or any whitespace.
     
+  [% ELSIF error == "illegal_group_control_combination" %]
+    [% title = "Your Group Control Combination Is Illegal" %]
+    Your group control combination for group &quot;
+    [% groupname FILTER html %]
+    &quot; is illegal.
+
   [% ELSIF error == "illegal_is_obsolete" %]
     [% title = "Your Query Makes No Sense" %]
     The only legal values for the <em>Attachment is obsolete</em> field are
@@ -462,6 +468,10 @@
     [% title = "Access Denied" %]
     You do not have the permissions necessary to access that product.
      
+  [% ELSIF error == "product_edit_denied" %]
+    [% title = "Product Edit Access Denied" %]
+    You are not permitted to edit bugs in product [% product %].
+    
   [% ELSIF error == "query_name_missing" %]
     [% title = "No Query Name Specified" %]
     You must enter a name for your query.
-- 
2.24.1