karadav/lib/KaraDAV/Users.php

326 lines
7.7 KiB
PHP
Raw Normal View History

2022-08-30 05:01:39 +00:00
<?php
namespace KaraDAV;
2022-08-31 06:06:27 +00:00
use stdClass;
2022-08-30 05:01:39 +00:00
class Users
{
2022-09-04 00:27:40 +00:00
protected ?stdClass $current = null;
2022-10-28 21:42:55 +00:00
public function __construct()
{
if (!session_id()) {
// Protect the cookie : CSRF/JS stealing the cookie
session_set_cookie_params(['samesite' => 'Lax', 'httponly' => true]);
}
}
2022-08-31 06:06:27 +00:00
static public function generatePassword(): string
{
$password = base64_encode(random_bytes(16));
$password = substr(str_replace(['/', '+', '='], '', $password), 0, 16);
return $password;
}
2022-08-30 05:01:39 +00:00
public function list(): array
{
return array_map([$this, 'makeUserObjectGreatAgain'], iterator_to_array(DB::getInstance()->iterate('SELECT * FROM users ORDER BY login;')));
2022-08-31 06:06:27 +00:00
}
2022-08-30 05:01:39 +00:00
2022-10-24 22:35:52 +00:00
public function fetch(string $login): ?stdClass
{
return DB::getInstance()->first('SELECT * FROM users WHERE login = ?;', $login);
}
2022-08-31 06:06:27 +00:00
public function get(string $login): ?stdClass
{
2022-10-24 22:35:52 +00:00
$user = $this->fetch($login);
if (!$user && LDAP::enabled() && LDAP::checkUser($login)) {
$this->create($login, self::generatePassword(), DEFAULT_QUOTA);
$user = $this->fetch($login);
if (!$user) {
throw new \LogicException('User does not exist after getting created?');
}
$user->is_admin = LDAP::checkIsAdmin($login);
}
elseif (!$user) {
return null;
}
2022-08-31 06:06:27 +00:00
return $this->makeUserObjectGreatAgain($user);
}
public function getById(int $id): ?stdClass
{
$user = DB::getInstance()->first('SELECT * FROM users WHERE id = ?;', $id);
return $this->makeUserObjectGreatAgain($user);
}
2022-08-31 06:06:27 +00:00
protected function makeUserObjectGreatAgain(?stdClass $user): ?stdClass
{
if ($user) {
$user->path = sprintf(STORAGE_PATH, $user->login);
$user->path = rtrim($user->path, '/') . '/';
if (!file_exists($user->path)) {
mkdir($user->path, 0770, true);
}
$user->dav_url = WWW_URL . 'files/' . $user->login . '/';
2022-08-30 05:01:39 +00:00
}
2022-08-31 06:06:27 +00:00
return $user;
2022-08-30 05:01:39 +00:00
}
public function create(string $login, string $password, int $quota = DEFAULT_QUOTA, bool $is_admin = false)
2022-08-30 05:01:39 +00:00
{
2022-08-31 06:06:27 +00:00
$login = strtolower(trim($login));
$hash = password_hash(trim($password), null);
DB::getInstance()->run('INSERT OR IGNORE INTO users (login, password, quota, is_admin) VALUES (?, ?, ?, ?);',
$login, $hash, $quota * 1024 * 1024, $is_admin ? 1 : 0);
2022-08-30 05:01:39 +00:00
}
public function edit(int $id, array $data)
2022-08-30 05:01:39 +00:00
{
2022-08-31 06:06:27 +00:00
$params = [];
if (!empty($data['password'])) {
2022-08-31 06:06:27 +00:00
$params['password'] = password_hash(trim($data['password']), null);
}
if (!empty($data['login'])) {
$params['login'] = trim($data['login']);
}
2022-08-31 06:06:27 +00:00
if (isset($data['quota'])) {
$params['quota'] = $data['quota'] <= 0 ? (int) $data['quota'] : (int) $data['quota'] * 1024 * 1024;
2022-08-30 05:01:39 +00:00
}
2022-08-31 06:06:27 +00:00
if (isset($data['is_admin'])) {
$params['is_admin'] = (int) $data['is_admin'];
}
$update = array_map(fn($k) => $k . ' = ?', array_keys($params));
$update = implode(', ', $update);
$params = array_values($params);
$params[] = $id;
2022-08-31 06:06:27 +00:00
DB::getInstance()->run(sprintf('UPDATE users SET %s WHERE id = ?;', $update), ...$params);
2022-08-31 06:06:27 +00:00
}
public function current(): ?stdClass
{
2022-09-04 00:27:40 +00:00
if ($this->current) {
return $this->current;
}
2022-08-30 05:01:39 +00:00
if (isset($_COOKIE[session_name()]) && !isset($_SESSION)) {
session_start();
}
2022-09-04 00:27:40 +00:00
$this->current = $this->makeUserObjectGreatAgain($_SESSION['user'] ?? null);
return $this->current;
}
public function setCurrent(string $login): bool
{
$user = $this->get($login);
if (!$user) {
return false;
}
$this->current = $user;
return true;
2022-08-31 06:06:27 +00:00
}
public function login(?string $login, ?string $password, ?string $app_password = null): ?stdClass
{
$login = null !== $login ? strtolower(trim($login)) : null;
2022-08-30 05:01:39 +00:00
// Check if user already has a session
2022-08-31 06:06:27 +00:00
$current = $this->current();
if ($current && (!$login || $current->login == $login)) {
return $current;
}
2022-08-31 07:57:49 +00:00
if (!$login || !$password) {
2022-08-31 06:06:27 +00:00
return null;
2022-08-30 05:01:39 +00:00
}
// If not, try to login
2022-08-31 06:06:27 +00:00
$user = $this->get($login);
2022-08-30 05:01:39 +00:00
2022-08-31 06:06:27 +00:00
if (!$user) {
2022-08-30 05:01:39 +00:00
return null;
}
2022-10-24 22:35:52 +00:00
if (LDAP::enabled()) {
if (!LDAP::checkPassword($login, $password)) {
return null;
}
}
elseif (!password_verify(trim($password), $user->password)) {
2022-08-30 05:01:39 +00:00
return null;
}
@session_start();
2022-08-31 06:06:27 +00:00
$_SESSION['user'] = $user;
return $user;
}
public function logout(): void
{
session_destroy();
}
2022-08-31 07:57:49 +00:00
public function appSessionCreate(?string $token = null): ?stdClass
2022-08-31 06:06:27 +00:00
{
$current = $this->current();
if (!$current) {
return null;
}
2022-08-31 07:57:49 +00:00
if (null !== $token) {
if (!ctype_alnum($token) || strlen($token) > 100) {
return null;
}
2022-08-31 06:06:27 +00:00
$expiry = '+10 minutes';
$hash = null;
$password = null;
}
else {
$expiry = '+1 month';
$password = $this->generatePassword();
// The app password contains the user password hash
// this way we can invalidate all sessions if we change
// the user password
$hash = password_hash($password . $current->password, null);
2022-08-31 07:57:49 +00:00
$token = $this->generatePassword();
2022-08-31 06:06:27 +00:00
}
DB::getInstance()->run(
'INSERT OR IGNORE INTO app_sessions (user, password, expiry, token) VALUES (?, ?, datetime(\'now\', ?), ?);',
2022-10-24 22:35:52 +00:00
$current->id, $hash, $expiry, $token);
2022-08-31 06:06:27 +00:00
2022-08-31 07:57:49 +00:00
return (object) compact('password', 'token');
}
public function appSessionCreateAndGetRedirectURL(): string
{
$session = $this->appSessionCreate();
$current = $this->current();
return sprintf(NextCloud::AUTH_REDIRECT_URL, WWW_URL, $current->login, $session->token . ':' . $session->password);
2022-08-31 06:06:27 +00:00
}
public function appSessionValidateToken(string $token): ?stdClass
{
$session = DB::getInstance()->first('SELECT * FROM app_sessions WHERE token = ?;', $token);
if (!$session) {
return null;
}
// the token can only be exchanged against a session once,
// so we set a password and remove the token
$session->password = $this->generatePassword();
// The app password contains the user password hash
// this way we can invalidate all sessions if we change
// the user password
2022-10-24 22:35:52 +00:00
$user = $this->getById($session->user);
2022-08-31 07:57:49 +00:00
$hash = password_hash($session->password . $user->password, null);
$session->token = self::generatePassword();
$session->password = $session->token . ':' . $session->password;
2022-08-31 06:06:27 +00:00
DB::getInstance()->run('UPDATE app_sessions
2022-08-31 07:57:49 +00:00
SET token = ?, password = ?, expiry = datetime(\'now\', \'+1 month\')
2022-08-31 06:06:27 +00:00
WHERE token = ?;',
2022-08-31 07:57:49 +00:00
$session->token, $hash, $token);
2022-08-31 06:06:27 +00:00
2022-10-24 22:35:52 +00:00
$session->user = $user;
2022-08-31 06:06:27 +00:00
return $session;
}
public function appSessionLogin(?string $login, ?string $app_password): ?stdClass
{
// From time to time, clean up old sessions
2022-08-31 07:57:49 +00:00
if (time() % 100 == 0) {
2022-08-31 06:06:27 +00:00
DB::getInstance()->run('DELETE FROM app_sessions WHERE expiry < datetime();');
}
if (($user = $this->current()) && $login == $user->login) {
2022-08-31 07:57:49 +00:00
return $user;
}
if (!$app_password) {
return null;
}
$token = strtok($app_password, ':');
$password = strtok('');
2022-08-31 07:57:49 +00:00
$user = DB::getInstance()->first('SELECT s.password AS app_hash, u.*
2022-10-24 22:35:52 +00:00
FROM app_sessions s INNER JOIN users u ON u.id = s.user
WHERE s.token = ? AND s.expiry > datetime();', $token);
2022-08-31 07:57:49 +00:00
if (!$user) {
return null;
}
$password = trim($password) . $user->password;
2022-08-31 07:57:49 +00:00
if (!password_verify($password, $user->app_hash)) {
2022-08-31 07:57:49 +00:00
return null;
}
@session_start();
$_SESSION['user'] = $user;
return $this->makeUserObjectGreatAgain($user);
2022-08-31 06:06:27 +00:00
}
public function quota(?stdClass $user = null): stdClass
{
$user ??= $this->current();
2022-08-31 07:57:49 +00:00
$used = $total = $free = 0;
if ($user) {
if ($user->quota == -1) {
$total = (int) @disk_total_space($user->path);
$free = (int) @disk_free_space($user->path);
$used = $total - $free;
}
elseif ($user->quota == 0) {
$total = 0;
$free = 0;
$used = 0;
}
else {
$used = Storage::getDirectorySize($user->path);
$total = $user->quota;
$free = max(0, $total - $used);
}
2022-08-31 07:57:49 +00:00
}
2022-08-30 05:01:39 +00:00
2022-08-31 06:06:27 +00:00
return (object) compact('free', 'total', 'used');
2022-08-30 05:01:39 +00:00
}
public function delete(?stdClass $user)
{
Storage::deleteDirectory($user->path);
DB::getInstance()->run('DELETE FROM users WHERE id = ?;', $user->id);
}
2022-08-30 05:01:39 +00:00
}