From 8ad7fc81a89fc343344a3ee3f9bc69d27f2e005c Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 31 Oct 2020 21:26:11 -0400 Subject: [PATCH] Initially mapping out of Miniflux API --- lib/REST.php | 6 +- lib/REST/Miniflux/V1.php | 120 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 lib/REST/Miniflux/V1.php diff --git a/lib/REST.php b/lib/REST.php index 41fdaa1f..0d04be93 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -40,6 +40,11 @@ class REST { 'strip' => '/fever/', 'class' => REST\Fever\API::class, ], + 'miniflux' => [ // Miniflux https://miniflux.app/docs/api.html + 'match' => '/v1/', + 'strip' => '/v1', + 'class' => REST\Miniflux\API::class, + ], // Other candidates: // Microsub https://indieweb.org/Microsub // Google Reader http://feedhq.readthedocs.io/en/latest/api/index.html @@ -48,7 +53,6 @@ class REST { // Selfoss https://github.com/SSilence/selfoss/wiki/Restful-API-for-Apps-or-any-other-external-access // NewsBlur http://www.newsblur.com/api // Unclear if clients exist: - // Miniflux https://docs.miniflux.app/en/latest/api.html#api-reference // Nextcloud News v2 https://github.com/nextcloud/news/blob/master/docs/externalapi/External-Api.md // BirdReader https://github.com/glynnbird/birdreader/blob/master/API.md // Feedbin v1 https://github.com/feedbin/feedbin-api/commit/86da10aac5f1a57531a6e17b08744e5f9e7db8a9 diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php new file mode 100644 index 00000000..8fd1dc47 --- /dev/null +++ b/lib/REST/Miniflux/V1.php @@ -0,0 +1,120 @@ + ['GET' => "getCategories", 'POST' => "createCategory"], + '/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"], + '/discover' => ['POST' => "discoverSubscriptions"], + '/entries' => ['GET' => "getEntries", 'PUT' => "updateEntries"], + '/entries/1' => ['GET' => "getEntry"], + '/entries/1/bookmark' => ['PUT' => "toggleEntryBookmark"], + '/export' => ['GET' => "opmlExport"], + '/feeds' => ['GET' => "getFeeds", 'POST' => "createFeed"], + '/feeds/1' => ['GET' => "getFeed", 'PUT' => "updateFeed", 'DELETE' => "removeFeed"], + '/feeds/1/entries/1' => ['GET' => "getFeedEntry"], + '/feeds/1/entries' => ['GET' => "getFeedEntries"], + '/feeds/1/icon' => ['GET' => "getFeedIcon"], + '/feeds/1/refresh' => ['PUT' => "refreshFeed"], + '/feeds/refresh' => ['PUT' => "refreshAllFeeds"], + '/healthcheck' => ['GET' => "healthCheck"], + '/import' => ['POST' => "opmlImport"], + '/me' => ['GET' => "getCurrentUser"], + '/users' => ['GET' => "getUsers", 'POST' => "createUser"], + '/users/1' => ['GET' => "getUser", 'PUT' => "updateUser", 'DELETE' => "deleteUser"], + '/users/*' => ['GET' => "getUser"], + '/version' => ['GET' => "getVersion"], + ]; + + public function __construct() { + } + + public function dispatch(ServerRequestInterface $req): ResponseInterface { + // try to authenticate + if ($req->getAttribute("authenticated", false)) { + Arsse::$user->id = $req->getAttribute("authenticatedUser"); + } else { + return new EmptyResponse(401); + } + // get the request path only; this is assumed to already be normalized + $target = parse_url($req->getRequestTarget())['path'] ?? ""; + // handle HTTP OPTIONS requests + if ($req->getMethod() === "OPTIONS") { + return $this->handleHTTPOptions($target); + } + } + + protected function normalizePathIds(string $url): string { + $path = explode("/", $url); + // any path components which are database IDs (integers greater than zero) should be replaced with "1", for easier comparison (we don't care about the specific ID) + for ($a = 0; $a < sizeof($path); $a++) { + if (ValueInfo::id($path[$a])) { + $path[$a] = "1"; + } + } + return implode("/", $path); + } + + protected function handleHTTPOptions(string $url): ResponseInterface { + // normalize the URL path: change any IDs to 1 for easier comparison + $url = $this->normalizePathIDs($url); + if (isset($this->paths[$url])) { + // if the path is supported, respond with the allowed methods and other metadata + $allowed = array_keys($this->paths[$url]); + // if GET is allowed, so is HEAD + if (in_array("GET", $allowed)) { + array_unshift($allowed, "HEAD"); + } + return new EmptyResponse(204, [ + 'Allow' => implode(",", $allowed), + 'Accept' => self::ACCEPTED_TYPE, + ]); + } else { + // if the path is not supported, return 404 + return new EmptyResponse(404); + } + } + + protected function chooseCall(string $url, string $method): string { + // // normalize the URL path: change any IDs to 1 for easier comparison + $url = $this->normalizePathIds($url); + // normalize the HTTP method to uppercase + $method = strtoupper($method); + // 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 + if (isset($this->paths[$url])) { + // if the path is supported, make sure the method is allowed + if (isset($this->paths[$url][$method])) { + // if it is allowed, return the object method to run + return $this->paths[$url][$method]; + } else { + // otherwise return 405 + throw new Exception405(implode(", ", array_keys($this->paths[$url]))); + } + } else { + // if the path is not supported, return 404 + throw new Exception404(); + } + } +}