sanitycheck.cgi 34.2 KB
Newer Older
1
#!/usr/bin/perl -wT
2
# -*- Mode: perl; indent-tabs-mode: nil -*-
3
#
4 5 6 7 8 9 10 11 12 13
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
14
# The Original Code is the Bugzilla Bug Tracking System.
15
#
16
# The Initial Developer of the Original Code is Netscape Communications
17 18 19 20
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
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::Constants;
33
use Bugzilla::Util;
34
use Bugzilla::Error;
35
use Bugzilla::Status;
36

37 38 39
###########################################################################
# General subs
###########################################################################
40

41 42 43 44
sub get_string {
    my ($san_tag, $vars) = @_;
    $vars->{'san_tag'} = $san_tag;
    return get_text('sanitycheck', $vars);
45 46
}

47 48
sub Status {
    my ($san_tag, $vars, $alert) = @_;
49 50 51 52 53 54 55 56 57 58 59 60 61
    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>';
        print $start_tag . get_string($san_tag, $vars) . "</p>\n";
    }
62 63
}

64 65 66
###########################################################################
# Start
###########################################################################
67

68
my $user = Bugzilla->login(LOGIN_REQUIRED);
69

70
my $cgi = Bugzilla->cgi;
71
my $dbh = Bugzilla->dbh;
72
my $template = Bugzilla->template;
73 74
my $vars = {};

75
print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
76

77 78 79
# 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.
80
$user->in_group("editcomponents")
81
  || ($user->in_group('editkeywords') && $cgi->param('rebuildkeywordcache'))
82
  || ThrowUserError("auth_failure", {group  => "editcomponents",
83 84
                                     action => "run",
                                     object => "sanity_check"});
85

86 87 88 89
unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
    $template->process('admin/sanitycheck/list.html.tmpl', $vars)
      || ThrowTemplateError($template->error());
}
90

91 92 93 94 95
###########################################################################
# Users with 'editkeywords' privs only can only check keywords.
###########################################################################
unless ($user->in_group('editcomponents')) {
    check_votes_or_keywords('keywords');
96 97 98 99
    Status('checks_completed');

    $template->process('global/footer.html.tmpl', $vars)
        || ThrowTemplateError($template->error());
100 101 102
    exit;
}

103 104 105 106
###########################################################################
# Fix vote cache
###########################################################################

107 108
if ($cgi->param('rebuildvotecache')) {
    Status('vote_cache_rebuild_start');
109
    $dbh->bz_start_transaction();
110 111 112 113 114 115 116 117 118
    $dbh->do(q{UPDATE bugs SET votes = 0});
    my $sth_update = $dbh->prepare(q{UPDATE bugs 
                                        SET votes = ? 
                                      WHERE bug_id = ?});
    my $sth = $dbh->prepare(q{SELECT bug_id, SUM(vote_count)
                                FROM votes }. $dbh->sql_group_by('bug_id'));
    $sth->execute();
    while (my ($id, $v) = $sth->fetchrow_array) {
        $sth_update->execute($v, $id);
119
    }
120
    $dbh->bz_commit_transaction();
121
    Status('vote_cache_rebuild_end');
122 123
}

124 125 126 127
###########################################################################
# Create missing group_control_map entries
###########################################################################

128 129
if ($cgi->param('createmissinggroupcontrolmapentries')) {
    Status('group_control_map_entries_creation');
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166

    my $na    = CONTROLMAPNA;
    my $shown = CONTROLMAPSHOWN;
    my $insertsth = $dbh->prepare(
        qq{INSERT INTO group_control_map (
                       group_id, product_id, entry,
                       membercontrol, othercontrol, canedit
                      )
               VALUES (
                       ?, ?, 0,
                       $shown, $na, 0
                      )});
    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
167 168
          } . $dbh->sql_group_by('bugs.product_id, bgm.group_id',
                                 'gcm.membercontrol, groups.name, products.name'));
169 170 171 172 173 174 175

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

        $counter++;
        if (defined($currentmembercontrol)) {
176 177
            Status('group_control_map_entries_update',
                   {group_name => $group_name, product_name => $product_name});
178 179 180
            $updatesth->execute($group_id, $product_id);
        }
        else {
181 182
            Status('group_control_map_entries_generation',
                   {group_name => $group_name, product_name => $product_name});
183 184 185 186
            $insertsth->execute($group_id, $product_id);
        }
    }

