diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index a60129d..4170644 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/lib/REST/NextCloudNews/V1_2.php @@ -76,6 +76,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { if (!Arsse::$user->authHTTP()) { return new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.self::REALM.'"']); } + // handle HTTP OPTIONS requests + if ($req->method=="OPTIONS") { + return $this->handleHTTPOptions($req->paths); + } // normalize the input if ($req->body) { // if the entity body is not JSON according to content type, return "415 Unsupported Media Type" @@ -144,7 +148,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { throw new Exception405(implode(", ", array_keys($this->paths[$url]))); } } else { - // if the path is not supported, return 501 + // if the path is not supported, return 404 throw new Exception404(); } } @@ -202,6 +206,26 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { ], $this->dateFormat); return $article; } + + protected function handleHTTPOptions(array $url): Response { + // normalize the URL path + $url = $this->normalizePath($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 Response(204, "", "", [ + "Allow: ".implode(",", $allowed), + "Accept: application/json", + ]); + } else { + // if the path is not supported, return 404 + return new Response(404); + } + } // list folders protected function folderList(array $url, array $data): Response { diff --git a/lib/REST/NextCloudNews/Versions.php b/lib/REST/NextCloudNews/Versions.php index 556d765..b51773f 100644 --- a/lib/REST/NextCloudNews/Versions.php +++ b/lib/REST/NextCloudNews/Versions.php @@ -13,21 +13,23 @@ class Versions implements \JKingWeb\Arsse\REST\Handler { } public function dispatch(\JKingWeb\Arsse\REST\Request $req): Response { - // if a method other than GET was used, this is an error - if ($req->method != "GET") { - return new Response(405, "", "", ["Allow: GET"]); - } - if (preg_match("<^/?$>", $req->path)) { - // if the request path is an empty string or just a slash, return the supported versions + if (!preg_match("<^/?$>", $req->path)) { + // if the request path is an empty string or just a slash, the client is probably trying a version we don't support + return new Response(404); + } elseif ($req->method=="OPTIONS") { + // if the request method is OPTIONS, respond accordingly + return new Response(204, "", "", ["Allow: HEAD,GET"]); + } elseif ($req->method != "GET") { + // if a method other than GET was used, this is an error + return new Response(405, "", "", ["Allow: HEAD,GET"]); + } else { + // otherwise return the supported versions $out = [ 'apiLevels' => [ 'v1-2', ] ]; return new Response(200, $out); - } else { - // if the URL path was anything else, the client is probably trying a version we don't support - return new Response(404); } } } diff --git a/tests/REST/NextCloudNews/TestNCNV1_2.php b/tests/REST/NextCloudNews/TestNCNV1_2.php index 7f8b47a..b1ac0e4 100644 --- a/tests/REST/NextCloudNews/TestNCNV1_2.php +++ b/tests/REST/NextCloudNews/TestNCNV1_2.php @@ -311,6 +311,12 @@ class TestNCNV1_2 extends Test\AbstractTest { $this->clearData(); } + public function testSendAuthenticationChallenge() { + Phake::when(Arsse::$user)->authHTTP->thenReturn(false); + $exp = new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.REST\NextCloudNews\V1_2::REALM.'"']); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/"))); + } + public function testRespondToInvalidPaths() { $errs = [ 404 => [ @@ -364,10 +370,24 @@ class TestNCNV1_2 extends Test\AbstractTest { $this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "/folders/1", '', 'application/json'))); } - public function testSendAuthenticationChallenge() { - Phake::when(Arsse::$user)->authHTTP->thenReturn(false); - $exp = new Response(401, "", "", ['WWW-Authenticate: Basic realm="'.REST\NextCloudNews\V1_2::REALM.'"']); - $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/"))); + public function testRespondToOptionsRequests() { + $exp = new Response(204, "", "", [ + "Allow: HEAD,GET,POST", + "Accept: application/json", + ]); + $this->assertEquals($exp, $this->h->dispatch(new Request("OPTIONS", "/feeds"))); + $exp = new Response(204, "", "", [ + "Allow: DELETE", + "Accept: application/json", + ]); + $this->assertEquals($exp, $this->h->dispatch(new Request("OPTIONS", "/feeds/2112"))); + $exp = new Response(204, "", "", [ + "Allow: HEAD,GET", + "Accept: application/json", + ]); + $this->assertEquals($exp, $this->h->dispatch(new Request("OPTIONS", "/user"))); + $exp = new Response(404); + $this->assertEquals($exp, $this->h->dispatch(new Request("OPTIONS", "/invalid/path"))); } public function testListFolders() { diff --git a/tests/REST/NextCloudNews/TestNCNVersionDiscovery.php b/tests/REST/NextCloudNews/TestNCNVersionDiscovery.php index ee4c9ed..3074917 100644 --- a/tests/REST/NextCloudNews/TestNCNVersionDiscovery.php +++ b/tests/REST/NextCloudNews/TestNCNVersionDiscovery.php @@ -29,8 +29,16 @@ class TestNCNVersionDiscovery extends Test\AbstractTest { $this->assertEquals($exp, $res); } + public function testRespondToOptionsRequest() { + $exp = new Response(204, "", "", ["Allow: HEAD,GET"]); + $h = new REST\NextCloudNews\Versions(); + $req = new Request("OPTIONS", "/"); + $res = $h->dispatch($req); + $this->assertEquals($exp, $res); + } + public function testUseIncorrectMethod() { - $exp = new Response(405, "", "", ["Allow: GET"]); + $exp = new Response(405, "", "", ["Allow: HEAD,GET"]); $h = new REST\NextCloudNews\Versions(); $req = new Request("POST", "/"); $res = $h->dispatch($req); @@ -43,5 +51,8 @@ class TestNCNVersionDiscovery extends Test\AbstractTest { $req = new Request("GET", "/ook"); $res = $h->dispatch($req); $this->assertEquals($exp, $res); + $req = new Request("OPTIONS", "/ook"); + $res = $h->dispatch($req); + $this->assertEquals($exp, $res); } }