id = int_escape($row['id']); $this->name = $row['name']; $this->email = $row['email']; $this->join_date = $row['joindate']; $this->passhash = $row['pass']; if (array_key_exists($row["class"], $_shm_user_classes)) { $this->class = $_shm_user_classes[$row["class"]]; } else { throw new SCoreException("User '{$this->name}' has invalid class '{$row["class"]}'"); } } public static function by_session(string $name, string $session): ?User { global $config, $database; $row = $database->cache->get("user-session:$name-$session"); if (!$row) { if ($database->get_driver_name() === DatabaseDriver::MYSQL) { $query = "SELECT * FROM users WHERE name = :name AND md5(concat(pass, :ip)) = :sess"; } else { $query = "SELECT * FROM users WHERE name = :name AND md5(pass || :ip) = :sess"; } $row = $database->get_row($query, ["name"=>$name, "ip"=>get_session_ip($config), "sess"=>$session]); $database->cache->set("user-session:$name-$session", $row, 600); } return is_null($row) ? null : new User($row); } public static function by_id(int $id): ?User { global $database; if ($id === 1) { $cached = $database->cache->get('user-id:'.$id); if ($cached) { return new User($cached); } } $row = $database->get_row("SELECT * FROM users WHERE id = :id", ["id"=>$id]); if ($id === 1) { $database->cache->set('user-id:'.$id, $row, 600); } return is_null($row) ? null : new User($row); } public static function by_name(string $name): ?User { global $database; $row = $database->get_row($database->scoreql_to_sql("SELECT * FROM users WHERE SCORE_STRNORM(name) = SCORE_STRNORM(:name)"), ["name"=>$name]); return is_null($row) ? null : new User($row); } public static function by_name_and_pass(string $name, string $pass): ?User { $user = User::by_name($name); if ($user) { if ($user->passhash == md5(strtolower($name) . $pass)) { log_info("core-user", "Migrating from md5 to bcrypt for ".html_escape($name)); $user->set_password($pass); } if (password_verify($pass, $user->passhash)) { log_info("core-user", "Logged in as ".html_escape($name)." ({$user->class->name})"); return $user; } else { log_warning("core-user", "Failed to log in as ".html_escape($name)." (Invalid password)"); } } else { log_warning("core-user", "Failed to log in as ".html_escape($name)." (Invalid username)"); } return null; } /* useful user object functions start here */ public function can(string $ability): bool { return $this->class->can($ability); } public function is_anonymous(): bool { global $config; return ($this->id === $config->get_int('anon_id')); } public function is_logged_in(): bool { global $config; return ($this->id !== $config->get_int('anon_id')); } public function is_admin(): bool { return ($this->class->name === "admin"); } public function set_class(string $class): void { global $database; $database->Execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]); log_info("core-user", 'Set class for '.$this->name.' to '.$class); } public function set_name(string $name): void { global $database; if (User::by_name($name)) { throw new Exception("Desired username is already in use"); } $old_name = $this->name; $this->name = $name; $database->Execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]); log_info("core-user", "Changed username for {$old_name} to {$this->name}"); } public function set_password(string $password): void { global $database; $hash = password_hash($password, PASSWORD_BCRYPT); if (is_string($hash)) { $this->passhash = $hash; $database->Execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]); log_info("core-user", 'Set password for '.$this->name); } else { throw new SCoreException("Failed to hash password"); } } public function set_email(string $address): void { global $database; $database->Execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]); log_info("core-user", 'Set email for '.$this->name); } /** * Get a snippet of HTML which will render the user's avatar, be that * a local file, a remote file, a gravatar, a something else, etc. */ public function get_avatar_html(): string { // FIXME: configurable global $config; if ($config->get_string("avatar_host") === "gravatar") { if (!empty($this->email)) { $hash = md5(strtolower($this->email)); $s = $config->get_string("avatar_gravatar_size"); $d = urlencode($config->get_string("avatar_gravatar_default")); $r = $config->get_string("avatar_gravatar_rating"); $cb = date("Y-m-d"); return ""; } } return ""; } /** * Get an auth token to be used in POST forms * * password = secret, avoid storing directly * passhash = bcrypt(password), so someone who gets to the database can't get passwords * sesskey = md5(passhash . IP), so if it gets sniffed it can't be used from another IP, * and it can't be used to get the passhash to generate new sesskeys * authtok = md5(sesskey, salt), presented to the user in web forms, to make sure that * the form was generated within the session. Salted and re-hashed so that * reading a web page from the user's cache doesn't give access to the session key */ public function get_auth_token(): string { global $config; $salt = DATABASE_DSN; $addr = get_session_ip($config); return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt); } public function get_auth_html(): string { $at = $this->get_auth_token(); return ''; } public function check_auth_token(): bool { return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token()); } }