@ -21,21 +21,21 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected $dateFormat = "unix";
protected $dateFormat = "unix";
protected $validInput = [
protected $validInput = [
'name' => "string" ,
'name' => ValueInfo::T_STRING ,
'url' => "string" ,
'url' => ValueInfo::T_STRING ,
'folderId' => "int" ,
'folderId' => ValueInfo::T_INT ,
'feedTitle' => "string" ,
'feedTitle' => ValueInfo::T_STRING ,
'userId' => "string" ,
'userId' => ValueInfo::T_STRING ,
'feedId' => "int" ,
'feedId' => ValueInfo::T_INT ,
'newestItemId' => "int" ,
'newestItemId' => ValueInfo::T_INT ,
'batchSize' => "int" ,
'batchSize' => ValueInfo::T_INT ,
'offset' => "int" ,
'offset' => ValueInfo::T_INT ,
'type' => "int" ,
'type' => ValueInfo::T_INT ,
'id' => "int" ,
'id' => ValueInfo::T_INT ,
'getRead' => "bool" ,
'getRead' => ValueInfo::T_BOOL ,
'oldestFirst' => "bool" ,
'oldestFirst' => ValueInfo::T_BOOL ,
'lastModified' => "datetime" ,
'lastModified' => ValueInfo::T_DATE ,
// 'items' => "array int", // just pass these through
'items' => ValueInfo::T_MIXED | ValueInfo::M_ARRAY,
];
];
public function __construct() {
public function __construct() {
@ -61,10 +61,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
$data = [];
$data = [];
}
}
// FIXME: Do query parameters take precedence in NextCloud? Is there a conflict error when values differ?
// FIXME: Do query parameters take precedence in NextCloud? Is there a conflict error when values differ?
$data = $this->normalizeInput($data, $this->validInput, "U");
$data = $this->normalizeInput(array_merge($data, $req->query), $this->validInput, "unix");
$query = $this->normalizeInput($req->query, $this->validInput, "U");
$data = array_merge($data, $query);
unset($query);
// check to make sure the requested function is implemented
// check to make sure the requested function is implemented
try {
try {
$func = $this->chooseCall($req->paths, $req->method);
$func = $this->chooseCall($req->paths, $req->method);
@ -233,7 +230,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// create a folder
// create a folder
protected function folderAdd(array $url, array $data): Response {
protected function folderAdd(array $url, array $data): Response {
try {
try {
$folder = Arsse::$db->folderAdd(Arsse::$user->id, $data);
$folder = Arsse::$db->folderAdd(Arsse::$user->id, ['name' => $data['name']] );
} catch (ExceptionInput $e) {
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
switch ($e->getCode()) {
// folder already exists
// folder already exists
@ -263,13 +260,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// rename a folder (also supports moving nesting folders, but this is not a feature of the API)
// rename a folder (also supports moving nesting folders, but this is not a feature of the API)
protected function folderRename(array $url, array $data): Response {
protected function folderRename(array $url, array $data): Response {
// there must be some change to be made
if (!sizeof($data)) {
return new Response(422);
}
// perform the edit
try {
try {
Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $url[1], $data);
Arsse::$db->folderPropertiesSet(Arsse::$user->id, (int) $url[1], ['name' => $data['name']]);
} catch (ExceptionInput $e) {
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
switch ($e->getCode()) {
// folder does not exist
// folder does not exist
@ -288,15 +280,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// mark all articles associated with a folder as read
// mark all articles associated with a folder as read
protected function folderMarkRead(array $url, array $data): Response {
protected function folderMarkRead(array $url, array $data): Response {
$c = new Context;
if (!ValueInfo::id($data['newestItemId'])) {
if (isset($data['newestItemId'])) {
// if the item ID is invalid (i.e. not a positive integer), this is an error
// if the item ID is valid (i.e. an integer), add it to the context
$c->latestEdition($data['newestItemId']);
} else {
// otherwise return an error
return new Response(422);
return new Response(422);
}
}
// add the folder ID to the context
// build the context
$c = new Context;
$c->latestEdition((int) $data['newestItemId']);
$c->folder((int) $url[1]);
$c->folder((int) $url[1]);
// perform the operation
// perform the operation
try {
try {
@ -330,10 +320,6 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
if (Arsse::$user->rightsGet(Arsse::$user->id)==User::RIGHTS_NONE) {
return new Response(403);
return new Response(403);
}
}
// perform an update of a single feed
if (!isset($data['feedId'])) {
return new Response(422);
}
try {
try {
Arsse::$db->feedUpdate($data['feedId']);
Arsse::$db->feedUpdate($data['feedId']);
} catch (ExceptionInput $e) {
} catch (ExceptionInput $e) {
@ -351,16 +337,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// add a new feed
// add a new feed
protected function subscriptionAdd(array $url, array $data): Response {
protected function subscriptionAdd(array $url, array $data): Response {
// normalize the feed URL
if (!isset($data['url'])) {
return new Response(422);
}
// normalize the folder ID, if specified
$folder = isset($data['folderId']) ? $data['folderId'] : null;
// try to add the feed
// try to add the feed
$tr = Arsse::$db->begin();
$tr = Arsse::$db->begin();
try {
try {
$id = Arsse::$db->subscriptionAdd(Arsse::$user->id, $data['url']);
$id = Arsse::$db->subscriptionAdd(Arsse::$user->id, (string) $data['url']);
} catch (ExceptionInput $e) {
} catch (ExceptionInput $e) {
// feed already exists
// feed already exists
return new Response(409);
return new Response(409);
@ -369,9 +349,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
return new Response(422);
return new Response(422);
}
}
// if a folder was specified, move the feed to the correct folder; silently ignore errors
// if a folder was specified, move the feed to the correct folder; silently ignore errors
if ($folder) {
if ($data[' folderId'] ) {
try {
try {
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => $folder]);
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, $id, ['folder' => $data[' folderId'] ]);
} catch (ExceptionInput $e) {
} catch (ExceptionInput $e) {
}
}
}
}
@ -416,16 +396,8 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// rename a feed
// rename a feed
protected function subscriptionRename(array $url, array $data): Response {
protected function subscriptionRename(array $url, array $data): Response {
// normalize input
$in = [];
if (array_key_exists('feedTitle', $data)) { // we use array_key_exists because null is a valid input
$in['title'] = $data['feedTitle'];
} else {
return new Response(422);
}
// perform the renaming
try {
try {
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in );
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], ['title' => (string) $data['feedTitle']]);
} catch (ExceptionInput $e) {
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
switch ($e->getCode()) {
// subscription does not exist
// subscription does not exist
@ -442,16 +414,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// move a feed to a folder
// move a feed to a folder
protected function subscriptionMove(array $url, array $data): Response {
protected function subscriptionMove(array $url, array $data): Response {
// normalize input
// if no folder is specified this is an error
$in = [];
if (!isset($data['folderId'])) {
if (isset($data['folderId'])) {
$in['folder'] = $data['folderId'] ? $data['folderId'] : null;
} else {
return new Response(422);
return new Response(422);
}
}
// perform the move
// perform the move
try {
try {
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], $in );
Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $url[1], ['folder' => $data['folderId']] );
} catch (ExceptionInput $e) {
} catch (ExceptionInput $e) {
switch ($e->getCode()) {
switch ($e->getCode()) {
case 10239: // subscription does not exist
case 10239: // subscription does not exist
@ -468,14 +437,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// mark all articles associated with a subscription as read
// mark all articles associated with a subscription as read
protected function subscriptionMarkRead(array $url, array $data): Response {
protected function subscriptionMarkRead(array $url, array $data): Response {
$c = new Context;
if (!ValueInfo::id($data['newestItemId'])) {
if (isset($data['newestItemId'])) {
// if the item ID is invalid (i.e. not a positive integer), this is an error
$c->latestEdition($data['newestItemId']);
} else {
// otherwise return an error
return new Response(422);
return new Response(422);
}
}
// add the subscription ID to the context
// build the context
$c = new Context;
$c->latestEdition((int) $data['newestItemId']);
$c->subscription((int) $url[1]);
$c->subscription((int) $url[1]);
// perform the operation
// perform the operation
try {
try {
@ -492,17 +460,17 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// set the context options supplied by the client
// set the context options supplied by the client
$c = new Context;
$c = new Context;
// set the batch size
// set the batch size
if (isset($data['batchSize']) & & $data['batchSize'] > 0) {
if ($data['batchSize'] > 0) {
$c->limit($data['batchSize']);
$c->limit($data['batchSize']);
}
}
// set the order of returned items
// set the order of returned items
if (isset($data['oldestFirst']) & & $data['oldestFirst']) {
if ($data['oldestFirst']) {
$c->reverse(false);
$c->reverse(false);
} else {
} else {
$c->reverse(true);
$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
// 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 (isset($data['offset']) & & $data['offset'] > 0) {
if ($data['offset'] > 0) {
if ($c->reverse) {
if ($c->reverse) {
$c->latestEdition($data['offset'] - 1);
$c->latestEdition($data['offset'] - 1);
} else {
} else {
@ -510,13 +478,11 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
}
}
}
}
// set whether to only return unread
// set whether to only return unread
if (isset($data['getRead']) & & !$data['getRead'] ) {
if (!ValueInfo::bool($data['getRead'], true) ) {
$c->unread(true);
$c->unread(true);
}
}
// if no type is specified assume 3 (All)
// if no type is specified assume 3 (All)
if (!isset($data['type'])) {
$data['type'] = $data['type'] ?? 3;
$data['type'] = 3;
}
switch ($data['type']) {
switch ($data['type']) {
case 0: // feed
case 0: // feed
if (isset($data['id'])) {
if (isset($data['id'])) {
@ -535,7 +501,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// return all items
// return all items
}
}
// whether to return only updated items
// whether to return only updated items
if (isset( $data['lastModified']) ) {
if ($data['lastModified']) {
$c->modifiedSince($data['lastModified']);
$c->modifiedSince($data['lastModified']);
}
}
// perform the fetch
// perform the fetch
@ -555,14 +521,13 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
// mark all articles as read
// mark all articles as read
protected function articleMarkReadAll(array $url, array $data): Response {
protected function articleMarkReadAll(array $url, array $data): Response {
$c = new Context;
if (!ValueInfo::id($data['newestItemId'])) {
if (isset($data['newestItemId'])) {
// if the item ID is invalid (i.e. not a positive integer), this is an error
// set the newest item ID as specified
$c->latestEdition($data['newestItemId']);
} else {
// otherwise return an error
return new Response(422);
return new Response(422);
}
}
// build the context
$c = new Context;
$c->latestEdition((int) $data['newestItemId']);
// perform the operation
// perform the operation
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
Arsse::$db->articleMark(Arsse::$user->id, ['read' => true], $c);
return new Response(204);
return new Response(204);
@ -604,13 +569,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function articleMarkReadMulti(array $url, array $data): Response {
protected function articleMarkReadMulti(array $url, array $data): Response {
// determine whether to mark read or unread
// determine whether to mark read or unread
$set = ($url[1]=="read");
$set = ($url[1]=="read");
// if the input data is not at all valid, return an error
if (!isset($data['items']) || !is_array($data['items'])) {
return new Response(422);
}
// start a transaction and loop through the items
// start a transaction and loop through the items
$t = Arsse::$db->begin();
$t = Arsse::$db->begin();
$in = array_chunk($data['items'], 50);
$in = array_chunk($data['items'] ?? [], 50);
for ($a = 0; $a < sizeof ( $ in ) ; $ a + + ) {
for ($a = 0; $a < sizeof ( $ in ) ; $ a + + ) {
// initialize the matching context
// initialize the matching context
$c = new Context;
$c = new Context;
@ -628,13 +589,9 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
protected function articleMarkStarredMulti(array $url, array $data): Response {
protected function articleMarkStarredMulti(array $url, array $data): Response {
// determine whether to mark starred or unstarred
// determine whether to mark starred or unstarred
$set = ($url[1]=="star");
$set = ($url[1]=="star");
// if the input data is not at all valid, return an error
if (!isset($data['items']) || !is_array($data['items'])) {
return new Response(422);
}
// start a transaction and loop through the items
// start a transaction and loop through the items
$t = Arsse::$db->begin();
$t = Arsse::$db->begin();
$in = array_chunk(array_column($data['items'], "guidHash"), 50);
$in = array_chunk(array_column($data['items'] ?? [], "guidHash"), 50);
for ($a = 0; $a < sizeof ( $ in ) ; $ a + + ) {
for ($a = 0; $a < sizeof ( $ in ) ; $ a + + ) {
// initialize the matching context
// initialize the matching context
$c = new Context;
$c = new Context;