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