sanitycheck.cgi 34.5 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 73 74 75 76 77 78 79 80
# 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) {
    $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
}
else {
    $template = Bugzilla->template;
}
81 82
my $vars = {};

83
print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
84

85 86 87
# 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.
88
$user->in_group("editcomponents")
89
  || ($user->in_group('editkeywords') && $cgi->param('rebuildkeywordcache'))
90
  || ThrowUserError("auth_failure", {group  => "editcomponents",
91 92
                                     action => "run",
                                     object => "sanity_check"});
93

94 95 96 97
unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
    $template->process('admin/sanitycheck/list.html.tmpl', $vars)
      || ThrowTemplateError($template->error());
}
98

99 100 101 102 103
###########################################################################
# Users with 'editkeywords' privs only can only check keywords.
###########################################################################
unless ($user->in_group('editcomponents')) {
    check_votes_or_keywords('keywords');
104 105 106 107
    Status('checks_completed');

    $template->process('global/footer.html.tmpl', $vars)
        || ThrowTemplateError($template->error());
108 109 110
    exit;
}

111 112 113 114
###########################################################################
# Fix vote cache
###########################################################################

115 116
if ($cgi->param('rebuildvotecache')) {
    Status('vote_cache_rebuild_start');
117
    $dbh->bz_start_transaction();
118 119 120 121 122 123 124 125 126
    $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);
127
    }
128
    $dbh->bz_commit_transaction();
129
    Status('vote_cache_rebuild_end');
130 131
}

132 133 134 135
###########################################################################
# Create missing group_control_map entries
###########################################################################

136 137
if ($cgi->param('createmissinggroupcontrolmapentries')) {
    Status('group_control_map_entries_creation');
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 167 168 169 170 171 172 173 174

    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
175 176
          } . $dbh->sql_group_by('bugs.product_id, bgm.group_id',
                                 'gcm.membercontrol, groups.name, products.name'));
177 178 179 180 181 182 183

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

        $counter++;
        if (defined($currentmembercontrol)) {
184 185
            Status('group_control_map_entries_update',
                   {group_name => $group_name, product_name => $product_name});
186 187 188
            $updatesth->execute($group_id, $product_id);
        }
        else {
189 190
            Status('group_control_map_entries_generation',
                   {group_name => $group_name, product_name => $product_name});
191 192 193 194
            $insertsth->execute($group_id, $product_id);
        }
    }

195
    Status('group_control_map_entries_repaired', {counter => $counter});
196 197
}

198 199 200 201
###########################################################################
# Fix missing creation date
###########################################################################

202 203
if ($cgi->param('repair_creation_date')) {
    Status('bug_creation_date_start');
204 205 206 207 208 209 210 211

    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,
212
    # even if no comment is required.
213 214 215 216 217 218 219 220
    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);
    }
221
    Status('bug_creation_date_fixed', {bug_count => scalar(@$bug_ids)});
222 223
}

224 225 226 227
###########################################################################
# Send unsent mail
###########################################################################

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

231
    Status('send_bugmail_start');
232
    my $time = $dbh->sql_interval(30, 'MINUTE');
233

234 235 236 237 238 239 240
    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});
241

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

244 245 246 247 248 249 250 251 252
    # 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;

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

257
    Status('send_bugmail_end') if scalar(@$list);
258

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

266 267 268 269
###########################################################################
# Remove all references to deleted bugs
###########################################################################

270 271
if ($cgi->param('remove_invalid_bug_references')) {
    Status('bug_reference_deletion_start');
272

273
    $dbh->bz_start_transaction();
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292

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

293
    $dbh->bz_commit_transaction();
294
    Status('bug_reference_deletion_end');
295 296
}

297 298 299 300
###########################################################################
# Remove all references to deleted attachments
###########################################################################

