Token.pm 9.78 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
# -*- 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.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s):    Myk Melez <myk@mozilla.org>

################################################################################
# Module Initialization
################################################################################

# Make it harder for us to do dangerous things in Perl.
use strict;

29
# Bundle the functions in this file together into the "Bugzilla::Token" package.
30
package Bugzilla::Token;
31

32
use Bugzilla::Config;
33
use Bugzilla::Error;
34
use Bugzilla::BugMail;
35
use Bugzilla::Util;
36

37
use Date::Format;
38
use Date::Parse;
39

40 41 42
# This module requires that its caller have said "require CGI.pl" to import
# relevant functions from that script and its companion globals.pl.

43 44 45 46 47 48 49
################################################################################
# Constants
################################################################################

# The maximum number of days a token will remain valid.
my $maxtokenage = 3;

50
################################################################################
51
# Public Functions
52 53
################################################################################

54 55 56
sub IssueEmailChangeToken {
    my ($userid, $old_email, $new_email) = @_;

57
    my ($token, $token_ts) = _create_token($userid, 'emailold', $old_email . ":" . $new_email);
58

59
    my $newtoken = _create_token($userid, 'emailnew', $old_email . ":" . $new_email);
60 61 62 63 64 65

    # Mail the user the token along with instructions for using it.

    my $template = $::template;
    my $vars = $::vars;

66 67
    $vars->{'oldemailaddress'} = $old_email . Param('emailsuffix');
    $vars->{'newemailaddress'} = $new_email . Param('emailsuffix');
68 69 70
    
    $vars->{'max_token_age'} = $maxtokenage;
    $vars->{'token_ts'} = $token_ts;
71

72
    $vars->{'token'} = $token;
73
    $vars->{'emailaddress'} = $old_email . Param('emailsuffix');
74 75

    my $message;
76
    $template->process("account/email/change-old.txt.tmpl", $vars, \$message)
77
      || ThrowTemplateError($template->error());
78

79
    Bugzilla::BugMail::MessageToMTA($message);
80

81
    $vars->{'token'} = $newtoken;
82
    $vars->{'emailaddress'} = $new_email . Param('emailsuffix');
83 84

    $message = "";
85
    $template->process("account/email/change-new.txt.tmpl", $vars, \$message)
86
      || ThrowTemplateError($template->error());
87

88
    Bugzilla::BugMail::MessageToMTA($message);
89 90
}

