diff --git a/lib/REST.php b/lib/REST.php index 0edfb538..6c9b8654 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -30,11 +30,14 @@ class REST { function dispatch(REST\Request $req = null): bool { if($req===null) $req = new REST\Request(); - $api = $this->apiMatch($url, $this->apis); - $req->url = substr($url,strlen($this->apis[$api]['strip'])); + $api = $this->apiMatch($req->url, $this->apis); + $req->url = substr($req->url,strlen($this->apis[$api]['strip'])); + $req->refreshURL(); $class = $this->apis[$api]['class']; $drv = new $class(); - $drv->dispatch($req); + $out = $drv->dispatch($req); + echo "Status: ".$out->code."\n"; + echo json_encode($out->payload,\JSON_PRETTY_PRINT); return true; } diff --git a/lib/REST/AbstractHandler.php b/lib/REST/AbstractHandler.php index d4fa891a..d218ffcd 100644 --- a/lib/REST/AbstractHandler.php +++ b/lib/REST/AbstractHandler.php @@ -5,29 +5,4 @@ namespace JKingWeb\Arsse\REST; abstract class AbstractHandler implements Handler { abstract function __construct(); abstract function dispatch(Request $req): Response; - - protected function parseURL(string $url): array { - // split the query string from the path - $parts = explode("?", $url); - $out = ['path' => $parts[0], 'query' => []]; - // if there is a query string, parse it - if(isset($parts[1])) { - // split along & to get key-value pairs - $query = explode("&", $parts[1]); - for($a = 0; $a < sizeof($query); $a++) { - // split each pair, into no more than two parts - $data = explode("=", $query[$a], 2); - // decode the key - $key = rawurldecode($data[0]); - // decode the value if there is one - $value = ""; - if(isset($data[1])) { - $value = rawurldecode($data[1]); - } - // add the pair to the query output, overwriting earlier values for the same key, is present - $out['query'][$key] = $value; - } - } - return $out; - } } \ No newline at end of file diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index d68c3e62..76675bad 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/lib/REST/NextCloudNews/V1_2.php @@ -1,6 +1,7 @@ parseURL($req->url)); - if(preg_match("<^/(items|folders|feeds|cleanup|version|status|user)(?:/([^/]+))?(?:/([^/]+))?(?:/([^/]+))?/?$>", $path, $matches)) { - $scope = $matches[1]; - var_export($scope); + // try to authenticate + if(!Data::$user->authHTTP()) return new Response(401, "", null, ['WWW-Authenticate: Basic realm="NextCloud News API"']); + // only accept GET, POST, or PUT + if(!in_array($req->method, ["GET", "POST", "PUT"])) return new Response(405); + // match the path + if(preg_match("<^/(items|folders|feeds|cleanup|version|status|user)(?:/([^/]+))?(?:/([^/]+))?(?:/([^/]+))?/?$>", $req->path, $match)) { + // dispatch + switch($match[1]) { + case "folders": + switch($req->method) { + case "GET": return $this->folderList(); + case "POST": return $this->folderAdd($this->normalizeInput($req)); + case "PUT": + list($path, $scope, $id, $action) = $match; + return $this->folderEdit($id, $action, $this->normalizeInput($req)); + } + default: return new Response(404); + } } else { return new Response(404); } } + + protected function folderList(): Response { + $folders = Data::$db->folderList(Data::$user->id, null, false)->getAll(); + return new Response(200, ['folders' => $folders]); + } } \ No newline at end of file diff --git a/lib/REST/NextCloudNews/Versions.php b/lib/REST/NextCloudNews/Versions.php index aa923e22..c561d59c 100644 --- a/lib/REST/NextCloudNews/Versions.php +++ b/lib/REST/NextCloudNews/Versions.php @@ -8,13 +8,11 @@ class Versions extends \JKingWeb\Arsse\REST\AbstractHandler { } function dispatch(\JKingWeb\Arsse\REST\Request $req): \JKingWeb\Arsse\REST\Response { - // parse the URL and populate $path and $query - extract($this->parseURL($req->url)); // if a method other than GET was used, this is an error if($req->method != "GET") { return new Response(405); } - if(preg_match("<^/?$>",$path)) { + if(preg_match("<^/?$>",$req->path)) { // if the request path is an empty string or just a slash, return the supported versions $out = [ 'apiLevels' => [ diff --git a/lib/REST/Request.php b/lib/REST/Request.php index dcfd7f87..12e6dd79 100644 --- a/lib/REST/Request.php +++ b/lib/REST/Request.php @@ -5,6 +5,8 @@ namespace JKingWeb\Arsse\REST; class Request { public $method = "GET"; public $url = ""; + public $path =""; + public $query = ""; public $type =""; public $stream = "php://input"; @@ -19,5 +21,37 @@ class Request { $this->url = $url; $this->stream = $bodyStream; $this->type = $contentType; + $this->refreshURL(); + } + + public function refreshURL() { + $url = $this->parseURL($this->url); + $this->path = $url['path']; + $this->query = $url['query']; + } + + protected function parseURL(string $url): array { + // split the query string from the path + $parts = explode("?", $url); + $out = ['path' => $parts[0], 'query' => []]; + // if there is a query string, parse it + if(isset($parts[1])) { + // split along & to get key-value pairs + $query = explode("&", $parts[1]); + for($a = 0; $a < sizeof($query); $a++) { + // split each pair, into no more than two parts + $data = explode("=", $query[$a], 2); + // decode the key + $key = rawurldecode($data[0]); + // decode the value if there is one + $value = ""; + if(isset($data[1])) { + $value = rawurldecode($data[1]); + } + // add the pair to the query output, overwriting earlier values for the same key, is present + $out['query'][$key] = $value; + } + } + return $out; } } \ No newline at end of file diff --git a/locale/en.php b/locale/en.php index b4c351fe..4f905dae 100644 --- a/locale/en.php +++ b/locale/en.php @@ -4,6 +4,12 @@ return [ 'Driver.Db.SQLite3.Name' => 'SQLite 3', + 'HTTP.Status.200' => 'OK', + 'HTTP.Status.204' => 'No Content', + 'HTTP.Status.401' => 'Unauthorized', + 'HTTP.Status.404' => 'Not Found', + 'HTTP.Status.405' => 'Method Not Allowed', + // this should only be encountered in testing (because tests should cover all exceptions!) 'Exception.JKingWeb/Arsse/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in AbstractException.php', // this should not usually be encountered diff --git a/tests/REST/NextCloudNews/TestNCNV1_2.php b/tests/REST/NextCloudNews/TestNCNV1_2.php new file mode 100644 index 00000000..180dfefb --- /dev/null +++ b/tests/REST/NextCloudNews/TestNCNV1_2.php @@ -0,0 +1,39 @@ +clearData(); + // create a mock user manager + Data::$user = Phake::mock(User::class); + Phake::when(Data::$user)->authHTTP->thenReturn(true); + Data::$user->id = "john.doe@example.com"; + // create a mock database interface + Data::$db = Phake::mock(Database::Class); + $this->h = new REST\NextCloudNews\V1_2(); + } + + function tearDown() { + $this->clearData(); + } + + function testListFolders() { + $list = [ + ['id' => 1, 'name' => "Software", 'parent' => null], + ['id' => 12, 'name' => "Hardware", 'parent' => null], + ]; + Phake::when(Data::$db)->folderList(Data::$user->id, null, false)->thenReturn(new Result($list)); + $exp = new Response(200, ['folders' => $list]); + $this->assertEquals($exp, $this->h->dispatch(new Request("GET", "/folders"))); + } +} \ No newline at end of file diff --git a/tests/lib/Result.php b/tests/lib/Result.php new file mode 100644 index 00000000..e7d43e82 --- /dev/null +++ b/tests/lib/Result.php @@ -0,0 +1,73 @@ +next(); + if($this->valid()) { + $keys = array_keys($arr); + return $arr[array_shift($keys)]; + } + return null; + } + + public function getRow() { + $arr = $this->next(); + return ($this->valid() ? $arr : null); + } + + public function getAll(): array { + return $this->set; + } + + public function changes() { + return $this->rows; + } + + public function lastId() { + return $this->id; + } + + // constructor/destructor + + public function __construct(array $result, int $changes = 0, int $lastID = 0) { + $this->set = $result; + $this->rows = $changes; + $this->id = $lastID; + } + + public function __destruct() { + } + + // PHP iterator methods + + public function valid() { + return !is_null(key($this->set)); + } + + public function next() { + return next($this->set); + } + + public function current() { + return current($this->set); + } + + public function key() { + return key($this->set); + } + + public function rewind() { + rewind($this->set); + } +} \ No newline at end of file diff --git a/tests/phpunit.xml b/tests/phpunit.xml index dbd3a1b0..2024784b 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -40,6 +40,7 @@ REST/NextCloudNews/TestNCNVersionDiscovery.php + REST/NextCloudNews/TestNCNV1_2.php \ No newline at end of file