301 302
if ($cgi->param('remove_invalid_attach_references')) {
    Status('attachment_reference_deletion_start');
303

304
    $dbh->bz_start_transaction();
305 306 307 308 309 310 311 312 313 314 315 316 317

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

318
    $dbh->bz_commit_transaction();
319
    Status('attachment_reference_deletion_end');
320
}
321

322
Status('checks_start');
323

324 325 326 327
###########################################################################
# Perform referential (cross) checks
###########################################################################

328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
# 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.
344
#        The same goes for series; no bug for that yet.
345

346 347 348
sub CrossCheck {
    my $table = shift @_;
    my $field = shift @_;
349
    my $dbh = Bugzilla->dbh;
350

351
    Status('cross_check_to', {table => $table, field => $field});
352 353 354 355 356 357 358 359

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

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

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

362 363 364 365 366 367 368
        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};
369

370 371
        my $sth = $dbh->prepare($query);
        $sth->execute;
372

373
        my $has_bad_references = 0;
374 375 376

        while (my ($value, $key) = $sth->fetchrow_array) {
            next if $exceptions{$value};
377 378 379
            Status('cross_check_alert', {value => $value, table => $refertable,
                                         field => $referfield, keyname => $keyname,
                                         key => $key}, 'alert');
380
            $has_bad_references = 1;
381
        }
382 383
        # References to non existent bugs can be safely removed, bug 288461
        if ($table eq 'bugs' && $has_bad_references) {
384
            Status('cross_check_bug_has_references');
385 386 387
        }
        # References to non existent attachments can be safely removed.
        if ($table eq 'attachments' && $has_bad_references) {
388
            Status('cross_check_attachment_has_references');
389
        }
390 391 392
    }
}

393 394 395
CrossCheck('classifications', 'id',
           ['products', 'classification_id']);

396 397 398
CrossCheck("keyworddefs", "id",
           ["keywords", "keywordid"]);

399
CrossCheck("fielddefs", "id",
400 401
           ["bugs_activity", "fieldid"],
           ['profiles_activity', 'fieldid']);
402

403 404
CrossCheck("flagtypes", "id",
           ["flags", "type_id"]);
405 406 407

CrossCheck("bugs", "bug_id",
           ["bugs_activity", "bug_id"],
408
           ["bug_group_map", "bug_id"],
409 410 411 412 413
           ["attachments", "bug_id"],
           ["cc", "bug_id"],
           ["longdescs", "bug_id"],
           ["dependencies", "blocked"],
           ["dependencies", "dependson"],
414
           ['flags', 'bug_id'],
415
           ["votes", "bug_id"],
416 417 418
           ["keywords", "bug_id"],
           ["duplicates", "dupe_of", "dupe"],
           ["duplicates", "dupe", "dupe_of"]);
419

420 421
CrossCheck("groups", "id",
           ["bug_group_map", "group_id"],
422
           ['category_group_map', 'group_id'],
423 424
           ["group_group_map", "grantor_id"],
           ["group_group_map", "member_id"],
425
           ["group_control_map", "group_id"],
426
           ["namedquery_group_map", "group_id"],
427 428
           ["user_group_map", "group_id"]);

429 430 431 432 433
CrossCheck("namedqueries", "id",
           ["namedqueries_link_in_footer", "namedquery_id"],
           ["namedquery_group_map", "namedquery_id"],
          );

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

463 464 465 466 467
CrossCheck("products", "id",
           ["bugs", "product_id", "bug_id"],
           ["components", "product_id", "name"],
           ["milestones", "product_id", "value"],
           ["versions", "product_id", "value"],
468
           ["group_control_map", "product_id"],
469 470
           ["flaginclusions", "product_id", "type_id"],
           ["flagexclusions", "product_id", "type_id"]);
471

472 473 474
CrossCheck("components", "id",
           ["component_cc", "component_id"]);

475
# Check the former enum types -mkanat@bugzilla.org
476
CrossCheck("bug_status", "value",
477
            ["bugs", "bug_status", "bug_id"]);
478 479

CrossCheck("resolution", "value",
480
            ["bugs", "resolution", "bug_id"]);
