2010-08-30 Alejandro G. Castro <alex@igalia.com>
[WebKit-https.git] / BugsSite / editproducts.cgi
1 #!/usr/bin/env 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 mozilla.org code.
15 #
16 # The Initial Developer of the Original Code is Holger
17 # Schurig. Portions created by Holger Schurig are
18 # Copyright (C) 1999 Holger Schurig. All
19 # Rights Reserved.
20 #
21 # Contributor(s): Holger Schurig <holgerschurig@nikocity.de>
22 #               Terry Weissman <terry@mozilla.org>
23 #               Dawn Endico <endico@mozilla.org>
24 #               Joe Robins <jmrobins@tgix.com>
25 #               Gavin Shelley <bugzilla@chimpychompy.org>
26 #               Frédéric Buclin <LpSolit@gmail.com>
27 #               Greg Hendricks <ghendricks@novell.com>
28 #               Lance Larsh <lance.larsh@oracle.com>
29 #               Elliotte Martin <elliotte.martin@yahoo.com>
30
31 use strict;
32 use lib qw(. lib);
33
34 use Bugzilla;
35 use Bugzilla::Constants;
36 use Bugzilla::Util;
37 use Bugzilla::Error;
38 use Bugzilla::Bug;
39 use Bugzilla::Series;
40 use Bugzilla::Mailer;
41 use Bugzilla::Product;
42 use Bugzilla::Classification;
43 use Bugzilla::Milestone;
44 use Bugzilla::Group;
45 use Bugzilla::User;
46 use Bugzilla::Field;
47 use Bugzilla::Token;
48 use Bugzilla::Status;
49
50 #
51 # Preliminary checks:
52 #
53
54 my $user = Bugzilla->login(LOGIN_REQUIRED);
55 my $whoid = $user->id;
56
57 my $dbh = Bugzilla->dbh;
58 my $cgi = Bugzilla->cgi;
59 my $template = Bugzilla->template;
60 my $vars = {};
61 # Remove this as soon as the documentation about products has been
62 # improved and each action has its own section.
63 $vars->{'doc_section'} = 'products.html';
64
65 print $cgi->header();
66
67 $user->in_group('editcomponents')
68   || scalar(@{$user->get_products_by_permission('editcomponents')})
69   || ThrowUserError("auth_failure", {group  => "editcomponents",
70                                      action => "edit",
71                                      object => "products"});
72
73 #
74 # often used variables
75 #
76 my $classification_name = trim($cgi->param('classification') || '');
77 my $product_name = trim($cgi->param('product') || '');
78 my $action  = trim($cgi->param('action')  || '');
79 my $showbugcounts = (defined $cgi->param('showbugcounts'));
80 my $token = $cgi->param('token');
81
82 #
83 # product = '' -> Show nice list of classifications (if
84 # classifications enabled)
85 #
86
87 if (Bugzilla->params->{'useclassification'} 
88     && !$classification_name
89     && !$product_name)
90 {
91     $vars->{'classifications'} = $user->in_group('editcomponents') ?
92       [Bugzilla::Classification::get_all_classifications] : $user->get_selectable_classifications;
93
94     $template->process("admin/products/list-classifications.html.tmpl", $vars)
95         || ThrowTemplateError($template->error());
96     exit;
97 }
98
99
100 #
101 # action = '' -> Show a nice list of products, unless a product
102 #                is already specified (then edit it)
103 #
104
105 if (!$action && !$product_name) {
106     my $classification;
107     my $products;
108
109     if (Bugzilla->params->{'useclassification'}) {
110         $classification =
111             Bugzilla::Classification::check_classification($classification_name);
112
113         $products = $user->get_selectable_products($classification->id);
114         $vars->{'classification'} = $classification;
115     } else {
116         $products = $user->get_selectable_products;
117     }
118
119     # If the user has editcomponents privs for some products only,
120     # we have to restrict the list of products to display.
121     unless ($user->in_group('editcomponents')) {
122         $products = $user->get_products_by_permission('editcomponents');
123         if (Bugzilla->params->{'useclassification'}) {
124             @$products = grep {$_->classification_id == $classification->id} @$products;
125         }
126     }
127     $vars->{'products'} = $products;
128     $vars->{'showbugcounts'} = $showbugcounts;
129
130     $template->process("admin/products/list.html.tmpl", $vars)
131       || ThrowTemplateError($template->error());
132     exit;
133 }
134
135
136
137
138 #
139 # action='add' -> present form for parameters for new product
140 #
141 # (next action will be 'new')
142 #
143
144 if ($action eq 'add') {
145     # The user must have the global editcomponents privs to add
146     # new products.
147     $user->in_group('editcomponents')
148       || ThrowUserError("auth_failure", {group  => "editcomponents",
149                                          action => "add",
150                                          object => "products"});
151
152     if (Bugzilla->params->{'useclassification'}) {
153         my $classification = 
154             Bugzilla::Classification::check_classification($classification_name);
155         $vars->{'classification'} = $classification;
156     }
157     $vars->{'token'} = issue_session_token('add_product');
158
159     $template->process("admin/products/create.html.tmpl", $vars)
160       || ThrowTemplateError($template->error());
161
162     exit;
163 }
164
165
166 #
167 # action='new' -> add product entered in the 'action=add' screen
168 #
169
170 if ($action eq 'new') {
171     # The user must have the global editcomponents privs to add
172     # new products.
173     $user->in_group('editcomponents')
174       || ThrowUserError("auth_failure", {group  => "editcomponents",
175                                          action => "add",
176                                          object => "products"});
177
178     check_token_data($token, 'add_product');
179     # Cleanups and validity checks
180
181     my $classification_id = 1;
182     if (Bugzilla->params->{'useclassification'}) {
183         my $classification = 
184             Bugzilla::Classification::check_classification($classification_name);
185         $classification_id = $classification->id;
186         $vars->{'classification'} = $classification;
187     }
188
189     unless ($product_name) {
190         ThrowUserError("product_blank_name");  
191     }
192
193     my $product = new Bugzilla::Product({name => $product_name});
194
195     if ($product) {
196
197         # Check for exact case sensitive match:
198         if ($product->name eq $product_name) {
199             ThrowUserError("product_name_already_in_use",
200                            {'product' => $product->name});
201         }
202
203         # Next check for a case-insensitive match:
204         if (lc($product->name) eq lc($product_name)) {
205             ThrowUserError("product_name_diff_in_case",
206                            {'product' => $product_name,
207                             'existing_product' => $product->name}); 
208         }
209     }
210
211     my $version = trim($cgi->param('version') || '');
212
213     if ($version eq '') {
214         ThrowUserError("product_must_have_version",
215                        {'product' => $product_name});
216     }
217
218     my $description  = trim($cgi->param('description')  || '');
219
220     if ($description eq '') {
221         ThrowUserError('product_must_have_description',
222                        {'product' => $product_name});
223     }
224
225     my $milestoneurl = trim($cgi->param('milestoneurl') || '');
226     my $disallownew = $cgi->param('disallownew') ? 1 : 0;
227     my $votesperuser = $cgi->param('votesperuser') || 0;
228     my $maxvotesperbug = defined($cgi->param('maxvotesperbug')) ?
229         $cgi->param('maxvotesperbug') : 10000;
230     my $votestoconfirm = $cgi->param('votestoconfirm') || 0;
231     my $defaultmilestone = $cgi->param('defaultmilestone') || "---";
232
233     # The following variables are used in placeholders only.
234     trick_taint($product_name);
235     trick_taint($version);
236     trick_taint($description);
237     trick_taint($milestoneurl);
238     trick_taint($defaultmilestone);
239     detaint_natural($disallownew);
240     detaint_natural($votesperuser);
241     detaint_natural($maxvotesperbug);
242     detaint_natural($votestoconfirm);
243
244     # Add the new product.
245     $dbh->do('INSERT INTO products
246               (name, description, milestoneurl, disallownew, votesperuser,
247                maxvotesperbug, votestoconfirm, defaultmilestone, classification_id)
248               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
249              undef, ($product_name, $description, $milestoneurl, $disallownew,
250              $votesperuser, $maxvotesperbug, $votestoconfirm, $defaultmilestone,
251              $classification_id));
252
253     $product = new Bugzilla::Product({name => $product_name});
254     
255     $dbh->do('INSERT INTO versions (value, product_id) VALUES (?, ?)',
256              undef, ($version, $product->id));
257
258     $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?, ?)',
259              undef, ($product->id, $defaultmilestone));
260
261     # If we're using bug groups, then we need to create a group for this
262     # product as well.  -JMR, 2/16/00
263     if (Bugzilla->params->{"makeproductgroups"}) {
264         # Next we insert into the groups table
265         my $productgroup = $product->name;
266         while (new Bugzilla::Group({name => $productgroup})) {
267             $productgroup .= '_';
268         }
269         my $group_description = "Access to bugs in the " .
270                                 $product->name . " product";
271
272         $dbh->do('INSERT INTO groups (name, description, isbuggroup)
273                   VALUES (?, ?, ?)',
274                   undef, ($productgroup, $group_description, 1));
275
276         my $gid = $dbh->bz_last_key('groups', 'id');
277
278         # If we created a new group, give the "admin" group privileges
279         # initially.
280         my $admin = Bugzilla::Group->new({name => 'admin'})->id();
281         
282         my $sth = $dbh->prepare('INSERT INTO group_group_map
283                                  (member_id, grantor_id, grant_type)
284                                  VALUES (?, ?, ?)');
285
286         $sth->execute($admin, $gid, GROUP_MEMBERSHIP);
287         $sth->execute($admin, $gid, GROUP_BLESS);
288         $sth->execute($admin, $gid, GROUP_VISIBLE);
289
290         # Associate the new group and new product.
291         $dbh->do('INSERT INTO group_control_map
292                   (group_id, product_id, entry, membercontrol,
293                    othercontrol, canedit)
294                   VALUES (?, ?, ?, ?, ?, ?)',
295                  undef, ($gid, $product->id, 
296                          Bugzilla->params->{'useentrygroupdefault'},
297                  CONTROLMAPDEFAULT, CONTROLMAPNA, 0));
298     }
299
300     if ($cgi->param('createseries')) {
301         # Insert default charting queries for this product.
302         # If they aren't using charting, this won't do any harm.
303         #
304         # $open_name and $product are sqlquoted by the series code 
305         # and never used again here, so we can trick_taint them.
306         my $open_name = $cgi->param('open_name');
307         trick_taint($open_name);
308     
309         my @series;
310     
311         # We do every status, every resolution, and an "opened" one as well.
312         foreach my $bug_status (@{get_legal_field_values('bug_status')}) {
313             push(@series, [$bug_status, 
314                            "bug_status=" . url_quote($bug_status)]);
315         }
316
317         foreach my $resolution (@{get_legal_field_values('resolution')}) {
318             next if !$resolution;
319             push(@series, [$resolution, "resolution=" .url_quote($resolution)]);
320         }
321
322         # For localization reasons, we get the name of the "global" subcategory
323         # and the title of the "open" query from the submitted form.
324         my @openedstatuses = BUG_STATE_OPEN;
325         my $query = 
326                join("&", map { "bug_status=" . url_quote($_) } @openedstatuses);
327         push(@series, [$open_name, $query]);
328     
329         foreach my $sdata (@series) {
330             my $series = new Bugzilla::Series(undef, $product->name, 
331                             scalar $cgi->param('subcategory'),
332                             $sdata->[0], $whoid, 1,
333                             $sdata->[1] . "&product=" .
334                             url_quote($product->name), 1);
335             $series->writeToDatabase();
336         }
337     }
338     delete_token($token);
339
340     $vars->{'message'} = 'product_created';
341     $vars->{'product'} = $product;
342     $vars->{'classification'} = new Bugzilla::Classification($product->classification_id)
343       if Bugzilla->params->{'useclassification'};
344     $vars->{'token'} = issue_session_token('edit_product');
345
346     $template->process("admin/products/edit.html.tmpl", $vars)
347         || ThrowTemplateError($template->error());
348     exit;
349 }
350
351 #
352 # action='del' -> ask if user really wants to delete
353 #
354 # (next action would be 'delete')
355 #
356
357 if ($action eq 'del') {
358     my $product = $user->check_can_admin_product($product_name);
359
360     if (Bugzilla->params->{'useclassification'}) {
361         my $classification = 
362             Bugzilla::Classification::check_classification($classification_name);
363         if ($classification->id != $product->classification_id) {
364             ThrowUserError('classification_doesnt_exist_for_product',
365                            { product => $product->name,
366                              classification => $classification->name });
367         }
368         $vars->{'classification'} = $classification;
369     }
370
371     $vars->{'product'} = $product;
372     $vars->{'token'} = issue_session_token('delete_product');
373     
374     Bugzilla::Hook::process("product-confirm_delete", { vars => $vars });
375     
376     $template->process("admin/products/confirm-delete.html.tmpl", $vars)
377         || ThrowTemplateError($template->error());
378     exit;
379 }
380
381 #
382 # action='delete' -> really delete the product
383 #
384
385 if ($action eq 'delete') {
386     my $product = $user->check_can_admin_product($product_name);
387     check_token_data($token, 'delete_product');
388
389     if (Bugzilla->params->{'useclassification'}) {
390         my $classification = 
391             Bugzilla::Classification::check_classification($classification_name);
392         if ($classification->id != $product->classification_id) {
393             ThrowUserError('classification_doesnt_exist_for_product',
394                            { product => $product->name,
395                              classification => $classification->name });
396         }
397         $vars->{'classification'} = $classification;
398     }
399
400     if ($product->bug_count) {
401         if (Bugzilla->params->{"allowbugdeletion"}) {
402             foreach my $bug_id (@{$product->bug_ids}) {
403                 # Note that we allow the user to delete bugs he can't see,
404                 # which is okay, because he's deleting the whole Product.
405                 my $bug = new Bugzilla::Bug($bug_id);
406                 $bug->remove_from_db();
407             }
408         }
409         else {
410             ThrowUserError("product_has_bugs", 
411                            { nb => $product->bug_count });
412         }
413     }
414
415     $dbh->bz_start_transaction();
416
417     my $comp_ids = $dbh->selectcol_arrayref('SELECT id FROM components
418                                              WHERE product_id = ?',
419                                              undef, $product->id);
420
421     $dbh->do('DELETE FROM component_cc WHERE component_id IN
422               (' . join(',', @$comp_ids) . ')') if scalar(@$comp_ids);
423
424     $dbh->do("DELETE FROM components WHERE product_id = ?",
425              undef, $product->id);
426
427     $dbh->do("DELETE FROM versions WHERE product_id = ?",
428              undef, $product->id);
429
430     $dbh->do("DELETE FROM milestones WHERE product_id = ?",
431              undef, $product->id);
432
433     $dbh->do("DELETE FROM group_control_map WHERE product_id = ?",
434              undef, $product->id);
435
436     $dbh->do("DELETE FROM flaginclusions WHERE product_id = ?",
437              undef, $product->id);
438              
439     $dbh->do("DELETE FROM flagexclusions WHERE product_id = ?",
440              undef, $product->id);
441              
442     $dbh->do("DELETE FROM products WHERE id = ?",
443              undef, $product->id);
444
445     $dbh->bz_commit_transaction();
446
447     # We have to delete these internal variables, else we get
448     # the old lists of products and classifications again.
449     delete $user->{selectable_products};
450     delete $user->{selectable_classifications};
451
452     delete_token($token);
453
454     $vars->{'message'} = 'product_deleted';
455     $vars->{'product'} = $product;
456     $vars->{'no_edit_product_link'} = 1;
457
458     if (Bugzilla->params->{'useclassification'}) {
459         $vars->{'classifications'} = $user->in_group('editcomponents') ?
460           [Bugzilla::Classification::get_all_classifications] : $user->get_selectable_classifications;
461
462         $template->process("admin/products/list-classifications.html.tmpl", $vars)
463           || ThrowTemplateError($template->error());
464     }
465     else {
466         my $products = $user->get_selectable_products;
467         # If the user has editcomponents privs for some products only,
468         # we have to restrict the list of products to display.
469         unless ($user->in_group('editcomponents')) {
470             $products = $user->get_products_by_permission('editcomponents');
471         }
472         $vars->{'products'} = $products;
473
474         $template->process("admin/products/list.html.tmpl", $vars)
475           || ThrowTemplateError($template->error());
476     }
477     exit;
478 }
479
480 #
481 # action='edit' -> present the 'edit product' form
482 # If a product is given with no action associated with it, then edit it.
483 #
484 # (next action would be 'update')
485 #
486
487 if ($action eq 'edit' || (!$action && $product_name)) {
488     my $product = $user->check_can_admin_product($product_name);
489
490     if (Bugzilla->params->{'useclassification'}) {
491         my $classification; 
492         if (!$classification_name) {
493             $classification = 
494                 new Bugzilla::Classification($product->classification_id);
495         } else {
496             $classification = 
497                 Bugzilla::Classification::check_classification($classification_name);
498             if ($classification->id != $product->classification_id) {
499                 ThrowUserError('classification_doesnt_exist_for_product',
500                                { product => $product->name,
501                                  classification => $classification->name });
502             }
503         }
504         $vars->{'classification'} = $classification;
505     }
506     $vars->{'product'} = $product;
507     $vars->{'token'} = issue_session_token('edit_product');
508
509     $template->process("admin/products/edit.html.tmpl", $vars)
510         || ThrowTemplateError($template->error());
511     exit;
512 }
513
514 #
515 # action='updategroupcontrols' -> update the product
516 #
517
518 if ($action eq 'updategroupcontrols') {
519     my $product = $user->check_can_admin_product($product_name);
520     check_token_data($token, 'edit_group_controls');
521
522     my @now_na = ();
523     my @now_mandatory = ();
524     foreach my $f ($cgi->param()) {
525         if ($f =~ /^membercontrol_(\d+)$/) {
526             my $id = $1;
527             if ($cgi->param($f) == CONTROLMAPNA) {
528                 push @now_na,$id;
529             } elsif ($cgi->param($f) == CONTROLMAPMANDATORY) {
530                 push @now_mandatory,$id;
531             }
532         }
533     }
534     if (!defined $cgi->param('confirmed')) {
535         my $na_groups;
536         if (@now_na) {
537             $na_groups = $dbh->selectall_arrayref(
538                     'SELECT groups.name, COUNT(bugs.bug_id) AS count
539                        FROM bugs
540                  INNER JOIN bug_group_map
541                          ON bug_group_map.bug_id = bugs.bug_id
542                  INNER JOIN groups
543                          ON bug_group_map.group_id = groups.id
544                       WHERE groups.id IN (' . join(', ', @now_na) . ')
545                         AND bugs.product_id = ? ' .
546                        $dbh->sql_group_by('groups.name'),
547                    {'Slice' => {}}, $product->id);
548         }
549
550 #
551 # return the mandatory groups which need to have bug entries added to the bug_group_map
552 # and the corresponding bug count
553 #
554         my $mandatory_groups;
555         if (@now_mandatory) {
556             $mandatory_groups = $dbh->selectall_arrayref(
557                     'SELECT groups.name,
558                            (SELECT COUNT(bugs.bug_id)
559                               FROM bugs
560                              WHERE bugs.product_id = ?
561                                AND bugs.bug_id NOT IN
562                                 (SELECT bug_group_map.bug_id FROM bug_group_map
563                                   WHERE bug_group_map.group_id = groups.id))
564                            AS count
565                       FROM groups
566                      WHERE groups.id IN (' . join(', ', @now_mandatory) . ')
567                      ORDER BY groups.name',
568                    {'Slice' => {}}, $product->id);
569             # remove zero counts
570             @$mandatory_groups = grep { $_->{count} } @$mandatory_groups;
571
572         }
573         if (($na_groups && scalar(@$na_groups))
574             || ($mandatory_groups && scalar(@$mandatory_groups)))
575         {
576             $vars->{'product'} = $product;
577             $vars->{'na_groups'} = $na_groups;
578             $vars->{'mandatory_groups'} = $mandatory_groups;
579             $template->process("admin/products/groupcontrol/confirm-edit.html.tmpl", $vars)
580                 || ThrowTemplateError($template->error());
581             exit;                
582         }
583     }
584
585     my $groups = $dbh->selectall_arrayref('SELECT id, name FROM groups
586                                            WHERE isbuggroup != 0
587                                            AND isactive != 0');
588     foreach my $group (@$groups) {
589         my ($groupid, $groupname) = @$group;
590         my $newmembercontrol = $cgi->param("membercontrol_$groupid") || 0;
591         my $newothercontrol = $cgi->param("othercontrol_$groupid") || 0;
592         #  Legality of control combination is a function of
593         #  membercontrol\othercontrol
594         #                 NA SH DE MA
595         #              NA  +  -  -  -
596         #              SH  +  +  +  +
597         #              DE  +  -  +  +
598         #              MA  -  -  -  +
599         unless (($newmembercontrol == $newothercontrol)
600               || ($newmembercontrol == CONTROLMAPSHOWN)
601               || (($newmembercontrol == CONTROLMAPDEFAULT)
602                && ($newothercontrol != CONTROLMAPSHOWN))) {
603             ThrowUserError('illegal_group_control_combination',
604                             {groupname => $groupname});
605         }
606     }
607     $dbh->bz_start_transaction();
608
609     my $sth_Insert = $dbh->prepare('INSERT INTO group_control_map
610                                     (group_id, product_id, entry, membercontrol,
611                                      othercontrol, canedit, editcomponents,
612                                      canconfirm, editbugs)
613                                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
614
615     my $sth_Update = $dbh->prepare('UPDATE group_control_map
616                                        SET entry = ?, membercontrol = ?,
617                                            othercontrol = ?, canedit = ?,
618                                            editcomponents = ?, canconfirm = ?,
619                                            editbugs = ?
620                                      WHERE group_id = ? AND product_id = ?');
621
622     my $sth_Delete = $dbh->prepare('DELETE FROM group_control_map
623                                      WHERE group_id = ? AND product_id = ?');
624
625     $groups = $dbh->selectall_arrayref('SELECT id, name, entry, membercontrol,
626                                                othercontrol, canedit,
627                                                editcomponents, canconfirm, editbugs
628                                           FROM groups
629                                      LEFT JOIN group_control_map
630                                             ON group_control_map.group_id = id
631                                            AND product_id = ?
632                                          WHERE isbuggroup != 0
633                                            AND isactive != 0',
634                                          undef, $product->id);
635
636     foreach my $group (@$groups) {
637         my ($groupid, $groupname, $entry, $membercontrol, $othercontrol,
638             $canedit, $editcomponents, $canconfirm, $editbugs) = @$group;
639         my $newentry = $cgi->param("entry_$groupid") || 0;
640         my $newmembercontrol = $cgi->param("membercontrol_$groupid") || 0;
641         my $newothercontrol = $cgi->param("othercontrol_$groupid") || 0;
642         my $newcanedit = $cgi->param("canedit_$groupid") || 0;
643         my $new_editcomponents = $cgi->param("editcomponents_$groupid") || 0;
644         my $new_canconfirm = $cgi->param("canconfirm_$groupid") || 0;
645         my $new_editbugs = $cgi->param("editbugs_$groupid") || 0;
646
647         my $oldentry = $entry;
648         # Set undefined values to 0.
649         $entry ||= 0;
650         $membercontrol ||= 0;
651         $othercontrol ||= 0;
652         $canedit ||= 0;
653         $editcomponents ||= 0;
654         $canconfirm ||= 0;
655         $editbugs ||= 0;
656
657         # We use them in placeholders only. So it's safe to detaint them.
658         detaint_natural($newentry);
659         detaint_natural($newothercontrol);
660         detaint_natural($newmembercontrol);
661         detaint_natural($newcanedit);
662         detaint_natural($new_editcomponents);
663         detaint_natural($new_canconfirm);
664         detaint_natural($new_editbugs);
665
666         if (!defined($oldentry)
667             && ($newentry || $newmembercontrol || $newcanedit
668                 || $new_editcomponents || $new_canconfirm || $new_editbugs))
669         {
670             $sth_Insert->execute($groupid, $product->id, $newentry,
671                                  $newmembercontrol, $newothercontrol, $newcanedit,
672                                  $new_editcomponents, $new_canconfirm, $new_editbugs);
673         }
674         elsif (($newentry != $entry)
675                || ($newmembercontrol != $membercontrol)
676                || ($newothercontrol != $othercontrol)
677                || ($newcanedit != $canedit)
678                || ($new_editcomponents != $editcomponents)
679                || ($new_canconfirm != $canconfirm)
680                || ($new_editbugs != $editbugs))
681         {
682             $sth_Update->execute($newentry, $newmembercontrol, $newothercontrol,
683                                  $newcanedit, $new_editcomponents, $new_canconfirm,
684                                  $new_editbugs, $groupid, $product->id);
685         }
686
687         if (!$newentry && !$newmembercontrol && !$newothercontrol
688             && !$newcanedit && !$new_editcomponents && !$new_canconfirm
689             && !$new_editbugs)
690         {
691             $sth_Delete->execute($groupid, $product->id);
692         }
693     }
694
695     my $sth_Select = $dbh->prepare(
696                      'SELECT bugs.bug_id,
697                    CASE WHEN (lastdiffed >= delta_ts) THEN 1 ELSE 0 END
698                         FROM bugs
699                   INNER JOIN bug_group_map
700                           ON bug_group_map.bug_id = bugs.bug_id
701                        WHERE group_id = ?
702                          AND bugs.product_id = ?
703                     ORDER BY bugs.bug_id');
704
705     my $sth_Select2 = $dbh->prepare('SELECT name, NOW() FROM groups WHERE id = ?');
706
707     $sth_Update = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?');
708
709     my $sth_Update2 = $dbh->prepare('UPDATE bugs SET delta_ts = ?, lastdiffed = ?
710                                      WHERE bug_id = ?');
711
712     $sth_Delete = $dbh->prepare('DELETE FROM bug_group_map
713                                  WHERE bug_id = ? AND group_id = ?');
714
715     my @removed_na;
716     foreach my $groupid (@now_na) {
717         my $count = 0;
718         my $bugs = $dbh->selectall_arrayref($sth_Select, undef,
719                                             ($groupid, $product->id));
720
721         my ($removed, $timestamp) =
722             $dbh->selectrow_array($sth_Select2, undef, $groupid);
723
724         foreach my $bug (@$bugs) {
725             my ($bugid, $mailiscurrent) = @$bug;
726             $sth_Delete->execute($bugid, $groupid);
727
728             LogActivityEntry($bugid, "bug_group", $removed, "",
729                              $whoid, $timestamp);
730
731             if ($mailiscurrent) {
732                 $sth_Update2->execute($timestamp, $timestamp, $bugid);
733             }
734             else {
735                 $sth_Update->execute($timestamp, $bugid);
736             }
737             $count++;
738         }
739         my %group = (name => $removed, bug_count => $count);
740
741         push(@removed_na, \%group);
742     }
743
744     $sth_Select = $dbh->prepare(
745                   'SELECT bugs.bug_id,
746                 CASE WHEN (lastdiffed >= delta_ts) THEN 1 ELSE 0 END
747                      FROM bugs
748                 LEFT JOIN bug_group_map
749                        ON bug_group_map.bug_id = bugs.bug_id
750                       AND group_id = ?
751                     WHERE bugs.product_id = ?
752                       AND bug_group_map.bug_id IS NULL
753                  ORDER BY bugs.bug_id');
754
755     $sth_Insert = $dbh->prepare('INSERT INTO bug_group_map
756                                  (bug_id, group_id) VALUES (?, ?)');
757
758     my @added_mandatory;
759     foreach my $groupid (@now_mandatory) {
760         my $count = 0;
761         my $bugs = $dbh->selectall_arrayref($sth_Select, undef,
762                                             ($groupid, $product->id));
763
764         my ($added, $timestamp) =
765             $dbh->selectrow_array($sth_Select2, undef, $groupid);
766
767         foreach my $bug (@$bugs) {
768             my ($bugid, $mailiscurrent) = @$bug;
769             $sth_Insert->execute($bugid, $groupid);
770
771             LogActivityEntry($bugid, "bug_group", "", $added,
772                              $whoid, $timestamp);
773
774             if ($mailiscurrent) {
775                 $sth_Update2->execute($timestamp, $timestamp, $bugid);
776             }
777             else {
778                 $sth_Update->execute($timestamp, $bugid);
779             }
780             $count++;
781         }
782         my %group = (name => $added, bug_count => $count);
783
784         push(@added_mandatory, \%group);
785     }
786     $dbh->bz_commit_transaction();
787
788     delete_token($token);
789
790     $vars->{'removed_na'} = \@removed_na;
791     $vars->{'added_mandatory'} = \@added_mandatory;
792     $vars->{'product'} = $product;
793
794     $template->process("admin/products/groupcontrol/updated.html.tmpl", $vars)
795         || ThrowTemplateError($template->error());
796     exit;
797 }
798
799 #
800 # action='update' -> update the product
801 #
802 if ($action eq 'update') {
803     check_token_data($token, 'edit_product');
804     my $product_old_name    = trim($cgi->param('product_old_name')    || '');
805     my $description         = trim($cgi->param('description')         || '');
806     my $disallownew         = trim($cgi->param('disallownew')         || '');
807     my $milestoneurl        = trim($cgi->param('milestoneurl')        || '');
808     my $votesperuser        = trim($cgi->param('votesperuser')        || 0);
809     my $maxvotesperbug      = trim($cgi->param('maxvotesperbug')      || 0);
810     my $votestoconfirm      = trim($cgi->param('votestoconfirm')      || 0);
811     my $defaultmilestone    = trim($cgi->param('defaultmilestone')    || '---');
812
813     my $checkvotes = 0;
814
815     my $product_old = $user->check_can_admin_product($product_old_name);
816
817     if (Bugzilla->params->{'useclassification'}) {
818         my $classification; 
819         if (!$classification_name) {
820             $classification = 
821                 new Bugzilla::Classification($product_old->classification_id);
822         } else {
823             $classification = 
824                 Bugzilla::Classification::check_classification($classification_name);
825             if ($classification->id != $product_old->classification_id) {
826                 ThrowUserError('classification_doesnt_exist_for_product',
827                                { product => $product_old->name,
828                                  classification => $classification->name });
829             }
830         }
831         $vars->{'classification'} = $classification;
832     }
833
834     unless ($product_name) {
835         ThrowUserError('product_cant_delete_name',
836                        {product => $product_old->name});
837     }
838
839     unless ($description) {
840         ThrowUserError('product_cant_delete_description',
841                        {product => $product_old->name});
842     }
843
844     my $stored_maxvotesperbug = $maxvotesperbug;
845     if (!detaint_natural($maxvotesperbug)) {
846         ThrowUserError('product_votes_per_bug_must_be_nonnegative',
847                        {maxvotesperbug => $stored_maxvotesperbug});
848     }
849
850     my $stored_votesperuser = $votesperuser;
851     if (!detaint_natural($votesperuser)) {
852         ThrowUserError('product_votes_per_user_must_be_nonnegative',
853                        {votesperuser => $stored_votesperuser});
854     }
855
856     my $stored_votestoconfirm = $votestoconfirm;
857     if (!detaint_natural($votestoconfirm)) {
858         ThrowUserError('product_votes_to_confirm_must_be_nonnegative',
859                        {votestoconfirm => $stored_votestoconfirm});
860     }
861
862     $dbh->bz_start_transaction();
863
864     my $testproduct = 
865         new Bugzilla::Product({name => $product_name});
866     if (lc($product_name) ne lc($product_old->name) &&
867         $testproduct) {
868         ThrowUserError('product_name_already_in_use',
869                        {product => $product_name});
870     }
871
872     # Only update milestone related stuff if 'usetargetmilestone' is on.
873     if (Bugzilla->params->{'usetargetmilestone'}) {
874         my $milestone = new Bugzilla::Milestone(
875             { product => $product_old, name => $defaultmilestone });
876
877         unless ($milestone) {
878             ThrowUserError('product_must_define_defaultmilestone',
879                            {product          => $product_old->name,
880                             defaultmilestone => $defaultmilestone,
881                             classification   => $classification_name});
882         }
883
884         if ($milestoneurl ne $product_old->milestone_url) {
885             trick_taint($milestoneurl);
886             $dbh->do('UPDATE products SET milestoneurl = ? WHERE id = ?',
887                      undef, ($milestoneurl, $product_old->id));
888         }
889
890         if ($milestone->name ne $product_old->default_milestone) {
891             $dbh->do('UPDATE products SET defaultmilestone = ? WHERE id = ?',
892                      undef, ($milestone->name, $product_old->id));
893         }
894     }
895
896     $disallownew = $disallownew ? 1 : 0;
897     if ($disallownew ne $product_old->disallow_new) {
898         $dbh->do('UPDATE products SET disallownew = ? WHERE id = ?',
899                  undef, ($disallownew, $product_old->id));
900     }
901
902     if ($description ne $product_old->description) {
903         trick_taint($description);
904         $dbh->do('UPDATE products SET description = ? WHERE id = ?',
905                  undef, ($description, $product_old->id));
906     }
907
908     if ($votesperuser ne $product_old->votes_per_user) {
909         $dbh->do('UPDATE products SET votesperuser = ? WHERE id = ?',
910                  undef, ($votesperuser, $product_old->id));
911         $checkvotes = 1;
912     }
913
914     if ($maxvotesperbug ne $product_old->max_votes_per_bug) {
915         $dbh->do('UPDATE products SET maxvotesperbug = ? WHERE id = ?',
916                  undef, ($maxvotesperbug, $product_old->id));
917         $checkvotes = 1;
918     }
919
920     if ($votestoconfirm ne $product_old->votes_to_confirm) {
921         $dbh->do('UPDATE products SET votestoconfirm = ? WHERE id = ?',
922                  undef, ($votestoconfirm, $product_old->id));
923         $checkvotes = 1;
924     }
925
926     if ($product_name ne $product_old->name) {
927         trick_taint($product_name);
928         $dbh->do('UPDATE products SET name = ? WHERE id = ?',
929                  undef, ($product_name, $product_old->id));
930     }
931
932     $dbh->bz_commit_transaction();
933
934     my $product = new Bugzilla::Product({name => $product_name});
935
936     if ($checkvotes) {
937         $vars->{'checkvotes'} = 1;
938
939         # 1. too many votes for a single user on a single bug.
940         my @toomanyvotes_list = ();
941         if ($maxvotesperbug < $votesperuser) {
942             my $votes = $dbh->selectall_arrayref(
943                         'SELECT votes.who, votes.bug_id
944                            FROM votes
945                      INNER JOIN bugs
946                              ON bugs.bug_id = votes.bug_id
947                           WHERE bugs.product_id = ?
948                             AND votes.vote_count > ?',
949                          undef, ($product->id, $maxvotesperbug));
950
951             foreach my $vote (@$votes) {
952                 my ($who, $id) = (@$vote);
953                 # If some votes are removed, RemoveVotes() returns a list
954                 # of messages to send to voters.
955                 my $msgs = RemoveVotes($id, $who, 'votes_too_many_per_bug');
956                 foreach my $msg (@$msgs) {
957                     MessageToMTA($msg);
958                 }
959                 my $name = user_id_to_login($who);
960
961                 push(@toomanyvotes_list,
962                      {id => $id, name => $name});
963             }
964         }
965         $vars->{'toomanyvotes'} = \@toomanyvotes_list;
966
967         # 2. too many total votes for a single user.
968         # This part doesn't work in the general case because RemoveVotes
969         # doesn't enforce votesperuser (except per-bug when it's less
970         # than maxvotesperbug).  See Bugzilla::Bug::RemoveVotes().
971
972         my $votes = $dbh->selectall_arrayref(
973                     'SELECT votes.who, votes.vote_count
974                        FROM votes
975                  INNER JOIN bugs
976                          ON bugs.bug_id = votes.bug_id
977                       WHERE bugs.product_id = ?',
978                      undef, $product->id);
979
980         my %counts;
981         foreach my $vote (@$votes) {
982             my ($who, $count) = @$vote;
983             if (!defined $counts{$who}) {
984                 $counts{$who} = $count;
985             } else {
986                 $counts{$who} += $count;
987             }
988         }
989         my @toomanytotalvotes_list = ();
990         foreach my $who (keys(%counts)) {
991             if ($counts{$who} > $votesperuser) {
992                 my $bug_ids = $dbh->selectcol_arrayref(
993                               'SELECT votes.bug_id
994                                  FROM votes
995                            INNER JOIN bugs
996                                    ON bugs.bug_id = votes.bug_id
997                                 WHERE bugs.product_id = ?
998                                   AND votes.who = ?',
999                                undef, ($product->id, $who));
1000
1001                 foreach my $bug_id (@$bug_ids) {
1002                     # RemoveVotes() returns a list of messages to send
1003                     # in case some voters had too many votes.
1004                     my $msgs = RemoveVotes($bug_id, $who, 'votes_too_many_per_user');
1005                     foreach my $msg (@$msgs) {
1006                         MessageToMTA($msg);
1007                     }
1008                     my $name = user_id_to_login($who);
1009
1010                     push(@toomanytotalvotes_list,
1011                          {id => $bug_id, name => $name});
1012                 }
1013             }
1014         }
1015         $vars->{'toomanytotalvotes'} = \@toomanytotalvotes_list;
1016
1017         # 3. enough votes to confirm
1018         my $bug_list = $dbh->selectcol_arrayref(
1019                        "SELECT bug_id FROM bugs
1020                          WHERE product_id = ?
1021                            AND bug_status = 'UNCONFIRMED'
1022                            AND votes >= ?",
1023                         undef, ($product->id, $votestoconfirm));
1024
1025         my @updated_bugs = ();
1026         foreach my $bug_id (@$bug_list) {
1027             my $confirmed = CheckIfVotedConfirmed($bug_id, $whoid);
1028             push (@updated_bugs, $bug_id) if $confirmed;
1029         }
1030
1031         $vars->{'confirmedbugs'} = \@updated_bugs;
1032         $vars->{'changer'} = $user->login;
1033     }
1034     delete_token($token);
1035
1036     $vars->{'old_product'} = $product_old;
1037     $vars->{'product'} = $product;
1038
1039     $template->process("admin/products/updated.html.tmpl", $vars)
1040         || ThrowTemplateError($template->error());
1041     exit;
1042 }
1043
1044 #
1045 # action='editgroupcontrols' -> update product group controls
1046 #
1047
1048 if ($action eq 'editgroupcontrols') {
1049     my $product = $user->check_can_admin_product($product_name);
1050
1051     # Display a group if it is either enabled or has bugs for this product.
1052     my $groups = $dbh->selectall_arrayref(
1053         'SELECT id, name, entry, membercontrol, othercontrol, canedit,
1054                 editcomponents, editbugs, canconfirm,
1055                 isactive, COUNT(bugs.bug_id) AS bugcount
1056            FROM groups
1057       LEFT JOIN group_control_map
1058              ON group_control_map.group_id = groups.id
1059             AND group_control_map.product_id = ?
1060       LEFT JOIN bug_group_map
1061              ON bug_group_map.group_id = groups.id
1062       LEFT JOIN bugs
1063              ON bugs.bug_id = bug_group_map.bug_id
1064             AND bugs.product_id = ?
1065           WHERE isbuggroup != 0
1066             AND (isactive != 0 OR entry IS NOT NULL OR bugs.bug_id IS NOT NULL) ' .
1067            $dbh->sql_group_by('name', 'id, entry, membercontrol,
1068                               othercontrol, canedit, isactive,
1069                               editcomponents, canconfirm, editbugs'),
1070         {'Slice' => {}}, ($product->id, $product->id));
1071
1072     $vars->{'product'} = $product;
1073     $vars->{'groups'} = $groups;
1074     $vars->{'token'} = issue_session_token('edit_group_controls');
1075
1076     $vars->{'const'} = {
1077         'CONTROLMAPNA' => CONTROLMAPNA,
1078         'CONTROLMAPSHOWN' => CONTROLMAPSHOWN,
1079         'CONTROLMAPDEFAULT' => CONTROLMAPDEFAULT,
1080         'CONTROLMAPMANDATORY' => CONTROLMAPMANDATORY,
1081     };
1082
1083     $template->process("admin/products/groupcontrol/edit.html.tmpl", $vars)
1084         || ThrowTemplateError($template->error());
1085     exit;                
1086 }
1087
1088
1089 #
1090 # No valid action found
1091 #
1092
1093 ThrowUserError('no_valid_action', {field => "product"});