karadav/lib/KaraDAV/Users.php

207 lines
5.1 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-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
{
2022-08-31 06:06:27 +00:00
return iterator_to_array(DB::getInstance()->iterate('SELECT * FROM users ORDER BY login;'));
}
2022-08-30 05:01:39 +00:00
2022-08-31 06:06:27 +00:00
public function get(string $login): ?stdClass
{
$user = DB::getInstance()->first('SELECT * FROM users WHERE login = ?;', $login);
return $this->makeUserObjectGreatAgain($user);
}
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
}
2022-08-31 06:06:27 +00:00
public function create(string $login, string $password)
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) VALUES (?, ?);', $login, $hash);
2022-08-30 05:01:39 +00:00
}
2022-08-31 06:06:27 +00:00
public function edit(string $login, array $data)
2022-08-30 05:01:39 +00:00
{
2022-08-31 06:06:27 +00:00
$params = [];
if (isset($data['password'])) {
$params['password'] = password_hash(trim($data['password']), null);
}
if (isset($data['quota'])) {
$params['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[] = $login;
DB::getInstance()->run(sprintf('UPDATE users SET %s WHERE login = ?;', $update), ...$params);
}
public function current(): ?stdClass
{
2022-08-30 05:01:39 +00:00
if (isset($_COOKIE[session_name()]) && !isset($_SESSION)) {
session_start();
}
2022-08-31 06:06:27 +00:00
return $this->makeUserObjectGreatAgain($_SESSION['user'] ?? null);
}
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;
}
if (!$login || (!$password && !$app_password)) {
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-08-31 06:06:27 +00:00
if ($app_password) {
$list = DB::getInstance()->iterate('SELECT password FROM app_sessions WHERE login = ? AND expiry > datetime();', $login);
$ok = false;
$app_password = trim($app_password) . $user->password;
// We have to iterate on all sessions, as NextCloud does not provide a unique login
foreach ($list as $session) {
if (password_verify($app_password, $hash)) {
$ok = true;
break;
}
}
if (!$ok) {
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 appSessionCreate(?string $token = null): ?string
{
$current = $this->current();
if (!$current) {
return null;
}
if (null === $token) {
$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);
}
DB::getInstance()->run(
'INSERT OR IGNORE INTO app_sessions (user, password, expiry, token) VALUES (?, ?, datetime(\'now\', ?), ?);',
$current->login, $hash, $expiry, $token);
return $password;
}
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
$hash = password_hash($session->password . $current->password, null);
DB::getInstance()->run('UPDATE app_sessions
SET token = NULL, password = ?, expiry = datetime(\'now\', \'+1 month\')
WHERE token = ?;',
$hash, $token);
return $session;
}
public function appSessionLogin(?string $login, ?string $app_password): ?stdClass
{
// From time to time, clean up old sessions
if (random_int() % 100 == 0) {
DB::getInstance()->run('DELETE FROM app_sessions WHERE expiry < datetime();');
}
return $this->login($login, null, $app_password);
}
public function quota(?stdClass $user = null): stdClass
{
$user ??= $this->current();
$used = get_directory_size($user->path);
$total = $user->quota;
$free = $user->quota - $used;
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
}
}