481 482

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

CrossCheck("op_sys", "value",
486
            ["bugs", "op_sys", "bug_id"]);
487 488

CrossCheck("priority", "value",
489
            ["bugs", "priority", "bug_id"]);
490 491

CrossCheck("rep_platform", "value",
492
            ["bugs", "rep_platform", "bug_id"]);
493

494 495 496 497 498 499 500 501 502 503
CrossCheck('series', 'series_id',
           ['series_data', 'series_id']);

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

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

504 505 506
CrossCheck('attachments', 'attach_id',
           ['attach_data', 'id']);

507 508 509 510
CrossCheck('bug_status', 'id',
           ['status_workflow', 'old_status'],
           ['status_workflow', 'new_status']);

511
###########################################################################
512
# Perform double field referential (cross) checks
513
###########################################################################
514
 
515 516 517 518 519 520 521 522 523 524 525 526 527 528
# 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

529 530 531 532
sub DoubleCrossCheck {
    my $table = shift @_;
    my $field1 = shift @_;
    my $field2 = shift @_;
533
    my $dbh = Bugzilla->dbh;
534 535 536 537

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

538 539 540
    while (@_) {
        my $ref = shift @_;
        my ($refertable, $referfield1, $referfield2, $keyname) = @$ref;
541 542 543

        Status('double_cross_check_from',
               {table => $refertable, field1 => $referfield1, field2 =>$referfield2});
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559

        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;
560 561 562 563 564
            Status('double_cross_check_alert',
                   {value1 => $value1, value2 => $value2,
                    table => $refertable,
                    field1 => $referfield1, field2 => $referfield2,
                    keyname => $keyname, key => $key}, 'alert');
565
        }
566 567 568
    }
}

569 570 571 572
DoubleCrossCheck('attachments', 'bug_id', 'attach_id',
                 ['flags', 'bug_id', 'attach_id'],
                 ['bugs_activity', 'bug_id', 'attach_id']);

573
DoubleCrossCheck("components", "product_id", "id",
574 575 576
                 ["bugs", "product_id", "component_id", "bug_id"],
                 ['flagexclusions', 'product_id', 'component_id'],
                 ['flaginclusions', 'product_id', 'component_id']);
577

578 579 580 581 582 583
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"]);
584

585 586 587
###########################################################################
# Perform login checks
###########################################################################
588 589

Status('profile_login_start');
590

591 592
my $sth = $dbh->prepare(q{SELECT userid, login_name FROM profiles});
$sth->execute;
593

594
while (my ($id, $email) = $sth->fetchrow_array) {
595
    validate_email_syntax($email)
596
      || Status('profile_login_alert', {id => $id, email => $email}, 'alert');
597
}
598

599 600 601
###########################################################################
# Perform vote/keyword cache checks
###########################################################################
602

603
check_votes_or_keywords();
604

605 606
sub check_votes_or_keywords {
    my $check = shift || 'all';
607

608 609 610 611 612
    my $dbh = Bugzilla->dbh;
    my $sth = $dbh->prepare(q{SELECT bug_id, votes, keywords
                                FROM bugs
                               WHERE votes != 0 OR keywords != ''});
    $sth->execute;
613

614 615
    my %votes;
    my %keyword;
616

617 618 619 620 621 622
    while (my ($id, $v, $k) = $sth->fetchrow_array) {
        if ($v != 0) {
            $votes{$id} = $v;
        }
        if ($k) {
            $keyword{$id} = $k;
623 624
        }
    }
625

626 627 628 629
    # 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');
630 631
}

