userprefs.cgi 22.2 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::BugMail;
17
use Bugzilla::Constants;
18
use Bugzilla::Mailer;
19
use Bugzilla::Search;
20
use Bugzilla::Util;
21
use Bugzilla::Error;
22
use Bugzilla::User;
23
use Bugzilla::User::APIKey;
24
use Bugzilla::User::Setting qw(clear_settings_cache);
25
use Bugzilla::Token;
26

27
my $template = Bugzilla->template;
28
local our $vars = {};
29

30 31 32
###############################################################################
# Each panel has two functions - panel Foo has a DoFoo, to get the data 
# necessary for displaying the panel, and a SaveFoo, to save the panel's 
33
# contents from the form data (if appropriate). 
34 35 36
# SaveFoo may be called before DoFoo.    
###############################################################################
sub DoAccount {
37
    my $dbh = Bugzilla->dbh;
38 39
    my $user = Bugzilla->user;

40
    $vars->{'realname'} = $user->name;
41

42 43 44
    if (Bugzilla->params->{'allowemailchange'}
        && $user->authorizer->can_change_email)
    {
45 46 47
       # First delete old tokens.
       Bugzilla::Token::CleanTokenTable();

48
        my @token = $dbh->selectrow_array(
49 50 51
            "SELECT tokentype, " .
                    $dbh->sql_date_math('issuedate', '+', MAX_TOKEN_AGE, 'DAY')
                    . ", eventdata
52 53 54
               FROM tokens
              WHERE userid = ?
                AND tokentype LIKE 'email%'
55
           ORDER BY tokentype ASC " . $dbh->sql_limit(1), undef, $user->id);
56 57
        if (scalar(@token) > 0) {
            my ($tokentype, $change_date, $eventdata) = @token;
58 59 60 61 62 63 64 65
            $vars->{'login_change_date'} = $change_date;

            if($tokentype eq 'emailnew') {
                my ($oldemail,$newemail) = split(/:/,$eventdata);
                $vars->{'new_login_name'} = $newemail;
            }
        }
    }
66 67 68
}

sub SaveAccount {
69
    my $cgi = Bugzilla->cgi;
70
    my $dbh = Bugzilla->dbh;
71 72 73
    
    $dbh->bz_start_transaction;

74
    my $user = Bugzilla->user;
75

76
    my $oldpassword = $cgi->param('old_password');
77 78
    my $pwd1 = $cgi->param('new_password1');
    my $pwd2 = $cgi->param('new_password2');
79 80
    my $new_login_name = trim($cgi->param('new_login_name'));

81
    if ($user->authorizer->can_change_password
82
        && ($oldpassword ne "" || $pwd1 ne "" || $pwd2 ne ""))
83
    {
84
        my $oldcryptedpwd = $user->cryptpassword;
85 86
        $oldcryptedpwd || ThrowCodeError("unable_to_retrieve_password");

87
        if (bz_crypt($oldpassword, $oldcryptedpwd) ne $oldcryptedpwd) {
88
            ThrowUserError("old_password_incorrect");
89
        }
90

91 92
        if ($pwd1 ne "" || $pwd2 ne "") {
            $pwd1 || ThrowUserError("new_password_missing");
93
            validate_password($pwd1, $pwd2);
94

95
            if ($oldpassword ne $pwd1) {
96
                $user->set_password($pwd1);
97 98 99
                # Invalidate all logins except for the current one
                Bugzilla->logout(LOGOUT_KEEP_CURRENT);
            }
100
        }
101 102
    }

103 104
    if ($user->authorizer->can_change_email
        && Bugzilla->params->{"allowemailchange"}
105
        && $new_login_name)
106
    {
107
        if ($user->login ne $new_login_name) {
108
            $oldpassword || ThrowUserError("old_password_required");
109 110

            # Block multiple email changes for the same user.
111
            if (Bugzilla::Token::HasEmailChangeToken($user->id)) {
112
                ThrowUserError("email_change_in_progress");
113 114 115
            }

            # Before changing an email address, confirm one does not exist.
116
            check_email_syntax($new_login_name);
117
            is_available_username($new_login_name)
118
              || ThrowUserError("account_exists", {email => $new_login_name});
119

120
            $vars->{'email_token'} = Bugzilla::Token::IssueEmailChangeToken($new_login_name);
121
            $vars->{'email_changes_saved'} = 1;
122 123
        }
    }
124

125 126 127
    $user->set_name($cgi->param('realname'));
    $user->update({ keep_session => 1, keep_tokens => 1 });
    $dbh->bz_commit_transaction;
128 129 130
}


131
sub DoSettings {
132 133 134
    my $user = Bugzilla->user;

    my $settings = $user->settings;
135
    $vars->{'settings'} = $settings;
136

137
    my @setting_list = sort keys %$settings;
138
    $vars->{'setting_names'} = \@setting_list;
139 140 141 142 143 144 145 146 147 148

    $vars->{'has_settings_enabled'} = 0;
    # Is there at least one user setting enabled?
    foreach my $setting_name (@setting_list) {
        if ($settings->{"$setting_name"}->{'is_enabled'}) {
            $vars->{'has_settings_enabled'} = 1;
            last;
        }
    }
    $vars->{'dont_show_button'} = !$vars->{'has_settings_enabled'};
149 150 151 152
}

sub SaveSettings {
    my $cgi = Bugzilla->cgi;
153
    my $user = Bugzilla->user;
154

155 156
    my $settings = $user->settings;
    my @setting_list = keys %$settings;
157 158 159 160

    foreach my $name (@setting_list) {
        next if ! ($settings->{$name}->{'is_enabled'});
        my $value = $cgi->param($name);
161
        next unless defined $value;
162
        my $setting = new Bugzilla::User::Setting($name);
163 164 165

        if ($value eq "${name}-isdefault" ) {
            if (! $settings->{$name}->{'is_default'}) {
166
                $settings->{$name}->reset_to_default;
167 168 169
            }
        }
        else {
170 171
            $setting->validate_value($value);
            $settings->{$name}->set($value);
172 173
        }
    }
174
    $vars->{'settings'} = $user->settings(1);
175
    clear_settings_cache($user->id);
176 177
}

178
sub DoEmail {
179
    my $dbh = Bugzilla->dbh;
180
    my $user = Bugzilla->user;
181 182 183 184
    
    ###########################################################################
    # User watching
    ###########################################################################
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
    my $watched_ref = $dbh->selectcol_arrayref(
        "SELECT profiles.login_name FROM watch INNER JOIN profiles" .
        " ON watch.watched = profiles.userid" .
        " WHERE watcher = ?" .
        " ORDER BY profiles.login_name",
        undef, $user->id);
    $vars->{'watchedusers'} = $watched_ref;

    my $watcher_ids = $dbh->selectcol_arrayref(
        "SELECT watcher FROM watch WHERE watched = ?",
        undef, $user->id);

    my @watchers;
    foreach my $watcher_id (@$watcher_ids) {
        my $watcher = new Bugzilla::User($watcher_id);
        push(@watchers, Bugzilla::User::identity($watcher));
201
    }
202

203 204
    @watchers = sort { lc($a) cmp lc($b) } @watchers;
    $vars->{'watchers'} = \@watchers;
205
}
206

207 208 209
sub SaveEmail {
    my $dbh = Bugzilla->dbh;
    my $cgi = Bugzilla->cgi;
210
    my $user = Bugzilla->user;
211

212
    Bugzilla::User::match_field({ 'new_watchedusers' => {'type' => 'multi'} });
213

214 215 216
    ###########################################################################
    # Role-based preferences
    ###########################################################################
217
    $dbh->bz_start_transaction();
218

219 220 221 222 223 224 225
    my $sth_insert = $dbh->prepare('INSERT INTO email_setting
                                    (user_id, relationship, event) VALUES (?, ?, ?)');

    my $sth_delete = $dbh->prepare('DELETE FROM email_setting
                                    WHERE user_id = ? AND relationship = ? AND event = ?');
    # Load current email preferences into memory before updating them.
    my $settings = $user->mail_settings;
226

227
    # Update the table - first, with normal events in the
228
    # relationship/event matrix.
229 230
    my %relationships = Bugzilla::BugMail::relationships();
    foreach my $rel (keys %relationships) {
231
        next if ($rel == REL_QA && !Bugzilla->params->{'useqacontact'});
232 233
        # Positive events: a ticked box means "send me mail."
        foreach my $event (POS_EVENTS) {
234 235 236 237 238 239 240 241
            my $is_set = $cgi->param("email-$rel-$event");
            if ($is_set xor $settings->{$rel}{$event}) {
                if ($is_set) {
                    $sth_insert->execute($user->id, $rel, $event);
                }
                else {
                    $sth_delete->execute($user->id, $rel, $event);
                }
242
            }
243 244 245 246
        }
        
        # Negative events: a ticked box means "don't send me mail."
        foreach my $event (NEG_EVENTS) {
247 248 249 250 251 252 253 254
            my $is_set = $cgi->param("neg-email-$rel-$event");
            if (!$is_set xor $settings->{$rel}{$event}) {
                if (!$is_set) {
                    $sth_insert->execute($user->id, $rel, $event);
                }
                else {
                    $sth_delete->execute($user->id, $rel, $event);
                }
255
            }
256
        }
257
    }
258

259 260
    # Global positive events: a ticked box means "send me mail."
    foreach my $event (GLOBAL_EVENTS) {
261 262 263 264 265 266 267 268
        my $is_set = $cgi->param("email-" . REL_ANY . "-$event");
        if ($is_set xor $settings->{+REL_ANY}{$event}) {
            if ($is_set) {
                $sth_insert->execute($user->id, REL_ANY, $event);
            }
            else {
                $sth_delete->execute($user->id, REL_ANY, $event);
            }
269
        }
270
    }
271

272
    $dbh->bz_commit_transaction();
273

274 275 276
    # We have to clear the cache about email preferences.
    delete $user->{'mail_settings'};

277 278 279
    ###########################################################################
    # User watching
    ###########################################################################
280 281
    if (defined $cgi->param('new_watchedusers')
        || defined $cgi->param('remove_watched_users'))
282
    {
283
        $dbh->bz_start_transaction();
284

285
        # Use this to protect error messages on duplicate submissions
286 287
        my $old_watch_ids =
            $dbh->selectcol_arrayref("SELECT watched FROM watch"
288
                                   . " WHERE watcher = ?", undef, $user->id);
289 290

        # The new information given to us by the user.
291 292
        my $new_watched_users = join(',', $cgi->param('new_watchedusers')) || '';
        my @new_watch_names = split(/[,\s]+/, $new_watched_users);
293
        my %new_watch_ids;
294

295
        foreach my $username (@new_watch_names) {
296
            my $watched_userid = login_to_id(trim($username), THROW_ERROR);
297
            $new_watch_ids{$watched_userid} = 1;
298 299 300 301 302
        }

        # Add people who were added.
        my $insert_sth = $dbh->prepare('INSERT INTO watch (watched, watcher)'
                                     . ' VALUES (?, ?)');
303 304
        foreach my $add_me (keys(%new_watch_ids)) {
            next if grep($_ == $add_me, @$old_watch_ids);
305
            $insert_sth->execute($add_me, $user->id);
306
        }
307

308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
        if (defined $cgi->param('remove_watched_users')) {
            my @removed = $cgi->param('watched_by_you');
            # Remove people who were removed.
            my $delete_sth = $dbh->prepare('DELETE FROM watch WHERE watched = ?'
                                         . ' AND watcher = ?');
            
            my %remove_watch_ids;
            foreach my $username (@removed) {
                my $watched_userid = login_to_id(trim($username), THROW_ERROR);
                $remove_watch_ids{$watched_userid} = 1;
            }
            foreach my $remove_me (keys(%remove_watch_ids)) {
                $delete_sth->execute($remove_me, $user->id);
            }
        }

324
        $dbh->bz_commit_transaction();
325
    }
326 327 328 329 330 331 332 333 334 335 336 337 338

    ###########################################################################
    # Ignore Bugs
    ###########################################################################
    my %ignored_bugs = map { $_->{'id'} => 1 } @{$user->bugs_ignored};

    # Validate the new bugs to ignore by checking that they exist and also
    # if the user gave an alias
    my @add_ignored = split(/[\s,]+/, $cgi->param('add_ignored_bugs'));
    @add_ignored = map { Bugzilla::Bug->check($_)->id } @add_ignored;
    map { $ignored_bugs{$_} = 1 } @add_ignored;

    # Remove any bug ids the user no longer wants to ignore
339
    foreach my $key (grep(/^remove_ignored_bug_/, $cgi->param)) {
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
        my ($bug_id) = $key =~ /(\d+)$/;
        delete $ignored_bugs{$bug_id};
    }

    # Update the database with any changes made
    my ($removed, $added) = diff_arrays([ map { $_->{'id'} } @{$user->bugs_ignored} ],
                                        [ keys %ignored_bugs ]);

    if (scalar @$removed || scalar @$added) {
        $dbh->bz_start_transaction();

        if (scalar @$removed) {
            $dbh->do('DELETE FROM email_bug_ignore WHERE user_id = ? AND ' . 
                     $dbh->sql_in('bug_id', $removed),
                     undef, $user->id);
        }
        if (scalar @$added) {
            my $sth = $dbh->prepare('INSERT INTO email_bug_ignore
                                     (user_id, bug_id) VALUES (?, ?)');
            $sth->execute($user->id, $_) foreach @$added;
        }

        # Reset the cache of ignored bugs if the list changed.
        delete $user->{bugs_ignored};

        $dbh->bz_commit_transaction();
    }
367 368 369
}


370
sub DoPermissions {
371
    my $dbh = Bugzilla->dbh;
372
    my $user = Bugzilla->user;
373
    my (@has_bits, @set_bits);
374

375
    my $groups = $dbh->selectall_arrayref(
376
               "SELECT DISTINCT name, description FROM groups WHERE id IN (" .
377
               $user->groups_as_string . ") ORDER BY name");
378 379
    foreach my $group (@$groups) {
        my ($nam, $desc) = @$group;
380
        push(@has_bits, {"desc" => $desc, "name" => $nam});
381
    }
382 383 384
    $groups = $dbh->selectall_arrayref('SELECT DISTINCT id, name, description
                                          FROM groups
                                         ORDER BY name');
385
    foreach my $group (@$groups) {
386 387
        my ($group_id, $nam, $desc) = @$group;
        if ($user->can_bless($group_id)) {
388
            push(@set_bits, {"desc" => $desc, "name" => $nam});
389 390
        }
    }
391

392
    # If the user has product specific privileges, inform them about that.
393 394 395 396 397
    foreach my $privs (PER_PRODUCT_PRIVILEGES) {
        next if $user->in_group($privs);
        $vars->{"local_$privs"} = $user->get_products_by_permission($privs);
    }

398 399
    $vars->{'has_bits'} = \@has_bits;
    $vars->{'set_bits'} = \@set_bits;    
400
}
401

402
# No SavePermissions() because this panel has no changeable fields.
403

404

405
sub DoSavedSearches {
406
    my $dbh = Bugzilla->dbh;
407 408
    my $user = Bugzilla->user;

409
    if ($user->queryshare_groups_as_string) {
410 411
        $vars->{'queryshare_groups'} =
            Bugzilla::Group->new_from_list($user->queryshare_groups);
412
    }
413
    $vars->{'bless_group_ids'} = [map { $_->id } @{$user->bless_groups}];
414 415
}

416
sub SaveSavedSearches {
417 418
    my $cgi = Bugzilla->cgi;
    my $dbh = Bugzilla->dbh;
419 420
    my $user = Bugzilla->user;

421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
    # We'll need this in a loop, so do the call once.
    my $user_id = $user->id;

    my $sth_insert_nl = $dbh->prepare('INSERT INTO namedqueries_link_in_footer
                                       (namedquery_id, user_id)
                                       VALUES (?, ?)');
    my $sth_delete_nl = $dbh->prepare('DELETE FROM namedqueries_link_in_footer
                                             WHERE namedquery_id = ?
                                               AND user_id = ?');
    my $sth_insert_ngm = $dbh->prepare('INSERT INTO namedquery_group_map
                                        (namedquery_id, group_id)
                                        VALUES (?, ?)');
    my $sth_update_ngm = $dbh->prepare('UPDATE namedquery_group_map
                                           SET group_id = ?
                                         WHERE namedquery_id = ?');
    my $sth_delete_ngm = $dbh->prepare('DELETE FROM namedquery_group_map
                                              WHERE namedquery_id = ?');
438 439 440 441 442

    # Update namedqueries_link_in_footer for this user.
    foreach my $q (@{$user->queries}, @{$user->queries_available}) {
        if (defined $cgi->param("link_in_footer_" . $q->id)) {
            $sth_insert_nl->execute($q->id, $user_id) if !$q->link_in_footer;
443 444
        }
        else {
445
            $sth_delete_nl->execute($q->id, $user_id) if $q->link_in_footer;
446
        }
447
    }
448

449 450 451 452 453 454 455 456 457
    # For user's own queries, update namedquery_group_map.
    foreach my $q (@{$user->queries}) {
        my $group_id;

        if ($user->in_group(Bugzilla->params->{'querysharegroup'})) {
            $group_id = $cgi->param("share_" . $q->id) || '';
        }

        if ($group_id) {
458
            # Don't allow the user to share queries with groups they're not
459 460 461 462 463 464 465 466
            # allowed to.
            next unless grep($_ eq $group_id, @{$user->queryshare_groups});

            # $group_id is now definitely a valid ID of a group the
            # user can share queries with, so we can trick_taint.
            detaint_natural($group_id);
            if ($q->shared_with_group) {
                $sth_update_ngm->execute($group_id, $q->id);
467 468
            }
            else {
469 470
                $sth_insert_ngm->execute($q->id, $group_id);
            }
471

472
            # If we're sharing our query with a group we can bless, we 
473 474 475
            # have the ability to add link to our search to the footer of
            # direct group members automatically.
            if ($user->can_bless($group_id) && $cgi->param('force_' . $q->id)) {
476 477 478 479 480 481 482
                my $group = new Bugzilla::Group($group_id);
                my $members = $group->members_non_inherited;
                foreach my $member (@$members) {
                    next if $member->id == $user->id;
                    $sth_insert_nl->execute($q->id, $member->id)
                        if !$q->link_in_footer($member);
                }
483 484
            }
        }
485 486 487 488 489 490 491 492 493 494 495
        else {
            # They have unshared that query.
            if ($q->shared_with_group) {
                $sth_delete_ngm->execute($q->id);
            }

            # Don't remove namedqueries_link_in_footer entries for users
            # subscribing to the shared query. The idea is that they will
            # probably want to be subscribers again should the sharing
            # user choose to share the query again.
        }
496 497
    }

498
    $user->flush_queries_cache;
499

500
    # Update profiles.mybugslink.
501
    my $showmybugslink = defined($cgi->param("showmybugslink")) ? 1 : 0;
502
    $dbh->do("UPDATE profiles SET mybugslink = ? WHERE userid = ?",
503
             undef, ($showmybugslink, $user->id));
504
    $user->{'showmybugslink'} = $showmybugslink;
505
    Bugzilla->memcached->clear({ table => 'profiles', id => $user->id });
506
}
507 508


509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
sub DoApiKey {
    my $user = Bugzilla->user;

    my $api_keys = Bugzilla::User::APIKey->match({ user_id => $user->id });
    $vars->{api_keys} = $api_keys;
    $vars->{any_revoked} = grep { $_->revoked } @$api_keys;
}

sub SaveApiKey {
    my $cgi = Bugzilla->cgi;
    my $dbh = Bugzilla->dbh;
    my $user = Bugzilla->user;

    # Do it in a transaction.
    $dbh->bz_start_transaction;

    # Update any existing keys
    my $api_keys = Bugzilla::User::APIKey->match({ user_id => $user->id });
    foreach my $api_key (@$api_keys) {
528 529
        my $description = $cgi->param('description_' . $api_key->id);
        my $revoked = $cgi->param('revoked_' . $api_key->id);
530 531 532 533 534 535 536 537 538 539 540 541

        if ($description ne $api_key->description
            || $revoked != $api_key->revoked)
        {
            $api_key->set_all({
                description => $description,
                revoked     => $revoked,
            });
            $api_key->update();
        }
    }

542
    # Create a new API key if requested.
543
    if ($cgi->param('new_key')) {
544
        $vars->{new_key} = Bugzilla::User::APIKey->create({
545
            user_id     => $user->id,
546
            description => scalar $cgi->param('new_description'),
547 548 549 550
        });

        # As a security precaution, we always sent out an e-mail when
        # an API key is created
551
        my $template = Bugzilla->template_inner($user->setting('lang'));
552
        my $message;
553 554
        $template->process('email/new-api-key.txt.tmpl', $vars, \$message)
          || ThrowTemplateError($template->error());
555 556 557 558 559 560 561

        MessageToMTA($message);
    }

    $dbh->bz_commit_transaction;
}

562 563 564
###############################################################################
# Live code (not subroutine definitions) starts here
###############################################################################
565

566 567
my $cgi = Bugzilla->cgi;

568
# Delete credentials before logging in in case we are in a sudo session.
569
$cgi->delete('Bugzilla_login', 'Bugzilla_password') if ($cgi->cookie('sudo'));
570 571 572
$cgi->delete('GoAheadAndLogIn');

# First try to get credentials from cookies.
573
my $user = Bugzilla->login(LOGIN_OPTIONAL);
574

575
if (!$user->id) {
576 577 578 579
    # Use credentials given in the form if login cookies are not available.
    $cgi->param('Bugzilla_login', $cgi->param('old_login'));
    $cgi->param('Bugzilla_password', $cgi->param('old_password'));
}
580
Bugzilla->login(LOGIN_REQUIRED);
581

582 583
my $save_changes = $cgi->param('dosave');
$vars->{'changes_saved'} = $save_changes;
584

585
my $current_tab_name = $cgi->param('tab') || "settings";
586

587 588 589
# The SWITCH below makes sure that this is valid
trick_taint($current_tab_name);

590
$vars->{'current_tab_name'} = $current_tab_name;
591

592
my $token = $cgi->param('token');
593
check_token_data($token, 'edit_user_prefs') if $save_changes;
594

595 596
# Do any saving, and then display the current tab.
SWITCH: for ($current_tab_name) {
597 598 599 600 601 602 603 604 605 606

    # Extensions must set it to 1 to confirm the tab is valid.
    my $handled = 0;
    Bugzilla::Hook::process('user_preferences',
                            { 'vars'       => $vars,
                              save_changes => $save_changes,
                              current_tab  => $current_tab_name,
                              handled      => \$handled });
    last SWITCH if $handled;

607
    /^account$/ && do {
608
        SaveAccount() if $save_changes;
609 610 611
        DoAccount();
        last SWITCH;
    };
612
    /^settings$/ && do {
613
        SaveSettings() if $save_changes;
614 615 616
        DoSettings();
        last SWITCH;
    };
617
    /^email$/ && do {
618
        SaveEmail() if $save_changes;
619 620 621 622 623 624 625
        DoEmail();
        last SWITCH;
    };
    /^permissions$/ && do {
        DoPermissions();
        last SWITCH;
    };
626
    /^saved-searches$/ && do {
627
        SaveSavedSearches() if $save_changes;
628 629 630
        DoSavedSearches();
        last SWITCH;
    };
631 632 633 634 635
    /^apikey$/ && do {
        SaveApiKey() if $save_changes;
        DoApiKey();
        last SWITCH;
    };
636

637 638
    ThrowUserError("unknown_tab",
                   { current_tab_name => $current_tab_name });
639 640
}

641
delete_token($token) if $save_changes;
642 643 644 645
if ($current_tab_name ne 'permissions') {
    $vars->{'token'} = issue_session_token('edit_user_prefs');
}

646
# Generate and return the UI (HTML page) from the appropriate template.
647
print $cgi->header();
648 649
$template->process("account/prefs/prefs.html.tmpl", $vars)
  || ThrowTemplateError($template->error());