187
    Status('group_control_map_entries_repaired', {counter => $counter});
188 189
}

190 191 192 193
###########################################################################
# Fix missing creation date
###########################################################################

194 195
if ($cgi->param('repair_creation_date')) {
    Status('bug_creation_date_start');
196 197 198 199 200 201 202 203

    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,
204
    # even if no comment is required.
205 206 207 208 209 210 211 212
    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);
    }
213
    Status('bug_creation_date_fixed', {bug_count => scalar(@$bug_ids)});
214 215
}

216 217 218 219
###########################################################################
# Send unsent mail
###########################################################################

220
if ($cgi->param('rescanallBugMail')) {
221 222
    require Bugzilla::BugMail;

223
    Status('send_bugmail_start');
224
    my $time = $dbh->sql_interval(30, 'MINUTE');
225

226 227 228 229 230 231 232
    my $list = $dbh->selectcol_arrayref(qq{
                                        SELECT bug_id
                                          FROM bugs 
                                         WHERE (lastdiffed IS NULL
                                                OR lastdiffed < delta_ts)
                                           AND delta_ts < now() - $time
                                      ORDER BY bug_id});
233

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

236 237 238 239 240 241 242 243 244
    # 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.
    $vars->{'changer'} = Bugzilla->user->login;

245
    foreach my $bugid (@$list) {
246
        Bugzilla::BugMail::Send($bugid, $vars);
247 248
    }

249
    Status('send_bugmail_end') if scalar(@$list);
250

251 252 253 254
    unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
        $template->process('global/footer.html.tmpl', $vars)
          || ThrowTemplateError($template->error());
    }
255 256 257
    exit;
}

258 259 260 261
###########################################################################
# Remove all references to deleted bugs
###########################################################################

262 263
if ($cgi->param('remove_invalid_bug_references')) {
    Status('bug_reference_deletion_start');
264

265
    $dbh->bz_start_transaction();
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284

    foreach my $pair ('attachments/', 'bug_group_map/', 'bugs_activity/', 'cc/',
                      'dependencies/blocked', 'dependencies/dependson',
                      'duplicates/dupe', 'duplicates/dupe_of',
                      'flags/', 'keywords/', 'longdescs/', 'votes/') {

        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) . ")");
        }
    }

285
    $dbh->bz_commit_transaction();
286
    Status('bug_reference_deletion_end');
287 288
}

289 290 291 292
###########################################################################
# Remove all references to deleted attachments
###########################################################################

293 294
if ($cgi->param('remove_invalid_attach_references')) {
    Status('attachment_reference_deletion_start');
295

296
    $dbh->bz_start_transaction();
297 298 299 300 301 302 303 304 305 306 307 308 309

    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) . ')');
    }

310
    $dbh->bz_commit_transaction();
311
    Status('attachment_reference_deletion_end');
312
}
313

314
Status('checks_start');
315

316 317 318 319
###########################################################################
# Perform referential (cross) checks
###########################################################################

320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
# 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.
336
#        The same goes for series; no bug for that yet.
337

338 339 340
sub CrossCheck {
    my $table = shift @_;
    my $field = shift @_;
341
    my $dbh = Bugzilla->dbh;
342

343
    Status('cross_check_to', {table => $table, field => $field});
344 345 346 347 348 349 350 351

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

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

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

354 355 356 357 358 359 360
        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};
361

362 363
        my $sth = $dbh->prepare($query);
        $sth->execute;
364

365
        my $has_bad_references = 0;
366 367 368

        while (my ($value, $key) = $sth->fetchrow_array) {
            next if $exceptions{$value};
369 370 371
            Status('cross_check_alert', {value => $value, table => $refertable,
                                         field => $referfield, keyname => $keyname,
                                         key => $key}, 'alert');
372
            $has_bad_references = 1;
373
        }
374 375
        # References to non existent bugs can be safely removed, bug 288461
        if ($table eq 'bugs' && $has_bad_references) {
376
            Status('cross_check_bug_has_references');
377 378 379
        }
        # References to non existent attachments can be safely removed.
        if ($table eq 'attachments' && $has_bad_references) {
380
            Status('cross_check_attachment_has_references');
381
        }
382 383 384
    }
}

385 386 387
CrossCheck('classifications', 'id',
           ['products', 'classification_id']);

388 389 390
CrossCheck("keyworddefs", "id",
           ["keywords", "keywordid"]);

