Merged BugsSite to Bugzilla-3.0.3
[WebKit-https.git] / BugsSite / editgroups.cgi
1 #!/usr/bin/perl -wT
2 # -*- Mode: perl; indent-tabs-mode: nil -*-
3 #
4 # The contents of this file are subject to the Mozilla Public
5 # License Version 1.1 (the "License"); you may not use this file
6 # except in compliance with the License. You may obtain a copy of
7 # the License at http://www.mozilla.org/MPL/
8 #
9 # Software distributed under the License is distributed on an "AS
10 # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
11 # implied. See the License for the specific language governing
12 # rights and limitations under the License.
13 #
14 # The Original Code is the Bugzilla Bug Tracking System.
15 #
16 # The Initial Developer of the Original Code is Netscape Communications
17 # Corporation. Portions created by Netscape are
18 # Copyright (C) 1998 Netscape Communications Corporation. All
19 # Rights Reserved.
20 #
21 # Contributor(s): Dave Miller <justdave@syndicomm.com>
22 #                 Joel Peshkin <bugreport@peshkin.net>
23 #                 Jacob Steenhagen <jake@bugzilla.org>
24 #                 Vlad Dascalu <jocuri@softhome.net>
25 #                 Frédéric Buclin <LpSolit@gmail.com>
26
27 use strict;
28 use lib ".";
29
30 use Bugzilla;
31 use Bugzilla::Constants;
32 use Bugzilla::Config qw(:admin);
33 use Bugzilla::Util;
34 use Bugzilla::Error;
35 use Bugzilla::Group;
36 use Bugzilla::Product;
37 use Bugzilla::User;
38 use Bugzilla::Token;
39
40 use constant SPECIAL_GROUPS => ('chartgroup', 'insidergroup',
41                                 'timetrackinggroup', 'querysharegroup');
42
43 my $cgi = Bugzilla->cgi;
44 my $dbh = Bugzilla->dbh;
45 my $template = Bugzilla->template;
46 my $vars = {};
47
48 my $user = Bugzilla->login(LOGIN_REQUIRED);
49
50 print $cgi->header();
51
52 $user->in_group('creategroups')
53   || ThrowUserError("auth_failure", {group  => "creategroups",
54                                      action => "edit",
55                                      object => "groups"});
56
57 my $action = trim($cgi->param('action') || '');
58 my $token  = $cgi->param('token');
59
60 # Add missing entries in bug_group_map for bugs created while
61 # a mandatory group was disabled and which is now enabled again.
62 sub fix_bug_permissions {
63     my $gid = shift;
64     my $dbh = Bugzilla->dbh;
65
66     detaint_natural($gid);
67     return unless $gid;
68
69     my $bug_ids =
70       $dbh->selectcol_arrayref('SELECT bugs.bug_id
71                                   FROM bugs
72                             INNER JOIN group_control_map
73                                     ON group_control_map.product_id = bugs.product_id
74                              LEFT JOIN bug_group_map
75                                     ON bug_group_map.bug_id = bugs.bug_id
76                                    AND bug_group_map.group_id = group_control_map.group_id
77                                  WHERE group_control_map.group_id = ?
78                                    AND group_control_map.membercontrol = ?
79                                    AND bug_group_map.group_id IS NULL',
80                                  undef, ($gid, CONTROLMAPMANDATORY));
81
82     my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)');
83     foreach my $bug_id (@$bug_ids) {
84         $sth->execute($bug_id, $gid);
85     }
86 }
87
88 # CheckGroupID checks that a positive integer is given and is
89 # actually a valid group ID. If all tests are successful, the
90 # trimmed group ID is returned.
91
92 sub CheckGroupID {
93     my ($group_id) = @_;
94     $group_id = trim($group_id || 0);
95     ThrowUserError("group_not_specified") unless $group_id;
96     (detaint_natural($group_id)
97       && Bugzilla->dbh->selectrow_array("SELECT id FROM groups WHERE id = ?",
98                                         undef, $group_id))
99       || ThrowUserError("invalid_group_ID");
100     return $group_id;
101 }
102
103 # This subroutine is called when:
104 # - a new group is created. CheckGroupName checks that its name
105 #   is not empty and is not already used by any existing group.
106 # - an existing group is edited. CheckGroupName checks that its
107 #   name has not been deleted or renamed to another existing
108 #   group name (whose group ID is different from $group_id).
109 # In both cases, an error message is returned to the user if any
110 # test fails! Else, the trimmed group name is returned.
111
112 sub CheckGroupName {
113     my ($name, $group_id) = @_;
114     $name = trim($name || '');
115     trick_taint($name);
116     ThrowUserError("empty_group_name") unless $name;
117     my $excludeself = (defined $group_id) ? " AND id != $group_id" : "";
118     my $name_exists = Bugzilla->dbh->selectrow_array("SELECT name FROM groups " .
119                                                      "WHERE name = ? $excludeself",
120                                                      undef, $name);
121     if ($name_exists) {
122         ThrowUserError("group_exists", { name => $name });
123     }
124     return $name;
125 }
126
127 # CheckGroupDesc checks that a non empty description is given. The
128 # trimmed description is returned.
129
130 sub CheckGroupDesc {
131     my ($desc) = @_;
132     $desc = trim($desc || '');
133     trick_taint($desc);
134     ThrowUserError("empty_group_description") unless $desc;
135     return $desc;
136 }
137
138 # CheckGroupRegexp checks that the regular expression is valid
139 # (the regular expression being optional, the test is successful
140 # if none is given, as expected). The trimmed regular expression
141 # is returned.
142
143 sub CheckGroupRegexp {
144     my ($regexp) = @_;
145     $regexp = trim($regexp || '');
146     trick_taint($regexp);
147     ThrowUserError("invalid_regexp") unless (eval {qr/$regexp/});
148     return $regexp;
149 }
150
151 # If no action is specified, get a list of all groups available.
152
153 unless ($action) {
154     my @groups = Bugzilla::Group->get_all;
155     $vars->{'groups'} = \@groups;
156     
157     print $cgi->header();
158     $template->process("admin/groups/list.html.tmpl", $vars)
159       || ThrowTemplateError($template->error());
160     exit;
161 }
162
163 #
164 # action='changeform' -> present form for altering an existing group
165 #
166 # (next action will be 'postchanges')
167 #
168
169 if ($action eq 'changeform') {
170     # Check that an existing group ID is given
171     my $group_id = CheckGroupID($cgi->param('group'));
172     my ($name, $description, $regexp, $isactive, $isbuggroup) =
173         $dbh->selectrow_array("SELECT name, description, userregexp, " .
174                               "isactive, isbuggroup " .
175                               "FROM groups WHERE id = ?", undef, $group_id);
176
177     # For each group, we use left joins to establish the existence of
178     # a record making that group a member of this group
179     # and the existence of a record permitting that group to bless
180     # this one
181
182     my @groups;
183     my $group_list =
184       $dbh->selectall_arrayref('SELECT groups.id, groups.name, groups.description,
185                                        CASE WHEN group_group_map.member_id IS NOT NULL
186                                             THEN 1 ELSE 0 END,
187                                        CASE WHEN B.member_id IS NOT NULL
188                                             THEN 1 ELSE 0 END,
189                                        CASE WHEN C.member_id IS NOT NULL
190                                             THEN 1 ELSE 0 END
191                                   FROM groups
192                                   LEFT JOIN group_group_map
193                                     ON group_group_map.member_id = groups.id
194                                    AND group_group_map.grantor_id = ?
195                                    AND group_group_map.grant_type = ?
196                                   LEFT JOIN group_group_map as B
197                                     ON B.member_id = groups.id
198                                    AND B.grantor_id = ?
199                                    AND B.grant_type = ?
200                                   LEFT JOIN group_group_map as C
201                                     ON C.member_id = groups.id
202                                    AND C.grantor_id = ?
203                                    AND C.grant_type = ?
204                                  ORDER by name',
205                                 undef, ($group_id, GROUP_MEMBERSHIP,
206                                         $group_id, GROUP_BLESS,
207                                         $group_id, GROUP_VISIBLE));
208
209     foreach (@$group_list) {
210         my ($grpid, $grpnam, $grpdesc, $grpmember, $blessmember, $membercansee) = @$_;
211         my $group = {};
212         $group->{'grpid'}       = $grpid;
213         $group->{'grpnam'}      = $grpnam;
214         $group->{'grpdesc'}     = $grpdesc;
215         $group->{'grpmember'}   = $grpmember;
216         $group->{'blessmember'} = $blessmember;
217         $group->{'membercansee'}= $membercansee;
218         push(@groups, $group);
219     }
220
221     $vars->{'group_id'}    = $group_id;
222     $vars->{'name'}        = $name;
223     $vars->{'description'} = $description;
224     $vars->{'regexp'}      = $regexp;
225     $vars->{'isactive'}    = $isactive;
226     $vars->{'isbuggroup'}  = $isbuggroup;
227     $vars->{'groups'}      = \@groups;
228     $vars->{'token'}       = issue_session_token('edit_group');
229
230     print $cgi->header();
231     $template->process("admin/groups/edit.html.tmpl", $vars)
232       || ThrowTemplateError($template->error());
233
234     exit;
235 }
236
237 #
238 # action='add' -> present form for parameters for new group
239 #
240 # (next action will be 'new')
241 #
242
243 if ($action eq 'add') {
244     $vars->{'token'} = issue_session_token('add_group');
245     print $cgi->header();
246     $template->process("admin/groups/create.html.tmpl", $vars)
247       || ThrowTemplateError($template->error());
248     
249     exit;
250 }
251
252
253
254 #
255 # action='new' -> add group entered in the 'action=add' screen
256 #
257
258 if ($action eq 'new') {
259     check_token_data($token, 'add_group');
260     # Check that a not already used group name is given, that
261     # a description is also given and check if the regular
262     # expression is valid (if any).
263     my $name = CheckGroupName($cgi->param('name'));
264     my $desc = CheckGroupDesc($cgi->param('desc'));
265     my $regexp = CheckGroupRegexp($cgi->param('regexp'));
266     my $isactive = $cgi->param('isactive') ? 1 : 0;
267
268     # Add the new group
269     $dbh->do('INSERT INTO groups
270               (name, description, isbuggroup, userregexp, isactive)
271               VALUES (?, ?, 1, ?, ?)',
272               undef, ($name, $desc, $regexp, $isactive));
273
274     my $gid = $dbh->bz_last_key('groups', 'id');
275     my $admin = Bugzilla::Group->new({name => 'admin'})->id();
276     # Since we created a new group, give the "admin" group all privileges
277     # initially.
278     my $sth = $dbh->prepare('INSERT INTO group_group_map
279                              (member_id, grantor_id, grant_type)
280                              VALUES (?, ?, ?)');
281
282     $sth->execute($admin, $gid, GROUP_MEMBERSHIP);
283     $sth->execute($admin, $gid, GROUP_BLESS);
284     $sth->execute($admin, $gid, GROUP_VISIBLE);
285
286     # Permit all existing products to use the new group if makeproductgroups.
287     if ($cgi->param('insertnew')) {
288         $dbh->do('INSERT INTO group_control_map
289                   (group_id, product_id, entry, membercontrol,
290                    othercontrol, canedit)
291                   SELECT ?, products.id, 0, ?, ?, 0 FROM products',
292                   undef, ($gid, CONTROLMAPSHOWN, CONTROLMAPNA));
293     }
294     Bugzilla::Group::RederiveRegexp($regexp, $gid);
295     delete_token($token);
296
297     print $cgi->header();
298     $template->process("admin/groups/created.html.tmpl", $vars)
299       || ThrowTemplateError($template->error());
300     exit;
301 }
302
303 #
304 # action='del' -> ask if user really wants to delete
305 #
306 # (next action would be 'delete')
307 #
308
309 if ($action eq 'del') {
310     # Check that an existing group ID is given
311     my $gid = CheckGroupID($cgi->param('group'));
312     my ($name, $desc, $isbuggroup) =
313         $dbh->selectrow_array("SELECT name, description, isbuggroup " .
314                               "FROM groups WHERE id = ?", undef, $gid);
315
316     # System groups cannot be deleted!
317     if (!$isbuggroup) {
318         ThrowUserError("system_group_not_deletable", { name => $name });
319     }
320     # Groups having a special role cannot be deleted.
321     my @special_groups;
322     foreach my $special_group (SPECIAL_GROUPS) {
323         if ($name eq Bugzilla->params->{$special_group}) {
324             push(@special_groups, $special_group);
325         }
326     }
327     if (scalar(@special_groups)) {
328         ThrowUserError('group_has_special_role', {'name'  => $name,
329                                                   'groups' => \@special_groups});
330     }
331
332     # Group inheritance no longer appears in user_group_map.
333     my $grouplist = join(',', @{Bugzilla::User->flatten_group_membership($gid)});
334     my $hasusers =
335         $dbh->selectrow_array("SELECT 1 FROM user_group_map
336                                WHERE group_id IN ($grouplist) AND isbless = 0 " .
337                                $dbh->sql_limit(1)) || 0;
338
339     my ($shared_queries) =
340         $dbh->selectrow_array('SELECT COUNT(*)
341                                  FROM namedquery_group_map
342                                 WHERE group_id = ?',
343                               undef, $gid);
344
345     my $bug_ids = $dbh->selectcol_arrayref('SELECT bug_id FROM bug_group_map
346                                             WHERE group_id = ?', undef, $gid);
347
348     my $hasbugs = scalar(@$bug_ids) ? 1 : 0;
349     my $buglist = join(',', @$bug_ids);
350
351     my $hasproduct = Bugzilla::Product->new({'name' => $name}) ? 1 : 0;
352
353     my $hasflags = $dbh->selectrow_array('SELECT 1 FROM flagtypes 
354                                            WHERE grant_group_id = ?
355                                               OR request_group_id = ? ' .
356                                           $dbh->sql_limit(1),
357                                           undef, ($gid, $gid)) || 0;
358
359     $vars->{'gid'}            = $gid;
360     $vars->{'name'}           = $name;
361     $vars->{'description'}    = $desc;
362     $vars->{'hasusers'}       = $hasusers;
363     $vars->{'hasbugs'}        = $hasbugs;
364     $vars->{'hasproduct'}     = $hasproduct;
365     $vars->{'hasflags'}       = $hasflags;
366     $vars->{'shared_queries'} = $shared_queries;
367     $vars->{'buglist'}        = $buglist;
368     $vars->{'token'}          = issue_session_token('delete_group');
369
370     print $cgi->header();
371     $template->process("admin/groups/delete.html.tmpl", $vars)
372       || ThrowTemplateError($template->error());
373     
374     exit;
375 }
376
377 #
378 # action='delete' -> really delete the group
379 #
380
381 if ($action eq 'delete') {
382     check_token_data($token, 'delete_group');
383     # Check that an existing group ID is given
384     my $gid = CheckGroupID($cgi->param('group'));
385     my ($name, $isbuggroup) =
386         $dbh->selectrow_array("SELECT name, isbuggroup FROM groups " .
387                               "WHERE id = ?", undef, $gid);
388
389     # System groups cannot be deleted!
390     if (!$isbuggroup) {
391         ThrowUserError("system_group_not_deletable", { name => $name });
392     }
393     # Groups having a special role cannot be deleted.
394     my @special_groups;
395     foreach my $special_group (SPECIAL_GROUPS) {
396         if ($name eq Bugzilla->params->{$special_group}) {
397             push(@special_groups, $special_group);
398         }
399     }
400     if (scalar(@special_groups)) {
401         ThrowUserError('group_has_special_role', {'name'  => $name,
402                                                   'groups' => \@special_groups});
403     }
404
405     my $cantdelete = 0;
406
407     # Group inheritance no longer appears in user_group_map.
408     my $grouplist = join(',', @{Bugzilla::User->flatten_group_membership($gid)});
409     my $hasusers =
410         $dbh->selectrow_array("SELECT 1 FROM user_group_map
411                                WHERE group_id IN ($grouplist) AND isbless = 0 " .
412                                $dbh->sql_limit(1)) || 0;
413
414     if ($hasusers && !defined $cgi->param('removeusers')) {
415         $cantdelete = 1;
416     }
417
418     my $hasbugs = $dbh->selectrow_array('SELECT 1 FROM bug_group_map
419                                          WHERE group_id = ? ' .
420                                          $dbh->sql_limit(1),
421                                          undef, $gid) || 0;
422     if ($hasbugs && !defined $cgi->param('removebugs')) {
423         $cantdelete = 1;
424     }
425
426     if (Bugzilla::Product->new({'name' => $name})
427         && !defined $cgi->param('unbind'))
428     {
429         $cantdelete = 1;
430     }
431
432     my $hasflags = $dbh->selectrow_array('SELECT 1 FROM flagtypes 
433                                            WHERE grant_group_id = ?
434                                               OR request_group_id = ? ' .
435                                           $dbh->sql_limit(1),
436                                           undef, ($gid, $gid)) || 0;
437     if ($hasflags && !defined $cgi->param('removeflags')) {
438         $cantdelete = 1;
439     }
440
441     $vars->{'gid'}        = $gid;
442     $vars->{'name'}       = $name;
443
444     ThrowUserError('group_cannot_delete', $vars) if $cantdelete;
445
446     $dbh->do('UPDATE flagtypes SET grant_group_id = ?
447                WHERE grant_group_id = ?',
448               undef, (undef, $gid));
449     $dbh->do('UPDATE flagtypes SET request_group_id = ?
450                WHERE request_group_id = ?',
451               undef, (undef, $gid));
452     $dbh->do('DELETE FROM namedquery_group_map WHERE group_id = ?',
453               undef, $gid);
454     $dbh->do('DELETE FROM user_group_map WHERE group_id = ?',
455               undef, $gid);
456     $dbh->do('DELETE FROM group_group_map 
457                WHERE grantor_id = ? OR member_id = ?',
458               undef, ($gid, $gid));
459     $dbh->do('DELETE FROM bug_group_map WHERE group_id = ?',
460               undef, $gid);
461     $dbh->do('DELETE FROM group_control_map WHERE group_id = ?',
462               undef, $gid);
463     $dbh->do('DELETE FROM whine_schedules
464                WHERE mailto_type = ? AND mailto = ?',
465               undef, (MAILTO_GROUP, $gid));
466     $dbh->do('DELETE FROM groups WHERE id = ?',
467               undef, $gid);
468
469     delete_token($token);
470
471     print $cgi->header();
472     $template->process("admin/groups/deleted.html.tmpl", $vars)
473       || ThrowTemplateError($template->error());
474
475     exit;
476 }
477
478 #
479 # action='postchanges' -> update the groups
480 #
481
482 if ($action eq 'postchanges') {
483     check_token_data($token, 'edit_group');
484     # ZLL: Bug 181589: we need to have something to remove explicitly listed users from
485     # groups in order for the conversion to 2.18 groups to work
486     my $action;
487
488     if ($cgi->param('remove_explicit_members')) {
489         $action = 1;
490     } elsif ($cgi->param('remove_explicit_members_regexp')) {
491         $action = 2;
492     } else {
493         $action = 3;
494     }
495     
496     my ($gid, $chgs, $name, $regexp) = doGroupChanges();
497     
498     $vars->{'action'}  = $action;
499     $vars->{'changes'} = $chgs;
500     $vars->{'gid'}     = $gid;
501     $vars->{'name'}    = $name;
502     if ($action == 2) {
503         $vars->{'regexp'} = $regexp;
504     }
505     delete_token($token);
506
507     print $cgi->header();
508     $template->process("admin/groups/change.html.tmpl", $vars)
509       || ThrowTemplateError($template->error());
510     exit;
511 }
512
513 if (($action eq 'remove_all_regexp') || ($action eq 'remove_all')) {
514     # remove all explicit users from the group with
515     # gid = $cgi->param('group') that match the regular expression
516     # stored in the DB for that group or all of them period
517
518     my $gid = CheckGroupID($cgi->param('group'));
519
520     my ($name, $regexp) =
521       $dbh->selectrow_array('SELECT name, userregexp FROM groups
522                              WHERE id = ?', undef, $gid);
523
524     $dbh->bz_lock_tables('groups WRITE', 'profiles READ',
525                          'user_group_map WRITE');
526
527     my $sth = $dbh->prepare("SELECT user_group_map.user_id, profiles.login_name
528                                FROM user_group_map
529                          INNER JOIN profiles
530                                  ON user_group_map.user_id = profiles.userid
531                               WHERE user_group_map.group_id = ?
532                                 AND grant_type = ?
533                                 AND isbless = 0");
534     $sth->execute($gid, GRANT_DIRECT);
535
536     my @users;
537     my $sth2 = $dbh->prepare("DELETE FROM user_group_map
538                               WHERE user_id = ?
539                               AND isbless = 0
540                               AND group_id = ?");
541
542     while ( my ($userid, $userlogin) = $sth->fetchrow_array() ) {
543         if ((($regexp =~ /\S/) && ($userlogin =~ m/$regexp/i))
544             || ($action eq 'remove_all'))
545         {
546             $sth2->execute($userid, $gid);
547
548             my $user = {};
549             $user->{'login'} = $userlogin;
550             push(@users, $user);
551         }
552     }
553     $dbh->bz_unlock_tables();
554
555     $vars->{'users'}      = \@users;
556     $vars->{'name'}       = $name;
557     $vars->{'regexp'}     = $regexp;
558     $vars->{'remove_all'} = ($action eq 'remove_all');
559     $vars->{'gid'}        = $gid;
560     
561     print $cgi->header();
562     $template->process("admin/groups/remove.html.tmpl", $vars)
563       || ThrowTemplateError($template->error());
564
565     exit;
566 }
567
568
569 #
570 # No valid action found
571 #
572
573 ThrowCodeError("action_unrecognized", $vars);
574
575
576 # Helper sub to handle the making of changes to a group
577 sub doGroupChanges {
578     my $cgi = Bugzilla->cgi;
579     my $dbh = Bugzilla->dbh;
580
581     $dbh->bz_lock_tables('groups WRITE', 'group_group_map WRITE',
582                          'bug_group_map WRITE', 'user_group_map WRITE',
583                          'group_control_map READ', 'bugs READ', 'profiles READ',
584                          # Due to the way Bugzilla::Config::BugFields::get_param_list()
585                          # works, we need to lock these tables too.
586                          'priority READ', 'bug_severity READ', 'rep_platform READ',
587                          'op_sys READ');
588
589     # Check that the given group ID and regular expression are valid.
590     # If tests are successful, trimmed values are returned by CheckGroup*.
591     my $gid = CheckGroupID($cgi->param('group'));
592     my $regexp = CheckGroupRegexp($cgi->param('regexp'));
593
594     # The name and the description of system groups cannot be edited.
595     # We then need to know if the group being edited is a system group.
596     my $isbuggroup = $dbh->selectrow_array('SELECT isbuggroup FROM groups
597                                             WHERE id = ?', undef, $gid);
598     my $name;
599     my $desc;
600     my $isactive;
601     my $chgs = 0;
602
603     # We trust old values given by the template. If they are hacked
604     # in a way that some of the tests below become negative, the
605     # corresponding attributes are not updated in the DB, which does
606     # not hurt.
607     if ($isbuggroup) {
608         # Check that the group name and its description are valid
609         # and return trimmed values if tests are successful.
610         $name = CheckGroupName($cgi->param('name'), $gid);
611         $desc = CheckGroupDesc($cgi->param('desc'));
612         $isactive = $cgi->param('isactive') ? 1 : 0;
613
614         if ($name ne $cgi->param('oldname')) {
615             $chgs = 1;
616             $dbh->do('UPDATE groups SET name = ? WHERE id = ?',
617                       undef, ($name, $gid));
618             # If the group is used by some parameters, we have to update
619             # these parameters too.
620             my $update_params = 0;
621             foreach my $group (SPECIAL_GROUPS) {
622                 if ($cgi->param('oldname') eq Bugzilla->params->{$group}) {
623                     SetParam($group, $name);
624                     $update_params = 1;
625                 }
626             }
627             write_params() if $update_params;
628         }
629         if ($desc ne $cgi->param('olddesc')) {
630             $chgs = 1;
631             $dbh->do('UPDATE groups SET description = ? WHERE id = ?',
632                       undef, ($desc, $gid));
633         }
634         if ($isactive ne $cgi->param('oldisactive')) {
635             $chgs = 1;
636             $dbh->do('UPDATE groups SET isactive = ? WHERE id = ?',
637                       undef, ($isactive, $gid));
638             # If the group was mandatory for some products before
639             # we deactivated it and we now activate this group again,
640             # we have to add all bugs created while this group was
641             # disabled in bug_group_map to correctly protect them.
642             if ($isactive) { fix_bug_permissions($gid); }
643         }
644     }
645     if ($regexp ne $cgi->param('oldregexp')) {
646         $chgs = 1;
647         $dbh->do('UPDATE groups SET userregexp = ? WHERE id = ?',
648                   undef, ($regexp, $gid));
649         Bugzilla::Group::RederiveRegexp($regexp, $gid);
650     }
651
652     my $sthInsert = $dbh->prepare('INSERT INTO group_group_map
653                                    (member_id, grantor_id, grant_type)
654                                    VALUES (?, ?, ?)');
655
656     my $sthDelete = $dbh->prepare('DELETE FROM group_group_map
657                                     WHERE member_id = ?
658                                       AND grantor_id = ?
659                                       AND grant_type = ?');
660
661     foreach my $b (grep {/^oldgrp-\d*$/} $cgi->param()) {
662         if (defined($cgi->param($b))) {
663             $b =~ /^oldgrp-(\d+)$/;
664             my $v = $1;
665             my $grp = $cgi->param("grp-$v") || 0;
666             if (($v != $gid) && ($cgi->param("oldgrp-$v") != $grp)) {
667                 $chgs = 1;
668                 if ($grp != 0) {
669                     $sthInsert->execute($v, $gid, GROUP_MEMBERSHIP);
670                 } else {
671                     $sthDelete->execute($v, $gid, GROUP_MEMBERSHIP);
672                 }
673             }
674
675             my $bless = $cgi->param("bless-$v") || 0;
676             my $oldbless = $cgi->param("oldbless-$v");
677             if ((defined $oldbless) and ($oldbless != $bless)) {
678                 $chgs = 1;
679                 if ($bless != 0) {
680                     $sthInsert->execute($v, $gid, GROUP_BLESS);
681                 } else {
682                     $sthDelete->execute($v, $gid, GROUP_BLESS);
683                 }
684             }
685
686             my $cansee = $cgi->param("cansee-$v") || 0;
687             if (Bugzilla->params->{"usevisibilitygroups"} 
688                && ($cgi->param("oldcansee-$v") != $cansee)) {
689                 $chgs = 1;
690                 if ($cansee != 0) {
691                     $sthInsert->execute($v, $gid, GROUP_VISIBLE);
692                 } else {
693                     $sthDelete->execute($v, $gid, GROUP_VISIBLE);
694                 }
695             }
696
697         }
698     }
699     $dbh->bz_unlock_tables();
700     return $gid, $chgs, $name, $regexp;
701 }