2021-12-14 18:32:47 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
2014-05-23 23:28:57 +00:00
|
|
|
|
2023-01-10 22:44:09 +00:00
|
|
|
namespace Shimmie2;
|
|
|
|
|
2023-02-04 13:27:27 +00:00
|
|
|
use GQLA\Type;
|
|
|
|
use GQLA\Field;
|
|
|
|
use GQLA\Query;
|
2023-02-01 12:50:00 +00:00
|
|
|
|
2019-05-28 16:59:38 +00:00
|
|
|
function _new_user(array $row): User
|
|
|
|
{
|
|
|
|
return new User($row);
|
2009-05-30 13:47:35 +00:00
|
|
|
}
|
|
|
|
|
2012-02-07 13:44:54 +00:00
|
|
|
|
2009-07-19 07:38:13 +00:00
|
|
|
/**
|
2014-04-29 05:33:03 +00:00
|
|
|
* Class User
|
|
|
|
*
|
2007-12-06 11:01:18 +00:00
|
|
|
* An object representing a row in the "users" table.
|
2009-07-19 07:38:13 +00:00
|
|
|
*
|
2014-04-29 05:33:03 +00:00
|
|
|
* The currently logged in user will always be accessible via the global variable $user.
|
2007-12-06 11:01:18 +00:00
|
|
|
*/
|
2023-02-04 13:27:27 +00:00
|
|
|
#[Type(name: "User")]
|
2019-05-28 16:59:38 +00:00
|
|
|
class User
|
|
|
|
{
|
2021-03-14 23:43:50 +00:00
|
|
|
public int $id;
|
2023-02-04 13:27:27 +00:00
|
|
|
#[Field]
|
2021-03-14 23:43:50 +00:00
|
|
|
public string $name;
|
|
|
|
public ?string $email;
|
2023-02-15 22:00:00 +00:00
|
|
|
#[Field]
|
2021-03-14 23:43:50 +00:00
|
|
|
public string $join_date;
|
|
|
|
public ?string $passhash;
|
2023-02-07 13:21:37 +00:00
|
|
|
#[Field]
|
2021-03-14 23:43:50 +00:00
|
|
|
public UserClass $class;
|
2019-05-28 16:59:38 +00:00
|
|
|
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
|
|
* Initialisation *
|
|
|
|
* *
|
|
|
|
* User objects shouldn't be created directly, they should be *
|
|
|
|
* fetched from the database like so: *
|
|
|
|
* *
|
|
|
|
* $user = User::by_name("bob"); *
|
|
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* One will very rarely construct a user directly, more common
|
|
|
|
* would be to use User::by_id, User::by_session, etc.
|
|
|
|
*
|
|
|
|
* @throws SCoreException
|
|
|
|
*/
|
|
|
|
public function __construct(array $row)
|
|
|
|
{
|
|
|
|
global $_shm_user_classes;
|
|
|
|
|
2020-01-26 18:10:58 +00:00
|
|
|
$this->id = int_escape((string)$row['id']);
|
2019-05-28 16:59:38 +00:00
|
|
|
$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"]}'");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-04 13:27:27 +00:00
|
|
|
#[Query]
|
2023-02-01 12:50:00 +00:00
|
|
|
public static function me(): User
|
|
|
|
{
|
|
|
|
global $user;
|
|
|
|
return $user;
|
|
|
|
}
|
|
|
|
|
2023-02-13 22:28:50 +00:00
|
|
|
#[Field(name: "user_id")]
|
|
|
|
public function graphql_oid(): int
|
|
|
|
{
|
|
|
|
return $this->id;
|
|
|
|
}
|
|
|
|
#[Field(name: "id")]
|
|
|
|
public function graphql_guid(): string
|
|
|
|
{
|
|
|
|
return "user:{$this->id}";
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-05-29 17:23:29 +00:00
|
|
|
public static function by_session(string $name, string $session): ?User
|
2019-05-28 16:59:38 +00:00
|
|
|
{
|
2019-10-02 09:49:32 +00:00
|
|
|
global $cache, $config, $database;
|
|
|
|
$row = $cache->get("user-session:$name-$session");
|
2023-02-02 16:04:35 +00:00
|
|
|
if (is_null($row)) {
|
2022-10-28 00:45:35 +00:00
|
|
|
if ($database->get_driver_id() === DatabaseDriverID::MYSQL) {
|
2019-05-28 16:59:38 +00:00
|
|
|
$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]);
|
2019-10-02 09:49:32 +00:00
|
|
|
$cache->set("user-session:$name-$session", $row, 600);
|
2019-05-28 16:59:38 +00:00
|
|
|
}
|
|
|
|
return is_null($row) ? null : new User($row);
|
|
|
|
}
|
|
|
|
|
2019-05-29 17:23:29 +00:00
|
|
|
public static function by_id(int $id): ?User
|
2019-05-28 16:59:38 +00:00
|
|
|
{
|
2019-10-02 09:49:32 +00:00
|
|
|
global $cache, $database;
|
2019-05-28 16:59:38 +00:00
|
|
|
if ($id === 1) {
|
2019-10-02 09:49:32 +00:00
|
|
|
$cached = $cache->get('user-id:'.$id);
|
2023-02-02 16:04:35 +00:00
|
|
|
if (!is_null($cached)) {
|
2019-05-28 16:59:38 +00:00
|
|
|
return new User($cached);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$row = $database->get_row("SELECT * FROM users WHERE id = :id", ["id"=>$id]);
|
|
|
|
if ($id === 1) {
|
2019-10-02 09:49:32 +00:00
|
|
|
$cache->set('user-id:'.$id, $row, 600);
|
2019-05-28 16:59:38 +00:00
|
|
|
}
|
|
|
|
return is_null($row) ? null : new User($row);
|
|
|
|
}
|
|
|
|
|
2023-02-13 22:28:50 +00:00
|
|
|
#[Query(name: "user")]
|
2019-05-29 17:23:29 +00:00
|
|
|
public static function by_name(string $name): ?User
|
2019-05-28 16:59:38 +00:00
|
|
|
{
|
|
|
|
global $database;
|
2020-01-26 17:39:55 +00:00
|
|
|
$row = $database->get_row("SELECT * FROM users WHERE LOWER(name) = LOWER(:name)", ["name"=>$name]);
|
2019-05-28 16:59:38 +00:00
|
|
|
return is_null($row) ? null : new User($row);
|
|
|
|
}
|
|
|
|
|
2019-11-11 16:31:14 +00:00
|
|
|
public static function name_to_id(string $name): int
|
|
|
|
{
|
|
|
|
$u = User::by_name($name);
|
|
|
|
if (is_null($u)) {
|
2021-11-10 19:33:51 +00:00
|
|
|
throw new UserDoesNotExist("Can't find any user named $name");
|
2019-11-11 16:31:14 +00:00
|
|
|
} else {
|
|
|
|
return $u->id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-29 17:23:29 +00:00
|
|
|
public static function by_name_and_pass(string $name, string $pass): ?User
|
2019-05-28 16:59:38 +00:00
|
|
|
{
|
2019-11-11 16:24:13 +00:00
|
|
|
$my_user = User::by_name($name);
|
2020-02-01 21:21:27 +00:00
|
|
|
|
|
|
|
// If user tried to log in as "foo bar" and failed, try "foo_bar"
|
2020-10-25 19:31:58 +00:00
|
|
|
if (!$my_user && str_contains($name, " ")) {
|
2019-12-16 09:01:09 +00:00
|
|
|
$my_user = User::by_name(str_replace(" ", "_", $name));
|
|
|
|
}
|
2020-02-01 21:37:07 +00:00
|
|
|
|
2019-11-11 16:24:13 +00:00
|
|
|
if ($my_user) {
|
|
|
|
if ($my_user->passhash == md5(strtolower($name) . $pass)) {
|
2020-01-26 17:39:55 +00:00
|
|
|
log_info("core-user", "Migrating from md5 to bcrypt for $name");
|
2019-11-11 16:24:13 +00:00
|
|
|
$my_user->set_password($pass);
|
2019-05-28 16:59:38 +00:00
|
|
|
}
|
2019-11-11 16:24:13 +00:00
|
|
|
if (password_verify($pass, $my_user->passhash)) {
|
2020-01-26 17:39:55 +00:00
|
|
|
log_info("core-user", "Logged in as $name ({$my_user->class->name})");
|
2019-11-11 16:24:13 +00:00
|
|
|
return $my_user;
|
2019-05-28 16:59:38 +00:00
|
|
|
} else {
|
2020-01-26 17:39:55 +00:00
|
|
|
log_warning("core-user", "Failed to log in as $name (Invalid password)");
|
2019-05-28 16:59:38 +00:00
|
|
|
}
|
|
|
|
} else {
|
2020-01-26 17:39:55 +00:00
|
|
|
log_warning("core-user", "Failed to log in as $name (Invalid username)");
|
2019-05-28 16:59:38 +00:00
|
|
|
}
|
|
|
|
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'));
|
|
|
|
}
|
|
|
|
|
2019-05-29 17:23:29 +00:00
|
|
|
public function set_class(string $class): void
|
2019-05-28 16:59:38 +00:00
|
|
|
{
|
|
|
|
global $database;
|
2020-10-25 21:34:52 +00:00
|
|
|
$database->execute("UPDATE users SET class=:class WHERE id=:id", ["class"=>$class, "id"=>$this->id]);
|
2019-05-28 16:59:38 +00:00
|
|
|
log_info("core-user", 'Set class for '.$this->name.' to '.$class);
|
|
|
|
}
|
|
|
|
|
2019-05-29 17:23:29 +00:00
|
|
|
public function set_name(string $name): void
|
2019-05-28 16:59:38 +00:00
|
|
|
{
|
|
|
|
global $database;
|
|
|
|
if (User::by_name($name)) {
|
2023-02-03 16:44:16 +00:00
|
|
|
throw new SCoreException("Desired username is already in use");
|
2019-05-28 16:59:38 +00:00
|
|
|
}
|
|
|
|
$old_name = $this->name;
|
|
|
|
$this->name = $name;
|
2020-10-25 21:34:52 +00:00
|
|
|
$database->execute("UPDATE users SET name=:name WHERE id=:id", ["name"=>$this->name, "id"=>$this->id]);
|
2019-05-28 16:59:38 +00:00
|
|
|
log_info("core-user", "Changed username for {$old_name} to {$this->name}");
|
|
|
|
}
|
|
|
|
|
2019-05-29 17:23:29 +00:00
|
|
|
public function set_password(string $password): void
|
2019-05-28 16:59:38 +00:00
|
|
|
{
|
|
|
|
global $database;
|
|
|
|
$hash = password_hash($password, PASSWORD_BCRYPT);
|
|
|
|
if (is_string($hash)) {
|
|
|
|
$this->passhash = $hash;
|
2020-10-25 21:34:52 +00:00
|
|
|
$database->execute("UPDATE users SET pass=:hash WHERE id=:id", ["hash"=>$this->passhash, "id"=>$this->id]);
|
2019-05-28 16:59:38 +00:00
|
|
|
log_info("core-user", 'Set password for '.$this->name);
|
|
|
|
} else {
|
|
|
|
throw new SCoreException("Failed to hash password");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-29 17:23:29 +00:00
|
|
|
public function set_email(string $address): void
|
2019-05-28 16:59:38 +00:00
|
|
|
{
|
|
|
|
global $database;
|
2020-10-25 21:34:52 +00:00
|
|
|
$database->execute("UPDATE users SET email=:email WHERE id=:id", ["email"=>$address, "id"=>$this->id]);
|
2019-05-28 16:59:38 +00:00
|
|
|
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
|
2023-02-08 01:28:22 +00:00
|
|
|
{
|
|
|
|
$url = $this->get_avatar_url();
|
|
|
|
if (!empty($url)) {
|
|
|
|
return "<img alt='avatar' class=\"avatar gravatar\" src=\"$url\">";
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
#[Field(name: "avatar_url")]
|
|
|
|
public function get_avatar_url(): ?string
|
2019-05-28 16:59:38 +00:00
|
|
|
{
|
|
|
|
// 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");
|
2023-02-08 01:28:22 +00:00
|
|
|
return "https://www.gravatar.com/avatar/$hash.jpg?s=$s&d=$d&r=$r&cacheBreak=$cb";
|
2019-05-28 16:59:38 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-08 01:28:22 +00:00
|
|
|
return null;
|
2019-05-28 16:59:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 '<input type="hidden" name="auth_token" value="'.$at.'">';
|
|
|
|
}
|
|
|
|
|
|
|
|
public function check_auth_token(): bool
|
|
|
|
{
|
2020-04-25 20:35:14 +00:00
|
|
|
if (defined("UNITTEST")) {
|
|
|
|
return true;
|
|
|
|
}
|
2019-05-28 16:59:38 +00:00
|
|
|
return (isset($_POST["auth_token"]) && $_POST["auth_token"] == $this->get_auth_token());
|
2019-12-15 15:31:44 +00:00
|
|
|
}
|
2019-11-27 21:06:14 +00:00
|
|
|
|
2019-12-15 15:31:44 +00:00
|
|
|
public function ensure_authed(): void
|
|
|
|
{
|
|
|
|
if (!$this->check_auth_token()) {
|
|
|
|
die("Invalid auth token");
|
|
|
|
}
|
2019-05-28 16:59:38 +00:00
|
|
|
}
|
2007-04-16 11:58:25 +00:00
|
|
|
}
|