391
CrossCheck("fielddefs", "id",
392 393
           ["bugs_activity", "fieldid"],
           ['profiles_activity', 'fieldid']);
394

395 396
CrossCheck("flagtypes", "id",
           ["flags", "type_id"]);
397 398 399

CrossCheck("bugs", "bug_id",
           ["bugs_activity", "bug_id"],
400
           ["bug_group_map", "bug_id"],
401 402 403 404 405
           ["attachments", "bug_id"],
           ["cc", "bug_id"],
           ["longdescs", "bug_id"],
           ["dependencies", "blocked"],
           ["dependencies", "dependson"],
406
           ['flags', 'bug_id'],
407
           ["votes", "bug_id"],
408 409 410
           ["keywords", "bug_id"],
           ["duplicates", "dupe_of", "dupe"],
           ["duplicates", "dupe", "dupe_of"]);
411

412 413
CrossCheck("groups", "id",
           ["bug_group_map", "group_id"],
414
           ['category_group_map', 'group_id'],
415 416
           ["group_group_map", "grantor_id"],
           ["group_group_map", "member_id"],
417
           ["group_control_map", "group_id"],
418
           ["namedquery_group_map", "group_id"],
419 420
           ["user_group_map", "group_id"]);

421 422 423 424 425
CrossCheck("namedqueries", "id",
           ["namedqueries_link_in_footer", "namedquery_id"],
           ["namedquery_group_map", "namedquery_id"],
          );

426
CrossCheck("profiles", "userid",
427 428
           ['profiles_activity', 'userid'],
           ['profiles_activity', 'who'],
429 430
           ['email_setting', 'user_id'],
           ['profile_setting', 'user_id'],
431 432
           ["bugs", "reporter", "bug_id"],
           ["bugs", "assigned_to", "bug_id"],
433
           ["bugs", "qa_contact", "bug_id"],
434
           ["attachments", "submitter_id", "bug_id"],
435 436
           ['flags', 'setter_id', 'bug_id'],
           ['flags', 'requestee_id', 'bug_id'],
437 438
           ["bugs_activity", "who", "bug_id"],
           ["cc", "who", "bug_id"],
439
           ['quips', 'userid'],
440 441
           ["votes", "who", "bug_id"],
           ["longdescs", "who", "bug_id"],
442
           ["logincookies", "userid"],
443
           ["namedqueries", "userid"],
444
           ["namedqueries_link_in_footer", "user_id"],
445
           ['series', 'creator', 'series_id'],
446 447
           ["watch", "watcher"],
           ["watch", "watched"],
448
           ['whine_events', 'owner_userid'],
449
           ["tokens", "userid"],
450
           ["user_group_map", "user_id"],
451
           ["components", "initialowner", "name"],
452 453
           ["components", "initialqacontact", "name"],
           ["component_cc", "user_id"]);
454

455 456 457 458 459
CrossCheck("products", "id",
           ["bugs", "product_id", "bug_id"],
           ["components", "product_id", "name"],
           ["milestones", "product_id", "value"],
           ["versions", "product_id", "value"],
460
           ["group_control_map", "product_id"],
461 462
           ["flaginclusions", "product_id", "type_id"],
           ["flagexclusions", "product_id", "type_id"]);
463

464 465 466
CrossCheck("components", "id",
           ["component_cc", "component_id"]);

467
# Check the former enum types -mkanat@bugzilla.org
468
CrossCheck("bug_status", "value",
469
            ["bugs", "bug_status", "bug_id"]);
470 471

CrossCheck("resolution", "value",
472
            ["bugs", "resolution", "bug_id"]);
473 474

CrossCheck("bug_severity", "value",
475
            ["bugs", "bug_severity", "bug_id"]);
476 477

CrossCheck("op_sys", "value",
478
            ["bugs", "op_sys", "bug_id"]);
479 480

CrossCheck("priority", "value",
481
            ["bugs", "priority", "bug_id"]);
482 483

CrossCheck("rep_platform", "value",
484
            ["bugs", "rep_platform", "bug_id"]);
485

486 487 488 489 490 491 492 493 494 495
CrossCheck('series', 'series_id',
           ['series_data', 'series_id']);

CrossCheck('series_categories', 'id',
           ['series', 'category']);

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

496 497 498
CrossCheck('attachments', 'attach_id',
           ['attach_data', 'id']);