632 633
sub _check_votes {
    my $votes = shift;
634

635
    Status('vote_count_start');
636 637 638 639 640
    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;
641

642
    my $offer_votecache_rebuild = 0;
643

644 645
    while (my ($id, $v) = $sth->fetchrow_array) {
        if ($v <= 0) {
646
            Status('vote_count_alert', {id => $id}, 'alert');
647 648
        } else {
            if (!defined $votes->{$id} || $votes->{$id} != $v) {
649
                Status('vote_cache_alert', {id => $id}, 'alert');
650 651 652 653
                $offer_votecache_rebuild = 1;
            }
            delete $votes->{$id};
        }
654
    }
655
    foreach my $id (keys %$votes) {
656
        Status('vote_cache_alert', {id => $id}, 'alert');
657
        $offer_votecache_rebuild = 1;
658 659
    }

660
    Status('vote_cache_rebuild_fix') if $offer_votecache_rebuild;
661 662
}

663 664
sub _check_keywords {
    my $keyword = shift;
665

666
    Status('keyword_check_start');
667 668
    my $dbh = Bugzilla->dbh;
    my $cgi = Bugzilla->cgi;
669

670 671 672
    my %keywordids;
    my $keywords = $dbh->selectall_arrayref(q{SELECT id, name
                                                FROM keyworddefs});
673

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

685 686 687 688 689 690 691 692
    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}) {
693
            Status('keyword_check_invalid_id', {id => $k}, 'alert');
694
        }
695
        if (defined $lastid && $id eq $lastid && $k eq $lastk) {
696
            Status('keyword_check_duplicated_ids', {id => $id}, 'alert');
697
        }
698 699
        $lastid = $id;
        $lastk = $k;
700 701
    }

702
    Status('keyword_cache_start');
703

704
    if ($cgi->param('rebuildkeywordcache')) {
705
        $dbh->bz_start_transaction();
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 732 733

    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);
734
    }
735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750

    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;

751 752 753 754
        if ($cgi->param('rebuildkeywordcache')) {
            my $sth_update = $dbh->prepare(q{UPDATE bugs
                                                SET keywords = ?
                                              WHERE bug_id = ?});
755

756
            Status('keyword_cache_fixing');
757 758 759 760 761 762
            foreach my $b (@badbugs) {
                my $k = '';
                if (exists($realk{$b})) {
                    $k = $realk{$b};
                }
                $sth_update->execute($k, $b);
763
            }
764
            Status('keyword_cache_fixed');
765
        } else {
766 767
            Status('keyword_cache_alert', {badbugs => \@badbugs}, 'alert');
            Status('keyword_cache_rebuild');
768 769 770
        }
    }

771
    if ($cgi->param('rebuildkeywordcache')) {
772
        $dbh->bz_commit_transaction();
773
    }
774
}
775

776 777 778 779
###########################################################################
# Check for flags being in incorrect products and components
###########################################################################

780
Status('flag_check_start');
781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808

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')) {
809
        Status('flag_deletion_start');
810 811 812
        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) .')');
813
        Status('flag_deletion_end');
814 815 816 817
    }
    else {
        foreach my $flag (@$invalid_flags) {
            my ($flag_id, $bug_id, $attach_id) = @$flag;
818 819 820
            Status('flag_alert',
                   {flag_id => $flag_id, attach_id => $attach_id, bug_id => $bug_id},
                   'alert');
821
        }
822
        Status('flag_fix');
823 824 825
    }
}

826
###########################################################################
827
# General bug checks
828 829
###########################################################################

830
sub BugCheck {
831
    my ($middlesql, $errortext, $repairparam, $repairtext) = @_;
832
    my $dbh = Bugzilla->dbh;
833 834 835 836
 
    my $badbugs = $dbh->selectcol_arrayref(qq{SELECT DISTINCT bugs.bug_id
                                                FROM $middlesql 
                                            ORDER BY bugs.bug_id});
837

838
    if (scalar(@$badbugs)) {
839 840 841 842
        Status('bug_check_alert',
               {errortext => get_string($errortext), badbugs => $badbugs},
               'alert');

843
        if ($repairparam) {
844 845 846
            $repairtext ||= 'repair_bugs';
            Status('bug_check_repair',
                   {param => $repairparam, text => get_string($repairtext)});
847
        }
848
    }
849 850
}

