editproducts.cgi 40 KB

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