# -*- 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 ################################################################################ # Module Initialization ################################################################################ # Make it harder for us to do dangerous things in Perl. use strict; # Bundle the functions in this file together into the "Bugzilla::Token" package. package Bugzilla::Token; use Bugzilla::Config; use Bugzilla::Error; use Bugzilla::BugMail; use Bugzilla::Util; use Date::Format; use Date::Parse; # This module requires that its caller have said "require globals.pl" to import # relevant functions from that script. ################################################################################ # Constants ################################################################################ # The maximum number of days a token will remain valid. my $maxtokenage = 3; ################################################################################ # Public Functions ################################################################################ sub IssueEmailChangeToken { my ($userid, $old_email, $new_email) = @_; my ($token, $token_ts) = _create_token($userid, 'emailold', $old_email . ":" . $new_email); my $newtoken = _create_token($userid, 'emailnew', $old_email . ":" . $new_email); # Mail the user the token along with instructions for using it. my $template = Bugzilla->template; my $vars = {}; $vars->{'oldemailaddress'} = $old_email . Param('emailsuffix'); $vars->{'newemailaddress'} = $new_email . Param('emailsuffix'); $vars->{'max_token_age'} = $maxtokenage; $vars->{'token_ts'} = $token_ts; $vars->{'token'} = $token; $vars->{'emailaddress'} = $old_email . Param('emailsuffix'); my $message; $template->process("account/email/change-old.txt.tmpl", $vars, \$message) || ThrowTemplateError($template->error()); Bugzilla::BugMail::MessageToMTA($message); $vars->{'token'} = $newtoken; $vars->{'emailaddress'} = $new_email . Param('emailsuffix'); $message = ""; $template->process("account/email/change-new.txt.tmpl", $vars, \$message) || ThrowTemplateError($template->error()); Bugzilla::BugMail::MessageToMTA($message); } 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) = @_; my $dbh = Bugzilla->dbh; # Retrieve the user's ID from the database. my $quotedloginname = &::SqlQuote($loginname); &::SendSQL("SELECT profiles.userid, tokens.issuedate FROM profiles LEFT JOIN tokens ON tokens.userid = profiles.userid AND tokens.tokentype = 'password' AND tokens.issuedate > NOW() - " . $dbh->sql_interval('10 MINUTE') . " WHERE " . $dbh->sql_istrcmp('login_name', $quotedloginname)); my ($userid, $toosoon) = &::FetchSQLData(); if ($toosoon) { ThrowUserError('too_soon_for_new_token'); }; my ($token, $token_ts) = _create_token($userid, 'password', $::ENV{'REMOTE_ADDR'}); # Mail the user the token along with instructions for using it. my $template = Bugzilla->template; my $vars = {}; $vars->{'token'} = $token; $vars->{'emailaddress'} = $loginname . Param('emailsuffix'); $vars->{'max_token_age'} = $maxtokenage; $vars->{'token_ts'} = $token_ts; my $message = ""; $template->process("account/password/forgotten-password.txt.tmpl", $vars, \$message) || ThrowTemplateError($template->error()); Bugzilla::BugMail::MessageToMTA($message); } 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); } sub CleanTokenTable { my $dbh = Bugzilla->dbh; $dbh->bz_lock_tables('tokens WRITE'); &::SendSQL("DELETE FROM tokens WHERE " . $dbh->sql_to_days('NOW()') . " - " . $dbh->sql_to_days('issuedate') . " >= " . $maxtokenage); $dbh->bz_unlock_tables(); } sub GenerateUniqueToken { # Generates a unique random token. Uses generate_random_password # 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; my $dbh = Bugzilla->dbh; my $sth = $dbh->prepare("SELECT userid FROM tokens WHERE token = ?"); while ($duplicate) { ++$tries; if ($tries > 100) { ThrowCodeError("token_generation_error"); } $token = generate_random_password(); $sth->execute($token); $duplicate = $sth->fetchrow_array; } 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, $vars) = @_; my $dbh = Bugzilla->dbh; $vars ||= {}; # Quote the token for inclusion in SQL statements. my $quotedtoken = &::SqlQuote($token); # Get information about the token being cancelled. &::SendSQL("SELECT " . $dbh->sql_date_format('issuedate') . ", tokentype , eventdata , login_name , realname 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. my $maintainer = Param('maintainer'); my $template = Bugzilla->template; $vars->{'emailaddress'} = $loginname . Param('emailsuffix'); $vars->{'maintainer'} = $maintainer; $vars->{'remoteaddress'} = $::ENV{'REMOTE_ADDR'}; $vars->{'token'} = $token; $vars->{'tokentype'} = $tokentype; $vars->{'issuedate'} = $issuedate; $vars->{'eventdata'} = $eventdata; $vars->{'cancelaction'} = $cancelaction; # Notify the user via email about the cancellation. my $message; $template->process("account/cancel-token.txt.tmpl", $vars, \$message) || ThrowTemplateError($template->error()); Bugzilla::BugMail::MessageToMTA($message); # Delete the token from the database. DeleteToken($token); } 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) { Bugzilla::Token::Cancel($token, $reason); } } sub HasEmailChangeToken { # Returns an email change token if the user has one. my ($userid) = @_; my $dbh = Bugzilla->dbh; &::SendSQL("SELECT token FROM tokens WHERE userid = $userid " . "AND (tokentype = 'emailnew' OR tokentype = 'emailold') " . $dbh->sql_limit(1)); my ($token) = &::FetchSQLData(); return $token; } 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; } } 1;