499 500 501 502
CrossCheck('bug_status', 'id',
           ['status_workflow', 'old_status'],
           ['status_workflow', 'new_status']);

503
###########################################################################
504
# Perform double field referential (cross) checks
505
###########################################################################
506
 
507 508 509 510 511 512 513 514 515 516 517 518 519 520
# 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

521 522 523 524
sub DoubleCrossCheck {
    my $table = shift @_;
    my $field1 = shift @_;
    my $field2 = shift @_;
525
    my $dbh = Bugzilla->dbh;
526 527 528 529

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

530 531 532
    while (@_) {
        my $ref = shift @_;
        my ($refertable, $referfield1, $referfield2, $keyname) = @$ref;
533 534 535

        Status('double_cross_check_from',
               {table => $refertable, field1 => $referfield1, field2 =>$referfield2});
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551

        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;
552 553 554 555 556
            Status('double_cross_check_alert',
                   {value1 => $value1, value2 => $value2,
                    table => $refertable,
                    field1 => $referfield1, field2 => $referfield2,
                    keyname => $keyname, key => $key}, 'alert');
557
        }
558 559 560
    }
}

561 562 563 564
DoubleCrossCheck('attachments', 'bug_id', 'attach_id',
                 ['flags', 'bug_id', 'attach_id'],
                 ['bugs_activity', 'bug_id', 'attach_id']);

565
DoubleCrossCheck("components", "product_id", "id",
566 567 568
                 ["bugs", "product_id", "component_id", "bug_id"],
                 ['flagexclusions', 'product_id', 'component_id'],
                 ['flaginclusions', 'product_id', 'component_id']);
569

570 571 572 573 574 575
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"]);
576

577 578 579
###########################################################################
# Perform login checks
###########################################################################
580 581

Status('profile_login_start');
582

583 584
my $sth = $dbh->prepare(q{SELECT userid, login_name FROM profiles});
$sth->execute;
585

586
while (my ($id, $email) = $sth->fetchrow_array) {
587
    validate_email_syntax($email)
588
      || Status('profile_login_alert', {id => $id, email => $email}, 'alert');
589
}
590

591 592 593
###########################################################################
# Perform vote/keyword cache checks
###########################################################################
594

595
check_votes_or_keywords();
596

597 598
sub check_votes_or_keywords {
    my $check = shift || 'all';
599

600 601 602 603 604
    my $dbh = Bugzilla->dbh;
    my $sth = $dbh->prepare(q{SELECT bug_id, votes, keywords
                                FROM bugs
                               WHERE votes != 0 OR keywords != ''});
    $sth->execute;
605

606 607
    my %votes;
    my %keyword;
608

609 610 611 612 613 614
    while (my ($id, $v, $k) = $sth->fetchrow_array) {
        if ($v != 0) {
            $votes{$id} = $v;
        }
        if ($k) {
            $keyword{$id} = $k;
615 616
        }
    }
617

618 619 620 621
    # If we only want to check keywords, skip checks about votes.
    _check_votes(\%votes) unless ($check eq 'keywords');
    # If we only want to check votes, skip checks about keywords.
    _check_keywords(\%keyword) unless ($check eq 'votes');
622 623
}

624 625
sub _check_votes {
    my $votes = shift;
626

627
    Status('vote_count_start');
628 629 630 631 632
    my $dbh = Bugzilla->dbh;
    my $sth = $dbh->prepare(q{SELECT bug_id, SUM(vote_count)
                                FROM votes }.
                                $dbh->sql_group_by('bug_id'));
    $sth->execute;
633

634
    my $offer_votecache_rebuild = 0;
635

636 637
    while (my ($id, $v) = $sth->fetchrow_array) {
        if ($v <= 0) {
638
            Status('vote_count_alert', {id => $id}, 'alert');
639 640
        } else {
            if (!defined $votes->{$id} || $votes->{$id} != $v) {
641
                Status('vote_cache_alert', {id => $id}, 'alert');
642 643 644 645
                $offer_votecache_rebuild = 1;
            }
            delete $votes->{$id};
        }
646
    }
647
    foreach my $id (keys %$votes) {
648
        Status('vote_cache_alert', {id => $id}, 'alert');
649
        $offer_votecache_rebuild = 1;
650 651
    }

652
    Status('vote_cache_rebuild_fix') if $offer_votecache_rebuild;
653 654
}