91 92 93 94 95 96
sub IssuePasswordToken {
    # Generates a random token, adds it to the tokens table, and sends it
    # to the user with instructions for using it to change their password.

    my ($loginname) = @_;

97 98
    my $dbh = Bugzilla->dbh;

99 100
    # Retrieve the user's ID from the database.
    my $quotedloginname = &::SqlQuote($loginname);
101 102 103 104
    &::SendSQL("SELECT profiles.userid, tokens.issuedate FROM profiles 
                    LEFT JOIN tokens
                    ON tokens.userid = profiles.userid
                    AND tokens.tokentype = 'password'
105 106
                    AND tokens.issuedate > NOW() - " .
                    $dbh->sql_interval('10 MINUTE') . "
107
                 WHERE " . $dbh->sql_istrcmp('login_name', $quotedloginname));
108 109 110 111 112
    my ($userid, $toosoon) = &::FetchSQLData();

    if ($toosoon) {
        ThrowUserError('too_soon_for_new_token');
    };
113

114
    my ($token, $token_ts) = _create_token($userid, 'password', $::ENV{'REMOTE_ADDR'});
115 116

    # Mail the user the token along with instructions for using it.
117 118 119 120 121
    
    my $template = $::template;
    my $vars = $::vars;

    $vars->{'token'} = $token;
122
    $vars->{'emailaddress'} = $loginname . Param('emailsuffix');
123

124 125 126
    $vars->{'max_token_age'} = $maxtokenage;
    $vars->{'token_ts'} = $token_ts;

127
    my $message = "";
128 129
    $template->process("account/password/forgotten-password.txt.tmpl", 
                                                               $vars, \$message)
130
      || ThrowTemplateError($template->error());
131

132
    Bugzilla::BugMail::MessageToMTA($message);
133 134
}

135 136 137 138 139 140 141
sub IssueSessionToken {
    # Generates a random token, adds it to the tokens table, and returns
    # the token to the caller.

    my $data = shift;
    return _create_token(Bugzilla->user->id, 'session', $data);
}
142

143
sub CleanTokenTable {
144 145
    my $dbh = Bugzilla->dbh;
    $dbh->bz_lock_tables('tokens WRITE');
146 147 148
    &::SendSQL("DELETE FROM tokens WHERE " .
               $dbh->sql_to_days('NOW()') . " - " .
               $dbh->sql_to_days('issuedate') . " >= " . $maxtokenage);
149
    $dbh->bz_unlock_tables();
150 151
}

152 153 154 155 156 157 158 159 160 161
sub GenerateUniqueToken {
    # Generates a unique random token.  Uses &GenerateRandomPassword 
    # for the tokens themselves and checks uniqueness by searching for
    # the token in the "tokens" table.  Gives up if it can't come up
    # with a token after about one hundred tries.

    my $token;
    my $duplicate = 1;
    my $tries = 0;

162 163 164 165
    my $dbh = Bugzilla->dbh;
    my $sth = $dbh->prepare("SELECT userid FROM tokens WHERE token = ?");

    while ($duplicate) {
166 167
        ++$tries;
        if ($tries > 100) {
168
            ThrowCodeError("token_generation_error");
169 170
        }
        $token = &::GenerateRandomPassword();
171 172
        $sth->execute($token);
        $duplicate = $sth->fetchrow_array;
173 174 175 176 177 178 179 180 181 182 183 184
    }

    return $token;
}

sub Cancel {
    # Cancels a previously issued token and notifies the system administrator.
    # This should only happen when the user accidentally makes a token request
    # or when a malicious hacker makes a token request on behalf of a user.
    
    my ($token, $cancelaction) = @_;

185 186
    my $dbh = Bugzilla->dbh;

187 188 189 190
    # Quote the token for inclusion in SQL statements.
    my $quotedtoken = &::SqlQuote($token);
    
    # Get information about the token being cancelled.
191 192
    &::SendSQL("SELECT  " . $dbh->sql_date_format('issuedate') . ",
                        tokentype , eventdata , login_name , realname
193 194 195 196 197 198
                FROM    tokens, profiles 
                WHERE   tokens.userid = profiles.userid
                AND     token = $quotedtoken");
    my ($issuedate, $tokentype, $eventdata, $loginname, $realname) = &::FetchSQLData();

    # Get the email address of the Bugzilla maintainer.
199
    my $maintainer = Param('maintainer');
200

201 202
    my $template = $::template;
    my $vars = $::vars;
203

204
    $vars->{'emailaddress'} = $loginname . Param('emailsuffix');
205 206
    $vars->{'maintainer'} = $maintainer;
    $vars->{'remoteaddress'} = $::ENV{'REMOTE_ADDR'};
207
    $vars->{'token'} = $token;
208 209 210 211
    $vars->{'tokentype'} = $tokentype;
    $vars->{'issuedate'} = $issuedate;
    $vars->{'eventdata'} = $eventdata;
    $vars->{'cancelaction'} = $cancelaction;
212

213
    # Notify the user via email about the cancellation.
214

215
    my $message;
216
    $template->process("account/cancel-token.txt.tmpl", $vars, \$message)
217
      || ThrowTemplateError($template->error());
218

219
    Bugzilla::BugMail::MessageToMTA($message);
220 221

    # Delete the token from the database.
222
    DeleteToken($token);
223 224
}

225 226 227 228 229 230 231 232 233
sub DeletePasswordTokens {
    my ($userid, $reason) = @_;

    my $dbh = Bugzilla->dbh;
    my $sth = $dbh->prepare("SELECT token " .
                            "FROM tokens " .
                            "WHERE userid=? AND tokentype='password'");
    $sth->execute($userid);
    while (my $token = $sth->fetchrow_array) {
234
        Bugzilla::Token::Cancel($token, $reason);
235
    }
236 237
}

238 239 240 241
sub HasEmailChangeToken {
    # Returns an email change token if the user has one. 
    
    my ($userid) = @_;
242 243

    my $dbh = Bugzilla->dbh;
244 245
    &::SendSQL("SELECT token FROM tokens WHERE userid = $userid " . 
               "AND (tokentype = 'emailnew' OR tokentype = 'emailold') " . 
246
               $dbh->sql_limit(1));
247 248 249 250 251
    my ($token) = &::FetchSQLData();
    
    return $token;
}

252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
sub GetTokenData($) {
    # Returns the userid, issuedate and eventdata for the specified token

    my ($token) = @_;
    return unless defined $token;
    trick_taint($token);
    
    my $dbh = Bugzilla->dbh;
    return $dbh->selectrow_array(
        "SELECT userid, " . $dbh->sql_date_format('issuedate') . ", eventdata 
         FROM   tokens 
         WHERE  token = ?", undef, $token);
}

sub DeleteToken($) {
    # Deletes specified token

    my ($token) = @_;
    return unless defined $token;
    trick_taint($token);

    my $dbh = Bugzilla->dbh;
    $dbh->bz_lock_tables('tokens WRITE');
    $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token);
    $dbh->bz_unlock_tables();
}

################################################################################
# Internal Functions
################################################################################

sub _create_token($$$) {
    # Generates a unique token and inserts it into the database
    # Returns the token and the token timestamp
    my ($userid, $tokentype, $eventdata) = @_;

    detaint_natural($userid);
    trick_taint($tokentype);
    trick_taint($eventdata);

    my $dbh = Bugzilla->dbh;
    $dbh->bz_lock_tables('tokens WRITE');

    my $token = GenerateUniqueToken();

    $dbh->do("INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata)
        VALUES (?, NOW(), ?, ?, ?)", undef, ($userid, $token, $tokentype, $eventdata));

    $dbh->bz_unlock_tables();

    if (wantarray) {
        my (undef, $token_ts, undef) = GetTokenData($token);
        $token_ts = str2time($token_ts);
        return ($token, $token_ts);
    } else {
        return $token;
    }
}
310

311
1;