userprefs.cgi 19.9 KB
Newer Older
1
#!/usr/bin/perl -wT
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# 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.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>
17
#                 Dan Mosedale <dmose@mozilla.org>
18
#                 Alan Raetz <al_raetz@yahoo.com>
19
#                 David Miller <justdave@syndicomm.com>
20
#                 Christopher Aillon <christopher@aillon.com>
21
#                 Gervase Markham <gerv@gerv.net>
22
#                 Vlad Dascalu <jocuri@softhome.net>
23
#                 Shane H. W. Travis <travis@sedsystems.ca>
24 25 26

use strict;

27
use lib qw(. lib);
28

29
use Bugzilla;
30
use Bugzilla::BugMail;
31
use Bugzilla::Constants;
32
use Bugzilla::Search;
33
use Bugzilla::Util;
34
use Bugzilla::Error;
35
use Bugzilla::User;
36
use Bugzilla::Token;
37

38
my $template = Bugzilla->template;
39
local our $vars = {};
40

41 42 43
###############################################################################
# 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 
44
# contents from the form data (if appropriate). 
45 46 47
# SaveFoo may be called before DoFoo.    
###############################################################################
sub DoAccount {
48
    my $dbh = Bugzilla->dbh;
49 50
    my $user = Bugzilla->user;

51
    ($vars->{'realname'}) = $dbh->selectrow_array(
52
        "SELECT realname FROM profiles WHERE userid = ?", undef, $user->id);
53

54
    if(Bugzilla->params->{'allowemailchange'} 
55
       && Bugzilla->user->authorizer->can_change_email) {
56 57 58
       # First delete old tokens.
       Bugzilla::Token::CleanTokenTable();

59
        my @token = $dbh->selectrow_array(
60 61 62
            "SELECT tokentype, " .
                    $dbh->sql_date_math('issuedate', '+', MAX_TOKEN_AGE, 'DAY')
                    . ", eventdata
63 64 65
               FROM tokens
              WHERE userid = ?
                AND tokentype LIKE 'email%'
66
           ORDER BY tokentype ASC " . $dbh->sql_limit(1), undef, $user->id);
67 68
        if (scalar(@token) > 0) {
            my ($tokentype, $change_date, $eventdata) = @token;
69 70 71 72 73 74 75 76
            $vars->{'login_change_date'} = $change_date;

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

sub SaveAccount {
80
    my $cgi = Bugzilla->cgi;
81
    my $dbh = Bugzilla->dbh;
82
    my $user = Bugzilla->user;
83

84
    my $oldpassword = $cgi->param('old_password');
85 86 87
    my $pwd1 = $cgi->param('new_password1');
    my $pwd2 = $cgi->param('new_password2');

88 89 90
    my $old_login_name = $cgi->param('old_login');
    my $new_login_name = trim($cgi->param('new_login_name'));

91
    if ($user->authorizer->can_change_password
92
        && ($oldpassword ne "" || $pwd1 ne "" || $pwd2 ne ""))
93
    {
94
        my $oldcryptedpwd = $user->cryptpassword;
95 96
        $oldcryptedpwd || ThrowCodeError("unable_to_retrieve_password");

97
        if (bz_crypt($oldpassword, $oldcryptedpwd) ne $oldcryptedpwd) {
98
            ThrowUserError("old_password_incorrect");
99
        }
100

101 102
        if ($pwd1 ne "" || $pwd2 ne "") {
            $pwd1 || ThrowUserError("new_password_missing");
103
            validate_password($pwd1, $pwd2);
104

105
            if ($oldpassword ne $pwd1) {
106 107 108 109 110 111 112 113 114
                my $cryptedpassword = bz_crypt($pwd1);
                $dbh->do(q{UPDATE profiles
                              SET cryptpassword = ?
                            WHERE userid = ?},
                         undef, ($cryptedpassword, $user->id));

                # Invalidate all logins except for the current one
                Bugzilla->logout(LOGOUT_KEEP_CURRENT);
            }
115
        }
116 117
    }

118 119
    if ($user->authorizer->can_change_email
        && Bugzilla->params->{"allowemailchange"}
120
        && $new_login_name)
121
    {
122 123
        if ($old_login_name ne $new_login_name) {
            $oldpassword || ThrowUserError("old_password_required");
124 125

            # Block multiple email changes for the same user.
126
            if (Bugzilla::Token::HasEmailChangeToken($user->id)) {
127
                ThrowUserError("email_change_in_progress");
128 129 130
            }

            # Before changing an email address, confirm one does not exist.
131 132
            validate_email_syntax($new_login_name)
              || ThrowUserError('illegal_email_address', {addr => $new_login_name});
133
            is_available_username($new_login_name)
134
              || ThrowUserError("account_exists", {email => $new_login_name});
135

136
            Bugzilla::Token::IssueEmailChangeToken($user, $old_login_name,
137
                                                   $new_login_name);
138

139
            $vars->{'email_changes_saved'} = 1;
140 141
        }
    }
142

143 144 145
    my $realname = trim($cgi->param('realname'));
    trick_taint($realname); # Only used in a placeholder
    $dbh->do("UPDATE profiles SET realname = ? WHERE userid = ?",
146
             undef, ($realname, $user->id));
147 148 149
}


150
sub DoSettings {
151 152 153
    my $user = Bugzilla->user;

    my $settings = $user->settings;
154
    $vars->{'settings'} = $settings;
155

156
    my @setting_list = keys %$settings;
157
    $vars->{'setting_names'} = \@setting_list;
158 159 160 161 162 163 164 165 166 167

    $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'};
168 169 170 171
}

sub SaveSettings {
    my $cgi = Bugzilla->cgi;
172
    my $user = Bugzilla->user;
173

174 175
    my $settings = $user->settings;
    my @setting_list = keys %$settings;
176 177 178 179

    foreach my $name (@setting_list) {
        next if ! ($settings->{$name}->{'is_enabled'});
        my $value = $cgi->param($name);
180
        next unless defined $value;
181
        my $setting = new Bugzilla::User::Setting($name);
182 183 184

        if ($value eq "${name}-isdefault" ) {
            if (! $settings->{$name}->{'is_default'}) {
185
                $settings->{$name}->reset_to_default;
186 187 188
            }
        }
        else {
189 190
            $setting->validate_value($value);
            $settings->{$name}->set($value);
191 192
        }
    }
193
    $vars->{'settings'} = $user->settings(1);
194 195
}

196
sub DoEmail {
197
    my $dbh = Bugzilla->dbh;
198
    my $user = Bugzilla->user;
199 200 201 202
    
    ###########################################################################
    # User watching
    ###########################################################################
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
    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));
219
    }
220

221 222
    @watchers = sort { lc($a) cmp lc($b) } @watchers;
    $vars->{'watchers'} = \@watchers;
223
}
224

225 226 227
sub SaveEmail {
    my $dbh = Bugzilla->dbh;
    my $cgi = Bugzilla->cgi;
228
    my $user = Bugzilla->user;
229

230
    Bugzilla::User::match_field({ 'new_watchedusers' => {'type' => 'multi'} });
231

232 233 234
    ###########################################################################
    # Role-based preferences
    ###########################################################################
235
    $dbh->bz_start_transaction();
236

237 238 239 240 241 242 243
    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;
244

245
    # Update the table - first, with normal events in the
246
    # relationship/event matrix.
247 248
    my %relationships = Bugzilla::BugMail::relationships();
    foreach my $rel (keys %relationships) {
249
        next if ($rel == REL_QA && !Bugzilla->params->{'useqacontact'});
250 251
        # Positive events: a ticked box means "send me mail."
        foreach my $event (POS_EVENTS) {
252 253 254 255 256 257 258 259
            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);
                }
260
            }
261 262 263 264
        }
        
        # Negative events: a ticked box means "don't send me mail."
        foreach my $event (NEG_EVENTS) {
265 266 267 268 269 270 271 272
            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);
                }
