Skip to content

Commit

Permalink
refactor user creation
Browse files Browse the repository at this point in the history
  • Loading branch information
Spine committed Oct 1, 2020
1 parent 18bfbc6 commit 07c95b7
Show file tree
Hide file tree
Showing 11 changed files with 476 additions and 305 deletions.
1 change: 1 addition & 0 deletions app/InviteTree.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public function add(int $userId) {
WHERE TreeID = ?
AND TreePosition > ?
AND TreeLevel <= ?
ORDER BY TreePosition LIMIT 1
", $treeId, $inviterPosition, $level
);
if (!$nextPosition) {
Expand Down
30 changes: 24 additions & 6 deletions app/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ public function info() {
return $this->db->has_results() ? $this->db->next_record(MYSQLI_ASSOC, false) : null;
}

public function id() {
public function id(): int {
return $this->id;
}

public function username() {
public function username(): string {
return $this->db->scalar("
SELECT Username
FROM users_main
Expand All @@ -90,21 +90,39 @@ public function username() {
);
}

public function idFromUsername(string $username) {
public function announceKey(): string {
return $this->db->scalar("
SELECT torrent_pass
FROM users_main
WHERE ID = ?
", $this->id
);
}

public function email(): string {
return $this->db->scalar("
SELECT Email
FROM users_main
WHERE ID = ?
", $this->id
);
}

public function idFromUsername(string $username): int {
return (int)$this->db->scalar("
SELECT ID
FROM users_main
WHERE Username = ?
", $username
);
}

public function url() {
public function url(): string {
return site_url() . "user.php?id=" . $this->id;
}

public function avatarMode(): int {
return \Users::user_heavy_info($this->id)['DisableAvatars'];
return \Users::user_heavy_info($this->id)['DisableAvatars'] ?? 0;
}

public function primaryClass(): int {
Expand Down Expand Up @@ -238,7 +256,7 @@ public function updatePassword($pw, $ipAddr) {
UPDATE users_main SET
PassHash = ?
WHERE ID = ?
', \Users::make_password_hash($pw), $this->id
', UserCreator::hashPassword($pw), $this->id
);
if ($this->db->affected_rows() == 1) {
$this->db->prepared_query('
Expand Down
316 changes: 316 additions & 0 deletions app/UserCreator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
<?php

namespace Gazelle;

class UserCreatorException extends \Exception {}

class UserCreator extends Base {
protected $newInstall;

protected $adminComment;
protected $email;
protected $enabled;
protected $id;
protected $inviteKey;
protected $ipaddr;
protected $passHash;
protected $permissionId;
protected $announceKey;
protected $userName;

public function __construct() {
parent::__construct();

$this->adminComment = [];
$this->email = [];
}

public function create() {
$this->newInstall = !$this->db->scalar("SELECT ID FROM users_main LIMIT 1");
if ($this->newInstall) {
$this->enabled = true;
$this->permissionId = SYSOP;
} else {
$this->enabled = false;
$this->permissionId = USER;
}
if (!$this->ipaddr) {
throw new UserCreatorException('ipaddr');
}
if (!$this->passHash) {
throw new UserCreatorException('password');
}
if (!$this->userName) {
throw new UserCreatorException('username');
}
if ($this->db->scalar("SELECT 1 FROM users_main WHERE Username = ?", $this->userName)) {
throw new UserCreatorException('duplicate');
}
if (in_array($this->userName, ['0', '1'])) {
// TODO: see what breaks if this is allowed
throw new UserCreatorException('username-invalid');
}

$this->announceKey = randomString();
$infoFields = ['AuthKey'];
$infoArgs = [randomString()];

if (!$this->inviteKey) {
$inviterId = null;
} else {
[$inviterId, $inviterReason, $email] = $this->db->row("
SELECT InviterID, Reason, Email
FROM invites
WHERE InviteKey = ?
", $this->inviteKey
);
if (!$inviterId) {
throw new UserCreatorException('invitation');
}
if ($this->email && strtolower($email) != strtolower($this->email[0])) {
// The invitation was sent to one email address, and the user
// supplied a different one during registration: consider both
// as belonging to them.
$this->email[] = $email;
}
$infoFields[] = 'Inviter';
$infoArgs[] = $inviterId;
if ($inviterReason) {
$this->adminComment[] = $inviterReason;
}
$this->adminComment[] = "invite key = " . $this->inviteKey;
}
if (!$this->email) {
// neither setEmail() nor setInviteKey() produced anything useful
throw new UserCreatorException('email');
}

// create users_main row
$mainFields = ['Username', 'Email', 'PassHash', 'torrent_pass', 'IP',
'PermissionID', 'Enabled', 'Invites', 'ipcc'
];
$mainArgs = [
$this->userName, end($this->email), $this->passHash, $this->announceKey, $this->ipaddr,
$this->permissionId, $this->enabled ? '1' : '0', STARTING_INVITES, \Tools::geoip($this->ipaddr)
];

if ($this->id) {
$mainFields[] = 'ID';
$mainArgs[] = $this->id;
}

$this->db->prepared_query("
INSERT INTO users_main
(" . implode(',', $mainFields) . ")
VALUES (" . placeholders($mainFields) . ")
", ...$mainArgs
);
if (!$this->id) {
$this->id = $this->db->inserted_id();
}

// create users_info row
$infoFields[] = 'UserID';
$infoArgs[] = $this->id;
if ($this->adminComment) {
$infoFields[] = 'AdminComment';
$infoArgs[] = sqltime() . " - " . implode("\n", $this->adminComment);
}
$this->db->prepared_query("
INSERT INTO users_info
(" . implode(',', $infoFields) . ", StyleID)
VALUES (" . placeholders($infoFields) . ", (SELECT s.ID FROM stylesheets s WHERE s.Default = '1' LIMIT 1))
", ...$infoArgs
);

// nuke the invite now: if things break from here on out the repairs are trivial
if ($inviterId) {
$this->db->prepared_query("
DELETE FROM invites WHERE InviteKey = ?
", $this->inviteKey
);
$inviteTree = new InviteTree($inviterId);
$inviteTree->add($this->id);
}

$this->db->prepared_query("
UPDATE referral_users SET
UserID = ?,
Active = 1,
Joined = now(),
InviteKey = ''
WHERE InviteKey = ?
", $this->id, $this->inviteKey
);

// Log the one or two email addresses known to be associated with the user.
// Each additional previous email address is staggered one second back in the past.
$past = count($this->email);
foreach ($this->email as $e) {
$this->db->prepared_query('
INSERT INTO users_history_emails
(UserID, Email, IP, Time)
VALUES (?, ?, ?, now() - INTERVAL ? SECOND)
', $this->id, $e, $this->ipaddr, $past--
);
}

// Create the remaining rows in auxilliary tables
$this->db->prepared_query("
INSERT INTO user_bonus (user_id) VALUES (?)
", $this->id
);

$this->db->prepared_query("
INSERT INTO user_flt (user_id) VALUES (?)
", $this->id
);

$this->db->prepared_query("
INSERT INTO users_history_ips (UserID, IP) VALUES (?, ?)
", $this->id, $this->ipaddr
);

$this->db->prepared_query("
INSERT INTO users_leech_stats (UserID, Uploaded) VALUES (?, ?)
", $this->id, STARTING_UPLOAD
);

$this->db->prepared_query("
INSERT INTO users_notifications_settings (UserID) VALUES (?)
", $this->id
);

$this->cache->increment('stats_user_count');
\Tracker::update_tracker('add_user', [
'id' => $this->id,
'passkey' => $this->announceKey
]);

$id = $this->id;
$this->reset(); // So we can create another user
return new User($id);
}

/**
* Reset the internal state so that a new user may be created.
* Calling create() calls this as a side effect.
*/
public function reset() {
$this->newInstall = false;
$this->adminComment = [];
$this->email = [];
$this->enabled = false;
$this->id = null;
$this->announceKey = null;
$this->inviteKey = null;
$this->ipaddr = null;
$this->passHash = null;
$this->permissionId = null;
$this->userName = null;
}

/**
* Is this the first user created?
*
* @return bool True if this is the first account (hence, enabled Sysop)
*/
public function newInstall(): bool {
return $this->newInstall;
}

/**
* Return the email address to which a registration email
* should be sent. An invite may have been sent to one
* address, but the user specified a new address, so prefer
* that one.
*
* @return string The email address to use.
*/
public function email(): string {
return end($this->email);
}

/**
* Set the initial admin comment. Not mandatory for creation
* param string $adminComment
*/
public function setAdminComment(string $adminComment) {
$this->adminComment[] = trim($adminComment);
return $this;
}

/**
* Set the email address. Does not have to be valid (for staff).
* If an invitation was used, this method does not need to be called:
* the email will be taken from the invitation. (Corollary: if an
* invitation was used, calling this method afterwards will override
* the invitation email).
*
* @param string email
*/
public function setEmail(string $email) {
$this->email[] = trim($email);
return $this;
}

/**
* Set the user id. Only needed when you want to specify the id
* of a user. Should not be higher than the current auto-increment
* value, otherwise regular creation will wind up stumbling over
* it and causing a duplicate key error.
*
* @param int id of the user
*/
public function setId(int $id) {
$this->id = $id;
return $this;
}

/**
* Set the invite key (only required if this is a creation via an invitation)
* @param string inviteKey
*/
public function setInviteKey(string $inviteKey) {
$this->inviteKey = trim($inviteKey);
return $this;
}

/**
* Set the user IPv4 address.
* @param string ipaddr
*/
public function setIpaddr(string $ipaddr) {
$this->ipaddr = trim($ipaddr);
return $this;
}

/**
* Set the password. Will be hashed before being stored.
* @param string password
*/
public function setPassword(string $password) {
$this->passHash = self::hashPassword($password);
return $this;
}

/**
* Set the user name.
* @param string password
*/
public function setUserName(string $userName) {
$this->userName = trim($userName);
return $this;
}

/**
* Create salted crypt hash for a given string with
* settings specified in CRYPT_HASH_PREFIX
*
* @param string $plaintext string to hash
* @return string hashed password
*/
static public function hashPassword(string $plaintext): string {
return password_hash(hash('sha256', $plaintext), PASSWORD_DEFAULT);
}
}
Loading

0 comments on commit 07c95b7

Please sign in to comment.