mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-23 09:02:41 +00:00
9eadd602bd
While the test suite passes, this commit yields a broken server: replacing ad hoc request objectss with PSR-7 ones is still required, as is emission of PSR-7 responses. Both will come in subsequent commits, with tests Diactoros was chosen specifically because it includes facilities for emitting responses, something which is awkward to test. The end of this refactoring should see both the Response and Request classes disappear, and the general REST class fully covered (as well as any speculative additions to AbstractHanlder).
699 lines
27 KiB
PHP
699 lines
27 KiB
PHP
<?php
|
|
/** @license MIT
|
|
* Copyright 2017 J. King, Dustin Wilson et al.
|
|
* See LICENSE and AUTHORS files for details */
|
|
|
|
declare(strict_types=1);
|
|
namespace JKingWeb\Arsse\REST\NextCloudNews;
|
|
|
|
use JKingWeb\Arsse\Arsse;
|
|
use JKingWeb\Arsse\Database;
|
|
use JKingWeb\Arsse\User;
|
|
use JKingWeb\Arsse\Service;
|
|
use JKingWeb\Arsse\Misc\Context;
|
|
use JKingWeb\Arsse\Misc\ValueInfo;
|
|
use JKingWeb\Arsse\AbstractException;
|
|
use JKingWeb\Arsse\Db\ExceptionInput;
|
|
use JKingWeb\Arsse\Feed\Exception as FeedException;
|
|
use \Psr\Http\Message\ResponseInterface;
|
|
use Zend\Diactoros\Response\JsonResponse as Response;
|
|
use Zend\Diactoros\Response\EmptyResponse;
|
|
|
|
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|
const REALM = "NextCloud News API v1-2";
|
|
const VERSION = "11.0.5";
|
|
|
|
protected $dateFormat = "unix";
|
|
|
|
protected $validInput = [
|
|
'name' => ValueInfo::T_STRING,
|
|
'url' => ValueInfo::T_STRING,
|
|
'folderId' => ValueInfo::T_INT,
|
|
'feedTitle' => ValueInfo::T_STRING,
|
|
'userId' => ValueInfo::T_STRING,
|
|
'feedId' => ValueInfo::T_INT,
|
|
'newestItemId' => ValueInfo::T_INT,
|
|
'batchSize' => ValueInfo::T_INT,
|
|
'offset' => ValueInfo::T_INT,
|
|
'type' => ValueInfo::T_INT,
|
|
'id' => ValueInfo::T_INT,
|
|
'getRead' => ValueInfo::T_BOOL,
|
|
'oldestFirst' => ValueInfo::T_BOOL,
|
|
'lastModified' => ValueInfo::T_DATE,
|
|
'items' => ValueInfo::T_MIXED | ValueInfo::M_ARRAY,
|
|
];
|
|
protected $paths = [
|
|
'folders' => ['GET' => "folderList", 'POST' => "folderAdd"],
|
|
'folders/1' => ['PUT' => "folderRename", 'DELETE' => "folderRemove"],
|
|
'folders/1/read' => ['PUT' => "folderMarkRead"],
|
|
'feeds' => ['GET' => "subscriptionList", 'POST' => "subscriptionAdd"],
|
|
'feeds/1' => ['DELETE' => "subscriptionRemove"],
|
|
'feeds/1/move' => ['PUT' => "subscriptionMove"],
|
|
'feeds/1/rename' => ['PUT' => "subscriptionRename"],
|
|
'feeds/1/read' => ['PUT' => "subscriptionMarkRead"],
|
|
'feeds/all' => ['GET' => "feedListStale"],
|
|
'feeds/update' => ['GET' => "feedUpdate"],
|
|
'items' => ['GET' => "articleList"],
|
|
'items/updated' => ['GET' => "articleList"],
|
|
'items/read' => ['PUT' => "articleMarkReadAll"],
|
|
'items/1/read' => ['PUT' => "articleMarkRead"],
|
|
'items/1/unread' => ['PUT' => "articleMarkRead"],
|
|
'items/read/multiple' => ['PUT' => "articleMarkReadMulti"],
|
|
'items/unread/multiple' => ['PUT' => "articleMarkReadMulti"],
|
|
'items/1/1/star' => ['PUT' => "articleMarkStarred"],
|
|
'items/1/1/unstar' => ['PUT' => "articleMarkStarred"],
|
|
'items/star/multiple' => ['PUT' => "articleMarkStarredMulti"],
|
|
'items/unstar/multiple' => ['PUT' => "articleMarkStarredMulti"],
|
|
'cleanup/before-update' => ['GET' => "cleanupBefore"],
|
|
'cleanup/after-update' => ['GET' => "cleanupAfter"],
|
|
'version' => ['GET' => "serverVersion"],
|
|
'status' => ['GET' => "serverStatus"],
|
|
'user' => ['GET' => "userStatus"],
|
|
];
|
|
|
|
public function __construct() {
|
|
}
|
|
|
|
public function dispatch(\JKingWeb\Arsse\REST\Request $req): ResponseInterface {
|
|
// try to authenticate
|
|
if (!Arsse::$user->authHTTP()) {
|
|
return new EmptyResponse(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"
|
|
if (!preg_match("<^application/json\b|^$>", $req->type)) {
|
|
return new EmptyResponse(415, ['Accept' => "application/json"]);
|
|
}
|
|
$data = @json_decode($req->body, true);
|
|
if (json_last_error() != \JSON_ERROR_NONE) {
|
|
// if the body could not be parsed as JSON, return "400 Bad Request"
|
|
return new EmptyResponse(400);
|
|
}
|
|
} else {
|
|
$data = [];
|
|
}
|
|
// FIXME: Do query parameters take precedence in NextCloud? Is there a conflict error when values differ?
|
|
$data = $this->normalizeInput(array_merge($data, $req->query), $this->validInput, "unix");
|
|
// check to make sure the requested function is implemented
|
|
try {
|
|
$func = $this->chooseCall($req->paths, $req->method);
|
|
} catch (Exception404 $e) {
|
|
return new EmptyResponse(404);
|
|
} catch (Exception405 $e) {
|
|
return new EmptyResponse(405, ['Allow' => $e->getMessage()]);
|
|
}
|
|
if (!method_exists($this, $func)) {
|
|
return new EmptyResponse(501); // @codeCoverageIgnore
|
|
}
|
|
// dispatch
|
|
try {
|
|
return $this->$func($req->paths, $data);
|
|
// @codeCoverageIgnoreStart
|
|
} catch (Exception $e) {
|
|
// if there was a REST exception return 400
|
|
return new EmptyResponse(400);
|
|
} catch (AbstractException $e) {
|
|
// if there was any other Arsse exception return 500
|
|
return new EmptyResponse(500);
|
|
}
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
protected function normalizePath(array $url): string {
|
|
// any URL components which are database IDs (integers greater than zero) should be replaced with "1", for easier comparison (we don't care about the specific ID)
|
|
for ($a = 0; $a < sizeof($url); $a++) {
|
|
if (ValueInfo::id($url[$a])) {
|
|
$url[$a] = "1";
|
|
}
|
|
}
|
|
return implode("/", $url);
|
|
}
|
|
|
|
protected function chooseCall(array $url, string $method): string {
|
|
// normalize the URL path
|
|
$url = $this->normalizePath($url);
|
|
// normalize the HTTP method to uppercase
|
|
$method = strtoupper($method);
|
|
// we now evaluate the supplied URL against every supported path for the selected scope
|
|
// the URL is evaluated as an array so as to avoid decoded escapes turning invalid URLs into valid ones
|
|
if (isset($this->paths[$url])) {
|
|
// if the path is supported, make sure the method is allowed
|
|
if (isset($this->paths[$url][$method])) {
|
|
// if it is allowed, return the object method to run
|
|
return $this->paths[$url][$method];
|
|
} else {
|
|
// otherwise return 405
|
|
throw new Exception405(implode(", ", array_keys($this->paths[$url])));
|
|
}
|
|
} else {
|
|
// if the path is not supported, return 404
|
|
throw new Exception404();
|
|
}
|
|
}
|
|
|
|
protected function folderTranslate(array $folder): array {
|
|
// map fields to proper names
|
|
$folder = $this->fieldMapNames($folder, [
|
|
'id' => "id",
|
|
'name' => "name",
|
|
]);
|
|
// cast values
|
|
$folder = $this->fieldMapTypes($folder, [
|
|
'id' => "int",
|
|
'name' => "string",
|
|
], $this->dateFormat);
|
|
return $folder;
|
|
}
|
|
|
|
protected function feedTranslate(array $feed): array {
|
|
// map fields to proper names
|
|
$feed = $this->fieldMapNames($feed, [
|
|
'id' => "id",
|
|
'url' => "url",
|
|
'title' => "title",
|
|
'added' => "added",
|
|
'pinned' => "pinned",
|
|
'link' => "source",
|
|
'faviconLink' => "favicon",
|
|
'folderId' => "top_folder",
|
|
'unreadCount' => "unread",
|
|
'ordering' => "order_type",
|
|
'updateErrorCount' => "err_count",
|
|
'lastUpdateError' => "err_msg",
|
|
]);
|
|
// cast values
|
|
$feed = $this->fieldMapTypes($feed, [
|
|
'id' => "int",
|
|
'url' => "string",
|
|
'title' => "string",
|
|
'added' => "datetime",
|
|
'pinned' => "bool",
|
|
'link' => "string",
|
|
'faviconLink' => "string",
|
|
'folderId' => "int",
|
|
'unreadCount' => "int",
|
|
'ordering' => "int",
|
|
'updateErrorCount' => "int",
|
|
'lastUpdateError' => "string",
|
|
], $this->dateFormat);
|
|
return $feed;
|
|
}
|
|
|
|
protected function articleTranslate(array $article) :array {
|
|
// map fields to proper names
|
|
$article = $this->fieldMapNames($article, [
|
|
'id' => "edition",
|
|
'guid' => "guid",
|
|
'guidHash' => "id",
|
|
'url' => "url",
|
|
'title' => "title",
|
|
'author' => "author",
|
|
'pubDate' => "edited_date",
|
|
'body' => "content",
|
|
'enclosureMime' => "media_type",
|
|
'enclosureLink' => "media_url",
|
|
'feedId' => "subscription",
|
|
'unread' => "unread",
|
|
'starred' => "starred",
|
|
'lastModified' => "modified_date",
|
|
'fingerprint' => "fingerprint",
|
|
]);
|
|
// cast values
|
|
$article = $this->fieldMapTypes($article, [
|
|
'id' => "int",
|
|
'guid' => "string",
|
|
'guidHash' => "string",
|
|
'url' => "string",
|
|
'title' => "string",
|
|
'author' => "string",
|
|
'pubDate' => "datetime",
|
|
'body' => "string",
|
|
'enclosureMime' => "string",
|
|
'enclosureLink' => "string",
|
|
'feedId' => "int",
|
|
'unread' => "bool",
|
|
'starred' => "bool",
|
|
'lastModified' => "datetime",
|
|
'fingerprint' => "string",
|
|
], $this->dateFormat);
|
|
return $article;
|
|
}
|
|
|
|
protected function handleHTTPOptions(array $url): ResponseInterface {
|
|
// 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 EmptyResponse(204, [
|
|
'Allow' => implode(",", $allowed),
|
|
'Accept' => "application/json",
|
|
]);
|
|
} else {
|
|
// if the path is not supported, return 404
|
|
return new EmptyResponse(404);
|
|
}
|
|
}
|
|
|
|
// list folders
|
|
protected function folderList(array $url, array $data): ResponseInterface {
|
|
$folders = [];
|
|
foreach (Arsse::$db->folderList(Arsse::$user->id, null, false) as $folder) {
|
|
$folders[] = $this->folderTranslate($folder);
|
|
}
|
|
return new Response(['folders' => $folders]);
|
|
}
|
|
|
|
// create a folder
|
|
protected function folderAdd(array $url, array $data): ResponseInterface {
|
|
try {
|
|
$folder = Arsse::$db->folderAdd(Arsse::$user->id, ['name' => $data['name']]);
|
|
} catch (ExceptionInput $e) {
|
|
switch ($e->getCode()) {
|
|
// folder already exists
|
|
case 10236: return new EmptyResponse(409);
|
|
// folder name not acceptable
|
|
case 10231:
|
|
case 10232: return new EmptyResponse(422);
|
|
// other errors related to input
|
|
default: return new EmptyResponse(400); // @codeCoverageIgnore
|
|
}
|
|
}
|
|
$folder = $this->folderTranslate(Arsse::$db->folderPropertiesGet(Arsse::$user->id, $folder));
|
|
return new Response(['folders' => [$folder]]);
|
|
}
|
|
|
|
// delete a folder
|
|
protected function folderRemove(array $url, array $data): ResponseInterface {
|
|
// perform the deletion
|
|
try {
|
|
Arsse::$db->folderRemove(Arsse::$user->id, (int) $url[1]);
|
|
} catch (ExceptionInput $e) {
|
|
// folder does not exist
|
|
return new EmptyResponse(404);
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// rename a folder (also supports moving nesting folders, but this is not a feature of the API)
|
|
protected function folderRename(array $url, array $data): ResponseInterface {
|
|
try {
|
|
Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $url[1], ['name' => $data['name']]);
|
|
} catch (ExceptionInput $e) {
|
|
switch ($e->getCode()) {
|
|
// folder does not exist
|
|
case 10239: return new EmptyResponse(404);
|
|
// folder already exists
|
|
case 10236: return new EmptyResponse(409);
|
|
// folder name not acceptable
|
|
case 10231:
|
|
case 10232: return new EmptyResponse(422);
|
|
// other errors related to input
|
|
default: return new EmptyResponse(400); // @codeCoverageIgnore
|
|
}
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// mark all articles associated with a folder as read
|
|
protected function folderMarkRead(array $url, array $data): ResponseInterface {
|
|
if (!ValueInfo::id($data['newestItemId'])) {
|
|
// if the item ID is invalid (i.e. not a positive integer), this is an error
|
|
return new EmptyResponse(422);
|
|
}
|
|
// build the context
|
|
$c = new Context;
|
|
$c->latestEdition((int) $data['newestItemId']);
|
|
$c->folder((int) $url[1]);
|
|
// perform the operation
|
|
try {
|
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
|
} catch (ExceptionInput $e) {
|
|
// folder does not exist
|
|
return new EmptyResponse(404);
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// return list of feeds which should be refreshed
|
|
protected function feedListStale(array $url, array $data): ResponseInterface {
|
|
// function requires admin rights per spec
|
|
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
|
|
return new EmptyResponse(403);
|
|
}
|
|
// list stale feeds which should be checked for updates
|
|
$feeds = Arsse::$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(['feeds' => $out]);
|
|
}
|
|
|
|
// refresh a feed
|
|
protected function feedUpdate(array $url, array $data): ResponseInterface {
|
|
// function requires admin rights per spec
|
|
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
|
|
return new EmptyResponse(403);
|
|
}
|
|
try {
|
|
Arsse::$db->feedUpdate($data['feedId']);
|
|
} catch (ExceptionInput $e) {
|
|
switch ($e->getCode()) {
|
|
case 10239: // feed does not exist
|
|
return new EmptyResponse(404);
|
|
case 10237: // feed ID invalid
|
|
return new EmptyResponse(422);
|
|
default: // other errors related to input
|
|
return new EmptyResponse(400); // @codeCoverageIgnore
|
|
}
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// add a new feed
|
|
protected function subscriptionAdd(array $url, array $data): ResponseInterface {
|
|
// try to add the feed
|
|
$tr = Arsse::$db->begin();
|
|
try {
|
|
$id = Arsse::$db->subscriptionAdd(Arsse::$user->id, (string) $data['url']);
|
|
} catch (ExceptionInput $e) {
|
|
// feed already exists
|
|
return new EmptyResponse(409);
|
|
} catch (FeedException $e) {
|
|
// feed could not be retrieved
|
|
return new EmptyResponse(422);
|
|
}
|
|
// if a folder was specified, move the feed to the correct folder; silently ignore errors
|
|
if ($data['folderId']) {
|
|
try {
|
|
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => $data['folderId']]);
|
|
} catch (ExceptionInput $e) {
|
|
}
|
|
}
|
|
$tr->commit();
|
|
// fetch the feed's metadata and format it appropriately
|
|
$feed = Arsse::$db->subscriptionPropertiesGet(Arsse::$user->id, $id);
|
|
$feed = $this->feedTranslate($feed);
|
|
$out = ['feeds' => [$feed]];
|
|
$newest = Arsse::$db->editionLatest(Arsse::$user->id, (new Context)->subscription($id));
|
|
if ($newest) {
|
|
$out['newestItemId'] = $newest;
|
|
}
|
|
return new Response($out);
|
|
}
|
|
|
|
// return list of feeds for the logged-in user
|
|
protected function subscriptionList(array $url, array $data): ResponseInterface {
|
|
$subs = Arsse::$db->subscriptionList(Arsse::$user->id);
|
|
$out = [];
|
|
foreach ($subs as $sub) {
|
|
$out[] = $this->feedTranslate($sub);
|
|
}
|
|
$out = ['feeds' => $out];
|
|
$out['starredCount'] = Arsse::$db->articleStarred(Arsse::$user->id)['total'];
|
|
$newest = Arsse::$db->editionLatest(Arsse::$user->id);
|
|
if ($newest) {
|
|
$out['newestItemId'] = $newest;
|
|
}
|
|
return new Response($out);
|
|
}
|
|
|
|
// delete a feed
|
|
protected function subscriptionRemove(array $url, array $data): ResponseInterface {
|
|
try {
|
|
Arsse::$db->subscriptionRemove(Arsse::$user->id, (int) $url[1]);
|
|
} catch (ExceptionInput $e) {
|
|
// feed does not exist
|
|
return new EmptyResponse(404);
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// rename a feed
|
|
protected function subscriptionRename(array $url, array $data): ResponseInterface {
|
|
try {
|
|
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], ['title' => (string) $data['feedTitle']]);
|
|
} catch (ExceptionInput $e) {
|
|
switch ($e->getCode()) {
|
|
// subscription does not exist
|
|
case 10239: return new EmptyResponse(404);
|
|
// name is invalid
|
|
case 10231:
|
|
case 10232: return new EmptyResponse(422);
|
|
// other errors related to input
|
|
default: return new EmptyResponse(400); // @codeCoverageIgnore
|
|
}
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// move a feed to a folder
|
|
protected function subscriptionMove(array $url, array $data): ResponseInterface {
|
|
// if no folder is specified this is an error
|
|
if (!isset($data['folderId'])) {
|
|
return new EmptyResponse(422);
|
|
}
|
|
// perform the move
|
|
try {
|
|
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], ['folder' => $data['folderId']]);
|
|
} catch (ExceptionInput $e) {
|
|
switch ($e->getCode()) {
|
|
case 10239: // subscription does not exist
|
|
return new EmptyResponse(404);
|
|
case 10235: // folder does not exist
|
|
case 10237: // folder ID is invalid
|
|
return new EmptyResponse(422);
|
|
default: // other errors related to input
|
|
return new EmptyResponse(400); // @codeCoverageIgnore
|
|
}
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// mark all articles associated with a subscription as read
|
|
protected function subscriptionMarkRead(array $url, array $data): ResponseInterface {
|
|
if (!ValueInfo::id($data['newestItemId'])) {
|
|
// if the item ID is invalid (i.e. not a positive integer), this is an error
|
|
return new EmptyResponse(422);
|
|
}
|
|
// build the context
|
|
$c = new Context;
|
|
$c->latestEdition((int) $data['newestItemId']);
|
|
$c->subscription((int) $url[1]);
|
|
// perform the operation
|
|
try {
|
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
|
} catch (ExceptionInput $e) {
|
|
// subscription does not exist
|
|
return new EmptyResponse(404);
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// list articles and their properties
|
|
protected function articleList(array $url, array $data): ResponseInterface {
|
|
// set the context options supplied by the client
|
|
$c = new Context;
|
|
// set the batch size
|
|
if ($data['batchSize'] > 0) {
|
|
$c->limit($data['batchSize']);
|
|
}
|
|
// set the order of returned items
|
|
if ($data['oldestFirst']) {
|
|
$c->reverse(false);
|
|
} else {
|
|
$c->reverse(true);
|
|
}
|
|
// set the edition mark-off; the database uses an or-equal comparison for internal consistency, but the protocol does not, so we must adjust by one
|
|
if ($data['offset'] > 0) {
|
|
if ($c->reverse) {
|
|
$c->latestEdition($data['offset'] - 1);
|
|
} else {
|
|
$c->oldestEdition($data['offset'] + 1);
|
|
}
|
|
}
|
|
// set whether to only return unread
|
|
if (!ValueInfo::bool($data['getRead'], true)) {
|
|
$c->unread(true);
|
|
}
|
|
// if no type is specified assume 3 (All)
|
|
$data['type'] = $data['type'] ?? 3;
|
|
switch ($data['type']) {
|
|
case 0: // feed
|
|
if (isset($data['id'])) {
|
|
$c->subscription($data['id']);
|
|
}
|
|
break;
|
|
case 1: // folder
|
|
if (isset($data['id'])) {
|
|
$c->folder($data['id']);
|
|
}
|
|
break;
|
|
case 2: // starred
|
|
$c->starred(true);
|
|
break;
|
|
default: // @codeCoverageIgnore
|
|
// return all items
|
|
}
|
|
// whether to return only updated items
|
|
if ($data['lastModified']) {
|
|
$c->markedSince($data['lastModified']);
|
|
}
|
|
// perform the fetch
|
|
try {
|
|
$items = Arsse::$db->articleList(Arsse::$user->id, $c, Database::LIST_TYPICAL);
|
|
} catch (ExceptionInput $e) {
|
|
// ID of subscription or folder is not valid
|
|
return new EmptyResponse(422);
|
|
}
|
|
$out = [];
|
|
foreach ($items as $item) {
|
|
$out[] = $this->articleTranslate($item);
|
|
}
|
|
$out = ['items' => $out];
|
|
return new Response($out);
|
|
}
|
|
|
|
// mark all articles as read
|
|
protected function articleMarkReadAll(array $url, array $data): ResponseInterface {
|
|
if (!ValueInfo::id($data['newestItemId'])) {
|
|
// if the item ID is invalid (i.e. not a positive integer), this is an error
|
|
return new EmptyResponse(422);
|
|
}
|
|
// build the context
|
|
$c = new Context;
|
|
$c->latestEdition((int) $data['newestItemId']);
|
|
// perform the operation
|
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// mark a single article as read
|
|
protected function articleMarkRead(array $url, array $data): ResponseInterface {
|
|
// initialize the matching context
|
|
$c = new Context;
|
|
$c->edition((int) $url[1]);
|
|
// determine whether to mark read or unread
|
|
$set = ($url[2]=="read");
|
|
try {
|
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c);
|
|
} catch (ExceptionInput $e) {
|
|
// ID is not valid
|
|
return new EmptyResponse(404);
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// mark a single article as read
|
|
protected function articleMarkStarred(array $url, array $data): ResponseInterface {
|
|
// initialize the matching context
|
|
$c = new Context;
|
|
$c->article((int) $url[2]);
|
|
// determine whether to mark read or unread
|
|
$set = ($url[3]=="star");
|
|
try {
|
|
Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c);
|
|
} catch (ExceptionInput $e) {
|
|
// ID is not valid
|
|
return new EmptyResponse(404);
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// mark an array of articles as read
|
|
protected function articleMarkReadMulti(array $url, array $data): ResponseInterface {
|
|
// determine whether to mark read or unread
|
|
$set = ($url[1]=="read");
|
|
// initialize the matching context
|
|
$c = new Context;
|
|
$c->editions($data['items'] ?? []);
|
|
try {
|
|
Arsse::$db->articleMark(Arsse::$user->id, ['read' => $set], $c);
|
|
} catch (ExceptionInput $e) {
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// mark an array of articles as starred
|
|
protected function articleMarkStarredMulti(array $url, array $data): ResponseInterface {
|
|
// determine whether to mark starred or unstarred
|
|
$set = ($url[1]=="star");
|
|
// initialize the matching context
|
|
$c = new Context;
|
|
$c->articles(array_column($data['items'] ?? [], "guidHash"));
|
|
try {
|
|
Arsse::$db->articleMark(Arsse::$user->id, ['starred' => $set], $c);
|
|
} catch (ExceptionInput $e) {
|
|
}
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
protected function userStatus(array $url, array $data): ResponseInterface {
|
|
$data = Arsse::$user->propertiesGet(Arsse::$user->id, true);
|
|
// construct the avatar structure, if an image is available
|
|
if (isset($data['avatar'])) {
|
|
$avatar = [
|
|
'data' => base64_encode($data['avatar']['data']),
|
|
'mime' => (string) $data['avatar']['type'],
|
|
];
|
|
} else {
|
|
$avatar = null;
|
|
}
|
|
// construct the rest of the structure
|
|
$out = [
|
|
'userId' => (string) Arsse::$user->id,
|
|
'displayName' => (string) ($data['name'] ?? Arsse::$user->id),
|
|
'lastLoginTimestamp' => time(),
|
|
'avatar' => $avatar,
|
|
];
|
|
return new Response($out);
|
|
}
|
|
|
|
protected function cleanupBefore(array $url, array $data): ResponseInterface {
|
|
// function requires admin rights per spec
|
|
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
|
|
return new EmptyResponse(403);
|
|
}
|
|
Service::cleanupPre();
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
protected function cleanupAfter(array $url, array $data): ResponseInterface {
|
|
// function requires admin rights per spec
|
|
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
|
|
return new EmptyResponse(403);
|
|
}
|
|
Service::cleanupPost();
|
|
return new EmptyResponse(204);
|
|
}
|
|
|
|
// return the server version
|
|
protected function serverVersion(array $url, array $data): ResponseInterface {
|
|
return new Response([
|
|
'version' => self::VERSION,
|
|
'arsse_version' => Arsse::VERSION,
|
|
]);
|
|
}
|
|
|
|
protected function serverStatus(array $url, array $data): ResponseInterface {
|
|
return new Response([
|
|
'version' => self::VERSION,
|
|
'arsse_version' => Arsse::VERSION,
|
|
'warnings' => [
|
|
'improperlyConfiguredCron' => !Service::hasCheckedIn(),
|
|
'incorrectDbCharset' => !Arsse::$db->driverCharsetAcceptable(),
|
|
]
|
|
]);
|
|
}
|
|
}
|