From c92bb12a116ccf65b85a420bdd26640b7aa9fc68 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 1 Nov 2020 19:09:17 -0500 Subject: [PATCH] Prototype Miniflux dispatcher --- lib/REST.php | 2 +- lib/REST/Miniflux/V1.php | 50 ++++++++++++++++++++++++++++++--- lib/REST/NextcloudNews/V1_2.php | 20 ++++++------- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/lib/REST.php b/lib/REST.php index 0d04be9..011d27d 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -43,7 +43,7 @@ class REST { 'miniflux' => [ // Miniflux https://miniflux.app/docs/api.html 'match' => '/v1/', 'strip' => '/v1', - 'class' => REST\Miniflux\API::class, + 'class' => REST\Miniflux\V1::class, ], // Other candidates: // Microsub https://indieweb.org/Microsub diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 8fd1dc4..9edff15 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -23,6 +23,8 @@ use Laminas\Diactoros\Response\JsonResponse as Response; use Laminas\Diactoros\Response\EmptyResponse; class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { + protected const ACCEPTED_TYPES_OPML = ["text/xml", "application/xml", "text/x-opml"]; + protected const ACCEPTED_TYPES_JSON = ["application/json", "text/json"]; protected $paths = [ '/categories' => ['GET' => "getCategories", 'POST' => "createCategory"], '/categories/1' => ['PUT' => "updateCategory", 'DELETE' => "deleteCategory"], @@ -55,14 +57,46 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { if ($req->getAttribute("authenticated", false)) { Arsse::$user->id = $req->getAttribute("authenticatedUser"); } else { + // TODO: Handle X-Auth-Token authentication return new EmptyResponse(401); } // get the request path only; this is assumed to already be normalized $target = parse_url($req->getRequestTarget())['path'] ?? ""; + $method = $req->getMethod(); // handle HTTP OPTIONS requests - if ($req->getMethod() === "OPTIONS") { + if ($method === "OPTIONS") { return $this->handleHTTPOptions($target); } + $func = $this->chooseCall($target, $method); + if ($func === "opmlImport") { + if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_OPML])) { + return new EmptyResponse(415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_OPML)]); + } + $data = (string) $req->getBody(); + } elseif ($method === "POST" || $method === "PUT") { + if (!HTTP::matchType($req, "", ...[self::ACCEPTED_TYPES_JSON])) { + return new EmptyResponse(415, ['Accept' => implode(", ", self::ACCEPTED_TYPES_JSON)]); + } + $data = @json_decode($data, true); + if (json_last_error() !== \JSON_ERROR_NONE) { + // if the body could not be parsed as JSON, return "400 Bad Request" + return new EmptyResponse(400); + } + } else { + $data = null; + } + try { + $path = explode("/", ltrim($target, "/")); + return $this->$func($path, $req->getQueryParams(), $data); + // @codeCoverageIgnoreStart + } catch (Exception $e) { + // if there was a REST exception return 400 + return new EmptyResponse(400); + } catch (AbstractException $e) { + // if there was any other Arsse exception return 500 + return new EmptyResponse(500); + } + // @codeCoverageIgnoreEnd } protected function normalizePathIds(string $url): string { @@ -73,6 +107,10 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { $path[$a] = "1"; } } + // handle special case "Get User By User Name", which can have any non-numeric string, non-empty as the last component + if (sizeof($path) === 3 && $path[0] === "" && $path[1] === "users" && !preg_match("/^(?:\d+)?$/", $path[2])) { + $path[2] = "*"; + } return implode("/", $path); } @@ -88,7 +126,7 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { } return new EmptyResponse(204, [ 'Allow' => implode(",", $allowed), - 'Accept' => self::ACCEPTED_TYPE, + 'Accept' => implode(", ", $url === "/import" ? self::ACCEPTED_TYPES_OPML : self::ACCEPTED_TYPES_JSON), ]); } else { // if the path is not supported, return 404 @@ -106,8 +144,12 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 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]; + // if it is allowed, return the object method to run, assuming the method exists + if (method_exists($this, $this->paths[$url][$method])) { + return $this->paths[$url][$method]; + } else { + throw new Exception501(); // @codeCoverageIgnore + } } else { // otherwise return 405 throw new Exception405(implode(", ", array_keys($this->paths[$url]))); diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index c7389df..4741e83 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -17,6 +17,7 @@ use JKingWeb\Arsse\Misc\HTTP; use JKingWeb\Arsse\REST\Exception; use JKingWeb\Arsse\REST\Exception404; use JKingWeb\Arsse\REST\Exception405; +use JKingWeb\Arsse\REST\Exception501; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Laminas\Diactoros\Response\JsonResponse as Response; @@ -109,20 +110,15 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { // merge GET and POST data, and normalize it. POST parameters are preferred over GET parameters $data = $this->normalizeInput(array_merge($req->getQueryParams(), $data), $this->validInput, "unix"); // check to make sure the requested function is implemented + // dispatch try { $func = $this->chooseCall($target, $req->getMethod()); + $path = explode("/", ltrim($target, "/")); + return $this->$func($path, $data); } catch (Exception404 $e) { return new EmptyResponse(404); } catch (Exception405 $e) { return new EmptyResponse(405, ['Allow' => $e->getMessage()]); - } - if (!method_exists($this, $func)) { - return new EmptyResponse(501); // @codeCoverageIgnore - } - // dispatch - try { - $path = explode("/", ltrim($target, "/")); - return $this->$func($path, $data); // @codeCoverageIgnoreStart } catch (Exception $e) { // if there was a REST exception return 400 @@ -155,8 +151,12 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { 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]; + // if it is allowed, return the object method to run, assuming the method exists + if (method_exists($this, $this->paths[$url][$method])) { + return $this->paths[$url][$method]; + } else { + throw new Exception501(); // @codeCoverageIgnore + } } else { // otherwise return 405 throw new Exception405(implode(", ", array_keys($this->paths[$url])));