851
Status('bug_check_creation_date');
852

853 854
BugCheck("bugs WHERE creation_ts IS NULL", 'bug_check_creation_date_error_text',
         'repair_creation_date', 'bug_check_creation_date_repair_text');
855

856
Status('bug_check_res_dupl');
857

858
BugCheck("bugs INNER JOIN duplicates ON bugs.bug_id = duplicates.dupe " .
859
         "WHERE bugs.resolution != 'DUPLICATE'", 'bug_check_res_dupl_error_text');
860

861 862
BugCheck("bugs LEFT JOIN duplicates ON bugs.bug_id = duplicates.dupe WHERE " .
         "bugs.resolution = 'DUPLICATE' AND " .
863
         "duplicates.dupe IS NULL", 'bug_check_res_dupl_error_text2');
864

865
Status('bug_check_status_res');
866

867
my @open_states = map($dbh->quote($_), BUG_STATE_OPEN);
868 869
my $open_states = join(', ', @open_states);

870
BugCheck("bugs WHERE bug_status IN ($open_states) AND resolution != ''",
871
         'bug_check_status_res_error_text');
872
BugCheck("bugs WHERE bug_status NOT IN ($open_states) AND resolution = ''",
873
         'bug_check_status_res_error_text2');
874

875
Status('bug_check_status_everconfirmed');
876

877
BugCheck("bugs WHERE bug_status = 'UNCONFIRMED' AND everconfirmed = 1",
878
         'bug_check_status_everconfirmed_error_text');
879 880 881 882 883

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",
884
         'bug_check_status_everconfirmed_error_text2');
885

886
Status('bug_check_votes_everconfirmed');
887

888 889
BugCheck("bugs INNER JOIN products ON bugs.product_id = products.id " .
         "WHERE everconfirmed = 0 AND votestoconfirm <= votes",
890
         'bug_check_votes_everconfirmed_error_text');
891

892 893 894 895 896 897
###########################################################################
# Control Values
###########################################################################

# Checks for values that are invalid OR
# not among the 9 valid combinations
898
Status('bug_check_control_values');
899 900
my $groups = join(", ", (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT,
CONTROLMAPMANDATORY));
901
my $query = qq{
902 903 904 905 906 907 908 909
     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{)))};
910

911 912 913 914
my $entries = $dbh->selectrow_array($query);
Status('bug_check_control_values_alert', {entries => $entries}, 'alert') if $entries;

Status('bug_check_control_values_violation');
915
BugCheck("bugs
916 917 918 919
         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
920
           AND bug_group_map.group_id = group_control_map.group_id
921
         WHERE ((group_control_map.membercontrol = " . CONTROLMAPNA . ")
922
         OR (group_control_map.membercontrol IS NULL))",
923
         'bug_check_control_values_error_text',
924
         'createmissinggroupcontrolmapentries',
925
         'bug_check_control_values_repair_text');
926

927
BugCheck("bugs
928
         INNER JOIN group_control_map
929
            ON bugs.product_id = group_control_map.product_id
930 931 932 933 934
         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
935
         WHERE group_control_map.membercontrol = " . CONTROLMAPMANDATORY . "
936 937
           AND bug_group_map.group_id IS NULL
           AND groups.isactive != 0",
938
         'bug_check_control_values_error_text2');
939

940 941 942 943
###########################################################################
# Unsent mail
###########################################################################

944
Status('unsent_bugmail_check');
945

946 947 948 949 950 951 952
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});
953 954


955
if (scalar(@$badbugs > 0)) {
956 957
    Status('unsent_bugmail_alert', {badbugs => $badbugs}, 'alert');
    Status('unsent_bugmail_fix');
958 959
}

960 961 962
###########################################################################
# End
###########################################################################
963

964 965
Status('checks_completed');

966 967 968 969
unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
    $template->process('global/footer.html.tmpl', $vars)
      || ThrowTemplateError($template->error());
}