sanitycheck.cgi 34.9 KB
Newer Older
1
#!/usr/bin/perl -T
2 3 4
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
#
6 7
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
8

9
use 5.10.1;
10
use strict;
11 12
use warnings;

13
use lib qw(. lib);
14

15
use Bugzilla;
16
use Bugzilla::Bug;
17
use Bugzilla::Constants;
18
use Bugzilla::Error;
19 20
use Bugzilla::Hook;
use Bugzilla::Util;
21
use Bugzilla::Status;
22
use Bugzilla::Token;
23

24 25 26
###########################################################################
# General subs
###########################################################################
27

28 29 30 31
sub get_string {
    my ($san_tag, $vars) = @_;
    $vars->{'san_tag'} = $san_tag;
    return get_text('sanitycheck', $vars);
32 33
}

34 35
sub Status {
    my ($san_tag, $vars, $alert) = @_;
36 37 38 39 40 41 42 43 44 45 46
    my $cgi = Bugzilla->cgi;
    return if (!$alert && Bugzilla->usage_mode == USAGE_MODE_CMDLINE && !$cgi->param('verbose'));

    if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
        my $output = $cgi->param('output') || '';
        my $linebreak = $alert ? "\nALERT: " : "\n";
        $cgi->param('error_found', 1) if $alert;
        $cgi->param('output', $output . $linebreak . get_string($san_tag, $vars));
    }
    else {
        my $start_tag = $alert ? '<p class="alert">' : '<p>';
47
        say $start_tag . get_string($san_tag, $vars) . "</p>";
48
    }
49 50
}

51 52 53
###########################################################################
# Start
###########################################################################
54

55
my $user = Bugzilla->login(LOGIN_REQUIRED);
56

57
my $cgi = Bugzilla->cgi;
58
my $dbh = Bugzilla->dbh;
59 60 61 62
# If the result of the sanity check is sent per email, then we have to
# take the user prefs into account rather than querying the web browser.
my $template;
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
63
    $template = Bugzilla->template_inner($user->setting('lang'));
64 65 66
}
else {
    $template = Bugzilla->template;
67 68 69 70 71 72 73 74 75

    # Only check the token if we are running this script from the
    # web browser and a parameter is passed to the script.
    # XXX - Maybe these two parameters should be deleted once logged in?
    $cgi->delete('GoAheadAndLogIn', 'Bugzilla_restrictlogin');
    if (scalar($cgi->param())) {
        my $token = $cgi->param('token');
        check_hash_token($token, ['sanitycheck']);
    }
76
}
77
my $vars = {};
78
my $clear_memcached = 0;
79

80
print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
81

82 83 84
# Make sure the user is authorized to access sanitycheck.cgi.
# As this script can now alter the group_control_map table, we no longer
# let users with editbugs privs run it anymore.
85
$user->in_group("editcomponents")
86
  || ThrowUserError("auth_failure", {group  => "editcomponents",
87 88
                                     action => "run",
                                     object => "sanity_check"});
89

90 91 92 93
unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
    $template->process('admin/sanitycheck/list.html.tmpl', $vars)
      || ThrowTemplateError($template->error());
}
94

95 96 97 98
###########################################################################
# Create missing group_control_map entries
###########################################################################

99 100
if ($cgi->param('createmissinggroupcontrolmapentries')) {
    Status('group_control_map_entries_creation');
101 102 103 104

    my $na    = CONTROLMAPNA;
    my $shown = CONTROLMAPSHOWN;
    my $insertsth = $dbh->prepare(
105 106 107 108
        qq{INSERT INTO group_control_map
                       (group_id, product_id, membercontrol, othercontrol)
                VALUES (?, ?, $shown, $na)});

109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
    my $updatesth = $dbh->prepare(qq{UPDATE group_control_map
                                        SET membercontrol = $shown
                                      WHERE group_id   = ?
                                        AND product_id = ?});
    my $counter = 0;

    # Find all group/product combinations used for bugs but not set up
    # correctly in group_control_map
    my $invalid_combinations = $dbh->selectall_arrayref(
        qq{    SELECT bugs.product_id,
                      bgm.group_id,
                      gcm.membercontrol,
                      groups.name,
                      products.name
                 FROM bugs
           INNER JOIN bug_group_map AS bgm
                   ON bugs.bug_id = bgm.bug_id
           INNER JOIN groups
                   ON bgm.group_id = groups.id
           INNER JOIN products
                   ON bugs.product_id = products.id
            LEFT JOIN group_control_map AS gcm
                   ON bugs.product_id = gcm.product_id
                  AND    bgm.group_id = gcm.group_id
                WHERE COALESCE(gcm.membercontrol, $na) = $na
134 135
          } . $dbh->sql_group_by('bugs.product_id, bgm.group_id',
                                 'gcm.membercontrol, groups.name, products.name'));
136 137 138 139 140 141 142

    foreach (@$invalid_combinations) {
        my ($product_id, $group_id, $currentmembercontrol,
            $group_name, $product_name) = @$_;

        $counter++;
        if (defined($currentmembercontrol)) {
143 144
            Status('group_control_map_entries_update',
                   {group_name => $group_name, product_name => $product_name});
145 146 147
            $updatesth->execute($group_id, $product_id);
        }
        else {
148 149
            Status('group_control_map_entries_generation',
                   {group_name => $group_name, product_name => $product_name});
150 151 152 153
            $insertsth->execute($group_id, $product_id);
        }
    }

154
    Status('group_control_map_entries_repaired', {counter => $counter});
155
    $clear_memcached = 1 if $counter;
156 157
}

