2 # -*- Mode: perl; indent-tabs-mode: nil -*-
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/
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.
14 # The Original Code is mozilla.org code.
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
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>
29 # Direct any questions on this source code to
31 # Holger Schurig <holgerschurig@nikocity.de>
35 use vars qw
($template $vars);
36 use Bugzilla
::Constants
;
42 use Bugzilla
::Config
qw(:DEFAULT $datadir);
44 # Shut up misguided -w warnings about "used only once". "use vars" just
45 # doesn't work for me.
46 use vars qw(@legal_bug_status @legal_resolution);
49 &::CONTROLMAPNA => 'NA',
50 &::CONTROLMAPSHOWN => 'Shown',
51 &::CONTROLMAPDEFAULT => 'Default',
52 &::CONTROLMAPMANDATORY => 'Mandatory'
55 # TestProduct: just returns if the specified product does exists
56 # CheckProduct: same check, optionally emit an error text
62 # does the product exist?
65 WHERE name=" . SqlQuote($prod));
66 return FetchOneColumn();
73 # do we have a product?
75 print "Sorry, you haven't specified a product.";
80 unless (TestProduct $prod) {
81 print "Sorry, product '$prod' does not exist.";
87 # TestClassification: just returns if the specified classification does exists
88 # CheckClassification: same check, optionally emit an error text
90 sub TestClassification ($)
94 # does the classification exist?
97 WHERE name=" . SqlQuote($cl));
98 return FetchOneColumn();
101 sub CheckClassification ($)
105 # do we have a classification?
107 print "Sorry, you haven't specified a classification.";
112 unless (TestClassification $cl) {
113 print "Sorry, classification '$cl' does not exist.";
119 # For the transition period, as this file is templatised bit by bit,
120 # we need this routine, which does things properly, and will
121 # eventually be the only version. (The older versions assume a
122 # PutHeader() call has been made)
123 sub CheckClassificationNew ($)
127 # do we have a classification?
129 ThrowUserError('classification_not_specified');
132 unless (TestClassification $cl) {
133 ThrowUserError('classification_doesnt_exist',
139 sub CheckClassificationProduct ($$)
143 my $dbh = Bugzilla->dbh;
145 CheckClassification($cl);
151 my $query = q{SELECT products.name
153 INNER JOIN classifications
154 ON products.classification_id = classifications.id
155 WHERE products.name = ?
156 AND classifications.name = ?};
157 my $res = $dbh->selectrow_array($query, undef, ($prod, $cl));
160 print "Sorry, classification->product '$cl'->'$prod' does not exist.";
166 sub CheckClassificationProductNew
($$)
168 my ($cl, $prod) = @_;
169 my $dbh = Bugzilla
->dbh;
171 CheckClassificationNew
($cl);
176 my ($res) = $dbh->selectrow_array(q{
179 INNER JOIN classifications
180 ON products.classification_id = classifications.id
181 WHERE products.name = ? AND classifications.name = ?},
182 undef, ($prod, $cl));
185 ThrowUserError
('classification_doesnt_exist_for_product',
186 { product
=> $prod, classification
=> $cl });
191 # Displays the form to edit a products parameters
194 sub EmitFormElements
($$$$$$$$$)
196 my ($classification, $product, $description, $milestoneurl, $disallownew,
197 $votesperuser, $maxvotesperbug, $votestoconfirm, $defaultmilestone)
200 $product = value_quote
($product);
201 $description = value_quote
($description);
203 if (Param
('useclassification')) {
204 print " <TH ALIGN=\"right\">Classification:</TH>\n";
205 print " <TD><b>",html_quote
($classification),"</b></TD>\n";
209 print " <TH ALIGN=\"right\">Product:</TH>\n";
210 print " <TD><INPUT SIZE=64 MAXLENGTH=64 NAME=\"product\" VALUE=\"$product\"></TD>\n";
213 print " <TH ALIGN=\"right\">Description:</TH>\n";
214 print " <TD><TEXTAREA ROWS=4 COLS=64 WRAP=VIRTUAL NAME=\"description\">$description</TEXTAREA></TD>\n";
216 $defaultmilestone = value_quote
($defaultmilestone);
217 if (Param
('usetargetmilestone')) {
218 $milestoneurl = value_quote
($milestoneurl);
220 print " <TH ALIGN=\"right\">URL describing milestones for this product:</TH>\n";
221 print " <TD><INPUT TYPE=TEXT SIZE=64 MAXLENGTH=255 NAME=\"milestoneurl\" VALUE=\"$milestoneurl\"></TD>\n";
224 print " <TH ALIGN=\"right\">Default milestone:</TH>\n";
226 print " <TD><INPUT TYPE=TEXT SIZE=20 MAXLENGTH=20 NAME=\"defaultmilestone\" VALUE=\"$defaultmilestone\"></TD>\n";
228 print qq{<INPUT TYPE
=HIDDEN NAME
="defaultmilestone" VALUE
="$defaultmilestone">\n};
233 print " <TH ALIGN=\"right\">Closed for bug entry:</TH>\n";
234 my $closed = $disallownew ?
"CHECKED" : "";
235 print " <TD><INPUT TYPE=CHECKBOX NAME=\"disallownew\" $closed VALUE=\"1\"></TD>\n";
238 print " <TH ALIGN=\"right\">Maximum votes per person:</TH>\n";
239 print " <TD><INPUT SIZE=5 MAXLENGTH=5 NAME=\"votesperuser\" VALUE=\"$votesperuser\"></TD>\n";
242 print " <TH ALIGN=\"right\">Maximum votes a person can put on a single bug:</TH>\n";
243 print " <TD><INPUT SIZE=5 MAXLENGTH=5 NAME=\"maxvotesperbug\" VALUE=\"$maxvotesperbug\"></TD>\n";
246 print " <TH ALIGN=\"right\">Number of votes a bug in this product needs to automatically get out of the <A HREF=\"page.cgi?id=fields.html#status\">UNCONFIRMED</A> state:</TH>\n";
247 print " <TD><INPUT SIZE=5 MAXLENGTH=5 NAME=\"votestoconfirm\" VALUE=\"$votestoconfirm\"></TD>\n";
252 # Displays a text like "a.", "a or b.", "a, b or c.", "a, b, c or d."
257 my (@links) = ("Back to the <A HREF=\"query.cgi\">query page</A>", @_);
264 if ($num == $count) {
267 elsif ($num == $count-1) {
285 # Preliminary checks:
288 my $user = Bugzilla
->login(LOGIN_REQUIRED
);
289 my $whoid = $user->id;
291 my $cgi = Bugzilla
->cgi;
292 print $cgi->header();
294 UserInGroup
("editcomponents")
295 || ThrowUserError
("auth_failure", {group
=> "editcomponents",
297 object
=> "products"});
300 # often used variables
302 my $classification = trim
($cgi->param('classification') || '');
303 my $product = trim
($cgi->param('product') || '');
304 my $action = trim
($cgi->param('action') || '');
306 my $localtrailer = "<A HREF=\"editproducts.cgi\">edit</A> more products";
307 my $classhtmlvarstart = "";
308 my $classhtmlvar = "";
309 my $dbh = Bugzilla
->dbh;
312 # product = '' -> Show nice list of classifications (if
313 # classifications enabled)
316 if (Param
('useclassification')) {
317 if ($classification) {
318 $classhtmlvar = "&classification=" . url_quote
($classification);
319 $classhtmlvarstart = "?classification=" . url_quote
($classification);
320 $localtrailer .= ", <A HREF=\"editproducts.cgi" . $classhtmlvarstart . "\">edit</A> in this classification";
324 "SELECT classifications.name, classifications.description,
325 COUNT(classification_id) AS product_count
328 ON classifications.id = products.classification_id " .
329 $dbh->sql_group_by('classifications.id',
330 'classifications.name,
331 classifications.description') . "
334 $vars->{'classifications'} = $dbh->selectall_arrayref($query,
337 $template->process("admin/products/list-classifications.html.tmpl",
339 || ThrowTemplateError
($template->error());
347 # action = '' -> Show a nice list of products, unless a product
348 # is already specified (then edit it)
351 if (!$action && !$product) {
353 if (Param
('useclassification')) {
354 CheckClassificationNew
($classification);
357 my @execute_params = ();
360 my $query = "SELECT products.name,
361 COALESCE(products.description,'') AS description,
362 disallownew = 0 AS status,
363 votesperuser, maxvotesperbug, votestoconfirm,
364 COUNT(bug_id) AS bug_count
367 if (Param
('useclassification')) {
368 $query .= " INNER JOIN classifications " .
369 "ON classifications.id = products.classification_id";
372 $query .= " LEFT JOIN bugs ON products.id = bugs.product_id";
374 if (Param
('useclassification')) {
375 $query .= " WHERE classifications.name = ? ";
377 # trick_taint is OK because we use this in a placeholder in a SELECT
378 trick_taint
($classification);
380 push(@execute_params,
384 $query .= " " . $dbh->sql_group_by('products.name',
385 'products.description, disallownew,
386 votesperuser, maxvotesperbug,
388 $query .= " ORDER BY products.name";
390 $vars->{'products'} = $dbh->selectall_arrayref($query,
394 $vars->{'classification'} = $classification;
395 $template->process("admin/products/list.html.tmpl",
397 || ThrowTemplateError
($template->error());
406 # action='add' -> present form for parameters for new product
408 # (next action will be 'new')
411 if ($action eq 'add') {
412 PutHeader
("Add Product");
414 if (Param
('useclassification')) {
415 CheckClassification
($classification);
417 #print "This page lets you add a new product to bugzilla.\n";
419 print "<FORM METHOD=POST ACTION=editproducts.cgi>\n";
420 print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n";
422 EmitFormElements
($classification,'', '', '', 0, 0, 10000, 0, "---");
425 print " <TH ALIGN=\"right\">Version:</TH>\n";
426 print " <TD><INPUT SIZE=64 MAXLENGTH=255 NAME=\"version\" VALUE=\"unspecified\"></TD>\n";
428 print " <TH ALIGN=\"right\">Create chart datasets for this product:</TH>\n";
429 print " <TD><INPUT TYPE=CHECKBOX NAME=\"createseries\" VALUE=1></TD>";
432 print "</TABLE>\n<HR>\n";
433 print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n";
434 print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"new\">\n";
435 print "<INPUT TYPE=HIDDEN NAME='subcategory' VALUE='-All-'>\n";
436 print "<INPUT TYPE=HIDDEN NAME='open_name' VALUE='All Open'>\n";
437 print "<INPUT TYPE=HIDDEN NAME='classification' VALUE='",html_quote
($classification),"'>\n";
440 my $other = $localtrailer;
441 $other =~ s/more/other/;
449 # action='new' -> add product entered in the 'action=add' screen
452 if ($action eq 'new') {
453 PutHeader
("Adding new product");
455 # Cleanups and validity checks
457 my $classification_id = 1;
458 if (Param
('useclassification')) {
459 CheckClassification
($classification);
460 $classification_id = get_classification_id
($classification);
464 print "You must enter a name for the new product. Please press\n";
465 print "<b>Back</b> and try again.\n";
466 PutTrailer
($localtrailer);
470 my $existing_product = TestProduct
($product);
472 if ($existing_product) {
474 # Check for exact case sensitive match:
475 if ($existing_product eq $product) {
476 print "The product '$product' already exists. Please press\n";
477 print "<b>Back</b> and try again.\n";
478 PutTrailer
($localtrailer);
482 # Next check for a case-insensitive match:
483 if (lc($existing_product) eq lc($product)) {
484 print "The new product '$product' differs from existing product ";
485 print "'$existing_product' only in case. Please press\n";
486 print "<b>Back</b> and try again.\n";
487 PutTrailer
($localtrailer);
492 my $version = trim
($cgi->param('version') || '');
494 if ($version eq '') {
495 print "You must enter a version for product '$product'. Please press\n";
496 print "<b>Back</b> and try again.\n";
497 PutTrailer
($localtrailer);
501 my $description = trim
($cgi->param('description') || '');
503 if ($description eq '') {
504 print "You must enter a description for product '$product'. Please press\n";
505 print "<b>Back</b> and try again.\n";
506 PutTrailer
($localtrailer);
510 my $milestoneurl = trim
($cgi->param('milestoneurl') || '');
512 $disallownew = 1 if $cgi->param('disallownew');
513 my $votesperuser = $cgi->param('votesperuser');
515 my $maxvotesperbug = $cgi->param('maxvotesperbug');
516 $maxvotesperbug = 10000 if !defined $maxvotesperbug;
517 my $votestoconfirm = $cgi->param('votestoconfirm');
518 $votestoconfirm ||= 0;
519 my $defaultmilestone = $cgi->param('defaultmilestone') || "---";
521 # Add the new product.
522 SendSQL
("INSERT INTO products ( " .
523 "name, description, milestoneurl, disallownew, votesperuser, " .
524 "maxvotesperbug, votestoconfirm, defaultmilestone, classification_id" .
526 SqlQuote
($product) . "," .
527 SqlQuote
($description) . "," .
528 SqlQuote
($milestoneurl) . "," .
529 # had tainting issues under cygwin, IIS 5.0, perl -T %s %s
530 # see bug 208647. http://bugzilla.mozilla.org/show_bug.cgi?id=208647
531 # had to de-taint $disallownew, $votesperuser, $maxvotesperbug,
532 # and $votestoconfirm w/ SqlQuote()
533 # - jpyeron@pyerotechnics.com
534 SqlQuote
($disallownew) . "," .
535 SqlQuote
($votesperuser) . "," .
536 SqlQuote
($maxvotesperbug) . "," .
537 SqlQuote
($votestoconfirm) . "," .
538 SqlQuote
($defaultmilestone) . "," .
539 SqlQuote
($classification_id) . ")");
540 my $product_id = $dbh->bz_last_key('products', 'id');
542 SendSQL
("INSERT INTO versions ( " .
543 "value, product_id" .
545 SqlQuote
($version) . "," .
548 SendSQL
("INSERT INTO milestones (product_id, value) VALUES (" .
549 $product_id . ", " . SqlQuote
($defaultmilestone) . ")");
551 # If we're using bug groups, then we need to create a group for this
552 # product as well. -JMR, 2/16/00
553 if (Param
("makeproductgroups")) {
554 # Next we insert into the groups table
555 my $productgroup = $product;
556 while (GroupExists
($productgroup)) {
557 $productgroup .= '_';
559 SendSQL
("INSERT INTO groups " .
560 "(name, description, isbuggroup, last_changed) " .
562 SqlQuote
($productgroup) . ", " .
563 SqlQuote
("Access to bugs in the $product product") . ", 1, NOW())");
564 my $gid = $dbh->bz_last_key('groups', 'id');
565 my $admin = GroupNameToId
('admin');
566 # If we created a new group, give the "admin" group priviledges
568 SendSQL
("INSERT INTO group_group_map (member_id, grantor_id, grant_type)
569 VALUES ($admin, $gid," . GROUP_MEMBERSHIP
.")");
570 SendSQL
("INSERT INTO group_group_map (member_id, grantor_id, grant_type)
571 VALUES ($admin, $gid," . GROUP_BLESS
.")");
572 SendSQL
("INSERT INTO group_group_map (member_id, grantor_id, grant_type)
573 VALUES ($admin, $gid," . GROUP_VISIBLE
.")");
575 # Associate the new group and new product.
576 SendSQL
("INSERT INTO group_control_map " .
577 "(group_id, product_id, entry, " .
578 "membercontrol, othercontrol, canedit) VALUES " .
579 "($gid, $product_id, " . Param
("useentrygroupdefault") .
580 ", " . CONTROLMAPDEFAULT
. ", " .
581 CONTROLMAPNA
. ", 0)");
584 if ($cgi->param('createseries')) {
585 # Insert default charting queries for this product.
586 # If they aren't using charting, this won't do any harm.
589 # $open_name and $product are sqlquoted by the series code
590 # and never used again here, so we can trick_taint them.
591 my $open_name = $cgi->param('open_name');
592 trick_taint
($open_name);
593 trick_taint
($product);
597 # We do every status, every resolution, and an "opened" one as well.
598 foreach my $bug_status (@
::legal_bug_status
) {
599 push(@series, [$bug_status,
600 "bug_status=" . url_quote
($bug_status)]);
603 foreach my $resolution (@
::legal_resolution
) {
604 next if !$resolution;
605 push(@series, [$resolution, "resolution=" .url_quote
($resolution)]);
608 # For localisation reasons, we get the name of the "global" subcategory
609 # and the title of the "open" query from the submitted form.
610 my @openedstatuses = OpenStates
();
612 join("&", map { "bug_status=" . url_quote
($_) } @openedstatuses);
613 push(@series, [$open_name, $query]);
615 foreach my $sdata (@series) {
616 my $series = new Bugzilla
::Series
(undef, $product,
617 scalar $cgi->param('subcategory'),
618 $sdata->[0], $::userid
, 1,
619 $sdata->[1] . "&product=" . url_quote
($product), 1);
620 $series->writeToDatabase();
623 # Make versioncache flush
624 unlink "$datadir/versioncache";
626 print "OK, done.<p>\n";
627 print "<div style='border: 1px red solid; padding: 1ex;'><b>You will need to
628 <a href=\"editcomponents.cgi?action=add&product=" .
629 url_quote
($product) . "\">add at least one
630 component</a> before you can enter bugs against this product.</b></div>";
631 PutTrailer
($localtrailer,
632 "<a href=\"editproducts.cgi?action=add\">add</a> a new product",
633 "<a href=\"editcomponents.cgi?action=add&product=" .
634 url_quote
($product) . $classhtmlvar .
635 "\">add</a> components to this new product");
642 # action='del' -> ask if user really wants to delete
644 # (next action would be 'delete')
647 if ($action eq 'del') {
650 ThrowUserError
('product_not_specified');
653 my $product_id = get_product_id
($product);
654 $product_id || ThrowUserError
('product_doesnt_exist',
655 {product
=> $product});
657 my $classification_id = 1;
659 if (Param
('useclassification')) {
660 CheckClassificationProductNew
($classification, $product);
661 $classification_id = get_classification_id
($classification);
662 $vars->{'classification'} = $classification;
665 # Extract some data about the product
666 my $query = q{SELECT classifications.description,
667 products.description,
668 products.milestoneurl,
671 INNER JOIN classifications
672 ON products.classification_id = classifications.id
673 WHERE products.id = ?};
675 my ($class_description,
678 $disallownew) = $dbh->selectrow_array($query, undef,
681 $vars->{'class_description'} = $class_description;
682 $vars->{'product_id'} = $product_id;
683 $vars->{'prod_description'} = $prod_description;
684 $vars->{'milestoneurl'} = $milestoneurl;
685 $vars->{'disallownew'} = $disallownew;
686 $vars->{'product_name'} = $product;
688 $vars->{'components'} = $dbh->selectall_arrayref(q{
689 SELECT name, description FROM components
690 WHERE product_id = ? ORDER BY name}, {'Slice' => {}},
693 $vars->{'versions'} = $dbh->selectcol_arrayref(q{
694 SELECT value FROM versions
695 WHERE product_id = ? ORDER BY value}, undef,
698 # Adding listing for associated target milestones -
699 # matthew@zeroknowledge.com
700 if (Param
('usetargetmilestone')) {
701 $vars->{'milestones'} = $dbh->selectcol_arrayref(q{
702 SELECT value FROM milestones
704 ORDER BY sortkey, value}, undef, $product_id);
707 ($vars->{'bug_count'}) = $dbh->selectrow_array(q{
708 SELECT COUNT(*) FROM bugs WHERE product_id = ?},
709 undef, $product_id) || 0;
711 $template->process("admin/products/confirm-delete.html.tmpl", $vars)
712 || ThrowTemplateError
($template->error());
717 # action='delete' -> really delete the product
720 if ($action eq 'delete') {
723 ThrowUserError
('product_not_specified');
726 my $product_id = get_product_id
($product);
727 $product_id || ThrowUserError
('product_doesnt_exist',
728 {product
=> $product});
730 $vars->{'product'} = $product;
731 $vars->{'classification'} = $classification;
733 my $bug_ids = $dbh->selectcol_arrayref(q{
734 SELECT bug_id FROM bugs
735 WHERE product_id = ?}, undef, $product_id);
737 my $nb_bugs = scalar(@
$bug_ids);
739 if (Param
("allowbugdeletion")) {
740 foreach my $bug_id (@
$bug_ids) {
741 my $bug = new Bugzilla
::Bug
($bug_id, $whoid);
742 $bug->remove_from_db();
746 ThrowUserError
("product_has_bugs", { nb
=> $nb_bugs });
748 $vars->{'nb_bugs'} = $nb_bugs;
751 $dbh->bz_lock_tables('products WRITE', 'components WRITE',
752 'versions WRITE', 'milestones WRITE',
753 'group_control_map WRITE',
754 'flaginclusions WRITE', 'flagexclusions WRITE');
756 $dbh->do("DELETE FROM components WHERE product_id = ?",
759 $dbh->do("DELETE FROM versions WHERE product_id = ?",
762 $dbh->do("DELETE FROM milestones WHERE product_id = ?",
765 $dbh->do("DELETE FROM group_control_map WHERE product_id = ?",
768 $dbh->do("DELETE FROM flaginclusions WHERE product_id = ?",
771 $dbh->do("DELETE FROM flagexclusions WHERE product_id = ?",
774 $dbh->do("DELETE FROM products WHERE id = ?",
777 $dbh->bz_unlock_tables();
779 unlink "$datadir/versioncache";
781 $template->process("admin/products/deleted.html.tmpl", $vars)
782 || ThrowTemplateError
($template->error());
787 # action='edit' -> present the 'edit product' form
788 # If a product is given with no action associated with it, then edit it.
790 # (next action would be 'update')
793 if ($action eq 'edit' || (!$action && $product)) {
794 PutHeader
("Edit Product");
795 CheckProduct
($product);
796 my $classification_id=1;
797 if (Param
('useclassification')) {
798 # If a product has been given with no classification associated
799 # with it, take this information from the DB
800 if ($classification) {
801 CheckClassificationProduct
($classification, $product);
803 trick_taint
($product);
805 $dbh->selectrow_array("SELECT classifications.name
806 FROM products, classifications
807 WHERE products.name = ?
808 AND classifications.id = products.classification_id",
811 $classification_id = get_classification_id
($classification);
814 # get data of product
815 SendSQL
("SELECT classifications.description,
816 products.id,products.description,milestoneurl,disallownew,
817 votesperuser,maxvotesperbug,votestoconfirm,defaultmilestone
818 FROM products,classifications
819 WHERE products.name=" . SqlQuote
($product) .
820 " AND classifications.id=" . SqlQuote
($classification_id));
821 my ($class_description, $product_id,$prod_description, $milestoneurl, $disallownew,
822 $votesperuser, $maxvotesperbug, $votestoconfirm, $defaultmilestone) =
825 print "<FORM METHOD=POST ACTION=editproducts.cgi>\n";
826 print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n";
828 EmitFormElements
($classification, $product, $prod_description, $milestoneurl,
829 $disallownew, $votesperuser, $maxvotesperbug,
830 $votestoconfirm, $defaultmilestone);
832 print "</TR><TR VALIGN=top>\n";
833 print " <TH ALIGN=\"right\"><A HREF=\"editcomponents.cgi?product=", url_quote
($product), $classhtmlvar, "\">Edit components:</A></TH>\n";
835 SendSQL
("SELECT name,description
837 WHERE product_id=$product_id");
840 while ( MoreSQLData
() ) {
841 my ($component, $description) = FetchSQLData
();
842 $description ||= "<FONT COLOR=\"red\">description missing</FONT>";
843 print "<tr><th align=right valign=top>$component:</th>";
844 print "<td valign=top>$description</td></tr>\n";
848 print "<FONT COLOR=\"red\">missing</FONT>";
852 print "</TD>\n</TR><TR>\n";
853 print " <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editversions.cgi?product=", url_quote
($product), $classhtmlvar, "\">Edit versions:</A></TH>\n";
855 SendSQL
("SELECT value
857 WHERE product_id=$product_id
861 while ( MoreSQLData
() ) {
862 my ($version) = FetchSQLData
();
868 print "<FONT COLOR=\"red\">missing</FONT>";
872 # Adding listing for associated target milestones - matthew@zeroknowledge.com
874 if (Param
('usetargetmilestone')) {
875 print "</TD>\n</TR><TR>\n";
876 print " <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editmilestones.cgi?product=", url_quote
($product), $classhtmlvar, "\">Edit milestones:</A></TH>\n";
878 SendSQL
("SELECT value
880 WHERE product_id=$product_id
881 ORDER BY sortkey,value");
884 while ( MoreSQLData
() ) {
885 my ($milestone) = FetchSQLData
();
891 print "<FONT COLOR=\"red\">missing</FONT>";
895 print "</TD>\n</TR><TR>\n";
896 print " <TH ALIGN=\"right\" VALIGN=\"top\"><A HREF=\"editproducts.cgi?action=editgroupcontrols&product=", url_quote
($product), $classhtmlvar,"\">Edit Group Access Controls:</A></TH>\n";
898 SendSQL
("SELECT id, name, isactive, entry, membercontrol, othercontrol, canedit " .
900 "group_control_map " .
901 "WHERE group_control_map.group_id = id AND product_id = $product_id " .
902 "AND isbuggroup != 0 ORDER BY name");
903 while (MoreSQLData
()) {
904 my ($id, $name, $isactive, $entry, $membercontrol, $othercontrol, $canedit)
906 print "<B>" . html_quote
($name) . ":</B> ";
908 print $ctl{$membercontrol} . "/" . $ctl{$othercontrol};
909 print ", ENTRY" if $entry;
910 print ", CANEDIT" if $canedit;
916 print "</TD>\n</TR><TR>\n";
917 print " <TH ALIGN=\"right\">Bugs:</TH>\n";
919 SendSQL
("SELECT count(bug_id), product_id
921 $dbh->sql_group_by('product_id') . "
922 HAVING product_id = $product_id");
924 $bugs = FetchOneColumn
() if MoreSQLData
();
925 print $bugs || 'none';
927 print "</TD>\n</TR></TABLE>\n";
929 print "<INPUT TYPE=HIDDEN NAME=\"classification\" VALUE=\"" .
930 html_quote
($classification) . "\">\n";
931 print "<INPUT TYPE=HIDDEN NAME=\"productold\" VALUE=\"" .
932 html_quote
($product) . "\">\n";
933 print "<INPUT TYPE=HIDDEN NAME=\"descriptionold\" VALUE=\"" .
934 html_quote
($prod_description) . "\">\n";
935 print "<INPUT TYPE=HIDDEN NAME=\"milestoneurlold\" VALUE=\"" .
936 html_quote
($milestoneurl) . "\">\n";
937 print "<INPUT TYPE=HIDDEN NAME=\"disallownewold\" VALUE=\"$disallownew\">\n";
938 print "<INPUT TYPE=HIDDEN NAME=\"votesperuserold\" VALUE=\"$votesperuser\">\n";
939 print "<INPUT TYPE=HIDDEN NAME=\"maxvotesperbugold\" VALUE=\"$maxvotesperbug\">\n";
940 print "<INPUT TYPE=HIDDEN NAME=\"votestoconfirmold\" VALUE=\"$votestoconfirm\">\n";
941 $defaultmilestone = value_quote
($defaultmilestone);
942 print "<INPUT TYPE=HIDDEN NAME=\"defaultmilestoneold\" VALUE=\"$defaultmilestone\">\n";
943 print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"update\">\n";
944 print "<INPUT TYPE=SUBMIT VALUE=\"Update\">\n";
948 my $x = $localtrailer;
955 # action='updategroupcontrols' -> update the product
958 if ($action eq 'updategroupcontrols') {
959 my $product_id = get_product_id
($product);
961 my @now_mandatory = ();
962 foreach my $f ($cgi->param()) {
963 if ($f =~ /^membercontrol_(\d+)$/) {
965 if ($cgi->param($f) == CONTROLMAPNA
) {
967 } elsif ($cgi->param($f) == CONTROLMAPMANDATORY
) {
968 push @now_mandatory,$id;
972 if (!defined $cgi->param('confirmed')) {
975 SendSQL
("SELECT groups.name, COUNT(bugs.bug_id)
976 FROM bugs, bug_group_map, groups
977 WHERE groups.id IN(" . join(', ', @now_na) . ")
978 AND bug_group_map.group_id = groups.id
979 AND bug_group_map.bug_id = bugs.bug_id
980 AND bugs.product_id = $product_id " .
981 $dbh->sql_group_by('groups.name'));
982 while (MoreSQLData
()) {
983 my ($groupname, $bugcount) = FetchSQLData
();
985 $g{'name'} = $groupname;
986 $g{'count'} = $bugcount;
991 my @mandatory_groups = ();
992 if (@now_mandatory) {
993 SendSQL
("SELECT groups.name, COUNT(bugs.bug_id)
995 LEFT JOIN bug_group_map
996 ON bug_group_map.bug_id = bugs.bug_id
998 ON bug_group_map.group_id = groups.id
999 WHERE groups.id IN(" . join(', ', @now_mandatory) . ")
1000 AND bugs.product_id = $product_id
1001 AND bug_group_map.bug_id IS NULL " .
1002 $dbh->sql_group_by('groups.name'));
1003 while (MoreSQLData
()) {
1004 my ($groupname, $bugcount) = FetchSQLData
();
1006 $g{'name'} = $groupname;
1007 $g{'count'} = $bugcount;
1008 push @mandatory_groups,\
%g;
1011 if ((@na_groups) || (@mandatory_groups)) {
1012 $vars->{'product'} = $product;
1013 $vars->{'na_groups'} = \
@na_groups;
1014 $vars->{'mandatory_groups'} = \
@mandatory_groups;
1015 $template->process("admin/products/groupcontrol/confirm-edit.html.tmpl", $vars)
1016 || ThrowTemplateError
($template->error());
1020 PutHeader
("Update group access controls for $product");
1022 SendSQL
("SELECT id, name FROM groups " .
1023 "WHERE isbuggroup != 0 AND isactive != 0");
1024 while (MoreSQLData
()){
1025 my ($groupid, $groupname) = FetchSQLData
();
1026 my $newmembercontrol = $cgi->param("membercontrol_$groupid") || 0;
1027 my $newothercontrol = $cgi->param("othercontrol_$groupid") || 0;
1028 # Legality of control combination is a function of
1029 # membercontrol\othercontrol
1035 unless (($newmembercontrol == $newothercontrol)
1036 || ($newmembercontrol == CONTROLMAPSHOWN
)
1037 || (($newmembercontrol == CONTROLMAPDEFAULT
)
1038 && ($newothercontrol != CONTROLMAPSHOWN
))) {
1039 ThrowUserError
('illegal_group_control_combination',
1040 {groupname
=> $groupname,
1044 $dbh->bz_lock_tables('groups READ',
1045 'group_control_map WRITE',
1047 'bugs_activity WRITE',
1048 'bug_group_map WRITE',
1050 SendSQL
("SELECT id, name, entry, membercontrol, othercontrol, canedit " .
1052 "LEFT JOIN group_control_map " .
1053 "ON group_control_map.group_id = id AND product_id = $product_id " .
1054 "WHERE isbuggroup != 0 AND isactive != 0");
1055 while (MoreSQLData
()) {
1056 my ($groupid, $groupname, $entry, $membercontrol,
1057 $othercontrol, $canedit) = FetchSQLData
();
1058 my $newentry = $cgi->param("entry_$groupid") || 0;
1059 my $newmembercontrol = $cgi->param("membercontrol_$groupid") || 0;
1060 my $newothercontrol = $cgi->param("othercontrol_$groupid") || 0;
1061 my $newcanedit = $cgi->param("canedit_$groupid") || 0;
1062 my $oldentry = $entry;
1063 $entry = $entry || 0;
1064 $membercontrol = $membercontrol || 0;
1065 $othercontrol = $othercontrol || 0;
1066 $canedit = $canedit || 0;
1067 detaint_natural
($newentry);
1068 detaint_natural
($newothercontrol);
1069 detaint_natural
($newmembercontrol);
1070 detaint_natural
($newcanedit);
1071 if ((!defined($oldentry)) &&
1072 (($newentry) || ($newmembercontrol) || ($newcanedit))) {
1073 PushGlobalSQLState
();
1074 SendSQL
("INSERT INTO group_control_map " .
1075 "(group_id, product_id, entry, " .
1076 "membercontrol, othercontrol, canedit) " .
1078 "($groupid, $product_id, $newentry, " .
1079 "$newmembercontrol, $newothercontrol, $newcanedit)");
1080 PopGlobalSQLState
();
1081 } elsif (($newentry != $entry)
1082 || ($newmembercontrol != $membercontrol)
1083 || ($newothercontrol != $othercontrol)
1084 || ($newcanedit != $canedit)) {
1085 PushGlobalSQLState
();
1086 SendSQL
("UPDATE group_control_map " .
1087 "SET entry = $newentry, " .
1088 "membercontrol = $newmembercontrol, " .
1089 "othercontrol = $newothercontrol, " .
1090 "canedit = $newcanedit " .
1091 "WHERE group_id = $groupid " .
1092 "AND product_id = $product_id");
1093 PopGlobalSQLState
();
1096 if (($newentry == 0) && ($newmembercontrol == 0)
1097 && ($newothercontrol == 0) && ($newcanedit == 0)) {
1098 PushGlobalSQLState
();
1099 SendSQL
("DELETE FROM group_control_map " .
1100 "WHERE group_id = $groupid " .
1101 "AND product_id = $product_id");
1102 PopGlobalSQLState
();
1106 foreach my $groupid (@now_na) {
1107 print "Removing bugs from NA group "
1108 . html_quote
(GroupIdToName
($groupid)) . "<P>\n";
1110 SendSQL
("SELECT bugs.bug_id,
1111 (lastdiffed >= delta_ts)
1112 FROM bugs, bug_group_map
1113 WHERE group_id = $groupid
1114 AND bug_group_map.bug_id = bugs.bug_id
1115 AND bugs.product_id = $product_id
1116 ORDER BY bugs.bug_id");
1117 while (MoreSQLData
()) {
1118 my ($bugid, $mailiscurrent) = FetchSQLData
();
1119 PushGlobalSQLState
();
1120 SendSQL
("DELETE FROM bug_group_map WHERE
1121 bug_id = $bugid AND group_id = $groupid");
1122 SendSQL
("SELECT name, NOW() FROM groups WHERE id = $groupid");
1123 my ($removed, $timestamp) = FetchSQLData
();
1124 LogActivityEntry
($bugid, "bug_group", $removed, "",
1125 $::userid
, $timestamp);
1127 if ($mailiscurrent) {
1128 $diffed = ", lastdiffed = " . SqlQuote
($timestamp);
1130 SendSQL
("UPDATE bugs SET delta_ts = " . SqlQuote
($timestamp) .
1131 $diffed . " WHERE bug_id = $bugid");
1132 PopGlobalSQLState
();
1135 print "dropped $count bugs<p>\n";
1138 foreach my $groupid (@now_mandatory) {
1139 print "Adding bugs to Mandatory group "
1140 . html_quote
(GroupIdToName
($groupid)) . "<P>\n";
1142 SendSQL
("SELECT bugs.bug_id,
1143 (lastdiffed >= delta_ts)
1145 LEFT JOIN bug_group_map
1146 ON bug_group_map.bug_id = bugs.bug_id
1147 AND group_id = $groupid
1148 WHERE bugs.product_id = $product_id
1149 AND bug_group_map.bug_id IS NULL
1150 ORDER BY bugs.bug_id");
1151 while (MoreSQLData
()) {
1152 my ($bugid, $mailiscurrent) = FetchSQLData
();
1153 PushGlobalSQLState
();
1154 SendSQL
("INSERT INTO bug_group_map (bug_id, group_id)
1155 VALUES ($bugid, $groupid)");
1156 SendSQL
("SELECT name, NOW() FROM groups WHERE id = $groupid");
1157 my ($added, $timestamp) = FetchSQLData
();
1158 LogActivityEntry
($bugid, "bug_group", "", $added,
1159 $::userid
, $timestamp);
1161 if ($mailiscurrent) {
1162 $diffed = ", lastdiffed = " . SqlQuote
($timestamp);
1164 SendSQL
("UPDATE bugs SET delta_ts = " . SqlQuote
($timestamp) .
1165 $diffed . " WHERE bug_id = $bugid");
1166 PopGlobalSQLState
();
1169 print "added $count bugs<p>\n";
1171 $dbh->bz_unlock_tables();
1173 print "Group control updates done<P>\n";
1175 PutTrailer
($localtrailer);
1180 # action='update' -> update the product
1183 if ($action eq 'update') {
1184 PutHeader
("Update product");
1186 my $productold = trim
($cgi->param('productold') || '');
1187 my $description = trim
($cgi->param('description') || '');
1188 my $descriptionold = trim
($cgi->param('descriptionold') || '');
1189 my $disallownew = trim
($cgi->param('disallownew') || '');
1190 my $disallownewold = trim
($cgi->param('disallownewold') || '');
1191 my $milestoneurl = trim
($cgi->param('milestoneurl') || '');
1192 my $milestoneurlold = trim
($cgi->param('milestoneurlold') || '');
1193 my $votesperuser = trim
($cgi->param('votesperuser') || 0);
1194 my $votesperuserold = trim
($cgi->param('votesperuserold') || 0);
1195 my $maxvotesperbug = trim
($cgi->param('maxvotesperbug') || 0);
1196 my $maxvotesperbugold = trim
($cgi->param('maxvotesperbugold') || 0);
1197 my $votestoconfirm = trim
($cgi->param('votestoconfirm') || 0);
1198 my $votestoconfirmold = trim
($cgi->param('votestoconfirmold') || 0);
1199 my $defaultmilestone = trim
($cgi->param('defaultmilestone') || '---');
1200 my $defaultmilestoneold = trim
($cgi->param('defaultmilestoneold') || '---');
1204 CheckProduct
($productold);
1205 my $product_id = get_product_id
($productold);
1207 if (!detaint_natural
($maxvotesperbug)) {
1208 print "Sorry, the max votes per bug must be an integer >= 0.";
1209 PutTrailer
($localtrailer);
1213 if (!detaint_natural
($votesperuser)) {
1214 print "Sorry, the votes per user must be an integer >= 0.";
1215 PutTrailer
($localtrailer);
1219 if (!detaint_natural
($votestoconfirm)) {
1220 print "Sorry, the votes to confirm must be an integer >= 0.";
1221 PutTrailer
($localtrailer);
1225 # Note that we got the $product_id using $productold above so it will
1226 # remain static even after we rename the product in the database.
1228 $dbh->bz_lock_tables('products WRITE',
1231 'group_control_map WRITE',
1235 if ($disallownew ne $disallownewold) {
1236 $disallownew = $disallownew ?
1 : 0;
1237 SendSQL
("UPDATE products
1238 SET disallownew=$disallownew
1239 WHERE id=$product_id");
1240 print "Updated bug submit status.<BR>\n";
1243 if ($description ne $descriptionold) {
1244 unless ($description) {
1245 print "Sorry, I can't delete the description.";
1246 $dbh->bz_unlock_tables(UNLOCK_ABORT
);
1247 PutTrailer
($localtrailer);
1250 SendSQL
("UPDATE products
1251 SET description=" . SqlQuote
($description) . "
1252 WHERE id=$product_id");
1253 print "Updated description.<BR>\n";
1256 if (Param
('usetargetmilestone') && $milestoneurl ne $milestoneurlold) {
1257 SendSQL
("UPDATE products
1258 SET milestoneurl=" . SqlQuote
($milestoneurl) . "
1259 WHERE id=$product_id");
1260 print "Updated milestone URL.<BR>\n";
1264 if ($votesperuser ne $votesperuserold) {
1265 SendSQL
("UPDATE products
1266 SET votesperuser=$votesperuser
1267 WHERE id=$product_id");
1268 print "Updated votes per user.<BR>\n";
1273 if ($maxvotesperbug ne $maxvotesperbugold) {
1274 SendSQL
("UPDATE products
1275 SET maxvotesperbug=$maxvotesperbug
1276 WHERE id=$product_id");
1277 print "Updated max votes per bug.<BR>\n";
1282 if ($votestoconfirm ne $votestoconfirmold) {
1283 SendSQL
("UPDATE products
1284 SET votestoconfirm=$votestoconfirm
1285 WHERE id=$product_id");
1286 print "Updated votes to confirm.<BR>\n";
1291 if ($defaultmilestone ne $defaultmilestoneold) {
1292 SendSQL
("SELECT value FROM milestones " .
1293 "WHERE value = " . SqlQuote
($defaultmilestone) .
1294 " AND product_id = $product_id");
1295 if (!FetchOneColumn
()) {
1296 print "Sorry, the milestone $defaultmilestone must be defined first.";
1297 $dbh->bz_unlock_tables(UNLOCK_ABORT
);
1298 PutTrailer
($localtrailer);
1301 SendSQL
("UPDATE products " .
1302 "SET defaultmilestone = " . SqlQuote
($defaultmilestone) .
1303 "WHERE id=$product_id");
1304 print "Updated default milestone.<BR>\n";
1307 my $qp = SqlQuote
($product);
1308 my $qpold = SqlQuote
($productold);
1310 if ($product ne $productold) {
1312 print "Sorry, I can't delete the product name.";
1313 $dbh->bz_unlock_tables(UNLOCK_ABORT
);
1314 PutTrailer
($localtrailer);
1318 if (lc($product) ne lc($productold) &&
1319 TestProduct
($product)) {
1320 print "Sorry, product name '$product' is already in use.";
1321 $dbh->bz_unlock_tables(UNLOCK_ABORT
);
1322 PutTrailer
($localtrailer);
1326 SendSQL
("UPDATE products SET name=$qp WHERE id=$product_id");
1327 print "Updated product name.<BR>\n";
1329 $dbh->bz_unlock_tables();
1330 unlink "$datadir/versioncache";
1333 # 1. too many votes for a single user on a single bug.
1334 if ($maxvotesperbug < $votesperuser) {
1335 print "<br>Checking existing votes in this product for anybody who now has too many votes for a single bug.";
1336 SendSQL
("SELECT votes.who, votes.bug_id " .
1337 "FROM votes, bugs " .
1338 "WHERE bugs.bug_id = votes.bug_id " .
1339 " AND bugs.product_id = $product_id " .
1340 " AND votes.vote_count > $maxvotesperbug");
1342 while (MoreSQLData
()) {
1343 my ($who, $id) = (FetchSQLData
());
1344 push(@list, [$who, $id]);
1346 foreach my $ref (@list) {
1347 my ($who, $id) = (@
$ref);
1348 RemoveVotes
($id, $who, "The rules for voting on this product has changed;\nyou had too many votes for a single bug.");
1349 my $name = DBID_to_name
($who);
1350 print qq{<br
>Removed votes
for bug
<A HREF
="show_bug.cgi?id=$id">$id</A
> from
$name\n};
1354 # 2. too many total votes for a single user.
1355 # This part doesn't work in the general case because RemoveVotes
1356 # doesn't enforce votesperuser (except per-bug when it's less
1357 # than maxvotesperbug). See RemoveVotes in globals.pl.
1358 print "<br>Checking existing votes in this product for anybody who now has too many total votes.";
1359 SendSQL
("SELECT votes.who, votes.vote_count FROM votes, bugs " .
1360 "WHERE bugs.bug_id = votes.bug_id " .
1361 " AND bugs.product_id = $product_id");
1363 while (MoreSQLData
()) {
1364 my ($who, $count) = (FetchSQLData
());
1365 if (!defined $counts{$who}) {
1366 $counts{$who} = $count;
1368 $counts{$who} += $count;
1371 foreach my $who (keys(%counts)) {
1372 if ($counts{$who} > $votesperuser) {
1373 SendSQL
("SELECT votes.bug_id FROM votes, bugs " .
1374 "WHERE bugs.bug_id = votes.bug_id " .
1375 " AND bugs.product_id = $product_id " .
1376 " AND votes.who = $who");
1377 while (MoreSQLData
()) {
1378 my ($id) = FetchSQLData
();
1379 RemoveVotes
($id, $who,
1380 "The rules for voting on this product has changed; you had too many\ntotal votes, so all votes have been removed.");
1381 my $name = DBID_to_name
($who);
1382 print qq{<br
>Removed votes
for bug
<A HREF
="show_bug.cgi?id=$id">$id</A
> from
$name\n};
1386 # 3. enough votes to confirm
1387 my $bug_list = $dbh->selectcol_arrayref("SELECT bug_id FROM bugs
1388 WHERE product_id = ?
1389 AND bug_status = 'UNCONFIRMED'
1391 undef, ($product_id, $votestoconfirm));
1392 if (scalar(@
$bug_list)) {
1393 print "<br>Checking unconfirmed bugs in this product for any which now have sufficient votes.";
1395 my @updated_bugs = ();
1396 foreach my $bug_id (@
$bug_list) {
1397 my $confirmed = CheckIfVotedConfirmed
($bug_id, $whoid);
1398 push (@updated_bugs, $bug_id) if $confirmed;
1401 $vars->{'type'} = "votes";
1402 $vars->{'mailrecipients'} = { 'changer' => $whoid };
1403 $vars->{'header_done'} = 1;
1405 foreach my $bug_id (@updated_bugs) {
1406 $vars->{'id'} = $bug_id;
1407 $template->process("bug/process/results.html.tmpl", $vars)
1408 || ThrowTemplateError
($template->error());
1412 PutTrailer
($localtrailer);
1417 # action='editgroupcontrols' -> update product group controls
1420 if ($action eq 'editgroupcontrols') {
1421 my $product_id = get_product_id
($product);
1423 || ThrowUserError
("invalid_product_name", { product
=> $product });
1424 # Display a group if it is either enabled or has bugs for this product.
1425 SendSQL
("SELECT id, name, entry, membercontrol, othercontrol, canedit, " .
1426 "isactive, COUNT(bugs.bug_id) " .
1428 "LEFT JOIN group_control_map " .
1429 "ON group_control_map.group_id = id " .
1430 "AND group_control_map.product_id = $product_id " .
1431 "LEFT JOIN bug_group_map " .
1432 "ON bug_group_map.group_id = groups.id " .
1434 "ON bugs.bug_id = bug_group_map.bug_id " .
1435 "AND bugs.product_id = $product_id " .
1436 "WHERE isbuggroup != 0 " .
1437 "AND (isactive != 0 OR entry IS NOT NULL " .
1438 "OR bugs.bug_id IS NOT NULL) " .
1439 $dbh->sql_group_by('name', 'id, entry, membercontrol,
1440 othercontrol, canedit, isactive'));
1442 while (MoreSQLData
()) {
1444 my ($groupid, $groupname, $entry, $membercontrol, $othercontrol,
1445 $canedit, $isactive, $bugcount) = FetchSQLData
();
1446 $group{'id'} = $groupid;
1447 $group{'name'} = $groupname;
1448 $group{'entry'} = $entry;
1449 $group{'membercontrol'} = $membercontrol;
1450 $group{'othercontrol'} = $othercontrol;
1451 $group{'canedit'} = $canedit;
1452 $group{'isactive'} = $isactive;
1453 $group{'bugcount'} = $bugcount;
1454 push @groups,\
%group;
1456 $vars->{'header_done'} = $headerdone;
1457 $vars->{'product'} = $product;
1458 $vars->{'classification'} = $classification;
1459 $vars->{'groups'} = \
@groups;
1460 $vars->{'const'} = {
1461 'CONTROLMAPNA' => CONTROLMAPNA
,
1462 'CONTROLMAPSHOWN' => CONTROLMAPSHOWN
,
1463 'CONTROLMAPDEFAULT' => CONTROLMAPDEFAULT
,
1464 'CONTROLMAPMANDATORY' => CONTROLMAPMANDATORY
,
1467 $template->process("admin/products/groupcontrol/edit.html.tmpl", $vars)
1468 || ThrowTemplateError
($template->error());
1474 # No valid action found
1478 print "I don't have a clue what you want.<BR>\n";