diff --git a/app/User.php b/app/User.php index f945e5ffe..86b662937 100644 --- a/app/User.php +++ b/app/User.php @@ -2,6 +2,7 @@ namespace Gazelle; +use Gazelle\Util\Irc; use Gazelle\Util\Mail; class User extends BaseObject { @@ -926,6 +927,49 @@ public function flush() { ]); } + public function recordEmailChange(string $newEmail, string $ipaddr): int { + $this->db->prepared_query(" + INSERT INTO users_history_emails + (UserID, Email, IP, useragent) + VALUES (?, ?, ?, ?) + ", $this->id, $newEmail, $ipaddr, $_SERVER['HTTP_USER_AGENT'] + ); + Irc::sendRaw("PRIVMSG " . $this->username() + . " :Security alert: Your email address was changed via $ipaddr with {$_SERVER['HTTP_USER_AGENT']}. Not you? Contact staff ASAP."); + (new Mail)->send($this->email(), 'Email address changed information for ' . SITE_NAME, + $this->twig->render('email/email-address-change.twig', [ + 'ipaddr' => $ipaddr, + 'new_email' => $newEmail, + 'now' => Date('Y-m-d H:i:s'), + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + 'username' => $this->username(), + ]) + ); + $this->cache->delete_value('user_email_count_' . $this->id); + return $this->db->affected_rows(); + } + + public function recordPasswordChange(string $ipaddr): int { + $this->db->prepared_query(" + INSERT INTO users_history_passwords + (UserID, ChangerIP, useragent) + VALUES (?, ?, ?) + ", $this->id, $ipaddr, $_SERVER['HTTP_USER_AGENT'] + ); + Irc::sendRaw("PRIVMSG " . $this->username() + . " :Security alert: Your password was changed via $ipaddr with {$_SERVER['HTTP_USER_AGENT']}. Not you? Contact staff ASAP."); + (new Mail)->send($this->email(), 'Password changed information for ' . SITE_NAME, + $this->twig->render('email/password-change.twig', [ + 'ipaddr' => $ipaddr, + 'now' => Date('Y-m-d H:i:s'), + 'user_agent' => $_SERVER['HTTP_USER_AGENT'], + 'username' => $this->username(), + ]) + ); + $this->cache->delete_value('user_pw_count_' . $this->id); + return $this->db->affected_rows(); + } + public function remove() { $this->db->prepared_query(" DELETE FROM users_main WHERE ID = ? @@ -1148,9 +1192,9 @@ public function updatePassword(string $pw, string $ipaddr): bool { } $this->db->prepared_query(' INSERT INTO users_history_passwords - (UserID, ChangerIP, ChangeTime) - VALUES (?, ?, now()) - ', $this->id, $ipaddr + (UserID, ChangerIP, useragent) + VALUES (?, ?, ?) + ', $this->id, $ipaddr, $_SERVER['HTTP_USER_AGENT'] ); if ($this->db->affected_rows() !== 1) { $this->db->rollback(); @@ -1164,7 +1208,8 @@ public function updatePassword(string $pw, string $ipaddr): bool { public function passwordHistory(): array { $this->db->prepared_query(" SELECT ChangeTime AS date, - ChangerIP AS ipaddr + ChangerIP AS ipaddr, + useragent FROM users_history_passwords WHERE UserID = ? ORDER BY ChangeTime DESC @@ -1265,7 +1310,7 @@ public function resetIpHistory(): int { ); $n += $this->db->affected_rows(); $this->db->prepared_query(" - UPDATE users_history_passwords SET ChangerIP = '' WHERE UserID = ? + UPDATE users_history_passwords SET ChangerIP = '', useragent = 'reset-ip-history' WHERE UserID = ? ", $this->id ); $n += $this->db->affected_rows(); @@ -1291,8 +1336,8 @@ public function resetEmailHistory(string $email, string $ipaddr): bool { ); $this->db->prepared_query(" INSERT INTO users_history_emails - (UserID, Email, IP) - VALUES (?, ?, ?) + (UserID, Email, IP, useragent) + VALUES (?, ?, ?, 'email-reset') ", $this->id, $email, $ipaddr ); $this->db->prepared_query(" @@ -1644,13 +1689,14 @@ public function collageUnreadCount(): int { /** * Email history * - * @return array [email address, ip, date] + * @return array [email address, ip, date, useragent] */ public function emailHistory(): array { $this->db->prepared_query(" SELECT h.Email, h.Time, - h.IP + h.IP, + h.useragent FROM users_history_emails AS h WHERE h.UserID = ? ORDER BY h.Time DESC @@ -1671,7 +1717,8 @@ public function emailDuplicateHistory(): array { Email AS email, UserID AS user_id, Time AS created, - IP AS ipv4 + IP AS ipv4, + useragent FROM users_history_emails AS uhe WHERE uhe.UserID != ? AND uhe.Email in (SELECT DISTINCT Email FROM users_history_emails WHERE UserID = ?) diff --git a/app/UserCreator.php b/app/UserCreator.php index b04d0a037..b9317a57f 100644 --- a/app/UserCreator.php +++ b/app/UserCreator.php @@ -150,9 +150,9 @@ public function create() { foreach ($this->email as $e) { $this->db->prepared_query(' INSERT INTO users_history_emails - (UserID, Email, IP, Time) + (UserID, Email, IP, useragent, Time) VALUES (?, ?, ?, now() - INTERVAL ? SECOND) - ', $this->id, $e, $this->ipaddr, $past-- + ', $this->id, $e, $this->ipaddr, $_SERVER['HTTP_USER_AGENT'], $past-- ); } diff --git a/boris b/boris index c49e400fc..6221d3458 100755 --- a/boris +++ b/boris @@ -22,6 +22,7 @@ */ define('BORIS', 1); +$_SERVER['HTTP_USER_AGENT'] = 'boris'; require_once(__DIR__ . '/classes/config.php'); require_once(__DIR__ . '/vendor/autoload.php'); diff --git a/classes/script_start.php b/classes/script_start.php index e0eec9c16..3c5dd1b6b 100644 --- a/classes/script_start.php +++ b/classes/script_start.php @@ -187,6 +187,7 @@ function log_token_attempt(DB_MYSQL $db, int $userId): void { // Because we <3 our staff if (check_perms('site_disable_ip_history')) { $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['HTTP_USER_AGENT'] = 'staff-browser'; } // IP changed diff --git a/db/migrations/20210720085440_users_history_passwords_now.php b/db/migrations/20210720085440_users_history_passwords_now.php new file mode 100644 index 000000000..7b9c91391 --- /dev/null +++ b/db/migrations/20210720085440_users_history_passwords_now.php @@ -0,0 +1,27 @@ +table('users_history_passwords') + ->changeColumn('ChangeTime', 'datetime', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) + ->addColumn('useragent', 'string', ['limit' => 768, 'encoding' => 'ascii', 'null' => true]) + ->save(); + + $this->execute("UPDATE users_history_passwords SET useragent = 'unknown'"); + + $this->table('users_history_passwords') + ->changeColumn('useragent', 'string', ['limit' => 768, 'encoding' => 'ascii', 'null' => false]) + ->save(); + } + + public function down(): void { + $this->table('users_history_passwords') + ->changeColumn('ChangeTime', 'datetime', ['null' => false]) + ->removeColumn('useragent') + ->save(); + } +} diff --git a/db/migrations/20210720093846_users_history_email_useragent.php b/db/migrations/20210720093846_users_history_email_useragent.php new file mode 100644 index 000000000..7538c00d6 --- /dev/null +++ b/db/migrations/20210720093846_users_history_email_useragent.php @@ -0,0 +1,25 @@ +table('users_history_emails') + ->addColumn('useragent', 'string', ['limit' => 768, 'encoding' => 'ascii', 'null' => true]) + ->save(); + + $this->execute("UPDATE users_history_emails SET useragent = 'unknown'"); + + $this->table('users_history_emails') + ->changeColumn('useragent', 'string', ['limit' => 768, 'encoding' => 'ascii', 'null' => false]) + ->save(); + } + + public function down(): void { + $this->table('users_history_emails') + ->removeColumn('useragent') + ->save(); + } +} diff --git a/db/seeds/InitialUserSeeder.php b/db/seeds/InitialUserSeeder.php index c687f3e30..beb45c0a7 100644 --- a/db/seeds/InitialUserSeeder.php +++ b/db/seeds/InitialUserSeeder.php @@ -96,13 +96,15 @@ public function run() { 'UserID' => $adminId, 'Email' => 'admin@example.com', 'Time' => Literal::from('now()'), - 'IP' => '127.0.0.1' + 'IP' => '127.0.0.1', + 'useragent' => 'initial-seed', ], [ 'UserID' => $userId, 'Email' => 'user@example.com', 'Time' => Literal::from('now()'), - 'IP' => '127.0.0.1' + 'IP' => '127.0.0.1', + 'useragent' => 'initial-seed', ] ])->saveData(); diff --git a/sections/user/take_edit.php b/sections/user/take_edit.php index 17c51373e..d03a5f132 100644 --- a/sections/user/take_edit.php +++ b/sections/user/take_edit.php @@ -141,19 +141,12 @@ function ($key) { $userMan->hideDonor($user); } -$CurEmail = $user->email(); -if ($CurEmail != $_POST['email']) { - // Non-admins have to authenticate to change email - if (!check_perms('users_edit_profiles') && !$user->validatePassword($_POST['cur_pass'])) { - error('You did not enter the correct password.'); +$NewEmail = false; +if ($user->email() != $_POST['email']) { + if (!$Viewer->permitted('users_edit_profiles') && !$user->validatePassword($_POST['cur_pass'])) { + error('You must enter your current password when changing your email address.'); } $NewEmail = $_POST['email']; - $DB->prepared_query(" - INSERT INTO users_history_emails - (UserID, Email, IP) - VALUES (?, ?, ?) - ", $userId, $NewEmail, $_SERVER['REMOTE_ADDR'] - ); } $ResetPassword = false; @@ -293,7 +286,6 @@ function ($key) { i.NotifyOnDeleteSeeding = ?, i.NotifyOnDeleteSnatched = ?, i.NotifyOnDeleteDownloaded = ?, - m.Email = ?, m.IRCKey = ?, m.Paranoia = ?, i.NavItems = ? @@ -312,7 +304,6 @@ function ($key) { $NotifyOnDeleteSeeding, $NotifyOnDeleteSnatched, $NotifyOnDeleteDownloaded, - $_POST['email'], $_POST['irckey'], serialize($Paranoia), $UserNavItems @@ -321,18 +312,19 @@ function ($key) { if ($ResetPassword) { $SQL .= ',m.PassHash = ?'; $Params[] = Gazelle\UserCreator::hashPassword($_POST['new_pass_1']); - $DB->prepared_query(' - INSERT INTO users_history_passwords - (UserID, ChangerIP, ChangeTime) - VALUES (?, ?, now()) - ', $userId, $LoggedUser['IP'] - ); + $user->recordPasswordChange($Viewer->ipaddr()); +} + +if ($NewEmail) { + $SQL .= ',m.email = ?'; + $Params[] = $NewEmail; + $user->recordEmailChange($NewEmail, $Viewer->ipaddr()); } if (isset($_POST['resetpasskey'])) { $OldPassKey = $user->announceKey(); $NewPassKey = randomString(); - $ChangerIP = $LoggedUser['IP']; + $ChangerIP = $Viewer->ipaddr(); $SQL .= ',m.torrent_pass = ?'; $Params[] = $NewPassKey; $DB->prepared_query(' diff --git a/sections/user/user.php b/sections/user/user.php index 5d96ccc06..90077718f 100644 --- a/sections/user/user.php +++ b/sections/user/user.php @@ -280,7 +280,7 @@ function display_rank(Gazelle\UserRank $r, string $dimension) { } if (check_perms('users_mod')) { ?> -
Email History | |||
---|---|---|---|
Address | Registered since | Registered from | +Useragent |
{{ i.3 }} |
Changed | IP H | +Useragent |
{{ change.date|time_diff }} | {{ change.ipaddr }} S {{ resolveIpv4(change.ipaddr) }} |
+ {{ change.useragent }} |