This repository has been archived on 2024-09-05. You can view files and clone it, but cannot push or open issues or pull requests.
shimmie2/core/user.php

285 lines
8.8 KiB
PHP
Raw Normal View History

2021-12-14 18:32:47 +00:00
<?php
declare(strict_types=1);
2014-05-23 23:28:57 +00:00
namespace Shimmie2;
2023-02-04 13:27:27 +00:00
use GQLA\Type;
use GQLA\Field;
use GQLA\Query;
2023-06-29 17:44:57 +00:00
use MicroHTML\HTMLElement;
use function MicroHTML\INPUT;
2023-02-01 12:50:00 +00:00
2009-07-19 07:38:13 +00:00
/**
2014-04-29 05:33:03 +00:00
* Class User
*
* 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.
*/
2023-02-04 13:27:27 +00:00
#[Type(name: "User")]
class User
{
public int $id;
2023-02-04 13:27:27 +00:00
#[Field]
public string $name;
public ?string $email;
2023-02-15 22:00:00 +00:00
#[Field]
public string $join_date;
public ?string $passhash;
2023-02-07 13:21:37 +00:00
#[Field]
public UserClass $class;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* 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.
*
* @param array<string|int, mixed> $row
*/
public function __construct(array $row)
{
2020-01-26 18:10:58 +00:00
$this->id = int_escape((string)$row['id']);
$this->name = $row['name'];
$this->email = $row['email'];
$this->join_date = $row['joindate'];
$this->passhash = $row['pass'];
2024-01-20 19:52:18 +00:00
if (array_key_exists($row["class"], UserClass::$known_classes)) {
$this->class = UserClass::$known_classes[$row["class"]];
} else {
2024-02-11 15:47:40 +00:00
throw new ServerError("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
{
global $cache, $config, $database;
$user = $cache->get("user-session-obj:$name-$session");
if (is_null($user)) {
$user_by_name = User::by_name($name);
if($user_by_name->get_session_id() === $session) {
$user = $user_by_name;
}
$cache->set("user-session-obj:$name-$session", $user, 600);
}
return $user;
}
2019-05-29 17:23:29 +00:00
public static function by_id(int $id): ?User
{
global $cache, $database;
if ($id === 1) {
$cached = $cache->get('user-id:'.$id);
if (!is_null($cached)) {
return new User($cached);
}
}
2023-11-11 21:49:12 +00:00
$row = $database->get_row("SELECT * FROM users WHERE id = :id", ["id" => $id]);
if ($id === 1) {
$cache->set('user-id:'.$id, $row, 600);
}
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
{
global $database;
2023-11-11 21:49:12 +00:00
$row = $database->get_row("SELECT * FROM users WHERE LOWER(name) = LOWER(:name)", ["name" => $name]);
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)) {
2024-02-11 15:47:40 +00:00
throw new UserNotFound("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
{
$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"
if (!$my_user && str_contains($name, " ")) {
$my_user = User::by_name(str_replace(" ", "_", $name));
}
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");
$my_user->set_password($pass);
}
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})");
return $my_user;
} else {
2020-01-26 17:39:55 +00:00
log_warning("core-user", "Failed to log in as $name (Invalid password)");
}
} else {
2020-01-26 17:39:55 +00:00
log_warning("core-user", "Failed to log in as $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'));
}
2019-05-29 17:23:29 +00:00
public function set_class(string $class): void
{
global $database;
2023-11-11 21:49:12 +00:00
$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);
}
2019-05-29 17:23:29 +00:00
public function set_name(string $name): void
{
global $database;
if (User::by_name($name)) {
2024-02-11 15:47:40 +00:00
throw new InvalidInput("Desired username is already in use");
}
$old_name = $this->name;
$this->name = $name;
2023-11-11 21:49:12 +00:00
$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}");
}
2019-05-29 17:23:29 +00:00
public function set_password(string $password): void
{
global $database;
$hash = password_hash($password, PASSWORD_BCRYPT);
2024-01-15 13:40:18 +00:00
$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);
}
2019-05-29 17:23:29 +00:00
public function set_email(string $address): void
{
global $database;
2023-11-11 21:49:12 +00:00
$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
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
{
// 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";
}
}
2023-02-08 01:28:22 +00:00
return null;
}
/**
* 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 = SECRET;
$addr = get_session_ip($config);
return md5(md5($this->passhash . $addr) . "salty-csrf-" . $salt);
}
public function get_session_id(): string
{
global $config;
$addr = get_session_ip($config);
$hash = $this->passhash;
return md5($hash . $addr);
}
public function set_login_cookie(): void
{
global $config, $page;
$page->add_cookie(
"user",
$this->name,
time() + 60 * 60 * 24 * 365,
'/'
);
$page->add_cookie(
"session",
$this->get_session_id(),
time() + 60 * 60 * 24 * $config->get_int('login_memory'),
'/'
);
}
}