273
            }
274
        }
275
    }
276

277 278
    # Global positive events: a ticked box means "send me mail."
    foreach my $event (GLOBAL_EVENTS) {
279 280 281 282 283 284 285 286
        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);
            }
287
        }
288
    }
289

290
    $dbh->bz_commit_transaction();
291

292 293 294
    # We have to clear the cache about email preferences.
    delete $user->{'mail_settings'};

295 296 297
    ###########################################################################
    # User watching
    ###########################################################################
298 299
    if (defined $cgi->param('new_watchedusers')
        || defined $cgi->param('remove_watched_users'))
300
    {
301
        $dbh->bz_start_transaction();
302

303
        # Use this to protect error messages on duplicate submissions
304 305
        my $old_watch_ids =
            $dbh->selectcol_arrayref("SELECT watched FROM watch"
306
                                   . " WHERE watcher = ?", undef, $user->id);
307 308

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

313
        foreach my $username (@new_watch_names) {
314
            my $watched_userid = login_to_id(trim($username), THROW_ERROR);
315
            $new_watch_ids{$watched_userid} = 1;
316 317 318 319 320
        }

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

326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
        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);
            }
        }

342
        $dbh->bz_commit_transaction();
343
    }
344 345 346
}


347
sub DoPermissions {
348
    my $dbh = Bugzilla->dbh;
349
    my $user = Bugzilla->user;
350 351
    my (@has_bits, @set_bits);
    
352 353
    my $groups = $dbh->selectall_arrayref(
               "SELECT DISTINCT name, description FROM groups WHERE id IN (" . 
354
               $user->groups_as_string . ") ORDER BY name");
355 356
    foreach my $group (@$groups) {
        my ($nam, $desc) = @$group;
357
        push(@has_bits, {"desc" => $desc, "name" => $nam});
358
    }
359 360 361
    $groups = $dbh->selectall_arrayref('SELECT DISTINCT id, name, description
                                          FROM groups
                                         ORDER BY name');
362
    foreach my $group (@$groups) {
363 364
        my ($group_id, $nam, $desc) = @$group;
        if ($user->can_bless($group_id)) {
365
            push(@set_bits, {"desc" => $desc, "name" => $nam});
366 367
        }
    }
368 369 370 371 372 373 374

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

375 376
    $vars->{'has_bits'} = \@has_bits;
    $vars->{'set_bits'} = \@set_bits;    
377
}
378

379
# No SavePermissions() because this panel has no changeable fields.
380

381

382
sub DoSavedSearches {
383
    my $dbh = Bugzilla->dbh;
384 385
    my $user = Bugzilla->user;

386
    if ($user->queryshare_groups_as_string) {
387 388
        $vars->{'queryshare_groups'} =
            Bugzilla::Group->new_from_list($user->queryshare_groups);
389
    }
390
    $vars->{'bless_group_ids'} = [map { $_->id } @{$user->bless_groups}];
391 392
}

393
sub SaveSavedSearches {
394 395
    my $cgi = Bugzilla->cgi;
    my $dbh = Bugzilla->dbh;
396 397
    my $user = Bugzilla->user;

398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
    # 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 = ?');
415 416 417 418 419

    # 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;
420 421
        }
        else {
422
            $sth_delete_nl->execute($q->id, $user_id) if $q->link_in_footer;
423
        }
424
    }