655 656
sub _check_keywords {
    my $keyword = shift;
657

658
    Status('keyword_check_start');
659 660
    my $dbh = Bugzilla->dbh;
    my $cgi = Bugzilla->cgi;
661

662 663 664
    my %keywordids;
    my $keywords = $dbh->selectall_arrayref(q{SELECT id, name
                                                FROM keyworddefs});
665

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

677 678 679 680 681 682 683 684
    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}) {
685
            Status('keyword_check_invalid_id', {id => $k}, 'alert');
686
        }
687
        if (defined $lastid && $id eq $lastid && $k eq $lastk) {
688
            Status('keyword_check_duplicated_ids', {id => $id}, 'alert');
689
        }
690 691
        $lastid = $id;
        $lastk = $k;
692 693
    }

694
    Status('keyword_cache_start');
695

696
    if ($cgi->param('rebuildkeywordcache')) {
697
        $dbh->bz_start_transaction();
698
    }
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725

    my $query = q{SELECT keywords.bug_id, keyworddefs.name
                    FROM keywords
              INNER JOIN keyworddefs
                      ON keyworddefs.id = keywords.keywordid
              INNER JOIN bugs
                      ON keywords.bug_id = bugs.bug_id
                ORDER BY keywords.bug_id, keyworddefs.name};

    $sth = $dbh->prepare($query);
    $sth->execute;

    my $lastb = 0;
    my @list;
    my %realk;
    while (1) {
        my ($b, $k) = $sth->fetchrow_array;
        if (!defined $b || $b != $lastb) {
            if (@list) {
                $realk{$lastb} = join(', ', @list);
            }
            last unless $b;

            $lastb = $b;
            @list = ();
        }
        push(@list, $k);
726
    }
727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742

    my @badbugs = ();

    foreach my $b (keys(%$keyword)) {
        if (!exists $realk{$b} || $realk{$b} ne $keyword->{$b}) {
            push(@badbugs, $b);
        }
    }
    foreach my $b (keys(%realk)) {
        if (!exists $keyword->{$b}) {
            push(@badbugs, $b);
        }
    }
    if (@badbugs) {
        @badbugs = sort {$a <=> $b} @badbugs;

743 744 745 746
        if ($cgi->param('rebuildkeywordcache')) {
            my $sth_update = $dbh->prepare(q{UPDATE bugs
                                                SET keywords = ?
                                              WHERE bug_id = ?});
747

748
            Status('keyword_cache_fixing');
749 750 751 752 753 754
            foreach my $b (@badbugs) {
                my $k = '';
                if (exists($realk{$b})) {
                    $k = $realk{$b};
                }
                $sth_update->execute($k, $b);
755
            }
756
            Status('keyword_cache_fixed');
757
        } else {
758 759
            Status('keyword_cache_alert', {badbugs => \@badbugs}, 'alert');
            Status('keyword_cache_rebuild');
760 761 762
        }
    }

763
    if ($cgi->param('rebuildkeywordcache')) {
764
        $dbh->bz_commit_transaction();
765
    }
766
}
767

768 769 770 771
###########################################################################
# Check for flags being in incorrect products and components
###########################################################################

772
Status('flag_check_start');
773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800

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')) {
801
        Status('flag_deletion_start');
802 803 804
        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) .')');
805
        Status('flag_deletion_end');
806 807 808 809
    }
    else {
        foreach my $flag (@$invalid_flags) {
            my ($flag_id, $bug_id, $attach_id) = @$flag;
810 811 812
            Status('flag_alert',
                   {flag_id => $flag_id, attach_id => $attach_id, bug_id => $bug_id},
                   'alert');
813
        }
814
        Status('flag_fix');
815 816 817
    }
}

818
###########################################################################
819
# General bug checks
820 821
###########################################################################

822
sub BugCheck {
823
    my ($middlesql, $errortext, $repairparam, $repairtext) = @_;
824
    my $dbh = Bugzilla->dbh;
825 826 827 828
 
    my $badbugs = $dbh->selectcol_arrayref(qq{SELECT DISTINCT bugs.bug_id
                                                FROM $middlesql 
                                            ORDER BY bugs.bug_id});
829

830
    if (scalar(@$badbugs)) {
831 832 833 834
        Status('bug_check_alert',
               {errortext => get_string($errortext), badbugs => $badbugs},
               'alert');

835
        if ($repairparam) {
836 837 838
            $repairtext ||= 'repair_bugs';
            Status('bug_check_repair',
                   {param => $repairparam, text => get_string($repairtext)});
839
        }
840
    }
841 842
}

