1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 21:22:40 +00:00

Prototype Miniflux user querying

This commit is contained in:
J. King 2020-12-08 15:34:31 -05:00
parent 2eedf7d38c
commit d85988f09d
3 changed files with 85 additions and 13 deletions

View file

@ -6,7 +6,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Misc; namespace JKingWeb\Arsse\Misc;
class Date { abstract class Date {
public static function transform($date, string $outFormat = null, string $inFormat = null) { public static function transform($date, string $outFormat = null, string $inFormat = null) {
$date = ValueInfo::normalize($date, ValueInfo::T_DATE, $inFormat); $date = ValueInfo::normalize($date, ValueInfo::T_DATE, $inFormat);
if (!$date) { if (!$date) {

View file

@ -12,6 +12,7 @@ use JKingWeb\Arsse\Feed\Exception as FeedException;
use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\AbstractException;
use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\Misc\HTTP;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\Misc\ValueInfo as V;
use JKingWeb\Arsse\REST\Exception; use JKingWeb\Arsse\REST\Exception;
use JKingWeb\Arsse\User\ExceptionConflict as UserException; use JKingWeb\Arsse\User\ExceptionConflict as UserException;
@ -32,8 +33,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
'password' => "string", 'password' => "string",
'user_agent' => "string", 'user_agent' => "string",
]; ];
protected const PATHS = [
protected $paths = [
'/categories' => ['GET' => "getCategories", 'POST' => "createCategory"], '/categories' => ['GET' => "getCategories", 'POST' => "createCategory"],
'/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"], '/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"],
'/discover' => ['POST' => "discoverSubscriptions"], '/discover' => ['POST' => "discoverSubscriptions"],
@ -51,8 +51,16 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
'/import' => ['POST' => "opmlImport"], '/import' => ['POST' => "opmlImport"],
'/me' => ['GET' => "getCurrentUser"], '/me' => ['GET' => "getCurrentUser"],
'/users' => ['GET' => "getUsers", 'POST' => "createUser"], '/users' => ['GET' => "getUsers", 'POST' => "createUser"],
'/users/1' => ['GET' => "getUser", 'PUT' => "updateUser", 'DELETE' => "deleteUser"], '/users/1' => ['GET' => "getUserByNum", 'PUT' => "updateUserByNum", 'DELETE' => "deleteUser"],
'/users/*' => ['GET' => "getUser"], '/users/*' => ['GET' => "getUserById"],
];
protected const ADMIN_FUNCTIONS = [
'getUsers' => true,
'getUserByNum' => true,
'getUserById' => true,
'createUser' => true,
'updateUserByNum' => true,
'deleteUser' => true,
]; ];
public function __construct() { public function __construct() {
@ -80,6 +88,11 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
return false; return false;
} }
protected function isAdmin(): bool {
return (bool) Arsse::$user->propertiesGet(Arsse::$user->id, false)['admin'];
}
public function dispatch(ServerRequestInterface $req): ResponseInterface { public function dispatch(ServerRequestInterface $req): ResponseInterface {
// try to authenticate // try to authenticate
if (!$this->authenticate($req)) { if (!$this->authenticate($req)) {
@ -96,6 +109,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
if ($func instanceof ResponseInterface) { if ($func instanceof ResponseInterface) {
return $func; return $func;
} }
if ((self::ADMIN_FUNCTIONS[$func] ?? false) && !$this->isAdmin()) {
return new ErrorResponse("403", 403);
}
$data = []; $data = [];
$query = []; $query = [];
if ($func === "opmlImport") { if ($func === "opmlImport") {
@ -148,9 +164,9 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function handleHTTPOptions(string $url): ResponseInterface { protected function handleHTTPOptions(string $url): ResponseInterface {
// normalize the URL path: change any IDs to 1 for easier comparison // normalize the URL path: change any IDs to 1 for easier comparison
$url = $this->normalizePathIDs($url); $url = $this->normalizePathIDs($url);
if (isset($this->paths[$url])) { if (isset(self::PATHS[$url])) {
// if the path is supported, respond with the allowed methods and other metadata // if the path is supported, respond with the allowed methods and other metadata
$allowed = array_keys($this->paths[$url]); $allowed = array_keys(self::PATHS[$url]);
// if GET is allowed, so is HEAD // if GET is allowed, so is HEAD
if (in_array("GET", $allowed)) { if (in_array("GET", $allowed)) {
array_unshift($allowed, "HEAD"); array_unshift($allowed, "HEAD");
@ -172,15 +188,15 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
$method = strtoupper($method); $method = strtoupper($method);
// we now evaluate the supplied URL against every supported path for the selected scope // we now evaluate the supplied URL against every supported path for the selected scope
// the URL is evaluated as an array so as to avoid decoded escapes turning invalid URLs into valid ones // the URL is evaluated as an array so as to avoid decoded escapes turning invalid URLs into valid ones
if (isset($this->paths[$url])) { if (isset(self::PATHS[$url])) {
// if the path is supported, make sure the method is allowed // if the path is supported, make sure the method is allowed
if (isset($this->paths[$url][$method])) { if (isset(self::PATHS[$url][$method])) {
// if it is allowed, return the object method to run, assuming the method exists // if it is allowed, return the object method to run, assuming the method exists
assert(method_exists($this, $this->paths[$url][$method]), new \Exception("Method is not implemented")); assert(method_exists($this, self::PATHS[$url][$method]), new \Exception("Method is not implemented"));
return $this->paths[$url][$method]; return self::PATHS[$url][$method];
} else { } else {
// otherwise return 405 // otherwise return 405
return new EmptyResponse(405, ['Allow' => implode(", ", array_keys($this->paths[$url]))]); return new EmptyResponse(405, ['Allow' => implode(", ", array_keys(self::PATHS[$url]))]);
} }
} else { } else {
// if the path is not supported, return 404 // if the path is not supported, return 404
@ -200,6 +216,40 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
return $body; return $body;
} }
protected function listUsers(array $users, bool $reportMissing): array {
$out = [];
$now = Date::transform("now", "iso8601m");
foreach ($users as $u) {
try {
$info = Arsse::$user->propertiesGet($u, true);
} catch (UserException $e) {
if ($reportMissing) {
throw $e;
} else {
continue;
}
}
$out[] = [
'id' => $info['num'],
'username' => $u,
'is_admin' => $info['admin'] ?? false,
'theme' => $info['theme'] ?? "light_serif",
'language' => $info['lang'] ?? "en_US",
'timezone' => $info['tz'] ?? "UTC",
'entry_sorting_direction' => ($info['sort_asc'] ?? false) ? "asc" : "desc",
'entries_per_page' => $info['page_size'] ?? 100,
'keyboard_shortcuts' => $info['shortcuts'] ?? true,
'show_reading_time' => $info['reading_time'] ?? true,
'last_login_at' => $now,
'entry_swipe' => $info['swipe'] ?? true,
'extra' => [
'custom_css' => $info['stylesheet'] ?? "",
],
];
}
return $out;
}
protected function discoverSubscriptions(array $path, array $query, array $data) { protected function discoverSubscriptions(array $path, array $query, array $data) {
try { try {
$list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']); $list = Feed::discoverAll((string) $data['url'], (string) $data['username'], (string) $data['password']);
@ -219,6 +269,26 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler {
return new Response($out); return new Response($out);
} }
protected function getUsers(array $path, array $query, array $data) {
return new Response($this->listUsers(Arsse::$user->list(), false));
}
protected function getUserById(array $path, array $query, array $data) {
try {
return $this->listUsers([$path[1]], true)[0] ?? [];
} catch (UserException $e) {
return new ErrorResponse("404", 404);
}
}
protected function getUserByNum(array $path, array $query, array $data) {
return $this->listUsers([Arsse::$user->id], false)[0] ?? [];
}
protected function getCurrentUser(array $path, array $query, array $data) {
return new Response($this->listUsers([Arsse::$user->id], false)[0] ?? new \stdClass);
}
public static function tokenGenerate(string $user, string $label): string { public static function tokenGenerate(string $user, string $label): string {
// Miniflux produces tokens in base64url alphabet // Miniflux produces tokens in base64url alphabet
$t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH)));

View file

@ -8,6 +8,8 @@ return [
'CLI.Auth.Failure' => 'Authentication failed', 'CLI.Auth.Failure' => 'Authentication failed',
'API.Miniflux.Error.401' => 'Access Unauthorized', 'API.Miniflux.Error.401' => 'Access Unauthorized',
'API.Miniflux.Error.403' => 'Access Forbidden',
'API.Miniflux.Error.404' => 'Resource Not Found',
'API.Miniflux.Error.invalidBodyJSON' => 'Invalid JSON payload: {0}', 'API.Miniflux.Error.invalidBodyJSON' => 'Invalid JSON payload: {0}',
'API.Miniflux.Error.invalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}', 'API.Miniflux.Error.invalidInputType' => 'Input key "{field}" of type {actual} was expected as {expected}',
'API.Miniflux.Error.fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL', 'API.Miniflux.Error.fetch404' => 'Resource not found (404), this feed doesn\'t exists anymore, check the feed URL',