425

426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
    # 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) {
            # Don't allow the user to share queries with groups he's not
            # 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);
444 445
            }
            else {
446 447
                $sth_insert_ngm->execute($q->id, $group_id);
            }
448

449
            # If we're sharing our query with a group we can bless, we 
450 451 452
            # 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)) {
453 454 455 456 457 458 459
                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);
                }
460 461
            }
        }
462 463 464 465 466 467 468 469 470 471 472
        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.
        }
473 474
    }

475
    $user->flush_queries_cache;
476
    
477
    # Update profiles.mybugslink.
478
    my $showmybugslink = defined($cgi->param("showmybugslink")) ? 1 : 0;
479 480 481
    $dbh->do("UPDATE profiles SET mybugslink = ? WHERE userid = ?",
             undef, ($showmybugslink, $user->id));    
    $user->{'showmybugslink'} = $showmybugslink;
482
}
483 484


485 486 487
###############################################################################
# Live code (not subroutine definitions) starts here
###############################################################################
488

489 490
my $cgi = Bugzilla->cgi;

491
# Delete credentials before logging in in case we are in a sudo session.
492
$cgi->delete('Bugzilla_login', 'Bugzilla_password') if ($cgi->cookie('sudo'));
493 494 495 496
$cgi->delete('GoAheadAndLogIn');

# First try to get credentials from cookies.
Bugzilla->login(LOGIN_OPTIONAL);
497

498 499 500 501 502
if (!Bugzilla->user->id) {
    # 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'));
}
503
Bugzilla->login(LOGIN_REQUIRED);
504

505 506
my $save_changes = $cgi->param('dosave');
$vars->{'changes_saved'} = $save_changes;
507

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

510 511 512
# The SWITCH below makes sure that this is valid
trick_taint($current_tab_name);

513
$vars->{'current_tab_name'} = $current_tab_name;
514

515
my $token = $cgi->param('token');
516
check_token_data($token, 'edit_user_prefs') if $save_changes;
517

518 519 520
# Do any saving, and then display the current tab.
SWITCH: for ($current_tab_name) {
    /^account$/ && do {
521
        SaveAccount() if $save_changes;
522 523 524
        DoAccount();
        last SWITCH;
    };
525
    /^settings$/ && do {
526
        SaveSettings() if $save_changes;
527 528 529
        DoSettings();
        last SWITCH;
    };
530
    /^email$/ && do {
531
        SaveEmail() if $save_changes;
532 533 534 535 536 537 538
        DoEmail();
        last SWITCH;
    };
    /^permissions$/ && do {
        DoPermissions();
        last SWITCH;
    };
539
    /^saved-searches$/ && do {
540
        SaveSavedSearches() if $save_changes;
541 542 543
        DoSavedSearches();
        last SWITCH;
    };
544 545 546 547 548 549 550 551 552
    # 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;

553 554
    ThrowUserError("unknown_tab",
                   { current_tab_name => $current_tab_name });
555 556
}

557
delete_token($token) if $save_changes;
558 559 560 561
if ($current_tab_name ne 'permissions') {
    $vars->{'token'} = issue_session_token('edit_user_prefs');
}

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