2017-04-01 19:42:10 +00:00
< ? php
declare ( strict_types = 1 );
namespace JKingWeb\Arsse\REST\NextCloudNews ;
2017-08-29 14:50:31 +00:00
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-09-28 14:16:24 +00:00
use JKingWeb\Arsse\Misc\ValueInfo ;
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 = [
2017-10-20 02:58:42 +00:00
'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 ,
2017-07-07 12:13:03 +00:00
];
2017-06-03 21:34:37 +00:00
2017-08-29 14:50:31 +00:00
public function __construct () {
2017-04-03 01:34:30 +00:00
}
2017-08-29 14:50:31 +00:00
public function dispatch ( \JKingWeb\Arsse\REST\Request $req ) : Response {
2017-04-03 01:34:30 +00:00
// try to authenticate
2017-08-29 14:50:31 +00:00
if ( ! Arsse :: $user -> authHTTP ()) {
2017-07-21 02:40:09 +00:00
return new Response ( 401 , " " , " " , [ 'WWW-Authenticate: Basic realm="' . self :: REALM . '"' ]);
}
2017-04-03 01:34:30 +00:00
// normalize the input
2017-08-29 14:50:31 +00:00
if ( $req -> body ) {
2017-04-03 01:34:30 +00:00
// if the entity body is not JSON according to content type, return "415 Unsupported Media Type"
2017-08-29 14:50:31 +00:00
if ( ! preg_match ( " <^application/json \ b|^ $ > " , $req -> type )) {
2017-07-21 02:40:09 +00:00
return new Response ( 415 , " " , " " , [ 'Accept: application/json' ]);
}
2017-07-08 01:06:38 +00:00
$data = @ json_decode ( $req -> body , true );
2017-08-29 14:50:31 +00:00
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-10-20 02:58:42 +00:00
$data = $this -> normalizeInput ( array_merge ( $data , $req -> query ), $this -> validInput , " unix " );
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 );
2017-08-29 14:50:31 +00:00
} catch ( Exception501 $e ) {
2017-05-20 03:52:26 +00:00
return new Response ( 501 );
2017-08-29 14:50:31 +00:00
} catch ( Exception405 $e ) {
2017-05-20 03:52:26 +00:00
return new Response ( 405 , " " , " " , [ " Allow: " . $e -> getMessage ()]);
}
2017-08-29 14:50:31 +00:00
if ( ! method_exists ( $this , $func )) {
2017-09-05 23:35:14 +00:00
return new Response ( 501 ); // @codeCoverageIgnore
2017-07-21 02:40:09 +00:00
}
2017-05-20 03:52:26 +00:00
// dispatch
try {
return $this -> $func ( $req -> paths , $data );
2017-09-28 14:16:24 +00:00
// @codeCoverageIgnoreStart
2017-08-29 14:50:31 +00:00
} catch ( Exception $e ) {
2017-05-20 03:52:26 +00:00
// if there was a REST exception return 400
return new Response ( 400 );
2017-08-29 14:50:31 +00:00
} catch ( AbstractException $e ) {
2017-05-20 03:52:26 +00:00
// if there was any other Arsse exception return 500
return new Response ( 500 );
}
2017-09-05 23:35:14 +00:00
// @codeCoverageIgnoreEnd
2017-05-20 03:52:26 +00:00
}
protected function chooseCall ( array $url , string $method ) : string {
$choices = [
'items' => [],
'folders' => [
'' => [ 'GET' => " folderList " , 'POST' => " folderAdd " ],
2017-09-28 14:16:24 +00:00
'1' => [ 'PUT' => " folderRename " , 'DELETE' => " folderRemove " ],
'1/read' => [ 'PUT' => " folderMarkRead " ],
2017-05-20 03:52:26 +00:00
],
'feeds' => [
'' => [ 'GET' => " subscriptionList " , 'POST' => " subscriptionAdd " ],
2017-09-28 14:16:24 +00:00
'1' => [ 'DELETE' => " subscriptionRemove " ],
'1/move' => [ 'PUT' => " subscriptionMove " ],
'1/rename' => [ 'PUT' => " subscriptionRename " ],
'1/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 " ],
2017-09-28 14:16:24 +00:00
'1/read' => [ 'PUT' => " articleMarkRead " ],
'1/unread' => [ 'PUT' => " articleMarkRead " ],
2017-07-07 12:13:03 +00:00
'read/multiple' => [ 'PUT' => " articleMarkReadMulti " ],
'unread/multiple' => [ 'PUT' => " articleMarkReadMulti " ],
2017-09-28 14:16:24 +00:00
'1/1/star' => [ 'PUT' => " articleMarkStarred " ],
'1/1/unstar' => [ 'PUT' => " articleMarkStarred " ],
2017-07-07 12:13:03 +00:00
'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-09-28 14:16:24 +00:00
// 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)
2017-08-29 14:50:31 +00:00
for ( $a = 0 ; $a < sizeof ( $url ); $a ++ ) {
2017-09-28 14:16:24 +00:00
if ( ValueInfo :: id ( $url [ $a ])) {
$url [ $a ] = " 1 " ;
2017-07-21 02:40:09 +00:00
}
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-08-29 14:50:31 +00:00
if ( ! array_key_exists ( $scope , $choices )) {
2017-07-21 02:40:09 +00:00
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
2017-08-29 14:50:31 +00:00
foreach ( $choices [ $scope ] as $path => $funcs ) {
2017-05-20 03:52:26 +00:00
// 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 );
2017-08-29 14:50:31 +00:00
if ( $path === $url ) {
2017-05-20 03:52:26 +00:00
// if the path matches, make sure the method is allowed
2017-08-29 14:50:31 +00:00
if ( array_key_exists ( $method , $funcs )) {
2017-05-20 03:52:26 +00:00
// 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-10-20 02:58:42 +00:00
$folder = Arsse :: $db -> folderAdd ( Arsse :: $user -> id , [ 'name' => $data [ 'name' ]]);
2017-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
switch ( $e -> getCode ()) {
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
}
}
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-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
2017-04-03 01:34:30 +00:00
// 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
try {
2017-10-20 02:58:42 +00:00
Arsse :: $db -> folderPropertiesSet ( Arsse :: $user -> id , ( int ) $url [ 1 ], [ 'name' => $data [ 'name' ]]);
2017-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
switch ( $e -> getCode ()) {
2017-04-03 01:34:30 +00:00
// 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 {
2017-10-20 02:58:42 +00:00
if ( ! ValueInfo :: id ( $data [ 'newestItemId' ])) {
// if the item ID is invalid (i.e. not a positive integer), this is an error
2017-07-07 12:13:03 +00:00
return new Response ( 422 );
}
2017-10-20 02:58:42 +00:00
// build the context
$c = new Context ;
$c -> latestEdition (( int ) $data [ 'newestItemId' ]);
2017-07-07 12:13:03 +00:00
$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-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
2017-07-07 12:13:03 +00:00
// 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-08-29 14:50:31 +00:00
if ( Arsse :: $user -> rightsGet ( Arsse :: $user -> id ) == User :: RIGHTS_NONE ) {
2017-07-21 02:40:09 +00:00
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 = [];
2017-08-29 14:50:31 +00:00
foreach ( $feeds as $feed ) {
2017-05-20 03:52:26 +00:00
// 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-08-29 14:50:31 +00:00
if ( Arsse :: $user -> rightsGet ( Arsse :: $user -> id ) == User :: RIGHTS_NONE ) {
2017-07-21 02:40:09 +00:00
return new Response ( 403 );
}
2017-05-20 03:52:26 +00:00
try {
2017-07-17 11:47:57 +00:00
Arsse :: $db -> feedUpdate ( $data [ 'feedId' ]);
2017-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
2017-09-28 14:16:24 +00:00
switch ( $e -> getCode ()) {
case 10239 : // feed does not exist
return new Response ( 404 );
case 10237 : // feed ID invalid
return new Response ( 422 );
default : // other errors related to input
return new Response ( 400 ); // @codeCoverageIgnore
}
2017-05-20 03:52:26 +00:00
}
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 {
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-10-20 02:58:42 +00:00
$id = Arsse :: $db -> subscriptionAdd ( Arsse :: $user -> id , ( string ) $data [ 'url' ]);
2017-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
2017-05-19 03:03:33 +00:00
// feed already exists
return new Response ( 409 );
2017-08-29 14:50:31 +00:00
} catch ( FeedException $e ) {
2017-05-19 03:03:33 +00:00
// feed could not be retrieved
return new Response ( 422 );
}
// if a folder was specified, move the feed to the correct folder; silently ignore errors
2017-10-20 02:58:42 +00:00
if ( $data [ 'folderId' ]) {
2017-05-19 03:03:33 +00:00
try {
2017-10-20 02:58:42 +00:00
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , $id , [ 'folder' => $data [ 'folderId' ]]);
2017-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
}
2017-05-19 03:03:33 +00:00
}
$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-08-29 14:50:31 +00:00
if ( $newest ) {
2017-07-21 02:40:09 +00:00
$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 = [];
2017-08-29 14:50:31 +00:00
foreach ( $subs as $sub ) {
2017-07-07 12:13:03 +00:00
$out [] = $this -> feedTranslate ( $sub );
}
$out = [ 'feeds' => $out ];
2017-10-11 16:55:50 +00:00
$out [ 'starredCount' ] = Arsse :: $db -> articleStarred ( Arsse :: $user -> id )[ 'total' ];
2017-07-17 11:47:57 +00:00
$newest = Arsse :: $db -> editionLatest ( Arsse :: $user -> id );
2017-08-29 14:50:31 +00:00
if ( $newest ) {
2017-07-21 02:40:09 +00:00
$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-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
2017-05-19 03:03:33 +00:00
// 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 {
try {
2017-10-20 02:58:42 +00:00
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , ( int ) $url [ 1 ], [ 'title' => ( string ) $data [ 'feedTitle' ]]);
2017-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
switch ( $e -> getCode ()) {
2017-05-21 14:10:36 +00:00
// 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-10-20 02:58:42 +00:00
// if no folder is specified this is an error
if ( ! isset ( $data [ 'folderId' ])) {
2017-05-20 03:52:26 +00:00
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-10-20 02:58:42 +00:00
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , ( int ) $url [ 1 ], [ 'folder' => $data [ 'folderId' ]]);
2017-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
switch ( $e -> getCode ()) {
2017-09-28 14:16:24 +00:00
case 10239 : // subscription does not exist
return new Response ( 404 );
case 10235 : // folder does not exist
case 10237 : // folder ID is invalid
return new Response ( 422 );
default : // other errors related to input
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 {
2017-10-20 02:58:42 +00:00
if ( ! ValueInfo :: id ( $data [ 'newestItemId' ])) {
// if the item ID is invalid (i.e. not a positive integer), this is an error
2017-07-07 12:13:03 +00:00
return new Response ( 422 );
}
2017-10-20 02:58:42 +00:00
// build the context
$c = new Context ;
$c -> latestEdition (( int ) $data [ 'newestItemId' ]);
2017-07-07 12:13:03 +00:00
$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-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
2017-07-07 12:13:03 +00:00
// 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-10-20 02:58:42 +00:00
if ( $data [ 'batchSize' ] > 0 ) {
2017-07-21 02:40:09 +00:00
$c -> limit ( $data [ 'batchSize' ]);
}
2017-07-07 12:13:03 +00:00
// set the order of returned items
2017-10-20 02:58:42 +00:00
if ( $data [ 'oldestFirst' ]) {
2017-07-07 12:13:03 +00:00
$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-10-20 02:58:42 +00:00
if ( $data [ 'offset' ] > 0 ) {
2017-08-29 14:50:31 +00:00
if ( $c -> reverse ) {
2017-07-07 12:13:03 +00:00
$c -> latestEdition ( $data [ 'offset' ] - 1 );
} else {
$c -> oldestEdition ( $data [ 'offset' ] + 1 );
}
}
// set whether to only return unread
2017-10-20 02:58:42 +00:00
if ( ! ValueInfo :: bool ( $data [ 'getRead' ], true )) {
2017-07-21 02:40:09 +00:00
$c -> unread ( true );
}
2017-07-07 12:13:03 +00:00
// if no type is specified assume 3 (All)
2017-10-20 02:58:42 +00:00
$data [ 'type' ] = $data [ 'type' ] ? ? 3 ;
2017-08-29 14:50:31 +00:00
switch ( $data [ 'type' ]) {
2017-07-07 12:13:03 +00:00
case 0 : // feed
2017-08-29 14:50:31 +00:00
if ( isset ( $data [ 'id' ])) {
2017-07-21 02:40:09 +00:00
$c -> subscription ( $data [ 'id' ]);
}
2017-07-07 12:13:03 +00:00
break ;
case 1 : // folder
2017-08-29 14:50:31 +00:00
if ( isset ( $data [ 'id' ])) {
2017-07-21 02:40:09 +00:00
$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-10-20 02:58:42 +00:00
if ( $data [ 'lastModified' ]) {
2017-07-21 02:40:09 +00:00
$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-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
2017-07-07 12:13:03 +00:00
// ID of subscription or folder is not valid
return new Response ( 422 );
}
$out = [];
2017-08-29 14:50:31 +00:00
foreach ( $items as $item ) {
2017-07-07 12:13:03 +00:00
$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 {
2017-10-20 02:58:42 +00:00
if ( ! ValueInfo :: id ( $data [ 'newestItemId' ])) {
// if the item ID is invalid (i.e. not a positive integer), this is an error
2017-07-07 12:13:03 +00:00
return new Response ( 422 );
}
2017-10-20 02:58:42 +00:00
// build the context
$c = new Context ;
$c -> latestEdition (( int ) $data [ 'newestItemId' ]);
2017-07-07 12:13:03 +00:00
// 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-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
2017-07-07 12:13:03 +00:00
// 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-08-29 14:50:31 +00:00
} catch ( ExceptionInput $e ) {
2017-07-07 12:13:03 +00:00
// 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 " );
2017-11-07 15:00:31 +00:00
// initialize the matching context
$c = new Context ;
$c -> editions ( $data [ 'items' ] ? ? []);
try {
Arsse :: $db -> articleMark ( Arsse :: $user -> id , [ 'read' => $set ], $c );
} catch ( ExceptionInput $e ) {
2017-07-07 12:13:03 +00:00
}
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 " );
2017-11-07 15:00:31 +00:00
// 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 ) {
2017-07-07 12:13:03 +00:00
}
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
2017-08-29 14:50:31 +00:00
if ( isset ( $data [ 'avatar' ])) {
2017-07-19 22:07:36 +00:00
$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-08-29 14:50:31 +00:00
if ( Arsse :: $user -> rightsGet ( Arsse :: $user -> id ) == User :: RIGHTS_NONE ) {
2017-07-21 02:40:09 +00:00
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-08-29 14:50:31 +00:00
if ( Arsse :: $user -> rightsGet ( Arsse :: $user -> id ) == User :: RIGHTS_NONE ) {
2017-07-21 02:40:09 +00:00
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 ,
2017-10-02 19:42:15 +00:00
'arsse_version' => Arsse :: VERSION ,
2017-07-15 20:44:06 +00:00
]);
}
protected function serverStatus ( array $url , array $data ) : Response {
return new Response ( 200 , [
'version' => self :: VERSION ,
2017-10-02 19:42:15 +00:00
'arsse_version' => Arsse :: VERSION ,
2017-07-15 20:44:06 +00:00
'warnings' => [
2017-08-02 22:27:04 +00:00
'improperlyConfiguredCron' => ! Service :: hasCheckedIn (),
2017-07-15 20:44:06 +00:00
]
]);
}
2017-08-29 14:50:31 +00:00
}