1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 13:12:41 +00:00

Implemented NCN API v1-2 folder list

- Fixes #2
- Also re-organized REST handling
This commit is contained in:
J. King 2017-04-01 23:06:52 -04:00
parent dc9f5e545e
commit 842e277d43
9 changed files with 184 additions and 36 deletions

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\REST\NextCloudNews;
use JKingWeb\Arsse\Data;
use JKingWeb\Arsse\REST\Response;
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 {
// parse the URL and populate $path and $query
extract($this->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]);
}
}

View file

@ -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' => [

View file

@ -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;
}
}

View file

@ -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

View 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
View 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);
}
}

View file

@ -40,6 +40,7 @@
</testsuite>
<testsuite name="NextCloud News API">
<file>REST/NextCloudNews/TestNCNVersionDiscovery.php</file>
<file>REST/NextCloudNews/TestNCNV1_2.php</file>
</testsuite>
</testsuites>
</phpunit>