843
Status('bug_check_creation_date');
844

845 846
BugCheck("bugs WHERE creation_ts IS NULL", 'bug_check_creation_date_error_text',
         'repair_creation_date', 'bug_check_creation_date_repair_text');
847

848
Status('bug_check_res_dupl');
849

850
BugCheck("bugs INNER JOIN duplicates ON bugs.bug_id = duplicates.dupe " .
851
         "WHERE bugs.resolution != 'DUPLICATE'", 'bug_check_res_dupl_error_text');
852

853 854
BugCheck("bugs LEFT JOIN duplicates ON bugs.bug_id = duplicates.dupe WHERE " .
         "bugs.resolution = 'DUPLICATE' AND " .
855
         "duplicates.dupe IS NULL", 'bug_check_res_dupl_error_text2');
856

857
Status('bug_check_status_res');
858

859
my @open_states = map($dbh->quote($_), BUG_STATE_OPEN);
860 861
my $open_states = join(', ', @open_states);

862
BugCheck("bugs WHERE bug_status IN ($open_states) AND resolution != ''",
863
         'bug_check_status_res_error_text');
864
BugCheck("bugs WHERE bug_status NOT IN ($open_states) AND resolution = ''",
865
         'bug_check_status_res_error_text2');
866

867
Status('bug_check_status_everconfirmed');
868

869
BugCheck("bugs WHERE bug_status = 'UNCONFIRMED' AND everconfirmed = 1",
870
         'bug_check_status_everconfirmed_error_text');
871 872 873 874 875

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",
876
         'bug_check_status_everconfirmed_error_text2');
877

878
Status('bug_check_votes_everconfirmed');
879

880 881
BugCheck("bugs INNER JOIN products ON bugs.product_id = products.id " .
         "WHERE everconfirmed = 0 AND votestoconfirm <= votes",
882
         'bug_check_votes_everconfirmed_error_text');
883

884 885 886 887 888 889
###########################################################################
# Control Values
###########################################################################

# Checks for values that are invalid OR
# not among the 9 valid combinations
890
Status('bug_check_control_values');
891 892
my $groups = join(", ", (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT,
CONTROLMAPMANDATORY));
893
my $query = qq{
894 895 896 897 898 899 900 901
     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{)))};
902

903 904 905 906
my $entries = $dbh->selectrow_array($query);
Status('bug_check_control_values_alert', {entries => $entries}, 'alert') if $entries;

Status('bug_check_control_values_violation');
907
BugCheck("bugs
908 909 910 911
         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
912
           AND bug_group_map.group_id = group_control_map.group_id
913
         WHERE ((group_control_map.membercontrol = " . CONTROLMAPNA . ")
914
         OR (group_control_map.membercontrol IS NULL))",
915
         'bug_check_control_values_error_text',
916
         'createmissinggroupcontrolmapentries',
917
         'bug_check_control_values_repair_text');
918

919
BugCheck("bugs
920
         INNER JOIN group_control_map
921
            ON bugs.product_id = group_control_map.product_id
922 923 924 925 926
         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
927
         WHERE group_control_map.membercontrol = " . CONTROLMAPMANDATORY . "
928 929
           AND bug_group_map.group_id IS NULL
           AND groups.isactive != 0",
930
         'bug_check_control_values_error_text2');
931

932 933 934 935
###########################################################################
# Unsent mail
###########################################################################

936
Status('unsent_bugmail_check');
937

938 939 940 941 942 943 944
my $time = $dbh->sql_interval(30, 'MINUTE');
my $badbugs = $dbh->selectcol_arrayref(qq{
                    SELECT bug_id 
                      FROM bugs 
                     WHERE (lastdiffed IS NULL OR lastdiffed < delta_ts)
                       AND delta_ts < now() - $time
                  ORDER BY bug_id});
945 946


947
if (scalar(@$badbugs > 0)) {
948 949
    Status('unsent_bugmail_alert', {badbugs => $badbugs}, 'alert');
    Status('unsent_bugmail_fix');
950 951
}

952 953 954
###########################################################################
# End
###########################################################################
955

956 957
Status('checks_completed');

958 959 960 961
unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
    $template->process('global/footer.html.tmpl', $vars)
      || ThrowTemplateError($template->error());
}