mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Implemented NCN API v1-2 folder list
- Fixes #2 - Also re-organized REST handling
This commit is contained in:
parent
dc9f5e545e
commit
842e277d43
9 changed files with 184 additions and 36 deletions
|
@ -30,11 +30,14 @@ class REST {
|
||||||
|
|
||||||
function dispatch(REST\Request $req = null): bool {
|
function dispatch(REST\Request $req = null): bool {
|
||||||
if($req===null) $req = new REST\Request();
|
if($req===null) $req = new REST\Request();
|
||||||
$api = $this->apiMatch($url, $this->apis);
|
$api = $this->apiMatch($req->url, $this->apis);
|
||||||
$req->url = substr($url,strlen($this->apis[$api]['strip']));
|
$req->url = substr($req->url,strlen($this->apis[$api]['strip']));
|
||||||
|
$req->refreshURL();
|
||||||
$class = $this->apis[$api]['class'];
|
$class = $this->apis[$api]['class'];
|
||||||
$drv = new $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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,29 +5,4 @@ namespace JKingWeb\Arsse\REST;
|
||||||
abstract class AbstractHandler implements Handler {
|
abstract class AbstractHandler implements Handler {
|
||||||
abstract function __construct();
|
abstract function __construct();
|
||||||
abstract function dispatch(Request $req): Response;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
||||||
|
use JKingWeb\Arsse\Data;
|
||||||
use JKingWeb\Arsse\REST\Response;
|
use JKingWeb\Arsse\REST\Response;
|
||||||
|
|
||||||
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
|
@ -8,13 +9,31 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatch(\JKingWeb\Arsse\REST\Request $req): \JKingWeb\Arsse\REST\Response {
|
function dispatch(\JKingWeb\Arsse\REST\Request $req): \JKingWeb\Arsse\REST\Response {
|
||||||
// parse the URL and populate $path and $query
|
// try to authenticate
|
||||||
extract($this->parseURL($req->url));
|
if(!Data::$user->authHTTP()) return new Response(401, "", null, ['WWW-Authenticate: Basic realm="NextCloud News API"']);
|
||||||
if(preg_match("<^/(items|folders|feeds|cleanup|version|status|user)(?:/([^/]+))?(?:/([^/]+))?(?:/([^/]+))?/?$>", $path, $matches)) {
|
// only accept GET, POST, or PUT
|
||||||
$scope = $matches[1];
|
if(!in_array($req->method, ["GET", "POST", "PUT"])) return new Response(405);
|
||||||
var_export($scope);
|
// 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 {
|
} else {
|
||||||
return new Response(404);
|
return new Response(404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function folderList(): Response {
|
||||||
|
$folders = Data::$db->folderList(Data::$user->id, null, false)->getAll();
|
||||||
|
return new Response(200, ['folders' => $folders]);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -8,13 +8,11 @@ class Versions extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatch(\JKingWeb\Arsse\REST\Request $req): \JKingWeb\Arsse\REST\Response {
|
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 a method other than GET was used, this is an error
|
||||||
if($req->method != "GET") {
|
if($req->method != "GET") {
|
||||||
return new Response(405);
|
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
|
// if the request path is an empty string or just a slash, return the supported versions
|
||||||
$out = [
|
$out = [
|
||||||
'apiLevels' => [
|
'apiLevels' => [
|
||||||
|
|
|
@ -5,6 +5,8 @@ namespace JKingWeb\Arsse\REST;
|
||||||
class Request {
|
class Request {
|
||||||
public $method = "GET";
|
public $method = "GET";
|
||||||
public $url = "";
|
public $url = "";
|
||||||
|
public $path ="";
|
||||||
|
public $query = "";
|
||||||
public $type ="";
|
public $type ="";
|
||||||
public $stream = "php://input";
|
public $stream = "php://input";
|
||||||
|
|
||||||
|
@ -19,5 +21,37 @@ class Request {
|
||||||
$this->url = $url;
|
$this->url = $url;
|
||||||
$this->stream = $bodyStream;
|
$this->stream = $bodyStream;
|
||||||
$this->type = $contentType;
|
$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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,12 @@ return [
|
||||||
|
|
||||||
'Driver.Db.SQLite3.Name' => 'SQLite 3',
|
'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!)
|
// 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',
|
'Exception.JKingWeb/Arsse/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in AbstractException.php',
|
||||||
// this should not usually be encountered
|
// this should not usually be encountered
|
||||||
|
|
39
tests/REST/NextCloudNews/TestNCNV1_2.php
Normal file
39
tests/REST/NextCloudNews/TestNCNV1_2.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace JKingWeb\Arsse;
|
||||||
|
use JKingWeb\Arsse\Rest\Request;
|
||||||
|
use JKingWeb\Arsse\Rest\Response;
|
||||||
|
use JKingWeb\Arsse\Test\Result;
|
||||||
|
use Phake;
|
||||||
|
|
||||||
|
|
||||||
|
class TestNCNV1_2 extends \PHPUnit\Framework\TestCase {
|
||||||
|
use Test\Tools;
|
||||||
|
|
||||||
|
protected $h;
|
||||||
|
|
||||||
|
function setUp() {
|
||||||
|
$this->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")));
|
||||||
|
}
|
||||||
|
}
|
73
tests/lib/Result.php
Normal file
73
tests/lib/Result.php
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace JKingWeb\Arsse\Test;
|
||||||
|
|
||||||
|
class Result implements \JKingWeb\Arsse\Db\Result {
|
||||||
|
protected $st;
|
||||||
|
protected $set;
|
||||||
|
protected $pos = 0;
|
||||||
|
protected $cur = null;
|
||||||
|
protected $rows = 0;
|
||||||
|
protected $id = 0;
|
||||||
|
|
||||||
|
// actual public methods
|
||||||
|
|
||||||
|
public function getValue() {
|
||||||
|
$arr = $this->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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,7 @@
|
||||||
</testsuite>
|
</testsuite>
|
||||||
<testsuite name="NextCloud News API">
|
<testsuite name="NextCloud News API">
|
||||||
<file>REST/NextCloudNews/TestNCNVersionDiscovery.php</file>
|
<file>REST/NextCloudNews/TestNCNVersionDiscovery.php</file>
|
||||||
|
<file>REST/NextCloudNews/TestNCNV1_2.php</file>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
</phpunit>
|
</phpunit>
|
Loading…
Reference in a new issue