diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 706465e0..ebeeccc3 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -75,6 +75,8 @@ abstract class AbstractException extends \Exception { "User/Exception.authFailed" => 10412, "User/ExceptionAuthz.notAuthorized" => 10421, "User/ExceptionSession.invalid" => 10431, + "User/ExceptionInput.invalidTimezone" => 10441, + "User/ExceptionInput.invalidBoolean" => 10442, "Feed/Exception.internalError" => 10500, "Feed/Exception.invalidCertificate" => 10501, "Feed/Exception.invalidUrl" => 10502, diff --git a/lib/Database.php b/lib/Database.php index 92ddb891..6dc02ddb 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -37,6 +37,9 @@ use JKingWeb\Arsse\Misc\URL; * associations with articles. There has been an effort to keep public method * names consistent throughout, but protected methods, having different * concerns, will typically follow different conventions. + * + * Note that operations on users should be performed with the User class rather + * than the Database class directly. This is to allow for alternate user sources. */ class Database { /** The version number of the latest schema the interface is aware of */ @@ -310,6 +313,35 @@ class Database { $this->db->prepare("UPDATE arsse_users set password = ? where id = ?", "str", "str")->run($hash, $user); return true; } + + public function userPropertiesGet(string $user): array { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } elseif (!$this->userExists($user)) { + throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + } + $out = $this->db->prepare("SELECT num, admin, lang, tz, sort_asc from arsse_users where id = ?", "str")->run($user)->getRow(); + settype($out['admin'], "bool"); + settype($out['sort_asc'], "bool"); + return $out; + } + + public function userPropertiesSet(string $user, array $data): bool { + if (!Arsse::$user->authorize($user, __FUNCTION__)) { + throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); + } elseif (!$this->userExists($user)) { + throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]); + } + $allowed = [ + 'admin' => "strict bool", + 'lang' => "str", + 'tz' => "strict str", + 'sort_asc' => "strict bool", + ]; + [$setClause, $setTypes, $setValues] = $this->generateSet($data, $allowed); + return (bool) $this->$db->prepare("UPDATE arsse_users set $setClause where user = ?", $setTypes, "str")->run($setValues, $user)->changes(); + + } /** Creates a new session for the given user and returns the session identifier */ public function sessionCreate(string $user): string { diff --git a/lib/User.php b/lib/User.php index f5299914..e39516ce 100644 --- a/lib/User.php +++ b/lib/User.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; +use JKingWeb\Arsse\Misc\ValueInfo as V; use PasswordGenerator\Generator as PassGen; class User { @@ -120,4 +121,44 @@ class User { public function generatePassword(): string { return (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get(); } + + public function propertiesGet(string $user): array { + // unconditionally retrieve from the database to get at least the user number, and anything else the driver does not provide + $out = Arsse::$db->userPropertiesGet($user); + // layer on the driver's data + $extra = $this->u->userPropertiesGet($user); + foreach (["lang", "tz", "admin", "sort_asc"] as $k) { + if (array_key_exists($k, $extra)) { + $out[$k] = $extra[$k] ?? $out[$k]; + } + } + return $out; + } + + public function propertiesSet(string $user, array $data): bool { + $in = []; + if (array_key_exists("tz", $data)) { + if (!is_string($data['tz'])) { + throw new User\ExceptionInput("invalidTimezone"); + } elseif (!in_array($data['tz'], \DateTimeZone::listIdentifiers())) { + throw new User\ExceptionInput("invalidTimezone", $data['tz']); + } + $in['tz'] = $data['tz']; + } + foreach (["admin", "sort_asc"] as $k) { + if (array_key_exists($k, $data)) { + if (($v = V::normalize($data[$k], V::T_BOOL)) === null) { + throw new User\ExceptionInput("invalidBoolean", $k); + } + $in[$k] = $v; + } + } + if (array_key_exists("lang", $data)) { + $in['lang'] = V::normalize($data['lang'], V::T_STRING | M_NULL); + } + $out = $this->u->userPropertiesSet($user, $in); + // synchronize the internal database + Arsse::$db->userPropertiesSet($user, $in); + return $out; + } } diff --git a/lib/User/ExceptionInput.php b/lib/User/ExceptionInput.php new file mode 100644 index 00000000..aea8c131 --- /dev/null +++ b/lib/User/ExceptionInput.php @@ -0,0 +1,10 @@ +