158 159 160 161
###########################################################################
# Fix missing creation date
###########################################################################

162 163
if ($cgi->param('repair_creation_date')) {
    Status('bug_creation_date_start');
164 165 166 167 168 169 170 171

    my $bug_ids = $dbh->selectcol_arrayref('SELECT bug_id FROM bugs
                                            WHERE creation_ts IS NULL');

    my $sth_UpdateDate = $dbh->prepare('UPDATE bugs SET creation_ts = ?
                                        WHERE bug_id = ?');

    # All bugs have an entry in the 'longdescs' table when they are created,
172
    # even if no comment is required.
173 174 175 176 177 178 179 180
    my $sth_getDate = $dbh->prepare('SELECT MIN(bug_when) FROM longdescs
                                     WHERE bug_id = ?');

    foreach my $bugid (@$bug_ids) {
        $sth_getDate->execute($bugid);
        my $date = $sth_getDate->fetchrow_array;
        $sth_UpdateDate->execute($date, $bugid);
    }
181
    Status('bug_creation_date_fixed', {bug_count => scalar(@$bug_ids)});
182
    $clear_memcached = 1 if @$bug_ids;
183 184
}

185 186 187 188 189 190 191 192 193 194 195 196 197 198
###########################################################################
# Fix everconfirmed
###########################################################################

if ($cgi->param('repair_everconfirmed')) {
    Status('everconfirmed_start');

    my @confirmed_open_states = grep {$_ ne 'UNCONFIRMED'} BUG_STATE_OPEN;
    my $confirmed_open_states = join(', ', map {$dbh->quote($_)} @confirmed_open_states);

    $dbh->do("UPDATE bugs SET everconfirmed = 0 WHERE bug_status = 'UNCONFIRMED'");
    $dbh->do("UPDATE bugs SET everconfirmed = 1 WHERE bug_status IN ($confirmed_open_states)");

    Status('everconfirmed_end');
199
    $clear_memcached = 1;
200 201
}

202 203 204 205 206 207 208 209 210 211 212 213 214
###########################################################################
# Fix entries in Bugs full_text
###########################################################################

if ($cgi->param('repair_bugs_fulltext')) {
    Status('bugs_fulltext_start');

    my $bug_ids = $dbh->selectcol_arrayref('SELECT bugs.bug_id
                                            FROM bugs
                                            LEFT JOIN bugs_fulltext
                                            ON bugs_fulltext.bug_id = bugs.bug_id
                                            WHERE bugs_fulltext.bug_id IS NULL');

215 216 217
    foreach my $bugid (@$bug_ids) {
        Bugzilla::Bug->new($bugid)->_sync_fulltext( new_bug => 1 );
    }
218

219 220
    Status('bugs_fulltext_fixed', {bug_count => scalar(@$bug_ids)});
    $clear_memcached = 1 if @$bug_ids;
221 222
}

223 224 225 226
###########################################################################
# Send unsent mail
###########################################################################

227
if ($cgi->param('rescanallBugMail')) {
228 229
    require Bugzilla::BugMail;

230
    Status('send_bugmail_start');
231
    my $time = $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE');
232

233 234 235 236 237
    my $list = $dbh->selectcol_arrayref(qq{
                                        SELECT bug_id
                                          FROM bugs 
                                         WHERE (lastdiffed IS NULL
                                                OR lastdiffed < delta_ts)
238
                                           AND delta_ts < $time
239
                                      ORDER BY bug_id});
240

241 242
    Status('send_bugmail_status', {bug_count => scalar(@$list)});

243 244 245 246 247 248 249
    # We cannot simply look at the bugs_activity table to find who did the
    # last change in a given bug, as e.g. adding a comment doesn't add any
    # entry to this table. And some other changes may be private
    # (such as time-related changes or private attachments or comments)
    # and so choosing this user as being the last one having done a change
    # for the bug may be problematic. So the best we can do at this point
    # is to choose the currently logged in user for email notification.
250
    $vars->{'changer'} = $user;
251

252
    foreach my $bugid (@$list) {
253
        Bugzilla::BugMail::Send($bugid, $vars);
254 255
    }

256 257 258 259
    if (@$list) {
        Status('send_bugmail_end');
        Bugzilla->memcached->clear_all();
    }
260

261 262 263 264
    unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
        $template->process('global/footer.html.tmpl', $vars)
          || ThrowTemplateError($template->error());
    }
265 266 267
    exit;
}

268 269 270 271
###########################################################################
# Remove all references to deleted bugs
###########################################################################

272 273
if ($cgi->param('remove_invalid_bug_references')) {
    Status('bug_reference_deletion_start');
274

275
    $dbh->bz_start_transaction();
276

277 278
    foreach my $pair ('attachments/', 'bug_group_map/', 'bugs_activity/',
                      'bugs_fulltext/', 'cc/',
279 280
                      'dependencies/blocked', 'dependencies/dependson',
                      'duplicates/dupe', 'duplicates/dupe_of',
281
                      'flags/', 'keywords/', 'longdescs/') {
282 283 284 285 286 287 288 289 290 291 292

        my ($table, $field) = split('/', $pair);
        $field ||= "bug_id";

        my $bug_ids =
          $dbh->selectcol_arrayref("SELECT $table.$field FROM $table
                                    LEFT JOIN bugs ON $table.$field = bugs.bug_id
                                    WHERE bugs.bug_id IS NULL");

        if (scalar(@$bug_ids)) {
            $dbh->do("DELETE FROM $table WHERE $field IN (" . join(',', @$bug_ids) . ")");
293
            $clear_memcached = 1;
294 295 296
        }
    }

297
    $dbh->bz_commit_transaction();
298
    Status('bug_reference_deletion_end');
299 300
}

301 302 303 304
###########################################################################
# Remove all references to deleted attachments
###########################################################################

305 306
if ($cgi->param('remove_invalid_attach_references')) {
    Status('attachment_reference_deletion_start');
307

308
    $dbh->bz_start_transaction();
309 310 311 312 313 314 315 316 317 318 319 320 321

    my $attach_ids =
        $dbh->selectcol_arrayref('SELECT attach_data.id
                                    FROM attach_data
                               LEFT JOIN attachments
                                      ON attachments.attach_id = attach_data.id
                                   WHERE attachments.attach_id IS NULL');

    if (scalar(@$attach_ids)) {
        $dbh->do('DELETE FROM attach_data WHERE id IN (' .
                 join(',', @$attach_ids) . ')');
    }

322
    $dbh->bz_commit_transaction();
323
    Status('attachment_reference_deletion_end');
324
    $clear_memcached = 1 if @$attach_ids;
325
}
326

327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
###########################################################################
# Remove all references to deleted users or groups from whines
###########################################################################

if ($cgi->param('remove_old_whine_targets')) {
    Status('whines_obsolete_target_deletion_start');

    $dbh->bz_start_transaction();

    foreach my $target (['groups', 'id', MAILTO_GROUP],
                        ['profiles', 'userid', MAILTO_USER])
    {
        my ($table, $col, $type) = @$target;
        my $old_ids =
          $dbh->selectcol_arrayref("SELECT DISTINCT mailto
                                      FROM whine_schedules
                                 LEFT JOIN $table
                                        ON $table.$col = whine_schedules.mailto
                                     WHERE mailto_type = $type AND $table.$col IS NULL");

        if (scalar(@$old_ids)) {
            $dbh->do("DELETE FROM whine_schedules
                       WHERE mailto_type = $type AND mailto IN (" .
                       join(',', @$old_ids) . ")");
351
            $clear_memcached = 1;
352 353 354 355 356 357
        }
    }
    $dbh->bz_commit_transaction();
    Status('whines_obsolete_target_deletion_end');
}

358 359 360 361
# If any repairs were attempted or made, we need to clear memcached to ensure
# state is consistent.
Bugzilla->memcached->clear_all() if $clear_memcached;

362 363 364 365
###########################################################################
# Repair hook
###########################################################################

366
Bugzilla::Hook::process('sanitycheck_repair', { status => \&Status });
367 368 369 370

###########################################################################
# Checks
###########################################################################
371
Status('checks_start');
372

373 374 375 376
###########################################################################
# Perform referential (cross) checks
###########################################################################

377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
# This checks that a simple foreign key has a valid primary key value.  NULL
# references are acceptable and cause no problem.
#
# The first parameter is the primary key table name.
# The second parameter is the primary key field name.
# Each successive parameter represents a foreign key, it must be a list
# reference, where the list has:
#   the first value is the foreign key table name.
#   the second value is the foreign key field name.
#   the third value is optional and represents a field on the foreign key
#     table to display when the check fails.
#   the fourth value is optional and is a list reference to values that
#     are excluded from checking.
#
# FIXME: The excluded values parameter should go away - the QA contact
#        fields should use NULL instead - see bug #109474.
393
#        The same goes for series; no bug for that yet.
394

395 396 397
sub CrossCheck {
    my $table = shift @_;
    my $field = shift @_;
398
    my $dbh = Bugzilla->dbh;
399

400
    Status('cross_check_to', {table => $table, field => $field});
401 402 403 404 405 406 407 408

    while (@_) {
        my $ref = shift @_;
        my ($refertable, $referfield, $keyname, $exceptions) = @$ref;

        $exceptions ||= [];
        my %exceptions = map { $_ => 1 } @$exceptions;

409 410
        Status('cross_check_from', {table => $refertable, field => $referfield});

411 412 413 414 415 416 417
        my $query = qq{SELECT DISTINCT $refertable.$referfield} .
            ($keyname ? qq{, $refertable.$keyname } : q{}) .
                     qq{ FROM $refertable
                    LEFT JOIN $table
                           ON $refertable.$referfield = $table.$field
                        WHERE $table.$field IS NULL
                          AND $refertable.$referfield IS NOT NULL};
418

419 420
        my $sth = $dbh->prepare($query);
        $sth->execute;
421

422
        my $has_bad_references = 0;
423 424 425

        while (my ($value, $key) = $sth->fetchrow_array) {
            next if $exceptions{$value};
426 427 428
            Status('cross_check_alert', {value => $value, table => $refertable,
                                         field => $referfield, keyname => $keyname,
                                         key => $key}, 'alert');
429
            $has_bad_references = 1;
430
        }
431 432
        # References to non existent bugs can be safely removed, bug 288461
        if ($table eq 'bugs' && $has_bad_references) {
433
            Status('cross_check_bug_has_references');
434 435 436
        }
        # References to non existent attachments can be safely removed.
        if ($table eq 'attachments' && $has_bad_references) {
437
            Status('cross_check_attachment_has_references');
438
        }
439 440 441
    }
}

442 443 444
CrossCheck('classifications', 'id',
           ['products', 'classification_id']);

445 446 447
CrossCheck("keyworddefs", "id",
           ["keywords", "keywordid"]);

448
CrossCheck("fielddefs", "id",
449 450
           ["bugs_activity", "fieldid"],
           ['profiles_activity', 'fieldid']);
451

452
CrossCheck("flagtypes", "id",
453 454 455
           ["flags", "type_id"],
           ["flagexclusions", "type_id"],
           ["flaginclusions", "type_id"]);
456 457 458

CrossCheck("bugs", "bug_id",
           ["bugs_activity", "bug_id"],
459
           ["bug_group_map", "bug_id"],
460
           ["bugs_fulltext", "bug_id"],
461 462 463 464 465
           ["attachments", "bug_id"],
           ["cc", "bug_id"],
           ["longdescs", "bug_id"],
           ["dependencies", "blocked"],
           ["dependencies", "dependson"],
466
           ['flags', 'bug_id'],
467 468 469
           ["keywords", "bug_id"],
           ["duplicates", "dupe_of", "dupe"],
           ["duplicates", "dupe", "dupe_of"]);
470

471 472
CrossCheck("groups", "id",
           ["bug_group_map", "group_id"],
473
           ['category_group_map', 'group_id'],
474 475
           ["group_group_map", "grantor_id"],
           ["group_group_map", "member_id"],
476
           ["group_control_map", "group_id"],
477
           ["namedquery_group_map", "group_id"],
478 479 480
           ["user_group_map", "group_id"],
           ["flagtypes", "grant_group_id"],
           ["flagtypes", "request_group_id"]);
481

482 483 484 485 486
CrossCheck("namedqueries", "id",
           ["namedqueries_link_in_footer", "namedquery_id"],
           ["namedquery_group_map", "namedquery_id"],
          );

487
CrossCheck("profiles", "userid",
488 489
           ['profiles_activity', 'userid'],
           ['profiles_activity', 'who'],
490 491
           ['email_setting', 'user_id'],
           ['profile_setting', 'user_id'],
492 493
           ["bugs", "reporter", "bug_id"],
           ["bugs", "assigned_to", "bug_id"],
494
           ["bugs", "qa_contact", "bug_id"],
495
           ["attachments", "submitter_id", "bug_id"],
496 497
           ['flags', 'setter_id', 'bug_id'],
           ['flags', 'requestee_id', 'bug_id'],
498 499
           ["bugs_activity", "who", "bug_id"],
           ["cc", "who", "bug_id"],
500
           ['quips', 'userid'],
501
           ["longdescs", "who", "bug_id"],
502
           ["logincookies", "userid"],
503
           ["namedqueries", "userid"],
504
           ["namedqueries_link_in_footer", "user_id"],
505
           ['series', 'creator', 'series_id'],
506 507
           ["watch", "watcher"],
           ["watch", "watched"],
508
           ['whine_events', 'owner_userid'],
509
           ["tokens", "userid"],
510
           ["user_group_map", "user_id"],
511
           ["components", "initialowner", "name"],
512 513
           ["components", "initialqacontact", "name"],
           ["component_cc", "user_id"]);
514

515 516 517 518 519
CrossCheck("products", "id",
           ["bugs", "product_id", "bug_id"],
           ["components", "product_id", "name"],
           ["milestones", "product_id", "value"],
           ["versions", "product_id", "value"],
520
           ["group_control_map", "product_id"],
521 522
           ["flaginclusions", "product_id", "type_id"],
           ["flagexclusions", "product_id", "type_id"]);
523

524
CrossCheck("components", "id",
525 526 527
           ["component_cc", "component_id"],
           ["flagexclusions", "component_id", "type_id"],
           ["flaginclusions", "component_id", "type_id"]);
528

529
# Check the former enum types -mkanat@bugzilla.org
530
CrossCheck("bug_status", "value",
531
            ["bugs", "bug_status", "bug_id"]);
532 533

CrossCheck("resolution", "value",
534
            ["bugs", "resolution", "bug_id"]);
535 536

CrossCheck("bug_severity", "value",
537
            ["bugs", "bug_severity", "bug_id"]);
538 539

CrossCheck("op_sys", "value",
540
            ["bugs", "op_sys", "bug_id"]);
541 542

CrossCheck("priority", "value",
543
            ["bugs", "priority", "bug_id"]);
544 545

CrossCheck("rep_platform", "value",
546
            ["bugs", "rep_platform", "bug_id"]);
547

548 549 550 551
CrossCheck('series', 'series_id',
           ['series_data', 'series_id']);

CrossCheck('series_categories', 'id',
552 553 554
           ['series', 'category'],
           ["category_group_map", "category_id"],
           ["series", "subcategory"]);
555 556 557 558 559

CrossCheck('whine_events', 'id',
           ['whine_queries', 'eventid'],
           ['whine_schedules', 'eventid']);

560
CrossCheck('attachments', 'attach_id',
561 562
           ['attach_data', 'id'],
           ['bugs_activity', 'attach_id']);
563

564 565 566 567
CrossCheck('bug_status', 'id',
           ['status_workflow', 'old_status'],
           ['status_workflow', 'new_status']);

568
###########################################################################
569
# Perform double field referential (cross) checks
570
###########################################################################
571
 
572 573 574 575 576 577 578 579 580 581 582 583 584 585
# This checks that a compound two-field foreign key has a valid primary key
# value.  NULL references are acceptable and cause no problem.
#
# The first parameter is the primary key table name.
# The second parameter is the primary key first field name.
# The third parameter is the primary key second field name.
# Each successive parameter represents a foreign key, it must be a list
# reference, where the list has:
#   the first value is the foreign key table name
#   the second value is the foreign key first field name.
#   the third value is the foreign key second field name.
#   the fourth value is optional and represents a field on the foreign key
#     table to display when the check fails

586 587 588 589
sub DoubleCrossCheck {
    my $table = shift @_;
    my $field1 = shift @_;
    my $field2 = shift @_;
590
    my $dbh = Bugzilla->dbh;
591 592 593 594

    Status('double_cross_check_to',
           {table => $table, field1 => $field1, field2 => $field2});

595 596 597
    while (@_) {
        my $ref = shift @_;
        my ($refertable, $referfield1, $referfield2, $keyname) = @$ref;
598 599 600

        Status('double_cross_check_from',
               {table => $refertable, field1 => $referfield1, field2 =>$referfield2});
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616

        my $d_cross_check = $dbh->selectall_arrayref(qq{
                        SELECT DISTINCT $refertable.$referfield1, 
                                        $refertable.$referfield2 } .
                       ($keyname ? qq{, $refertable.$keyname } : q{}) .
                      qq{ FROM $refertable
                     LEFT JOIN $table
                            ON $refertable.$referfield1 = $table.$field1
                           AND $refertable.$referfield2 = $table.$field2 
                         WHERE $table.$field1 IS NULL 
                           AND $table.$field2 IS NULL 
                           AND $refertable.$referfield1 IS NOT NULL 
                           AND $refertable.$referfield2 IS NOT NULL});

        foreach my $check (@$d_cross_check) {
            my ($value1, $value2, $key) = @$check;
617 618 619 620 621
            Status('double_cross_check_alert',
                   {value1 => $value1, value2 => $value2,
                    table => $refertable,
                    field1 => $referfield1, field2 => $referfield2,
                    keyname => $keyname, key => $key}, 'alert');
622
        }
623 624 625
    }
}

626 627 628 629
DoubleCrossCheck('attachments', 'bug_id', 'attach_id',
                 ['flags', 'bug_id', 'attach_id'],
                 ['bugs_activity', 'bug_id', 'attach_id']);

630
DoubleCrossCheck("components", "product_id", "id",
631 632 633
                 ["bugs", "product_id", "component_id", "bug_id"],
                 ['flagexclusions', 'product_id', 'component_id'],
                 ['flaginclusions', 'product_id', 'component_id']);
634

635 636 637 638 639 640
DoubleCrossCheck("versions", "product_id", "value",
                 ["bugs", "product_id", "version", "bug_id"]);
 
DoubleCrossCheck("milestones", "product_id", "value",
                 ["bugs", "product_id", "target_milestone", "bug_id"],
                 ["products", "id", "defaultmilestone", "name"]);
641

642 643 644
###########################################################################
# Perform login checks
###########################################################################
645 646

Status('profile_login_start');
647

648 649
my $sth = $dbh->prepare(q{SELECT userid, login_name FROM profiles});
$sth->execute;
650

651
while (my ($id, $email) = $sth->fetchrow_array) {
652
    validate_email_syntax($email)
653
      || Status('profile_login_alert', {id => $id, email => $email}, 'alert');
654
}
655

656
###########################################################################
657
# Perform keyword checks
658
###########################################################################
659

660
sub check_keywords {
661
    my $dbh = Bugzilla->dbh;
662
    my $cgi = Bugzilla->cgi;
663

664
    Status('keyword_check_start');
665

666 667 668
    my %keywordids;
    my $keywords = $dbh->selectall_arrayref(q{SELECT id, name
                                                FROM keyworddefs});
669

670 671 672
    foreach (@$keywords) {
        my ($id, $name) = @$_;
        if ($keywordids{$id}) {
673
            Status('keyword_check_alert', {id => $id}, 'alert');
674 675 676
        }
        $keywordids{$id} = 1;
        if ($name =~ /[\s,]/) {
677
            Status('keyword_check_invalid_name', {id => $id}, 'alert');
678 679
        }
    }
680

681 682 683 684 685 686 687 688
    my $sth = $dbh->prepare(q{SELECT bug_id, keywordid
                                FROM keywords
                            ORDER BY bug_id, keywordid});
    $sth->execute;
    my $lastid;
    my $lastk;
    while (my ($id, $k) = $sth->fetchrow_array) {
        if (!$keywordids{$k}) {
689
            Status('keyword_check_invalid_id', {id => $k}, 'alert');
690
        }
691
        if (defined $lastid && $id eq $lastid && $k eq $lastk) {
692
            Status('keyword_check_duplicated_ids', {id => $id}, 'alert');
693
        }
694 695
        $lastid = $id;
        $lastk = $k;
696
    }
697
}
698

699 700 701 702
###########################################################################
# Check for flags being in incorrect products and components
###########################################################################

703
Status('flag_check_start');
704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731

my $invalid_flags = $dbh->selectall_arrayref(
       'SELECT DISTINCT flags.id, flags.bug_id, flags.attach_id
          FROM flags
    INNER JOIN bugs
            ON flags.bug_id = bugs.bug_id
     LEFT JOIN flaginclusions AS i
            ON flags.type_id = i.type_id
           AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
           AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
         WHERE i.type_id IS NULL');

my @invalid_flags = @$invalid_flags;

$invalid_flags = $dbh->selectall_arrayref(
       'SELECT DISTINCT flags.id, flags.bug_id, flags.attach_id
          FROM flags
    INNER JOIN bugs
            ON flags.bug_id = bugs.bug_id
    INNER JOIN flagexclusions AS e
            ON flags.type_id = e.type_id
         WHERE (bugs.product_id = e.product_id OR e.product_id IS NULL)
           AND (bugs.component_id = e.component_id OR e.component_id IS NULL)');

push(@invalid_flags, @$invalid_flags);

if (scalar(@invalid_flags)) {
    if ($cgi->param('remove_invalid_flags')) {
732
        Status('flag_deletion_start');
733 734 735
        my @flag_ids = map {$_->[0]} @invalid_flags;
        # Silently delete these flags, with no notification to requesters/setters.
        $dbh->do('DELETE FROM flags WHERE id IN (' . join(',', @flag_ids) .')');
736
        Status('flag_deletion_end');
737
        Bugzilla->memcached->clear_all();
738 739 740 741
    }
    else {
        foreach my $flag (@$invalid_flags) {
            my ($flag_id, $bug_id, $attach_id) = @$flag;
742 743 744
            Status('flag_alert',
                   {flag_id => $flag_id, attach_id => $attach_id, bug_id => $bug_id},
                   'alert');
745
        }
746
        Status('flag_fix');
747 748 749
    }
}

750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769
###########################################################################
# Check for products with no component
###########################################################################

Status('product_check_start');

my $products_missing_data = $dbh->selectcol_arrayref(
      'SELECT DISTINCT products.name
         FROM products
    LEFT JOIN components
           ON components.product_id = products.id
    LEFT JOIN versions
           ON versions.product_id = products.id
        WHERE components.id IS NULL
           OR versions.id IS NULL');

if (scalar(@$products_missing_data)) {
    Status('product_alert', { name => $_ }, 'alert') foreach @$products_missing_data;
}

770
###########################################################################
771
# General bug checks
772 773
###########################################################################

774
sub BugCheck {
775
    my ($middlesql, $errortext, $repairparam, $repairtext) = @_;
776
    my $dbh = Bugzilla->dbh;
777 778 779 780
 
    my $badbugs = $dbh->selectcol_arrayref(qq{SELECT DISTINCT bugs.bug_id
                                                FROM $middlesql 
                                            ORDER BY bugs.bug_id});
781

782
    if (scalar(@$badbugs)) {
783 784 785 786
        Status('bug_check_alert',
               {errortext => get_string($errortext), badbugs => $badbugs},
               'alert');

787
        if ($repairparam) {
788 789 790
            $repairtext ||= 'repair_bugs';
            Status('bug_check_repair',
                   {param => $repairparam, text => get_string($repairtext)});
791
        }
792
    }
793 794
}

795
Status('bug_check_creation_date');
796

797 798
BugCheck("bugs WHERE creation_ts IS NULL", 'bug_check_creation_date_error_text',
         'repair_creation_date', 'bug_check_creation_date_repair_text');
799

800 801 802 803 804 805
Status('bug_check_bugs_fulltext');

BugCheck("bugs LEFT JOIN bugs_fulltext ON bugs_fulltext.bug_id = bugs.bug_id " .
         "WHERE bugs_fulltext.bug_id IS NULL", 'bug_check_bugs_fulltext_error_text',
         'repair_bugs_fulltext', 'bug_check_bugs_fulltext_repair_text');

806
Status('bug_check_res_dupl');
807

808
BugCheck("bugs INNER JOIN duplicates ON bugs.bug_id = duplicates.dupe " .
809
         "WHERE bugs.resolution != 'DUPLICATE'", 'bug_check_res_dupl_error_text');
810

811 812
BugCheck("bugs LEFT JOIN duplicates ON bugs.bug_id = duplicates.dupe WHERE " .
         "bugs.resolution = 'DUPLICATE' AND " .
813
         "duplicates.dupe IS NULL", 'bug_check_res_dupl_error_text2');
814

815
Status('bug_check_status_res');
816

817
my @open_states = map($dbh->quote($_), BUG_STATE_OPEN);
818 819
my $open_states = join(', ', @open_states);

820
BugCheck("bugs WHERE bug_status IN ($open_states) AND resolution != ''",
821
         'bug_check_status_res_error_text');
822
BugCheck("bugs WHERE bug_status NOT IN ($open_states) AND resolution = ''",
823
         'bug_check_status_res_error_text2');
824

825
Status('bug_check_status_everconfirmed');
826

827
BugCheck("bugs WHERE bug_status = 'UNCONFIRMED' AND everconfirmed = 1",
828
         'bug_check_status_everconfirmed_error_text', 'repair_everconfirmed');
829 830 831 832 833

my @confirmed_open_states = grep {$_ ne 'UNCONFIRMED'} BUG_STATE_OPEN;
my $confirmed_open_states = join(', ', map {$dbh->quote($_)} @confirmed_open_states);

BugCheck("bugs WHERE bug_status IN ($confirmed_open_states) AND everconfirmed = 0",
834
         'bug_check_status_everconfirmed_error_text2', 'repair_everconfirmed');
835

836 837 838 839 840 841
###########################################################################
# Control Values
###########################################################################

# Checks for values that are invalid OR
# not among the 9 valid combinations
842
Status('bug_check_control_values');
843 844
my $groups = join(", ", (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT,
CONTROLMAPMANDATORY));
845
my $query = qq{
846 847 848 849 850 851 852 853
     SELECT COUNT(product_id) 
       FROM group_control_map 
      WHERE membercontrol NOT IN( $groups )
         OR othercontrol NOT IN( $groups )
         OR ((membercontrol != othercontrol)
             AND (membercontrol != } . CONTROLMAPSHOWN . q{)
             AND ((membercontrol != } . CONTROLMAPDEFAULT . q{)
                  OR (othercontrol = } . CONTROLMAPSHOWN . q{)))};
854

855 856 857 858
my $entries = $dbh->selectrow_array($query);
Status('bug_check_control_values_alert', {entries => $entries}, 'alert') if $entries;

Status('bug_check_control_values_violation');
859
BugCheck("bugs
860 861 862 863
         INNER JOIN bug_group_map
            ON bugs.bug_id = bug_group_map.bug_id
          LEFT JOIN group_control_map
            ON bugs.product_id = group_control_map.product_id
864
           AND bug_group_map.group_id = group_control_map.group_id
865
         WHERE ((group_control_map.membercontrol = " . CONTROLMAPNA . ")
866
         OR (group_control_map.membercontrol IS NULL))",
867
         'bug_check_control_values_error_text',
868
         'createmissinggroupcontrolmapentries',
869
         'bug_check_control_values_repair_text');
870

871
BugCheck("bugs
872
         INNER JOIN group_control_map
873
            ON bugs.product_id = group_control_map.product_id
874 875 876 877 878
         INNER JOIN groups
            ON group_control_map.group_id = groups.id
          LEFT JOIN bug_group_map
            ON bugs.bug_id = bug_group_map.bug_id
           AND group_control_map.group_id = bug_group_map.group_id
879
         WHERE group_control_map.membercontrol = " . CONTROLMAPMANDATORY . "
880 881
           AND bug_group_map.group_id IS NULL
           AND groups.isactive != 0",
882
         'bug_check_control_values_error_text2');
883

884 885 886 887
###########################################################################
# Unsent mail
###########################################################################

888
Status('unsent_bugmail_check');
889

890
my $time = $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE');
891 892 893 894
my $badbugs = $dbh->selectcol_arrayref(qq{
                    SELECT bug_id 
                      FROM bugs 
                     WHERE (lastdiffed IS NULL OR lastdiffed < delta_ts)
895
                       AND delta_ts < $time
896
                  ORDER BY bug_id});
897 898


899
if (scalar(@$badbugs > 0)) {
900 901
    Status('unsent_bugmail_alert', {badbugs => $badbugs}, 'alert');
    Status('unsent_bugmail_fix');
902 903
}

904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927
###########################################################################
# Whines
###########################################################################

Status('whines_obsolete_target_start');

my $display_repair_whines_link = 0;
foreach my $target (['groups', 'id', MAILTO_GROUP],
                    ['profiles', 'userid', MAILTO_USER])
{
    my ($table, $col, $type) = @$target;
    my $old = $dbh->selectall_arrayref("SELECT whine_schedules.id, mailto
                                          FROM whine_schedules
                                     LEFT JOIN $table
                                            ON $table.$col = whine_schedules.mailto
                                         WHERE mailto_type = $type AND $table.$col IS NULL");

    if (scalar(@$old)) {
        Status('whines_obsolete_target_alert', {schedules => $old, type => $type}, 'alert');
        $display_repair_whines_link = 1;
    }
}
Status('whines_obsolete_target_fix') if $display_repair_whines_link;

928 929 930 931
###########################################################################
# Check hook
###########################################################################

932
Bugzilla::Hook::process('sanitycheck_check', { status => \&Status });
933

934 935 936
###########################################################################
# End
###########################################################################
937

938 939
Status('checks_completed');

940 941 942 943
unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
    $template->process('global/footer.html.tmpl', $vars)
      || ThrowTemplateError($template->error());
}