2017-04-01 19:42:10 +00:00
< ? php
declare ( strict_types = 1 );
namespace JKingWeb\Arsse\REST\NextCloudNews ;
2017-07-17 11:47:57 +00:00
use JKingWeb\Arsse\Arsse ;
2017-05-19 03:03:33 +00:00
use JKingWeb\Arsse\User ;
2017-08-02 22:27:04 +00:00
use JKingWeb\Arsse\Service ;
2017-06-18 14:23:37 +00:00
use JKingWeb\Arsse\Misc\Context ;
2017-04-02 16:14:15 +00:00
use JKingWeb\Arsse\AbstractException ;
2017-04-03 01:34:30 +00:00
use JKingWeb\Arsse\Db\ExceptionInput ;
2017-05-19 03:03:33 +00:00
use JKingWeb\Arsse\Feed\Exception as FeedException ;
2017-04-03 01:34:30 +00:00
use JKingWeb\Arsse\REST\Response ;
2017-05-20 03:52:26 +00:00
use JKingWeb\Arsse\REST\Exception501 ;
use JKingWeb\Arsse\REST\Exception405 ;
2017-04-01 19:42:10 +00:00
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
2017-06-03 21:34:37 +00:00
const REALM = " NextCloud News API v1-2 " ;
2017-07-15 20:44:06 +00:00
const VERSION = " 11.0.5 " ;
2017-07-07 12:13:03 +00:00
2017-07-07 19:25:47 +00:00
protected $dateFormat = " unix " ;
2017-07-07 12:13:03 +00:00
protected $validInput = [
'name' => " string " ,
'url' => " string " ,
'folderId' => " int " ,
'feedTitle' => " string " ,
'userId' => " string " ,
'feedId' => " int " ,
'newestItemId' => " int " ,
'batchSize' => " int " ,
'offset' => " int " ,
'type' => " int " ,
'id' => " int " ,
'getRead' => " bool " ,
'oldestFirst' => " bool " ,
'lastModified' => " datetime " ,
// 'items' => "array int", // just pass these through
];
2017-06-03 21:34:37 +00:00
2017-04-03 01:34:30 +00:00
function __construct () {
}
function dispatch ( \JKingWeb\Arsse\REST\Request $req ) : Response {
// try to authenticate
2017-07-21 02:40:09 +00:00
if ( ! Arsse :: $user -> authHTTP ()) {
return new Response ( 401 , " " , " " , [ 'WWW-Authenticate: Basic realm="' . self :: REALM . '"' ]);
}
2017-04-03 01:34:30 +00:00
// normalize the input
if ( $req -> body ) {
// if the entity body is not JSON according to content type, return "415 Unsupported Media Type"
2017-07-21 02:40:09 +00:00
if ( ! preg_match ( " <^application/json \ b|^ $ > " , $req -> type )) {
return new Response ( 415 , " " , " " , [ 'Accept: application/json' ]);
}
2017-07-08 01:06:38 +00:00
$data = @ json_decode ( $req -> body , true );
if ( json_last_error () != \JSON_ERROR_NONE ) {
2017-04-03 01:34:30 +00:00
// if the body could not be parsed as JSON, return "400 Bad Request"
return new Response ( 400 );
}
} else {
$data = [];
}
// FIXME: Do query parameters take precedence in NextCloud? Is there a conflict error when values differ?
2017-07-07 12:13:03 +00:00
$data = $this -> normalizeInput ( $data , $this -> validInput , " U " );
$query = $this -> normalizeInput ( $req -> query , $this -> validInput , " U " );
$data = array_merge ( $data , $query );
unset ( $query );
2017-05-20 03:52:26 +00:00
// check to make sure the requested function is implemented
try {
$func = $this -> chooseCall ( $req -> paths , $req -> method );
} catch ( Exception501 $e ) {
return new Response ( 501 );
} catch ( Exception405 $e ) {
return new Response ( 405 , " " , " " , [ " Allow: " . $e -> getMessage ()]);
}
2017-07-21 02:40:09 +00:00
if ( ! method_exists ( $this , $func )) {
return new Response ( 501 );
}
2017-05-20 03:52:26 +00:00
// dispatch
try {
return $this -> $func ( $req -> paths , $data );
} catch ( Exception $e ) {
// if there was a REST exception return 400
return new Response ( 400 );
} catch ( AbstractException $e ) {
// if there was any other Arsse exception return 500
return new Response ( 500 );
}
}
protected function chooseCall ( array $url , string $method ) : string {
$choices = [
'items' => [],
'folders' => [
'' => [ 'GET' => " folderList " , 'POST' => " folderAdd " ],
2017-05-20 12:57:24 +00:00
'0' => [ 'PUT' => " folderRename " , 'DELETE' => " folderRemove " ],
'0/read' => [ 'PUT' => " folderMarkRead " ],
2017-05-20 03:52:26 +00:00
],
'feeds' => [
'' => [ 'GET' => " subscriptionList " , 'POST' => " subscriptionAdd " ],
2017-05-20 12:57:24 +00:00
'0' => [ 'DELETE' => " subscriptionRemove " ],
'0/move' => [ 'PUT' => " subscriptionMove " ],
'0/rename' => [ 'PUT' => " subscriptionRename " ],
'0/read' => [ 'PUT' => " subscriptionMarkRead " ],
2017-05-20 03:52:26 +00:00
'all' => [ 'GET' => " feedListStale " ],
'update' => [ 'GET' => " feedUpdate " ],
],
2017-07-07 12:13:03 +00:00
'items' => [
'' => [ 'GET' => " articleList " ],
'updated' => [ 'GET' => " articleList " ],
'read' => [ 'PUT' => " articleMarkReadAll " ],
'0/read' => [ 'PUT' => " articleMarkRead " ],
'0/unread' => [ 'PUT' => " articleMarkRead " ],
'read/multiple' => [ 'PUT' => " articleMarkReadMulti " ],
'unread/multiple' => [ 'PUT' => " articleMarkReadMulti " ],
'0/0/star' => [ 'PUT' => " articleMarkStarred " ],
'0/0/unstar' => [ 'PUT' => " articleMarkStarred " ],
'star/multiple' => [ 'PUT' => " articleMarkStarredMulti " ],
'unstar/multiple' => [ 'PUT' => " articleMarkStarredMulti " ],
],
2017-07-15 20:44:06 +00:00
'cleanup' => [
'before-update' => [ 'GET' => " cleanupBefore " ],
'after-update' => [ 'GET' => " cleanupAfter " ],
],
2017-05-20 03:52:26 +00:00
'version' => [
2017-07-15 20:44:06 +00:00
'' => [ 'GET' => " serverVersion " ],
],
'status' => [
'' => [ 'GET' => " serverStatus " ],
],
'user' => [
'' => [ 'GET' => " userStatus " ],
2017-05-20 03:52:26 +00:00
],
];
// the first path element is the overall scope of the request
$scope = $url [ 0 ];
2017-07-09 21:57:18 +00:00
// any URL components which are only digits should be replaced with "0", for easier comparison (integer segments are IDs, and we don't care about the specific ID)
2017-05-20 03:52:26 +00:00
for ( $a = 0 ; $a < sizeof ( $url ); $a ++ ) {
2017-07-21 02:40:09 +00:00
if ( $this -> validateInt ( $url [ $a ])) {
$url [ $a ] = " 0 " ;
}
2017-05-20 03:52:26 +00:00
}
// normalize the HTTP method to uppercase
$method = strtoupper ( $method );
// if the scope is not supported, return 501
2017-07-21 02:40:09 +00:00
if ( ! array_key_exists ( $scope , $choices )) {
throw new Exception501 ();
}
2017-05-20 03:52:26 +00:00
// 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
foreach ( $choices [ $scope ] as $path => $funcs ) {
// add the scope to the path to match against and split it
2017-05-20 12:57:24 +00:00
$path = ( string ) $path ;
2017-05-20 03:52:26 +00:00
$path = ( strlen ( $path )) ? " $scope / $path " : $scope ;
$path = explode ( " / " , $path );
if ( $path === $url ) {
// if the path matches, make sure the method is allowed
if ( array_key_exists ( $method , $funcs )) {
// if it is allowed, return the object method to run
return $funcs [ $method ];
} else {
// otherwise return 405
throw new Exception405 ( implode ( " , " , array_keys ( $funcs )));
}
2017-04-03 01:34:30 +00:00
}
}
2017-05-20 03:52:26 +00:00
// if the path was not found, return 501
throw new Exception501 ();
2017-04-03 01:34:30 +00:00
}
2017-07-07 12:13:03 +00:00
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 , [
'folderId' => " int " ,
'pinned' => " bool " ,
2017-07-07 19:25:47 +00:00
'added' => " datetime " ,
], $this -> dateFormat );
2017-07-07 12:13:03 +00:00
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 " ,
2017-07-09 21:57:18 +00:00
'enclosureMime' => " media_type " ,
2017-07-07 12:13:03 +00:00
'enclosureLink' => " media_url " ,
2017-07-09 21:57:18 +00:00
'feedId' => " subscription " ,
2017-07-07 12:13:03 +00:00
'unread' => " unread " ,
'starred' => " starred " ,
'lastModified' => " modified_date " ,
'fingerprint' => " fingerprint " ,
]);
// cast values
$article = $this -> fieldMapTypes ( $article , [
2017-07-07 19:25:47 +00:00
'unread' => " bool " ,
'starred' => " bool " ,
'pubDate' => " datetime " ,
'lastModified' => " datetime " ,
], $this -> dateFormat );
2017-07-07 12:13:03 +00:00
return $article ;
}
2017-05-19 03:03:33 +00:00
2017-04-03 01:34:30 +00:00
// list folders
2017-05-20 03:52:26 +00:00
protected function folderList ( array $url , array $data ) : Response {
2017-07-17 11:47:57 +00:00
$folders = Arsse :: $db -> folderList ( Arsse :: $user -> id , null , false ) -> getAll ();
2017-04-03 01:34:30 +00:00
return new Response ( 200 , [ 'folders' => $folders ]);
}
2017-04-01 19:42:10 +00:00
2017-04-03 01:34:30 +00:00
// create a folder
2017-05-20 03:52:26 +00:00
protected function folderAdd ( array $url , array $data ) : Response {
2017-04-03 01:34:30 +00:00
try {
2017-07-17 11:47:57 +00:00
$folder = Arsse :: $db -> folderAdd ( Arsse :: $user -> id , $data );
2017-04-03 01:34:30 +00:00
} catch ( ExceptionInput $e ) {
switch ( $e -> getCode ()) {
// folder already exists
case 10236 : return new Response ( 409 );
// folder name not acceptable
case 10231 :
case 10232 : return new Response ( 422 );
// other errors related to input
2017-07-24 12:15:37 +00:00
default : return new Response ( 400 ); // @codeCoverageIgnore
2017-04-03 01:34:30 +00:00
}
}
2017-07-17 11:47:57 +00:00
$folder = Arsse :: $db -> folderPropertiesGet ( Arsse :: $user -> id , $folder );
2017-04-03 01:34:30 +00:00
return new Response ( 200 , [ 'folders' => [ $folder ]]);
}
2017-04-02 03:06:52 +00:00
2017-04-03 01:34:30 +00:00
// delete a folder
2017-05-20 03:52:26 +00:00
protected function folderRemove ( array $url , array $data ) : Response {
2017-04-03 01:34:30 +00:00
// perform the deletion
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> folderRemove ( Arsse :: $user -> id , ( int ) $url [ 1 ]);
2017-04-03 01:34:30 +00:00
} catch ( ExceptionInput $e ) {
// folder does not exist
return new Response ( 404 );
}
return new Response ( 204 );
}
2017-04-02 16:14:15 +00:00
2017-04-03 01:34:30 +00:00
// rename a folder (also supports moving nesting folders, but this is not a feature of the API)
2017-05-20 03:52:26 +00:00
protected function folderRename ( array $url , array $data ) : Response {
2017-04-03 01:34:30 +00:00
// there must be some change to be made
2017-07-21 02:40:09 +00:00
if ( ! sizeof ( $data )) {
return new Response ( 422 );
}
2017-04-03 01:34:30 +00:00
// perform the edit
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> folderPropertiesSet ( Arsse :: $user -> id , ( int ) $url [ 1 ], $data );
2017-04-03 01:34:30 +00:00
} catch ( ExceptionInput $e ) {
switch ( $e -> getCode ()) {
// folder does not exist
2017-05-21 14:10:36 +00:00
case 10239 : return new Response ( 404 );
2017-04-03 01:34:30 +00:00
// folder already exists
case 10236 : return new Response ( 409 );
// folder name not acceptable
case 10231 :
case 10232 : return new Response ( 422 );
// other errors related to input
2017-07-24 12:15:37 +00:00
default : return new Response ( 400 ); // @codeCoverageIgnore
2017-04-03 01:34:30 +00:00
}
}
return new Response ( 204 );
}
2017-04-03 01:49:37 +00:00
2017-07-09 21:57:18 +00:00
// mark all articles associated with a folder as read
2017-07-07 12:13:03 +00:00
protected function folderMarkRead ( array $url , array $data ) : Response {
$c = new Context ;
if ( isset ( $data [ 'newestItemId' ])) {
// 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 );
}
// add the folder ID to the context
$c -> folder (( int ) $url [ 1 ]);
// perform the operation
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> articleMark ( Arsse :: $user -> id , [ 'read' => true ], $c );
2017-07-07 12:13:03 +00:00
} catch ( ExceptionInput $e ) {
// folder does not exist
return new Response ( 404 );
}
return new Response ( 204 );
}
2017-05-20 03:52:26 +00:00
2017-05-19 03:03:33 +00:00
// return list of feeds which should be refreshed
2017-05-20 03:52:26 +00:00
protected function feedListStale ( array $url , array $data ) : Response {
// function requires admin rights per spec
2017-07-21 02:40:09 +00:00
if ( Arsse :: $user -> rightsGet ( Arsse :: $user -> id ) == User :: RIGHTS_NONE ) {
return new Response ( 403 );
}
2017-05-20 03:52:26 +00:00
// list stale feeds which should be checked for updates
2017-07-17 11:47:57 +00:00
$feeds = Arsse :: $db -> feedListStale ();
2017-05-20 03:52:26 +00:00
$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' => " " ];
2017-05-19 03:03:33 +00:00
}
2017-05-20 03:52:26 +00:00
return new Response ( 200 , [ 'feeds' => $out ]);
}
// refresh a feed
protected function feedUpdate ( array $url , array $data ) : Response {
2017-06-03 21:34:37 +00:00
// function requires admin rights per spec
2017-07-21 02:40:09 +00:00
if ( Arsse :: $user -> rightsGet ( Arsse :: $user -> id ) == User :: RIGHTS_NONE ) {
return new Response ( 403 );
}
2017-05-20 03:52:26 +00:00
// perform an update of a single feed
2017-07-21 02:40:09 +00:00
if ( ! isset ( $data [ 'feedId' ])) {
return new Response ( 422 );
}
2017-05-20 03:52:26 +00:00
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> feedUpdate ( $data [ 'feedId' ]);
2017-05-20 03:52:26 +00:00
} catch ( ExceptionInput $e ) {
return new Response ( 404 );
}
2017-06-03 21:34:37 +00:00
return new Response ( 204 );
2017-05-19 03:03:33 +00:00
}
// add a new feed
2017-05-20 03:52:26 +00:00
protected function subscriptionAdd ( array $url , array $data ) : Response {
// normalize the feed URL
2017-07-21 02:40:09 +00:00
if ( ! isset ( $data [ 'url' ])) {
return new Response ( 422 );
}
2017-07-07 12:13:03 +00:00
// normalize the folder ID, if specified; zero should be transformed to null
$folder = ( isset ( $data [ 'folderId' ]) && $data [ 'folderId' ]) ? $data [ 'folderId' ] : null ;
2017-05-19 03:03:33 +00:00
// try to add the feed
2017-07-17 11:47:57 +00:00
$tr = Arsse :: $db -> begin ();
2017-05-19 03:03:33 +00:00
try {
2017-07-17 11:47:57 +00:00
$id = Arsse :: $db -> subscriptionAdd ( Arsse :: $user -> id , $data [ 'url' ]);
2017-05-19 03:03:33 +00:00
} 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 {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , $id , [ 'folder' => $folder ]);
2017-05-19 03:03:33 +00:00
} catch ( ExceptionInput $e ) {}
}
$tr -> commit ();
// fetch the feed's metadata and format it appropriately
2017-07-17 11:47:57 +00:00
$feed = Arsse :: $db -> subscriptionPropertiesGet ( Arsse :: $user -> id , $id );
2017-05-19 03:03:33 +00:00
$feed = $this -> feedTranslate ( $feed );
$out = [ 'feeds' => [ $feed ]];
2017-07-17 11:47:57 +00:00
$newest = Arsse :: $db -> editionLatest ( Arsse :: $user -> id , ( new Context ) -> subscription ( $id ));
2017-07-21 02:40:09 +00:00
if ( $newest ) {
$out [ 'newestItemId' ] = $newest ;
}
2017-05-19 03:03:33 +00:00
return new Response ( 200 , $out );
}
2017-07-07 12:13:03 +00:00
// return list of feeds for the logged-in user
protected function subscriptionList ( array $url , array $data ) : Response {
2017-07-17 11:47:57 +00:00
$subs = Arsse :: $db -> subscriptionList ( Arsse :: $user -> id );
2017-07-07 12:13:03 +00:00
$out = [];
foreach ( $subs as $sub ) {
$out [] = $this -> feedTranslate ( $sub );
}
$out = [ 'feeds' => $out ];
2017-07-17 11:47:57 +00:00
$out [ 'starredCount' ] = Arsse :: $db -> articleStarredCount ( Arsse :: $user -> id );
$newest = Arsse :: $db -> editionLatest ( Arsse :: $user -> id );
2017-07-21 02:40:09 +00:00
if ( $newest ) {
$out [ 'newestItemId' ] = $newest ;
}
2017-07-07 12:13:03 +00:00
return new Response ( 200 , $out );
}
2017-05-19 03:03:33 +00:00
// delete a feed
2017-05-20 03:52:26 +00:00
protected function subscriptionRemove ( array $url , array $data ) : Response {
2017-05-19 03:03:33 +00:00
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> subscriptionRemove ( Arsse :: $user -> id , ( int ) $url [ 1 ]);
2017-05-19 03:03:33 +00:00
} catch ( ExceptionInput $e ) {
// feed does not exist
return new Response ( 404 );
}
return new Response ( 204 );
}
// rename a feed
2017-05-20 03:52:26 +00:00
protected function subscriptionRename ( array $url , array $data ) : Response {
// normalize input
2017-05-19 03:03:33 +00:00
$in = [];
2017-07-07 12:13:03 +00:00
if ( array_key_exists ( 'feedTitle' , $data )) { // we use array_key_exists because null is a valid input
2017-05-19 03:03:33 +00:00
$in [ 'title' ] = $data [ 'feedTitle' ];
2017-05-20 03:52:26 +00:00
} else {
return new Response ( 422 );
}
// perform the renaming
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , ( int ) $url [ 1 ], $in );
2017-05-20 03:52:26 +00:00
} catch ( ExceptionInput $e ) {
2017-05-21 14:10:36 +00:00
switch ( $e -> getCode ()) {
// subscription does not exist
case 10239 : return new Response ( 404 );
// name is invalid
case 10231 :
case 10232 : return new Response ( 422 );
// other errors related to input
2017-07-24 12:15:37 +00:00
default : return new Response ( 400 ); // @codeCoverageIgnore
2017-05-21 14:10:36 +00:00
}
2017-05-19 03:03:33 +00:00
}
2017-05-20 03:52:26 +00:00
return new Response ( 204 );
}
// move a feed to a folder
protected function subscriptionMove ( array $url , array $data ) : Response {
2017-07-07 12:13:03 +00:00
// normalize input
2017-05-20 03:52:26 +00:00
$in = [];
2017-07-07 12:13:03 +00:00
if ( isset ( $data [ 'folderId' ])) {
$in [ 'folder' ] = $data [ 'folderId' ] ? $data [ 'folderId' ] : null ;
2017-05-20 03:52:26 +00:00
} else {
return new Response ( 422 );
2017-05-19 03:03:33 +00:00
}
2017-05-20 03:52:26 +00:00
// perform the move
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , ( int ) $url [ 1 ], $in );
2017-05-20 03:52:26 +00:00
} catch ( ExceptionInput $e ) {
2017-05-21 14:10:36 +00:00
switch ( $e -> getCode ()) {
// subscription does not exist
case 10239 : return new Response ( 404 );
// folder does not exist
case 10235 : return new Response ( 422 );
// other errors related to input
2017-07-24 12:15:37 +00:00
default : return new Response ( 400 ); // @codeCoverageIgnore
2017-05-21 14:10:36 +00:00
}
2017-05-19 03:03:33 +00:00
}
return new Response ( 204 );
}
2017-07-07 12:13:03 +00:00
2017-07-09 21:57:18 +00:00
// mark all articles associated with a subscription as read
2017-07-07 12:13:03 +00:00
protected function subscriptionMarkRead ( array $url , array $data ) : Response {
$c = new Context ;
if ( isset ( $data [ 'newestItemId' ])) {
$c -> latestEdition ( $data [ 'newestItemId' ]);
} else {
// otherwise return an error
return new Response ( 422 );
}
// add the subscription ID to the context
$c -> subscription (( int ) $url [ 1 ]);
// perform the operation
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> articleMark ( Arsse :: $user -> id , [ 'read' => true ], $c );
2017-07-07 12:13:03 +00:00
} catch ( ExceptionInput $e ) {
// subscription does not exist
return new Response ( 404 );
}
return new Response ( 204 );
}
2017-07-09 21:57:18 +00:00
// list articles and their properties
2017-07-07 12:13:03 +00:00
protected function articleList ( array $url , array $data ) : Response {
// set the context options supplied by the client
$c = new Context ;
// set the batch size
2017-07-21 02:40:09 +00:00
if ( isset ( $data [ 'batchSize' ]) && $data [ 'batchSize' ] > 0 ) {
$c -> limit ( $data [ 'batchSize' ]);
}
2017-07-07 12:13:03 +00:00
// set the order of returned items
if ( isset ( $data [ 'oldestFirst' ]) && $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
2017-07-15 20:44:06 +00:00
if ( isset ( $data [ 'offset' ]) && $data [ 'offset' ] > 0 ) {
2017-07-07 12:13:03 +00:00
if ( $c -> reverse ) {
$c -> latestEdition ( $data [ 'offset' ] - 1 );
} else {
$c -> oldestEdition ( $data [ 'offset' ] + 1 );
}
}
// set whether to only return unread
2017-07-21 02:40:09 +00:00
if ( isset ( $data [ 'getRead' ]) && ! $data [ 'getRead' ]) {
$c -> unread ( true );
}
2017-07-07 12:13:03 +00:00
// if no type is specified assume 3 (All)
2017-07-21 02:40:09 +00:00
if ( ! isset ( $data [ 'type' ])) {
$data [ 'type' ] = 3 ;
}
2017-07-07 12:13:03 +00:00
switch ( $data [ 'type' ]) {
case 0 : // feed
2017-07-21 02:40:09 +00:00
if ( isset ( $data [ 'id' ])) {
$c -> subscription ( $data [ 'id' ]);
}
2017-07-07 12:13:03 +00:00
break ;
case 1 : // folder
2017-07-21 02:40:09 +00:00
if ( isset ( $data [ 'id' ])) {
$c -> folder ( $data [ 'id' ]);
}
2017-07-07 12:13:03 +00:00
break ;
case 2 : // starred
$c -> starred ( true );
break ;
2017-07-24 12:15:37 +00:00
default : // @codeCoverageIgnore
2017-07-07 12:13:03 +00:00
// return all items
}
// whether to return only updated items
2017-07-21 02:40:09 +00:00
if ( isset ( $data [ 'lastModified' ])) {
$c -> modifiedSince ( $data [ 'lastModified' ]);
}
2017-07-07 12:13:03 +00:00
// perform the fetch
try {
2017-07-17 11:47:57 +00:00
$items = Arsse :: $db -> articleList ( Arsse :: $user -> id , $c );
2017-07-07 12:13:03 +00:00
} catch ( ExceptionInput $e ) {
// ID of subscription or folder is not valid
return new Response ( 422 );
}
$out = [];
foreach ( $items as $item ) {
$out [] = $this -> articleTranslate ( $item );
}
$out = [ 'items' => $out ];
return new Response ( 200 , $out );
}
2017-07-09 21:57:18 +00:00
// mark all articles as read
2017-07-07 12:13:03 +00:00
protected function articleMarkReadAll ( array $url , array $data ) : Response {
$c = new Context ;
if ( isset ( $data [ 'newestItemId' ])) {
// set the newest item ID as specified
$c -> latestEdition ( $data [ 'newestItemId' ]);
} else {
// otherwise return an error
return new Response ( 422 );
}
// perform the operation
2017-07-17 11:47:57 +00:00
Arsse :: $db -> articleMark ( Arsse :: $user -> id , [ 'read' => true ], $c );
2017-07-07 12:13:03 +00:00
return new Response ( 204 );
}
2017-07-09 21:57:18 +00:00
// mark a single article as read
2017-07-07 12:13:03 +00:00
protected function articleMarkRead ( array $url , array $data ) : Response {
// initialize the matching context
$c = new Context ;
$c -> edition (( int ) $url [ 1 ]);
// determine whether to mark read or unread
$set = ( $url [ 2 ] == " read " );
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> articleMark ( Arsse :: $user -> id , [ 'read' => $set ], $c );
2017-07-07 12:13:03 +00:00
} catch ( ExceptionInput $e ) {
// ID is not valid
return new Response ( 404 );
}
return new Response ( 204 );
}
2017-07-09 21:57:18 +00:00
// mark a single article as read
2017-07-07 12:13:03 +00:00
protected function articleMarkStarred ( array $url , array $data ) : Response {
// initialize the matching context
$c = new Context ;
$c -> article (( int ) $url [ 2 ]);
// determine whether to mark read or unread
$set = ( $url [ 3 ] == " star " );
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> articleMark ( Arsse :: $user -> id , [ 'starred' => $set ], $c );
2017-07-07 12:13:03 +00:00
} catch ( ExceptionInput $e ) {
// ID is not valid
return new Response ( 404 );
}
return new Response ( 204 );
}
2017-07-09 21:57:18 +00:00
// mark an array of articles as read
2017-07-07 12:13:03 +00:00
protected function articleMarkReadMulti ( array $url , array $data ) : Response {
// determine whether to mark read or unread
$set = ( $url [ 1 ] == " read " );
// if the input data is not at all valid, return an error
2017-07-21 02:40:09 +00:00
if ( ! isset ( $data [ 'items' ]) || ! is_array ( $data [ 'items' ])) {
return new Response ( 422 );
}
2017-07-07 12:13:03 +00:00
// start a transaction and loop through the items
2017-07-17 11:47:57 +00:00
$t = Arsse :: $db -> begin ();
2017-07-07 12:13:03 +00:00
$in = array_chunk ( $data [ 'items' ], 50 );
for ( $a = 0 ; $a < sizeof ( $in ); $a ++ ) {
2017-07-09 21:57:18 +00:00
// initialize the matching context
$c = new Context ;
2017-07-07 12:13:03 +00:00
$c -> editions ( $in [ $a ]);
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> articleMark ( Arsse :: $user -> id , [ 'read' => $set ], $c );
2017-07-07 12:13:03 +00:00
} catch ( ExceptionInput $e ) {}
}
$t -> commit ();
return new Response ( 204 );
}
2017-07-09 21:57:18 +00:00
// mark an array of articles as starred
2017-07-07 12:13:03 +00:00
protected function articleMarkStarredMulti ( array $url , array $data ) : Response {
2017-07-09 21:57:18 +00:00
// determine whether to mark starred or unstarred
2017-07-07 12:13:03 +00:00
$set = ( $url [ 1 ] == " star " );
// if the input data is not at all valid, return an error
2017-07-21 02:40:09 +00:00
if ( ! isset ( $data [ 'items' ]) || ! is_array ( $data [ 'items' ])) {
return new Response ( 422 );
}
2017-07-07 12:13:03 +00:00
// start a transaction and loop through the items
2017-07-17 11:47:57 +00:00
$t = Arsse :: $db -> begin ();
2017-07-07 12:13:03 +00:00
$in = array_chunk ( array_column ( $data [ 'items' ], " guidHash " ), 50 );
for ( $a = 0 ; $a < sizeof ( $in ); $a ++ ) {
2017-07-09 21:57:18 +00:00
// initialize the matching context
$c = new Context ;
2017-07-07 12:13:03 +00:00
$c -> articles ( $in [ $a ]);
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> articleMark ( Arsse :: $user -> id , [ 'starred' => $set ], $c );
2017-07-07 12:13:03 +00:00
} catch ( ExceptionInput $e ) {}
}
$t -> commit ();
return new Response ( 204 );
}
2017-07-15 20:44:06 +00:00
protected function userStatus ( array $url , array $data ) : Response {
2017-08-20 03:56:32 +00:00
$data = Arsse :: $user -> propertiesGet ( Arsse :: $user -> id , true );
2017-07-19 22:07:36 +00:00
// construct the avatar structure, if an image is available
if ( isset ( $data [ 'avatar' ])) {
$avatar = [
'data' => base64_encode ( $data [ 'avatar' ][ 'data' ]),
'mime' => $data [ 'avatar' ][ 'type' ],
];
} else {
$avatar = null ;
}
// construct the rest of the structure
2017-07-15 20:44:06 +00:00
$out = [
2017-07-17 11:47:57 +00:00
'userId' => Arsse :: $user -> id ,
'displayName' => $data [ 'name' ] ? ? Arsse :: $user -> id ,
2017-07-15 20:44:06 +00:00
'lastLoginTimestamp' => time (),
2017-07-19 22:07:36 +00:00
'avatar' => $avatar ,
2017-07-15 20:44:06 +00:00
];
return new Response ( 200 , $out );
}
protected function cleanupBefore ( array $url , array $data ) : Response {
// function requires admin rights per spec
2017-07-21 02:40:09 +00:00
if ( Arsse :: $user -> rightsGet ( Arsse :: $user -> id ) == User :: RIGHTS_NONE ) {
return new Response ( 403 );
}
2017-08-02 22:27:04 +00:00
Service :: cleanupPre ();
2017-07-15 20:44:06 +00:00
return new Response ( 204 );
}
protected function cleanupAfter ( array $url , array $data ) : Response {
// function requires admin rights per spec
2017-07-21 02:40:09 +00:00
if ( Arsse :: $user -> rightsGet ( Arsse :: $user -> id ) == User :: RIGHTS_NONE ) {
return new Response ( 403 );
}
2017-08-18 02:36:15 +00:00
Service :: cleanupPost ();
2017-07-15 20:44:06 +00:00
return new Response ( 204 );
}
// return the server version
protected function serverVersion ( array $url , array $data ) : Response {
return new Response ( 200 , [
'version' => self :: VERSION ,
'arsse_version' => \JKingWeb\Arsse\VERSION ,
]);
}
protected function serverStatus ( array $url , array $data ) : Response {
return new Response ( 200 , [
'version' => self :: VERSION ,
'arsse_version' => \JKingWeb\Arsse\VERSION ,
'warnings' => [
2017-08-02 22:27:04 +00:00
'improperlyConfiguredCron' => ! Service :: hasCheckedIn (),
2017-07-15 20:44:06 +00:00
]
]);
}
2017-04-01 19:42:10 +00:00
}