mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
NCNv1 feed calls and other changes
- Implemented all but one feed-related function (it's more ofan item function) - Fixed time conversion for input into SQL; dates in PM were previously wrong - Added miscellaneous tentative functions to Database to help with peculiarities of NCNv1; these may change - Tests to come soon
This commit is contained in:
parent
0bc2841837
commit
4a816f827b
11 changed files with 281 additions and 29 deletions
|
@ -26,7 +26,7 @@ class Database {
|
|||
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
|
||||
}
|
||||
|
||||
function dateFormatDefault(string $set = null): string {
|
||||
public function dateFormatDefault(string $set = null): string {
|
||||
if(is_null($set)) return $this->dateFormatDefault;
|
||||
$set = strtolower($set);
|
||||
if(in_array($set, ["sql", "iso8601", "unix", "http"])) {
|
||||
|
@ -101,6 +101,10 @@ class Database {
|
|||
}
|
||||
}
|
||||
|
||||
public function begin(): Db\Transaction {
|
||||
return $this->db->begin();
|
||||
}
|
||||
|
||||
public function settingSet(string $key, $in, string $type = null): bool {
|
||||
if(!$type) {
|
||||
switch(gettype($in)) {
|
||||
|
@ -513,6 +517,43 @@ class Database {
|
|||
return $out;
|
||||
}
|
||||
|
||||
protected function subscriptionValidateId(string $user, int $id): array {
|
||||
$out = $this->db->prepare("SELECT feed from arsse_subscriptions where id is ? and owner is ?", "int", "str")->run($id, $user)->getRow();
|
||||
if(!$out) throw new Db\ExceptionInput("idMissing", ["action" => $this->caller(), "field" => "subscription", 'id' => $id]);
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function articleStarredCount(string $user, array $context = []): int {
|
||||
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
return $this->db->prepare("SELECT count(*) from arsse_marks where owner is ? and starred is 1", "str")->run($user)->getValue();
|
||||
}
|
||||
|
||||
public function editionLatest(string $user, array $context = []): int {
|
||||
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
if(array_key_exists("subscription", $context)) {
|
||||
$id = $context['subscription'];
|
||||
$sub = $this->subscriptionValidateId($user, $id);
|
||||
return (int) $this->db->prepare(
|
||||
"SELECT max(arsse_editions.id)
|
||||
from arsse_editions
|
||||
left join arsse_articles on article is arsse_articles.id
|
||||
left join arsse_feeds on arsse_articles.feed is arsse_feeds.id
|
||||
where arsse_feeds.id is ?",
|
||||
"int"
|
||||
)->run($sub['feed'])->getValue();
|
||||
}
|
||||
return (int) $this->db->prepare("SELECT max(id) from arsse_editions")->run()->getValue();
|
||||
}
|
||||
|
||||
public function feedListStale(): array {
|
||||
$feeds = $this->db->prepare("SELECT distinct feed from arsse_subscriptions left join arsse_feeds on arsse_feeds.id = feed where next_fetch <= CURRENT_TIMESTAMP")->run();
|
||||
$out = [];
|
||||
foreach($feeds as $feed) {
|
||||
$out[] = $feed['feed'];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function feedUpdate(int $feedID, bool $throwError = false): bool {
|
||||
$tr = $this->db->begin();
|
||||
// check to make sure the feed exists
|
||||
|
|
|
@ -79,11 +79,7 @@ abstract class AbstractStatement implements Statement {
|
|||
}
|
||||
|
||||
protected function formatDate($date, int $part = self::TS_BOTH) {
|
||||
// Force UTC.
|
||||
$timezone = date_default_timezone_get();
|
||||
date_default_timezone_set('UTC');
|
||||
// convert input to a Unix timestamp
|
||||
// FIXME: there are more kinds of date representations
|
||||
if($date instanceof \DateTimeInterface) {
|
||||
$time = $date->getTimestamp();
|
||||
} else if(is_numeric($date)) {
|
||||
|
@ -99,8 +95,6 @@ abstract class AbstractStatement implements Statement {
|
|||
$time = (int) $date;
|
||||
}
|
||||
// ISO 8601 with space in the middle instead of T.
|
||||
$date = date($this->dateFormat($part), $time);
|
||||
date_default_timezone_set($timezone);
|
||||
return $date;
|
||||
return date($this->dateFormat($part), $time);
|
||||
}
|
||||
}
|
|
@ -39,9 +39,9 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
|
|||
|
||||
public static function dateFormat(int $part = self::TS_BOTH): string {
|
||||
return ([
|
||||
self::TS_TIME => 'h:i:s',
|
||||
self::TS_TIME => 'H:i:s',
|
||||
self::TS_DATE => 'Y-m-d',
|
||||
self::TS_BOTH => 'Y-m-d h:i:s',
|
||||
self::TS_BOTH => 'Y-m-d H:i:s',
|
||||
])[$part];
|
||||
}
|
||||
|
||||
|
|
|
@ -5,4 +5,32 @@ namespace JKingWeb\Arsse\REST;
|
|||
abstract class AbstractHandler implements Handler {
|
||||
abstract function __construct();
|
||||
abstract function dispatch(Request $req): Response;
|
||||
|
||||
protected function mapFieldNames(array $data, array $map, bool $overwrite = false): array {
|
||||
foreach($map as $from => $to) {
|
||||
if(array_key_exists($from, $data)) {
|
||||
if($overwrite || !array_key_exists($to, $data)) $data[$to] = $data[$from];
|
||||
unset($data[$from]);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function mapFieldTypes(array $data, array $map): array {
|
||||
foreach($map as $key => $type) {
|
||||
if(array_key_exists($key, $data)) settype($data[$key], $type);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function validateId($id):bool {
|
||||
try {
|
||||
$ch1 = strval(intval($id));
|
||||
$ch2 = strval($id);
|
||||
} catch(\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
return ($ch1 === $ch2);
|
||||
}
|
||||
|
||||
}
|
|
@ -2,8 +2,10 @@
|
|||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
||||
use JKingWeb\Arsse\Data;
|
||||
use JKingWeb\Arsse\User;
|
||||
use JKingWeb\Arsse\AbstractException;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
use JKingWeb\Arsse\Feed\Exception as FeedException;
|
||||
use JKingWeb\Arsse\REST\Response;
|
||||
|
||||
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
||||
|
@ -38,11 +40,14 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
unset($url[1]);
|
||||
$url = array_filter($url);
|
||||
$url = array_values($url);
|
||||
// decode any % sequences in the URL
|
||||
$url = array_map(function($v){return rawurldecode($v);}, $url);
|
||||
// check to make sure the requested function is implemented
|
||||
$func = $scope.$req->method;
|
||||
if(!method_exists($this, $func)) return new Response(501);
|
||||
// dispatch
|
||||
try {
|
||||
Data::$db->dateFormatDefault("unix");
|
||||
return $this->$func($url, $data);
|
||||
} catch(Exception $e) {
|
||||
// if there was a REST exception return 400
|
||||
|
@ -93,7 +98,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
if(sizeof($url) < 1) return new Response(405, "", "", ['Allow: GET, POST']);
|
||||
if(sizeof($url) > 1) return new Response(404);
|
||||
// folder ID must be integer
|
||||
if(strval(intval($url[0])) !== $url[0]) return new Response(404);
|
||||
if(!$this->validateId($url[0])) return new Response(404);
|
||||
// perform the deletion
|
||||
try {
|
||||
Data::$db->folderRemove(Data::$user->id, (int) $url[0]);
|
||||
|
@ -110,7 +115,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
if(sizeof($url) < 1) return new Response(405, "", "", ['Allow: GET, POST']);
|
||||
if(sizeof($url) > 1) return new Response(404);
|
||||
// folder ID must be integer
|
||||
if(strval(intval($url[0])) !== $url[0]) return new Response(404);
|
||||
if(!$this->validateId($url[0])) return new Response(404);
|
||||
// there must be some change to be made
|
||||
if(!sizeof($data)) return new Response(422);
|
||||
// perform the edit
|
||||
|
@ -160,4 +165,178 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
return new Response(405, "", "", ['Allow: GET']);
|
||||
}
|
||||
|
||||
protected function feedTranslate(array $feed, bool $overwrite = false): array {
|
||||
// cast values
|
||||
$feed = $this->mapFieldTypes($feed, [
|
||||
'folder' => "int",
|
||||
'pinned' => "bool",
|
||||
]);
|
||||
// map fields to proper names
|
||||
$feed = $this->mapFieldNames($feed, [
|
||||
'source' => "link",
|
||||
'favicon' => "faviconLink",
|
||||
'folder' => "folderId",
|
||||
'unread' => "unreadCount",
|
||||
'order_type' => "ordering",
|
||||
'err_count' => "updateErrorCount",
|
||||
'err_msg' => "lastUpdateError",
|
||||
], $overwrite);
|
||||
return $feed;
|
||||
}
|
||||
|
||||
// return list of feeds for the logged-in user
|
||||
// return list of feeds which should be refreshed
|
||||
// refresh a feed
|
||||
protected function feedsGET(array $url, array $data): Response {
|
||||
// URL may be /feeds/[all|update] only
|
||||
$args = sizeof($url);
|
||||
if($args==2 && in_array($url[1], ["rename","move","read"])) return new Response(405, "", "", ['Allow: PUT, DELETE']);
|
||||
if($args > 1) return new Response(404);
|
||||
if($args==1 && !in_array($url[0], ["all","update"])) return new Response(405, "", "", ['Allow: PUT, DELETE']);
|
||||
// valid action are listing owned subscriptions or (for admins) listing stale feeds or updating a feed
|
||||
if($args==1) {
|
||||
// listing stale feeds for updating and updating itself require admin rights per spec
|
||||
if(Data::$user->rightsGet(Data::$user->id)==User::RIGHTS_NONE) return new Response(403);
|
||||
if($url[0]=="all") {
|
||||
// list stale feeds which should be checked for updates
|
||||
$feeds = Data::$db->feedListStale();
|
||||
$out = [];
|
||||
foreach($feeds as $feed) {
|
||||
// since in our implementation feeds don't belong the users, the 'userId' field will always be an empty string
|
||||
$out[] = ['id' => $feed, 'userId' => ""];
|
||||
}
|
||||
return new Response(200, ['feeds' => $out]);
|
||||
} elseif($url[0]=="update") {
|
||||
// perform an update of a single feed
|
||||
if(!array_key_exists("feedId", $data) || $data['feedId'] < 1) return new Response(422);
|
||||
try {
|
||||
Data::$db->feedUpdate($data['feedId']);
|
||||
} catch(ExceptionInput $e) {
|
||||
return new Response(404);
|
||||
}
|
||||
return new Response(200);
|
||||
}
|
||||
} else {
|
||||
// list subscriptions for the logged-in user
|
||||
$subs = Data::$db->subscriptionList(Data::$user->id);
|
||||
$out = [];
|
||||
foreach($subs as $sub) {
|
||||
$sub = $this->feedTranslate($sub);
|
||||
$out[] = $sub;
|
||||
}
|
||||
$out = ['feeds' => $out];
|
||||
$out['starredCount'] = Data::$db->articleStarredCount(Data::$user->id);
|
||||
$newest = Data::$db->editionLatest(Data::$user->id, ['subscription' => $id]);
|
||||
if($newest) $out['newestItemId'] = $newest;
|
||||
return new Response(200, $out);
|
||||
}
|
||||
}
|
||||
|
||||
// add a new feed
|
||||
protected function feedsPOST(array $url, array $data): Response {
|
||||
// if URL is more than '/feeds' this is an error
|
||||
$args = sizeof($url);
|
||||
if($args==1 && in_array($url[0], ["all","update"])) return new Response(405, "", "", ['Allow: GET']);
|
||||
if($args==1) return new Response(405, "", "", ['Allow: PUT, DELETE']);
|
||||
if($args==2 && in_array($url[1], ["rename","move","read"])) return new Response(405, "", "", ['Allow: PUT']);
|
||||
if($args) return new Response(404);
|
||||
// normalize the URL
|
||||
if(!array_key_exists("url", $data)) {
|
||||
$url = "";
|
||||
} else {
|
||||
$url = $data['url'];
|
||||
}
|
||||
// normalize the folder ID, if specified
|
||||
if(!array_key_exists("folderId", $data)) {
|
||||
$folder = null;
|
||||
} else {
|
||||
$folder = $data['folderId'];
|
||||
$folder = $folder ? null : $folder;
|
||||
}
|
||||
// try to add the feed
|
||||
$tr = Data::$db->begin();
|
||||
try {
|
||||
$id = Data::$db->subscriptionAdd(Data::$user->id, $url);
|
||||
} catch(ExceptionInput $e) {
|
||||
// feed already exists
|
||||
return new Response(409);
|
||||
} catch(FeedException $e) {
|
||||
// feed could not be retrieved
|
||||
return new Response(422);
|
||||
}
|
||||
// if a folder was specified, move the feed to the correct folder; silently ignore errors
|
||||
if($folder) {
|
||||
try {
|
||||
Data::$db->subscriptionPropertiesSet(Data::$user->id, $id, ['folder' => $folder]);
|
||||
} catch(ExceptionInput $e) {}
|
||||
}
|
||||
$tr->commit();
|
||||
// fetch the feed's metadata and format it appropriately
|
||||
$feed = Data::$db->subscriptionPropertiesGet(Data::$user->id, $id);
|
||||
$feed = $this->feedTranslate($feed);
|
||||
$out = ['feeds' => [$feed]];
|
||||
$newest = Data::$db->editionLatest(Data::$user->id, ['subscription' => $id]);
|
||||
if($newest) $out['newestItemId'] = $newest;
|
||||
return new Response(200, $out);
|
||||
}
|
||||
|
||||
// delete a feed
|
||||
protected function feedsDELETE(array $url, array $data): Response {
|
||||
// if URL is more or less than '/feeds/$id' this is an error
|
||||
if(sizeof($url) < 1) return new Response(405, "", "", ['Allow: GET, POST']);
|
||||
if(sizeof($url) > 1) return new Response(404);
|
||||
// folder ID must be integer
|
||||
if(!$this->validateId($url[0])) return new Response(404);
|
||||
// perform the deletion
|
||||
try {
|
||||
Data::$db->subscriptionRemove(Data::$user->id, (int) $url[0]);
|
||||
} catch(ExceptionInput $e) {
|
||||
// feed does not exist
|
||||
return new Response(404);
|
||||
}
|
||||
return new Response(204);
|
||||
}
|
||||
|
||||
// rename a feed
|
||||
// move a feed to a folder
|
||||
// mark items from a feed as read
|
||||
protected function feedsPUT(array $url, array $data): Response {
|
||||
// URL may be /feeds/<id>/[rename|move|read]
|
||||
$args = sizeof($url);
|
||||
if(!$args) return new Response(405, "", "", ['Allow: GET, POST']);
|
||||
if($args > 2) return new Response(404);
|
||||
if(in_array($url[0], ["all", "update"])) {
|
||||
if($args==1) return new Response(405, "", "", ['Allow: GET']);
|
||||
return new Response(404);
|
||||
}
|
||||
if($args==2 && !in_array($url[1], ["rename","move","read"])) return new Response(404);
|
||||
// if the feed ID is not an integer, this is also an error
|
||||
if(!$this->validateId($url[0])) return new Response(404);
|
||||
// normalize input for move and rename
|
||||
$in = [];
|
||||
if(array_key_exists("feedTitle", $data)) {
|
||||
$in['title'] = $data['feedTitle'];
|
||||
}
|
||||
if(array_key_exists("folderId", $data)) {
|
||||
$folder = $data['folderId'];
|
||||
if(!$this->validateId($folder)) return new Response(422);
|
||||
if(!$folder) $folder = null;
|
||||
$in['folder'] = $folder;
|
||||
}
|
||||
// perform the move and/or rename
|
||||
if($in) {
|
||||
try {
|
||||
Data::$db->subscriptionPropertiesSet(Data::$user->id, (int) $url[0], $in);
|
||||
} catch(ExceptionInput $e) {
|
||||
return new Response(404);
|
||||
}
|
||||
}
|
||||
// mark items as read, if requested
|
||||
if(array_key_exists("newestItemId", $data)) {
|
||||
$newest = $data['newestItemId'];
|
||||
if(!$this->validateId($newest)) return new Response(422);
|
||||
// FIXME: do the marking as read
|
||||
}
|
||||
return new Response(204);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,12 @@ declare(strict_types=1);
|
|||
namespace JKingWeb\Arsse;
|
||||
|
||||
class User {
|
||||
const RIGHTS_NONE = 0; // normal user
|
||||
const RIGHTS_DOMAIN_MANAGER = 25; // able to act for any normal users on same domain; cannot elevate other users
|
||||
const RIGHTS_DOMAIN_ADMIN = 50; // able to act for any users on same domain not above themselves; may elevate users on same domain to domain manager or domain admin
|
||||
const RIGHTS_GLOBAL_MANAGER = 75; // able to act for any normal users on any domain; cannot elevate other users
|
||||
const RIGHTS_GLOBAL_ADMIN = 100; // is completely unrestricted
|
||||
|
||||
public $id = null;
|
||||
|
||||
protected $u;
|
||||
|
|
|
@ -7,9 +7,13 @@ return [
|
|||
'HTTP.Status.200' => 'OK',
|
||||
'HTTP.Status.204' => 'No Content',
|
||||
'HTTP.Status.401' => 'Unauthorized',
|
||||
'HTTP.Status.403' => 'Forbidden',
|
||||
'HTTP.Status.404' => 'Not Found',
|
||||
'HTTP.Status.405' => 'Method Not Allowed',
|
||||
'HTTP.Status.409' => 'Conflict',
|
||||
'HTTP.Status.415' => 'Unsupported Media Type',
|
||||
'HTTP.Status.422' => 'Unprocessable Entity',
|
||||
'HTTP.Status.501' => 'Not Implemented',
|
||||
|
||||
// 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',
|
||||
|
|
|
@ -98,7 +98,8 @@ create table arsse_marks(
|
|||
-- IDs for specific editions of articles (required for at least NextCloud News)
|
||||
create table arsse_editions(
|
||||
id integer primary key,
|
||||
article integer not null references arsse_articles(id) on delete cascade
|
||||
article integer not null references arsse_articles(id) on delete cascade,
|
||||
modified datetime not null default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- user labels associated with newsfeed entries
|
||||
|
|
|
@ -11,7 +11,6 @@ class TestDbStatementSQLite3 extends \PHPUnit\Framework\TestCase {
|
|||
static protected $imp = Db\SQLite3\Statement::class;
|
||||
|
||||
function setUp() {
|
||||
date_default_timezone_set("UTC");
|
||||
$c = new \SQLite3(":memory:");
|
||||
$c->enableExceptions(true);
|
||||
$this->c = $c;
|
||||
|
|
|
@ -12,16 +12,17 @@ trait SeriesSubscription {
|
|||
$data = [
|
||||
'arsse_feeds' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'url' => "str",
|
||||
'title' => "str",
|
||||
'username' => "str",
|
||||
'password' => "str",
|
||||
'id' => "int",
|
||||
'url' => "str",
|
||||
'title' => "str",
|
||||
'username' => "str",
|
||||
'password' => "str",
|
||||
'next_fetch' => "datetime",
|
||||
],
|
||||
'rows' => [
|
||||
[1,"http://example.com/feed1", "Ook", "", ""],
|
||||
[2,"http://example.com/feed2", "Eek", "", ""],
|
||||
[3,"http://example.com/feed3", "Ack", "", ""],
|
||||
[1,"http://example.com/feed1", "Ook", "", "",strtotime("now")],
|
||||
[2,"http://example.com/feed2", "Eek", "", "",strtotime("now - 1 hour")],
|
||||
[3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour")],
|
||||
]
|
||||
],
|
||||
'arsse_subscriptions' => [
|
||||
|
|
|
@ -20,11 +20,10 @@ Data::$user->authorizationEnabled(false);
|
|||
Data::$user->rightsSet($user, User\Driver::RIGHTS_GLOBAL_ADMIN);
|
||||
Data::$user->authorizationEnabled(true);
|
||||
Data::$db->folderAdd($user, ['name' => 'ook']);
|
||||
Data::$db->subscriptionAdd($user, "https://jkingweb.ca/test.atom");
|
||||
/*Data::$db->subscriptionAdd($user, "http://linuxfr.org/news.atom");
|
||||
Data::$db->subscriptionPropertiesSet($user, 1, [
|
||||
'title' => "OOOOOOOOK!",
|
||||
'folder' => null,
|
||||
'order_type' => null,
|
||||
'pinned' => null,
|
||||
]);
|
||||
var_export(Data::$db->subscriptionList($user)->getAll());
|
||||
]);*/
|
||||
(new REST())->dispatch(new REST\Request(
|
||||
"POST", "/index.php/apps/news/api/v1-2/feeds/", json_encode(['url'=> "http://linuxfr.org/news.atom"])
|
||||
));
|
Loading…
Reference in a new issue