sanitycheck.cgi 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  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 the Bugzilla Bug Tracking System.
  15. #
  16. # The Initial Developer of the Original Code is Netscape Communications
  17. # Corporation. Portions created by Netscape are
  18. # Copyright (C) 1998 Netscape Communications Corporation. All
  19. # Rights Reserved.
  20. #
  21. # Contributor(s): Terry Weissman <terry@mozilla.org>
  22. # Matthew Tuck <matty@chariot.net.au>
  23. # Max Kanat-Alexander <mkanat@bugzilla.org>
  24. # Marc Schumann <wurblzap@gmail.com>
  25. # Frédéric Buclin <LpSolit@gmail.com>
  26. use strict;
  27. use lib qw(. lib);
  28. use Bugzilla;
  29. use Bugzilla::Bug;
  30. use Bugzilla::Constants;
  31. use Bugzilla::Util;
  32. use Bugzilla::Error;
  33. use Bugzilla::Status;
  34. ###########################################################################
  35. # General subs
  36. ###########################################################################
  37. sub get_string {
  38. my ($san_tag, $vars) = @_;
  39. $vars->{'san_tag'} = $san_tag;
  40. return get_text('sanitycheck', $vars);
  41. }
  42. sub Status {
  43. my ($san_tag, $vars, $alert) = @_;
  44. my $cgi = Bugzilla->cgi;
  45. return if (!$alert && Bugzilla->usage_mode == USAGE_MODE_CMDLINE && !$cgi->param('verbose'));
  46. if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
  47. my $output = $cgi->param('output') || '';
  48. my $linebreak = $alert ? "\nALERT: " : "\n";
  49. $cgi->param('error_found', 1) if $alert;
  50. $cgi->param('output', $output . $linebreak . get_string($san_tag, $vars));
  51. }
  52. else {
  53. my $start_tag = $alert ? '<p class="alert">' : '<p>';
  54. print $start_tag . get_string($san_tag, $vars) . "</p>\n";
  55. }
  56. }
  57. ###########################################################################
  58. # Start
  59. ###########################################################################
  60. my $user = Bugzilla->login(LOGIN_REQUIRED);
  61. my $cgi = Bugzilla->cgi;
  62. my $dbh = Bugzilla->dbh;
  63. # If the result of the sanity check is sent per email, then we have to
  64. # take the user prefs into account rather than querying the web browser.
  65. my $template;
  66. if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
  67. $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
  68. }
  69. else {
  70. $template = Bugzilla->template;
  71. }
  72. my $vars = {};
  73. print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
  74. # Make sure the user is authorized to access sanitycheck.cgi.
  75. # As this script can now alter the group_control_map table, we no longer
  76. # let users with editbugs privs run it anymore.
  77. $user->in_group("editcomponents")
  78. || ($user->in_group('editkeywords') && $cgi->param('rebuildkeywordcache'))
  79. || ThrowUserError("auth_failure", {group => "editcomponents",
  80. action => "run",
  81. object => "sanity_check"});
  82. unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
  83. $template->process('admin/sanitycheck/list.html.tmpl', $vars)
  84. || ThrowTemplateError($template->error());
  85. }
  86. ###########################################################################
  87. # Users with 'editkeywords' privs only can only check keywords.
  88. ###########################################################################
  89. unless ($user->in_group('editcomponents')) {
  90. check_votes_or_keywords('keywords');
  91. Status('checks_completed');
  92. $template->process('global/footer.html.tmpl', $vars)
  93. || ThrowTemplateError($template->error());
  94. exit;
  95. }
  96. ###########################################################################
  97. # Fix vote cache
  98. ###########################################################################
  99. if ($cgi->param('rebuildvotecache')) {
  100. Status('vote_cache_rebuild_start');
  101. $dbh->bz_start_transaction();
  102. $dbh->do(q{UPDATE bugs SET votes = 0});
  103. my $sth_update = $dbh->prepare(q{UPDATE bugs
  104. SET votes = ?
  105. WHERE bug_id = ?});
  106. my $sth = $dbh->prepare(q{SELECT bug_id, SUM(vote_count)
  107. FROM votes }. $dbh->sql_group_by('bug_id'));
  108. $sth->execute();
  109. while (my ($id, $v) = $sth->fetchrow_array) {
  110. $sth_update->execute($v, $id);
  111. }
  112. $dbh->bz_commit_transaction();
  113. Status('vote_cache_rebuild_end');
  114. }
  115. ###########################################################################
  116. # Create missing group_control_map entries
  117. ###########################################################################
  118. if ($cgi->param('createmissinggroupcontrolmapentries')) {
  119. Status('group_control_map_entries_creation');
  120. my $na = CONTROLMAPNA;
  121. my $shown = CONTROLMAPSHOWN;
  122. my $insertsth = $dbh->prepare(
  123. qq{INSERT INTO group_control_map (
  124. group_id, product_id, entry,
  125. membercontrol, othercontrol, canedit
  126. )
  127. VALUES (
  128. ?, ?, 0,
  129. $shown, $na, 0
  130. )});
  131. my $updatesth = $dbh->prepare(qq{UPDATE group_control_map
  132. SET membercontrol = $shown
  133. WHERE group_id = ?
  134. AND product_id = ?});
  135. my $counter = 0;
  136. # Find all group/product combinations used for bugs but not set up
  137. # correctly in group_control_map
  138. my $invalid_combinations = $dbh->selectall_arrayref(
  139. qq{ SELECT bugs.product_id,
  140. bgm.group_id,
  141. gcm.membercontrol,
  142. groups.name,
  143. products.name
  144. FROM bugs
  145. INNER JOIN bug_group_map AS bgm
  146. ON bugs.bug_id = bgm.bug_id
  147. INNER JOIN groups
  148. ON bgm.group_id = groups.id
  149. INNER JOIN products
  150. ON bugs.product_id = products.id
  151. LEFT JOIN group_control_map AS gcm
  152. ON bugs.product_id = gcm.product_id
  153. AND bgm.group_id = gcm.group_id
  154. WHERE COALESCE(gcm.membercontrol, $na) = $na
  155. } . $dbh->sql_group_by('bugs.product_id, bgm.group_id',
  156. 'gcm.membercontrol, groups.name, products.name'));
  157. foreach (@$invalid_combinations) {
  158. my ($product_id, $group_id, $currentmembercontrol,
  159. $group_name, $product_name) = @$_;
  160. $counter++;
  161. if (defined($currentmembercontrol)) {
  162. Status('group_control_map_entries_update',
  163. {group_name => $group_name, product_name => $product_name});
  164. $updatesth->execute($group_id, $product_id);
  165. }
  166. else {
  167. Status('group_control_map_entries_generation',
  168. {group_name => $group_name, product_name => $product_name});
  169. $insertsth->execute($group_id, $product_id);
  170. }
  171. }
  172. Status('group_control_map_entries_repaired', {counter => $counter});
  173. }
  174. ###########################################################################
  175. # Fix missing creation date
  176. ###########################################################################
  177. if ($cgi->param('repair_creation_date')) {
  178. Status('bug_creation_date_start');
  179. my $bug_ids = $dbh->selectcol_arrayref('SELECT bug_id FROM bugs
  180. WHERE creation_ts IS NULL');
  181. my $sth_UpdateDate = $dbh->prepare('UPDATE bugs SET creation_ts = ?
  182. WHERE bug_id = ?');
  183. # All bugs have an entry in the 'longdescs' table when they are created,
  184. # even if no comment is required.
  185. my $sth_getDate = $dbh->prepare('SELECT MIN(bug_when) FROM longdescs
  186. WHERE bug_id = ?');
  187. foreach my $bugid (@$bug_ids) {
  188. $sth_getDate->execute($bugid);
  189. my $date = $sth_getDate->fetchrow_array;
  190. $sth_UpdateDate->execute($date, $bugid);
  191. }
  192. Status('bug_creation_date_fixed', {bug_count => scalar(@$bug_ids)});
  193. }
  194. ###########################################################################
  195. # Fix entries in Bugs full_text
  196. ###########################################################################
  197. if ($cgi->param('repair_bugs_fulltext')) {
  198. Status('bugs_fulltext_start');
  199. my $bug_ids = $dbh->selectcol_arrayref('SELECT bugs.bug_id
  200. FROM bugs
  201. LEFT JOIN bugs_fulltext
  202. ON bugs_fulltext.bug_id = bugs.bug_id
  203. WHERE bugs_fulltext.bug_id IS NULL');
  204. foreach my $bugid (@$bug_ids) {
  205. Bugzilla::Bug->new($bugid)->_sync_fulltext('new_bug');
  206. }
  207. Status('bugs_fulltext_fixed', {bug_count => scalar(@$bug_ids)});
  208. }
  209. ###########################################################################
  210. # Send unsent mail
  211. ###########################################################################
  212. if ($cgi->param('rescanallBugMail')) {
  213. require Bugzilla::BugMail;
  214. Status('send_bugmail_start');
  215. my $time = $dbh->sql_interval(30, 'MINUTE');
  216. my $list = $dbh->selectcol_arrayref(qq{
  217. SELECT bug_id
  218. FROM bugs
  219. WHERE (lastdiffed IS NULL
  220. OR lastdiffed < delta_ts)
  221. AND delta_ts < now() - $time
  222. ORDER BY bug_id});
  223. Status('send_bugmail_status', {bug_count => scalar(@$list)});
  224. # We cannot simply look at the bugs_activity table to find who did the
  225. # last change in a given bug, as e.g. adding a comment doesn't add any
  226. # entry to this table. And some other changes may be private
  227. # (such as time-related changes or private attachments or comments)
  228. # and so choosing this user as being the last one having done a change
  229. # for the bug may be problematic. So the best we can do at this point
  230. # is to choose the currently logged in user for email notification.
  231. $vars->{'changer'} = Bugzilla->user->login;
  232. foreach my $bugid (@$list) {
  233. Bugzilla::BugMail::Send($bugid, $vars);
  234. }
  235. Status('send_bugmail_end') if scalar(@$list);
  236. unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
  237. $template->process('global/footer.html.tmpl', $vars)
  238. || ThrowTemplateError($template->error());
  239. }
  240. exit;
  241. }
  242. ###########################################################################
  243. # Remove all references to deleted bugs
  244. ###########################################################################
  245. if ($cgi->param('remove_invalid_bug_references')) {
  246. Status('bug_reference_deletion_start');
  247. $dbh->bz_start_transaction();
  248. # Include custom multi-select fields to the list.
  249. my @multi_selects = Bugzilla->get_fields({custom => 1, type => FIELD_TYPE_MULTI_SELECT});
  250. my @addl_fields = map { 'bug_' . $_->name . '/' } @multi_selects;
  251. foreach my $pair ('attachments/', 'bug_group_map/', 'bugs_activity/',
  252. 'bugs_fulltext/', 'cc/',
  253. 'dependencies/blocked', 'dependencies/dependson',
  254. 'duplicates/dupe', 'duplicates/dupe_of',
  255. 'flags/', 'keywords/', 'longdescs/', 'votes/',
  256. @addl_fields)
  257. {
  258. my ($table, $field) = split('/', $pair);
  259. $field ||= "bug_id";
  260. my $bug_ids =
  261. $dbh->selectcol_arrayref("SELECT $table.$field FROM $table
  262. LEFT JOIN bugs ON $table.$field = bugs.bug_id
  263. WHERE bugs.bug_id IS NULL");
  264. if (scalar(@$bug_ids)) {
  265. $dbh->do("DELETE FROM $table WHERE $field IN (" . join(',', @$bug_ids) . ")");
  266. }
  267. }
  268. $dbh->bz_commit_transaction();
  269. Status('bug_reference_deletion_end');
  270. }
  271. ###########################################################################
  272. # Remove all references to deleted attachments
  273. ###########################################################################
  274. if ($cgi->param('remove_invalid_attach_references')) {
  275. Status('attachment_reference_deletion_start');
  276. $dbh->bz_start_transaction();
  277. my $attach_ids =
  278. $dbh->selectcol_arrayref('SELECT attach_data.id
  279. FROM attach_data
  280. LEFT JOIN attachments
  281. ON attachments.attach_id = attach_data.id
  282. WHERE attachments.attach_id IS NULL');
  283. if (scalar(@$attach_ids)) {
  284. $dbh->do('DELETE FROM attach_data WHERE id IN (' .
  285. join(',', @$attach_ids) . ')');
  286. }
  287. $dbh->bz_commit_transaction();
  288. Status('attachment_reference_deletion_end');
  289. }
  290. ###########################################################################
  291. # Remove all references to deleted users or groups from whines
  292. ###########################################################################
  293. if ($cgi->param('remove_old_whine_targets')) {
  294. Status('whines_obsolete_target_deletion_start');
  295. $dbh->bz_start_transaction();
  296. foreach my $target (['groups', 'id', MAILTO_GROUP],
  297. ['profiles', 'userid', MAILTO_USER])
  298. {
  299. my ($table, $col, $type) = @$target;
  300. my $old_ids =
  301. $dbh->selectcol_arrayref("SELECT DISTINCT mailto
  302. FROM whine_schedules
  303. LEFT JOIN $table
  304. ON $table.$col = whine_schedules.mailto
  305. WHERE mailto_type = $type AND $table.$col IS NULL");
  306. if (scalar(@$old_ids)) {
  307. $dbh->do("DELETE FROM whine_schedules
  308. WHERE mailto_type = $type AND mailto IN (" .
  309. join(',', @$old_ids) . ")");
  310. }
  311. }
  312. $dbh->bz_commit_transaction();
  313. Status('whines_obsolete_target_deletion_end');
  314. }
  315. Status('checks_start');
  316. ###########################################################################
  317. # Perform referential (cross) checks
  318. ###########################################################################
  319. # This checks that a simple foreign key has a valid primary key value. NULL
  320. # references are acceptable and cause no problem.
  321. #
  322. # The first parameter is the primary key table name.
  323. # The second parameter is the primary key field name.
  324. # Each successive parameter represents a foreign key, it must be a list
  325. # reference, where the list has:
  326. # the first value is the foreign key table name.
  327. # the second value is the foreign key field name.
  328. # the third value is optional and represents a field on the foreign key
  329. # table to display when the check fails.
  330. # the fourth value is optional and is a list reference to values that
  331. # are excluded from checking.
  332. #
  333. # FIXME: The excluded values parameter should go away - the QA contact
  334. # fields should use NULL instead - see bug #109474.
  335. # The same goes for series; no bug for that yet.
  336. sub CrossCheck {
  337. my $table = shift @_;
  338. my $field = shift @_;
  339. my $dbh = Bugzilla->dbh;
  340. Status('cross_check_to', {table => $table, field => $field});
  341. while (@_) {
  342. my $ref = shift @_;
  343. my ($refertable, $referfield, $keyname, $exceptions) = @$ref;
  344. $exceptions ||= [];
  345. my %exceptions = map { $_ => 1 } @$exceptions;
  346. Status('cross_check_from', {table => $refertable, field => $referfield});
  347. my $query = qq{SELECT DISTINCT $refertable.$referfield} .
  348. ($keyname ? qq{, $refertable.$keyname } : q{}) .
  349. qq{ FROM $refertable
  350. LEFT JOIN $table
  351. ON $refertable.$referfield = $table.$field
  352. WHERE $table.$field IS NULL
  353. AND $refertable.$referfield IS NOT NULL};
  354. my $sth = $dbh->prepare($query);
  355. $sth->execute;
  356. my $has_bad_references = 0;
  357. while (my ($value, $key) = $sth->fetchrow_array) {
  358. next if $exceptions{$value};
  359. Status('cross_check_alert', {value => $value, table => $refertable,
  360. field => $referfield, keyname => $keyname,
  361. key => $key}, 'alert');
  362. $has_bad_references = 1;
  363. }
  364. # References to non existent bugs can be safely removed, bug 288461
  365. if ($table eq 'bugs' && $has_bad_references) {
  366. Status('cross_check_bug_has_references');
  367. }
  368. # References to non existent attachments can be safely removed.
  369. if ($table eq 'attachments' && $has_bad_references) {
  370. Status('cross_check_attachment_has_references');
  371. }
  372. }
  373. }
  374. CrossCheck('classifications', 'id',
  375. ['products', 'classification_id']);
  376. CrossCheck("keyworddefs", "id",
  377. ["keywords", "keywordid"]);
  378. CrossCheck("fielddefs", "id",
  379. ["bugs_activity", "fieldid"],
  380. ['profiles_activity', 'fieldid']);
  381. CrossCheck("flagtypes", "id",
  382. ["flags", "type_id"],
  383. ["flagexclusions", "type_id"],
  384. ["flaginclusions", "type_id"]);
  385. # Include custom multi-select fields to the list.
  386. my @multi_selects = Bugzilla->get_fields({custom => 1, type => FIELD_TYPE_MULTI_SELECT});
  387. my @addl_fields = map { ['bug_' . $_->name, 'bug_id'] } @multi_selects;
  388. CrossCheck("bugs", "bug_id",
  389. ["bugs_activity", "bug_id"],
  390. ["bug_group_map", "bug_id"],
  391. ["bugs_fulltext", "bug_id"],
  392. ["attachments", "bug_id"],
  393. ["cc", "bug_id"],
  394. ["longdescs", "bug_id"],
  395. ["dependencies", "blocked"],
  396. ["dependencies", "dependson"],
  397. ['flags', 'bug_id'],
  398. ["votes", "bug_id"],
  399. ["keywords", "bug_id"],
  400. ["duplicates", "dupe_of", "dupe"],
  401. ["duplicates", "dupe", "dupe_of"],
  402. @addl_fields);
  403. CrossCheck("groups", "id",
  404. ["bug_group_map", "group_id"],
  405. ['category_group_map', 'group_id'],
  406. ["group_group_map", "grantor_id"],
  407. ["group_group_map", "member_id"],
  408. ["group_control_map", "group_id"],
  409. ["namedquery_group_map", "group_id"],
  410. ["user_group_map", "group_id"],
  411. ["flagtypes", "grant_group_id"],
  412. ["flagtypes", "request_group_id"]);
  413. CrossCheck("namedqueries", "id",
  414. ["namedqueries_link_in_footer", "namedquery_id"],
  415. ["namedquery_group_map", "namedquery_id"],
  416. );
  417. CrossCheck("profiles", "userid",
  418. ['profiles_activity', 'userid'],
  419. ['profiles_activity', 'who'],
  420. ['email_setting', 'user_id'],
  421. ['profile_setting', 'user_id'],
  422. ["bugs", "reporter", "bug_id"],
  423. ["bugs", "assigned_to", "bug_id"],
  424. ["bugs", "qa_contact", "bug_id"],
  425. ["attachments", "submitter_id", "bug_id"],
  426. ['flags', 'setter_id', 'bug_id'],
  427. ['flags', 'requestee_id', 'bug_id'],
  428. ["bugs_activity", "who", "bug_id"],
  429. ["cc", "who", "bug_id"],
  430. ['quips', 'userid'],
  431. ["votes", "who", "bug_id"],
  432. ["longdescs", "who", "bug_id"],
  433. ["logincookies", "userid"],
  434. ["namedqueries", "userid"],
  435. ["namedqueries_link_in_footer", "user_id"],
  436. ['series', 'creator', 'series_id'],
  437. ["watch", "watcher"],
  438. ["watch", "watched"],
  439. ['whine_events', 'owner_userid'],
  440. ["tokens", "userid"],
  441. ["user_group_map", "user_id"],
  442. ["components", "initialowner", "name"],
  443. ["components", "initialqacontact", "name"],
  444. ["component_cc", "user_id"]);
  445. CrossCheck("products", "id",
  446. ["bugs", "product_id", "bug_id"],
  447. ["components", "product_id", "name"],
  448. ["milestones", "product_id", "value"],
  449. ["versions", "product_id", "value"],
  450. ["group_control_map", "product_id"],
  451. ["flaginclusions", "product_id", "type_id"],
  452. ["flagexclusions", "product_id", "type_id"]);
  453. CrossCheck("components", "id",
  454. ["component_cc", "component_id"],
  455. ["flagexclusions", "component_id", "type_id"],
  456. ["flaginclusions", "component_id", "type_id"]);
  457. # Check the former enum types -mkanat@bugzilla.org
  458. CrossCheck("bug_status", "value",
  459. ["bugs", "bug_status", "bug_id"]);
  460. CrossCheck("resolution", "value",
  461. ["bugs", "resolution", "bug_id"]);
  462. CrossCheck("bug_severity", "value",
  463. ["bugs", "bug_severity", "bug_id"]);
  464. CrossCheck("op_sys", "value",
  465. ["bugs", "op_sys", "bug_id"]);
  466. CrossCheck("priority", "value",
  467. ["bugs", "priority", "bug_id"]);
  468. CrossCheck("rep_platform", "value",
  469. ["bugs", "rep_platform", "bug_id"]);
  470. CrossCheck('series', 'series_id',
  471. ['series_data', 'series_id']);
  472. CrossCheck('series_categories', 'id',
  473. ['series', 'category'],
  474. ["category_group_map", "category_id"],
  475. ["series", "subcategory"]);
  476. CrossCheck('whine_events', 'id',
  477. ['whine_queries', 'eventid'],
  478. ['whine_schedules', 'eventid']);
  479. CrossCheck('attachments', 'attach_id',
  480. ['attach_data', 'id'],
  481. ['bugs_activity', 'attach_id']);
  482. CrossCheck('bug_status', 'id',
  483. ['status_workflow', 'old_status'],
  484. ['status_workflow', 'new_status']);
  485. ###########################################################################
  486. # Perform double field referential (cross) checks
  487. ###########################################################################
  488. # This checks that a compound two-field foreign key has a valid primary key
  489. # value. NULL references are acceptable and cause no problem.
  490. #
  491. # The first parameter is the primary key table name.
  492. # The second parameter is the primary key first field name.
  493. # The third parameter is the primary key second field name.
  494. # Each successive parameter represents a foreign key, it must be a list
  495. # reference, where the list has:
  496. # the first value is the foreign key table name
  497. # the second value is the foreign key first field name.
  498. # the third value is the foreign key second field name.
  499. # the fourth value is optional and represents a field on the foreign key
  500. # table to display when the check fails
  501. sub DoubleCrossCheck {
  502. my $table = shift @_;
  503. my $field1 = shift @_;
  504. my $field2 = shift @_;
  505. my $dbh = Bugzilla->dbh;
  506. Status('double_cross_check_to',
  507. {table => $table, field1 => $field1, field2 => $field2});
  508. while (@_) {
  509. my $ref = shift @_;
  510. my ($refertable, $referfield1, $referfield2, $keyname) = @$ref;
  511. Status('double_cross_check_from',
  512. {table => $refertable, field1 => $referfield1, field2 =>$referfield2});
  513. my $d_cross_check = $dbh->selectall_arrayref(qq{
  514. SELECT DISTINCT $refertable.$referfield1,
  515. $refertable.$referfield2 } .
  516. ($keyname ? qq{, $refertable.$keyname } : q{}) .
  517. qq{ FROM $refertable
  518. LEFT JOIN $table
  519. ON $refertable.$referfield1 = $table.$field1
  520. AND $refertable.$referfield2 = $table.$field2
  521. WHERE $table.$field1 IS NULL
  522. AND $table.$field2 IS NULL
  523. AND $refertable.$referfield1 IS NOT NULL
  524. AND $refertable.$referfield2 IS NOT NULL});
  525. foreach my $check (@$d_cross_check) {
  526. my ($value1, $value2, $key) = @$check;
  527. Status('double_cross_check_alert',
  528. {value1 => $value1, value2 => $value2,
  529. table => $refertable,
  530. field1 => $referfield1, field2 => $referfield2,
  531. keyname => $keyname, key => $key}, 'alert');
  532. }
  533. }
  534. }
  535. DoubleCrossCheck('attachments', 'bug_id', 'attach_id',
  536. ['flags', 'bug_id', 'attach_id'],
  537. ['bugs_activity', 'bug_id', 'attach_id']);
  538. DoubleCrossCheck("components", "product_id", "id",
  539. ["bugs", "product_id", "component_id", "bug_id"],
  540. ['flagexclusions', 'product_id', 'component_id'],
  541. ['flaginclusions', 'product_id', 'component_id']);
  542. DoubleCrossCheck("versions", "product_id", "value",
  543. ["bugs", "product_id", "version", "bug_id"]);
  544. DoubleCrossCheck("milestones", "product_id", "value",
  545. ["bugs", "product_id", "target_milestone", "bug_id"],
  546. ["products", "id", "defaultmilestone", "name"]);
  547. ###########################################################################
  548. # Perform login checks
  549. ###########################################################################
  550. Status('profile_login_start');
  551. my $sth = $dbh->prepare(q{SELECT userid, login_name FROM profiles});
  552. $sth->execute;
  553. while (my ($id, $email) = $sth->fetchrow_array) {
  554. validate_email_syntax($email)
  555. || Status('profile_login_alert', {id => $id, email => $email}, 'alert');
  556. }
  557. ###########################################################################
  558. # Perform vote/keyword cache checks
  559. ###########################################################################
  560. check_votes_or_keywords();
  561. sub check_votes_or_keywords {
  562. my $check = shift || 'all';
  563. my $dbh = Bugzilla->dbh;
  564. my $sth = $dbh->prepare(q{SELECT bug_id, votes, keywords
  565. FROM bugs
  566. WHERE votes != 0 OR keywords != ''});
  567. $sth->execute;
  568. my %votes;
  569. my %keyword;
  570. while (my ($id, $v, $k) = $sth->fetchrow_array) {
  571. if ($v != 0) {
  572. $votes{$id} = $v;
  573. }
  574. if ($k) {
  575. $keyword{$id} = $k;
  576. }
  577. }
  578. # If we only want to check keywords, skip checks about votes.
  579. _check_votes(\%votes) unless ($check eq 'keywords');
  580. # If we only want to check votes, skip checks about keywords.
  581. _check_keywords(\%keyword) unless ($check eq 'votes');
  582. }
  583. sub _check_votes {
  584. my $votes = shift;
  585. Status('vote_count_start');
  586. my $dbh = Bugzilla->dbh;
  587. my $sth = $dbh->prepare(q{SELECT bug_id, SUM(vote_count)
  588. FROM votes }.
  589. $dbh->sql_group_by('bug_id'));
  590. $sth->execute;
  591. my $offer_votecache_rebuild = 0;
  592. while (my ($id, $v) = $sth->fetchrow_array) {
  593. if ($v <= 0) {
  594. Status('vote_count_alert', {id => $id}, 'alert');
  595. } else {
  596. if (!defined $votes->{$id} || $votes->{$id} != $v) {
  597. Status('vote_cache_alert', {id => $id}, 'alert');
  598. $offer_votecache_rebuild = 1;
  599. }
  600. delete $votes->{$id};
  601. }
  602. }
  603. foreach my $id (keys %$votes) {
  604. Status('vote_cache_alert', {id => $id}, 'alert');
  605. $offer_votecache_rebuild = 1;
  606. }
  607. Status('vote_cache_rebuild_fix') if $offer_votecache_rebuild;
  608. }
  609. sub _check_keywords {
  610. my $keyword = shift;
  611. Status('keyword_check_start');
  612. my $dbh = Bugzilla->dbh;
  613. my $cgi = Bugzilla->cgi;
  614. my %keywordids;
  615. my $keywords = $dbh->selectall_arrayref(q{SELECT id, name
  616. FROM keyworddefs});
  617. foreach (@$keywords) {
  618. my ($id, $name) = @$_;
  619. if ($keywordids{$id}) {
  620. Status('keyword_check_alert', {id => $id}, 'alert');
  621. }
  622. $keywordids{$id} = 1;
  623. if ($name =~ /[\s,]/) {
  624. Status('keyword_check_invalid_name', {id => $id}, 'alert');
  625. }
  626. }
  627. my $sth = $dbh->prepare(q{SELECT bug_id, keywordid
  628. FROM keywords
  629. ORDER BY bug_id, keywordid});
  630. $sth->execute;
  631. my $lastid;
  632. my $lastk;
  633. while (my ($id, $k) = $sth->fetchrow_array) {
  634. if (!$keywordids{$k}) {
  635. Status('keyword_check_invalid_id', {id => $k}, 'alert');
  636. }
  637. if (defined $lastid && $id eq $lastid && $k eq $lastk) {
  638. Status('keyword_check_duplicated_ids', {id => $id}, 'alert');
  639. }
  640. $lastid = $id;
  641. $lastk = $k;
  642. }
  643. Status('keyword_cache_start');
  644. if ($cgi->param('rebuildkeywordcache')) {
  645. $dbh->bz_start_transaction();
  646. }
  647. my $query = q{SELECT keywords.bug_id, keyworddefs.name
  648. FROM keywords
  649. INNER JOIN keyworddefs
  650. ON keyworddefs.id = keywords.keywordid
  651. INNER JOIN bugs
  652. ON keywords.bug_id = bugs.bug_id
  653. ORDER BY keywords.bug_id, keyworddefs.name};
  654. $sth = $dbh->prepare($query);
  655. $sth->execute;
  656. my $lastb = 0;
  657. my @list;
  658. my %realk;
  659. while (1) {
  660. my ($b, $k) = $sth->fetchrow_array;
  661. if (!defined $b || $b != $lastb) {
  662. if (@list) {
  663. $realk{$lastb} = join(', ', @list);
  664. }
  665. last unless $b;
  666. $lastb = $b;
  667. @list = ();
  668. }
  669. push(@list, $k);
  670. }
  671. my @badbugs = ();
  672. foreach my $b (keys(%$keyword)) {
  673. if (!exists $realk{$b} || $realk{$b} ne $keyword->{$b}) {
  674. push(@badbugs, $b);
  675. }
  676. }
  677. foreach my $b (keys(%realk)) {
  678. if (!exists $keyword->{$b}) {
  679. push(@badbugs, $b);
  680. }
  681. }
  682. if (@badbugs) {
  683. @badbugs = sort {$a <=> $b} @badbugs;
  684. if ($cgi->param('rebuildkeywordcache')) {
  685. my $sth_update = $dbh->prepare(q{UPDATE bugs
  686. SET keywords = ?
  687. WHERE bug_id = ?});
  688. Status('keyword_cache_fixing');
  689. foreach my $b (@badbugs) {
  690. my $k = '';
  691. if (exists($realk{$b})) {
  692. $k = $realk{$b};
  693. }
  694. $sth_update->execute($k, $b);
  695. }
  696. Status('keyword_cache_fixed');
  697. } else {
  698. Status('keyword_cache_alert', {badbugs => \@badbugs}, 'alert');
  699. Status('keyword_cache_rebuild');
  700. }
  701. }
  702. if ($cgi->param('rebuildkeywordcache')) {
  703. $dbh->bz_commit_transaction();
  704. }
  705. }
  706. ###########################################################################
  707. # Check for flags being in incorrect products and components
  708. ###########################################################################
  709. Status('flag_check_start');
  710. my $invalid_flags = $dbh->selectall_arrayref(
  711. 'SELECT DISTINCT flags.id, flags.bug_id, flags.attach_id
  712. FROM flags
  713. INNER JOIN bugs
  714. ON flags.bug_id = bugs.bug_id
  715. LEFT JOIN flaginclusions AS i
  716. ON flags.type_id = i.type_id
  717. AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
  718. AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
  719. WHERE i.type_id IS NULL');
  720. my @invalid_flags = @$invalid_flags;
  721. $invalid_flags = $dbh->selectall_arrayref(
  722. 'SELECT DISTINCT flags.id, flags.bug_id, flags.attach_id
  723. FROM flags
  724. INNER JOIN bugs
  725. ON flags.bug_id = bugs.bug_id
  726. INNER JOIN flagexclusions AS e
  727. ON flags.type_id = e.type_id
  728. WHERE (bugs.product_id = e.product_id OR e.product_id IS NULL)
  729. AND (bugs.component_id = e.component_id OR e.component_id IS NULL)');
  730. push(@invalid_flags, @$invalid_flags);
  731. if (scalar(@invalid_flags)) {
  732. if ($cgi->param('remove_invalid_flags')) {
  733. Status('flag_deletion_start');
  734. my @flag_ids = map {$_->[0]} @invalid_flags;
  735. # Silently delete these flags, with no notification to requesters/setters.
  736. $dbh->do('DELETE FROM flags WHERE id IN (' . join(',', @flag_ids) .')');
  737. Status('flag_deletion_end');
  738. }
  739. else {
  740. foreach my $flag (@$invalid_flags) {
  741. my ($flag_id, $bug_id, $attach_id) = @$flag;
  742. Status('flag_alert',
  743. {flag_id => $flag_id, attach_id => $attach_id, bug_id => $bug_id},
  744. 'alert');
  745. }
  746. Status('flag_fix');
  747. }
  748. }
  749. ###########################################################################
  750. # General bug checks
  751. ###########################################################################
  752. sub BugCheck {
  753. my ($middlesql, $errortext, $repairparam, $repairtext) = @_;
  754. my $dbh = Bugzilla->dbh;
  755. my $badbugs = $dbh->selectcol_arrayref(qq{SELECT DISTINCT bugs.bug_id
  756. FROM $middlesql
  757. ORDER BY bugs.bug_id});
  758. if (scalar(@$badbugs)) {
  759. Status('bug_check_alert',
  760. {errortext => get_string($errortext), badbugs => $badbugs},
  761. 'alert');
  762. if ($repairparam) {
  763. $repairtext ||= 'repair_bugs';
  764. Status('bug_check_repair',
  765. {param => $repairparam, text => get_string($repairtext)});
  766. }
  767. }
  768. }
  769. Status('bug_check_creation_date');
  770. BugCheck("bugs WHERE creation_ts IS NULL", 'bug_check_creation_date_error_text',
  771. 'repair_creation_date', 'bug_check_creation_date_repair_text');
  772. Status('bug_check_bugs_fulltext');
  773. BugCheck("bugs LEFT JOIN bugs_fulltext ON bugs_fulltext.bug_id = bugs.bug_id " .
  774. "WHERE bugs_fulltext.bug_id IS NULL", 'bug_check_bugs_fulltext_error_text',
  775. 'repair_bugs_fulltext', 'bug_check_bugs_fulltext_repair_text');
  776. Status('bug_check_res_dupl');
  777. BugCheck("bugs INNER JOIN duplicates ON bugs.bug_id = duplicates.dupe " .
  778. "WHERE bugs.resolution != 'DUPLICATE'", 'bug_check_res_dupl_error_text');
  779. BugCheck("bugs LEFT JOIN duplicates ON bugs.bug_id = duplicates.dupe WHERE " .
  780. "bugs.resolution = 'DUPLICATE' AND " .
  781. "duplicates.dupe IS NULL", 'bug_check_res_dupl_error_text2');
  782. Status('bug_check_status_res');
  783. my @open_states = map($dbh->quote($_), BUG_STATE_OPEN);
  784. my $open_states = join(', ', @open_states);
  785. BugCheck("bugs WHERE bug_status IN ($open_states) AND resolution != ''",
  786. 'bug_check_status_res_error_text');
  787. BugCheck("bugs WHERE bug_status NOT IN ($open_states) AND resolution = ''",
  788. 'bug_check_status_res_error_text2');
  789. Status('bug_check_status_everconfirmed');
  790. BugCheck("bugs WHERE bug_status = 'UNCONFIRMED' AND everconfirmed = 1",
  791. 'bug_check_status_everconfirmed_error_text');
  792. my @confirmed_open_states = grep {$_ ne 'UNCONFIRMED'} BUG_STATE_OPEN;
  793. my $confirmed_open_states = join(', ', map {$dbh->quote($_)} @confirmed_open_states);
  794. BugCheck("bugs WHERE bug_status IN ($confirmed_open_states) AND everconfirmed = 0",
  795. 'bug_check_status_everconfirmed_error_text2');
  796. Status('bug_check_votes_everconfirmed');
  797. BugCheck("bugs INNER JOIN products ON bugs.product_id = products.id " .
  798. "WHERE everconfirmed = 0 AND votestoconfirm <= votes",
  799. 'bug_check_votes_everconfirmed_error_text');
  800. ###########################################################################
  801. # Control Values
  802. ###########################################################################
  803. # Checks for values that are invalid OR
  804. # not among the 9 valid combinations
  805. Status('bug_check_control_values');
  806. my $groups = join(", ", (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT,
  807. CONTROLMAPMANDATORY));
  808. my $query = qq{
  809. SELECT COUNT(product_id)
  810. FROM group_control_map
  811. WHERE membercontrol NOT IN( $groups )
  812. OR othercontrol NOT IN( $groups )
  813. OR ((membercontrol != othercontrol)
  814. AND (membercontrol != } . CONTROLMAPSHOWN . q{)
  815. AND ((membercontrol != } . CONTROLMAPDEFAULT . q{)
  816. OR (othercontrol = } . CONTROLMAPSHOWN . q{)))};
  817. my $entries = $dbh->selectrow_array($query);
  818. Status('bug_check_control_values_alert', {entries => $entries}, 'alert') if $entries;
  819. Status('bug_check_control_values_violation');
  820. BugCheck("bugs
  821. INNER JOIN bug_group_map
  822. ON bugs.bug_id = bug_group_map.bug_id
  823. LEFT JOIN group_control_map
  824. ON bugs.product_id = group_control_map.product_id
  825. AND bug_group_map.group_id = group_control_map.group_id
  826. WHERE ((group_control_map.membercontrol = " . CONTROLMAPNA . ")
  827. OR (group_control_map.membercontrol IS NULL))",
  828. 'bug_check_control_values_error_text',
  829. 'createmissinggroupcontrolmapentries',
  830. 'bug_check_control_values_repair_text');
  831. BugCheck("bugs
  832. INNER JOIN group_control_map
  833. ON bugs.product_id = group_control_map.product_id
  834. INNER JOIN groups
  835. ON group_control_map.group_id = groups.id
  836. LEFT JOIN bug_group_map
  837. ON bugs.bug_id = bug_group_map.bug_id
  838. AND group_control_map.group_id = bug_group_map.group_id
  839. WHERE group_control_map.membercontrol = " . CONTROLMAPMANDATORY . "
  840. AND bug_group_map.group_id IS NULL
  841. AND groups.isactive != 0",
  842. 'bug_check_control_values_error_text2');
  843. ###########################################################################
  844. # Unsent mail
  845. ###########################################################################
  846. Status('unsent_bugmail_check');
  847. my $time = $dbh->sql_interval(30, 'MINUTE');
  848. my $badbugs = $dbh->selectcol_arrayref(qq{
  849. SELECT bug_id
  850. FROM bugs
  851. WHERE (lastdiffed IS NULL OR lastdiffed < delta_ts)
  852. AND delta_ts < now() - $time
  853. ORDER BY bug_id});
  854. if (scalar(@$badbugs > 0)) {
  855. Status('unsent_bugmail_alert', {badbugs => $badbugs}, 'alert');
  856. Status('unsent_bugmail_fix');
  857. }
  858. ###########################################################################
  859. # Whines
  860. ###########################################################################
  861. Status('whines_obsolete_target_start');
  862. my $display_repair_whines_link = 0;
  863. foreach my $target (['groups', 'id', MAILTO_GROUP],
  864. ['profiles', 'userid', MAILTO_USER])
  865. {
  866. my ($table, $col, $type) = @$target;
  867. my $old = $dbh->selectall_arrayref("SELECT whine_schedules.id, mailto
  868. FROM whine_schedules
  869. LEFT JOIN $table
  870. ON $table.$col = whine_schedules.mailto
  871. WHERE mailto_type = $type AND $table.$col IS NULL");
  872. if (scalar(@$old)) {
  873. Status('whines_obsolete_target_alert', {schedules => $old, type => $type}, 'alert');
  874. $display_repair_whines_link = 1;
  875. }
  876. }
  877. Status('whines_obsolete_target_fix') if $display_repair_whines_link;
  878. ###########################################################################
  879. # End
  880. ###########################################################################
  881. Status('checks_completed');
  882. unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
  883. $template->process('global/footer.html.tmpl', $vars)
  884. || ThrowTemplateError($template->error());
  885. }