2017-04-01 19:42:10 +00:00
< ? php
2017-11-17 01:23:18 +00:00
/** @ license MIT
* Copyright 2017 J . King , Dustin Wilson et al .
* See LICENSE and AUTHORS files for details */
2017-04-01 19:42:10 +00:00
declare ( strict_types = 1 );
2019-12-05 18:02:02 +00:00
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-08-02 22:27:04 +00:00
use JKingWeb\Arsse\Service ;
2019-02-26 03:41:12 +00:00
use JKingWeb\Arsse\Context\Context ;
2017-09-28 02:25:45 +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 ;
2019-09-28 02:38:03 +00:00
use JKingWeb\Arsse\Misc\HTTP ;
2020-01-08 17:02:43 +00:00
use JKingWeb\Arsse\REST\Exception ;
2018-01-05 04:08:53 +00:00
use Psr\Http\Message\ServerRequestInterface ;
use Psr\Http\Message\ResponseInterface ;
2020-01-20 15:40:05 +00:00
use Laminas\Diactoros\Response\JsonResponse as Response ;
use Laminas\Diactoros\Response\EmptyResponse ;
2017-04-01 19:42:10 +00:00
class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
2020-03-01 23:32:01 +00:00
public const VERSION = " 11.0.5 " ;
protected const ACCEPTED_TYPE = " application/json " ;
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 00:35:45 +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-11-28 18:46:07 +00:00
protected $paths = [
2018-01-05 04:08:53 +00:00
'/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 " ],
2017-11-28 18:46:07 +00:00
];
2018-10-26 18:58:04 +00:00
2017-08-29 14:50:31 +00:00
public function __construct () {
2017-04-03 01:34:30 +00:00
}
2018-01-05 04:08:53 +00:00
public function dispatch ( ServerRequestInterface $req ) : ResponseInterface {
2019-09-05 14:19:05 +00:00
// get the request path only; this is assumed to already be normalized
$target = parse_url ( $req -> getRequestTarget ())[ 'path' ] ? ? " " ;
2017-11-29 20:28:33 +00:00
// handle HTTP OPTIONS requests
2019-01-11 15:38:06 +00:00
if ( $req -> getMethod () === " OPTIONS " ) {
2019-09-05 14:19:05 +00:00
return $this -> handleHTTPOptions ( $target );
2017-11-29 20:28:33 +00:00
}
2021-03-03 21:46:57 +00:00
// try to authenticate
if ( $req -> getAttribute ( " authenticated " , false )) {
Arsse :: $user -> id = $req -> getAttribute ( " authenticatedUser " );
} else {
return new EmptyResponse ( 401 );
}
2017-04-03 01:34:30 +00:00
// normalize the input
2018-01-05 04:08:53 +00:00
$data = ( string ) $req -> getBody ();
if ( $data ) {
2017-04-03 01:34:30 +00:00
// if the entity body is not JSON according to content type, return "415 Unsupported Media Type"
2019-09-28 02:38:03 +00:00
if ( ! HTTP :: matchType ( $req , " " , self :: ACCEPTED_TYPE )) {
return new EmptyResponse ( 415 , [ 'Accept' => self :: ACCEPTED_TYPE ]);
2017-07-21 02:40:09 +00:00
}
2018-01-05 04:08:53 +00:00
$data = @ json_decode ( $data , true );
2019-01-11 15:38:06 +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"
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 400 );
2017-04-03 01:34:30 +00:00
}
} else {
$data = [];
}
2018-11-07 18:01:46 +00:00
// merge GET and POST data, and normalize it. POST parameters are preferred over GET parameters
$data = $this -> normalizeInput ( array_merge ( $req -> getQueryParams (), $data ), $this -> validInput , " unix " );
2017-05-20 03:52:26 +00:00
// check to make sure the requested function is implemented
2020-12-01 16:06:29 +00:00
$func = $this -> chooseCall ( $target , $req -> getMethod ());
if ( $func instanceof ResponseInterface ) {
return $func ;
}
2020-11-02 00:09:17 +00:00
// dispatch
2017-05-20 03:52:26 +00:00
try {
2020-11-02 00:09:17 +00:00
$path = explode ( " / " , ltrim ( $target , " / " ));
return $this -> $func ( $path , $data );
2017-09-28 13:01:43 +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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 500 );
2017-05-20 03:52:26 +00:00
}
2017-09-05 23:35:14 +00:00
// @codeCoverageIgnoreEnd
2017-05-20 03:52:26 +00:00
}
2018-01-05 04:08:53 +00:00
protected function normalizePathIds ( string $url ) : string {
2019-09-05 14:19:05 +00:00
$path = explode ( " / " , $url );
2018-01-05 04:08:53 +00:00
// any path 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)
2019-09-05 14:19:05 +00:00
for ( $a = 0 ; $a < sizeof ( $path ); $a ++ ) {
if ( ValueInfo :: id ( $path [ $a ])) {
$path [ $a ] = " 1 " ;
2017-07-21 02:40:09 +00:00
}
2017-05-20 03:52:26 +00:00
}
2019-09-05 14:19:05 +00:00
return implode ( " / " , $path );
2017-11-28 18:46:07 +00:00
}
2018-10-26 18:58:04 +00:00
2021-02-09 14:37:31 +00:00
protected function normalizeInput ( array $data , array $types , string $dateFormat = null , int $mode = 0 ) : array {
$out = [];
foreach ( $types as $key => $type ) {
if ( isset ( $data [ $key ])) {
$out [ $key ] = ValueInfo :: normalize ( $data [ $key ], $type | $mode , $dateFormat );
} else {
$out [ $key ] = null ;
}
}
return $out ;
}
2020-12-01 16:06:29 +00:00
protected function chooseCall ( string $url , string $method ) {
2018-01-05 04:08:53 +00:00
// // normalize the URL path: change any IDs to 1 for easier comparison
$url = $this -> normalizePathIds ( $url );
2017-05-20 03:52:26 +00:00
// normalize the HTTP method to uppercase
$method = strtoupper ( $method );
// we now evaluate the supplied URL against every supported path for the selected scope
2017-11-29 18:41:26 +00:00
if ( isset ( $this -> paths [ $url ])) {
// if the path is supported, make sure the method is allowed
if ( isset ( $this -> paths [ $url ][ $method ])) {
2020-11-02 00:09:17 +00:00
// if it is allowed, return the object method to run, assuming the method exists
2020-12-01 16:06:29 +00:00
assert ( method_exists ( $this , $this -> paths [ $url ][ $method ]), new \Exception ( " Method is not implemented " ));
return $this -> paths [ $url ][ $method ];
2017-11-29 18:41:26 +00:00
} else {
// otherwise return 405
2020-12-01 16:06:29 +00:00
return new EmptyResponse ( 405 , [ 'Allow' => implode ( " , " , array_keys ( $this -> paths [ $url ]))]);
2017-11-30 03:42:50 +00:00
}
2017-11-29 18:41:26 +00:00
} else {
2017-11-29 20:28:33 +00:00
// if the path is not supported, return 404
2020-12-01 16:06:29 +00:00
return new EmptyResponse ( 404 );
2017-04-03 01:34:30 +00:00
}
}
2017-07-07 12:13:03 +00:00
2017-11-30 19:47:39 +00:00
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 ;
}
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 " ,
2021-01-16 04:15:22 +00:00
'faviconLink' => " icon_url " ,
2017-07-07 12:13:03 +00:00
'folderId' => " top_folder " ,
'unreadCount' => " unread " ,
'ordering' => " order_type " ,
'updateErrorCount' => " err_count " ,
'lastUpdateError' => " err_msg " ,
]);
// cast values
$feed = $this -> fieldMapTypes ( $feed , [
2017-11-30 19:47:39 +00:00
'id' => " int " ,
'url' => " string " ,
'title' => " string " ,
'added' => " datetime " ,
'pinned' => " bool " ,
'link' => " string " ,
'faviconLink' => " string " ,
'folderId' => " int " ,
'unreadCount' => " int " ,
'ordering' => " int " ,
'updateErrorCount' => " int " ,
'lastUpdateError' => " string " ,
2017-07-07 19:25:47 +00:00
], $this -> dateFormat );
2017-07-07 12:13:03 +00:00
return $feed ;
}
2020-03-01 20:16:50 +00:00
protected function articleTranslate ( array $article ) : array {
2017-07-07 12:13:03 +00:00
// 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-11-30 19:47:39 +00:00
'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 " ,
2017-07-07 19:25:47 +00:00
], $this -> dateFormat );
2017-07-07 12:13:03 +00:00
return $article ;
}
2017-11-29 20:28:33 +00:00
2018-01-05 04:08:53 +00:00
protected function handleHTTPOptions ( string $url ) : ResponseInterface {
// normalize the URL path: change any IDs to 1 for easier comparison
$url = $this -> normalizePathIDs ( $url );
2017-11-29 20:28:33 +00:00
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 " );
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 , [
'Allow' => implode ( " , " , $allowed ),
2019-09-28 02:38:03 +00:00
'Accept' => self :: ACCEPTED_TYPE ,
2017-11-29 20:28:33 +00:00
]);
} else {
// if the path is not supported, return 404
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 404 );
2017-11-29 20:28:33 +00:00
}
}
2018-10-26 18:58:04 +00:00
2017-04-03 01:34:30 +00:00
// list folders
2018-01-04 04:13:08 +00:00
protected function folderList ( array $url , array $data ) : ResponseInterface {
2017-11-30 19:47:39 +00:00
$folders = [];
foreach ( Arsse :: $db -> folderList ( Arsse :: $user -> id , null , false ) as $folder ) {
$folders [] = $this -> folderTranslate ( $folder );
}
2018-01-04 04:13:08 +00:00
return new Response ([ 'folders' => $folders ]);
2017-04-03 01:34:30 +00:00
}
2017-04-01 19:42:10 +00:00
2017-04-03 01:34:30 +00:00
// create a folder
2018-01-04 04:13:08 +00:00
protected function folderAdd ( array $url , array $data ) : ResponseInterface {
2017-04-03 01:34:30 +00:00
try {
2017-10-20 00:35:45 +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
2018-01-04 04:13:08 +00:00
case 10236 : return new EmptyResponse ( 409 );
2017-04-03 01:34:30 +00:00
// folder name not acceptable
case 10231 :
2018-01-04 04:13:08 +00:00
case 10232 : return new EmptyResponse ( 422 );
2017-04-03 01:34:30 +00:00
// other errors related to input
2018-01-04 04:13:08 +00:00
default : return new EmptyResponse ( 400 ); // @codeCoverageIgnore
2017-04-03 01:34:30 +00:00
}
}
2017-11-30 19:47:39 +00:00
$folder = $this -> folderTranslate ( Arsse :: $db -> folderPropertiesGet ( Arsse :: $user -> id , $folder ));
2018-01-04 04:13:08 +00:00
return new Response ([ 'folders' => [ $folder ]]);
2017-04-03 01:34:30 +00:00
}
2017-04-02 03:06:52 +00:00
2017-04-03 01:34:30 +00:00
// delete a folder
2018-01-04 04:13:08 +00:00
protected function folderRemove ( array $url , array $data ) : ResponseInterface {
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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 404 );
2017-04-03 01:34:30 +00:00
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-04-03 01:34:30 +00:00
}
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)
2018-01-04 04:13:08 +00:00
protected function folderRename ( array $url , array $data ) : ResponseInterface {
2017-04-03 01:34:30 +00:00
try {
2017-10-20 00:35:45 +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
2018-01-04 04:13:08 +00:00
case 10239 : return new EmptyResponse ( 404 );
2017-04-03 01:34:30 +00:00
// folder already exists
2018-01-04 04:13:08 +00:00
case 10236 : return new EmptyResponse ( 409 );
2017-04-03 01:34:30 +00:00
// folder name not acceptable
case 10231 :
2018-01-04 04:13:08 +00:00
case 10232 : return new EmptyResponse ( 422 );
2017-04-03 01:34:30 +00:00
// other errors related to input
2018-01-04 04:13:08 +00:00
default : return new EmptyResponse ( 400 ); // @codeCoverageIgnore
2017-04-03 01:34:30 +00:00
}
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-04-03 01:34:30 +00:00
}
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
2018-01-04 04:13:08 +00:00
protected function folderMarkRead ( array $url , array $data ) : ResponseInterface {
2017-10-20 00:35:45 +00:00
if ( ! ValueInfo :: id ( $data [ 'newestItemId' ])) {
// if the item ID is invalid (i.e. not a positive integer), this is an error
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 422 );
2017-07-07 12:13:03 +00:00
}
2017-10-20 00:35:45 +00:00
// build the context
2020-12-20 22:42:28 +00:00
$c = ( new Context ) -> hidden ( false );
2017-10-20 00:35:45 +00:00
$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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 404 );
2017-07-07 12:13:03 +00:00
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-07-07 12:13:03 +00:00
}
2018-10-26 18:58:04 +00:00
2017-05-19 03:03:33 +00:00
// return list of feeds which should be refreshed
2018-01-04 04:13:08 +00:00
protected function feedListStale ( array $url , array $data ) : ResponseInterface {
2021-02-09 15:05:44 +00:00
if ( ! $this -> isAdmin ()) {
return new EmptyResponse ( 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
2017-12-31 22:24:40 +00:00
$out [] = [ 'id' => ( int ) $feed , 'userId' => " " ];
2017-05-19 03:03:33 +00:00
}
2018-01-04 04:13:08 +00:00
return new Response ([ 'feeds' => $out ]);
2017-05-20 03:52:26 +00:00
}
2018-10-26 18:58:04 +00:00
2017-05-20 03:52:26 +00:00
// refresh a feed
2018-01-04 04:13:08 +00:00
protected function feedUpdate ( array $url , array $data ) : ResponseInterface {
2021-02-09 15:05:44 +00:00
if ( ! $this -> isAdmin ()) {
return new EmptyResponse ( 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 02:25:45 +00:00
switch ( $e -> getCode ()) {
case 10239 : // feed does not exist
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 404 );
2017-09-28 13:01:43 +00:00
case 10237 : // feed ID invalid
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 422 );
2017-09-28 02:25:45 +00:00
default : // other errors related to input
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 400 ); // @codeCoverageIgnore
2017-09-28 02:25:45 +00:00
}
2017-05-20 03:52:26 +00:00
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-05-19 03:03:33 +00:00
}
// add a new feed
2018-01-04 04:13:08 +00:00
protected function subscriptionAdd ( array $url , array $data ) : ResponseInterface {
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 00:35:45 +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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 422 );
2017-05-19 03:03:33 +00:00
}
// if a folder was specified, move the feed to the correct folder; silently ignore errors
2017-10-20 00:35:45 +00:00
if ( $data [ 'folderId' ]) {
2017-05-19 03:03:33 +00:00
try {
2017-10-20 00:35:45 +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 ]];
2020-12-20 22:42:28 +00:00
$newest = Arsse :: $db -> editionLatest ( Arsse :: $user -> id , ( new Context ) -> subscription ( $id ) -> hidden ( false ));
2017-08-29 14:50:31 +00:00
if ( $newest ) {
2017-07-21 02:40:09 +00:00
$out [ 'newestItemId' ] = $newest ;
}
2018-01-04 04:13:08 +00:00
return new Response ( $out );
2017-05-19 03:03:33 +00:00
}
2018-10-26 18:58:04 +00:00
2017-07-07 12:13:03 +00:00
// return list of feeds for the logged-in user
2018-01-04 04:13:08 +00:00
protected function subscriptionList ( array $url , array $data ) : ResponseInterface {
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-12-31 22:24:40 +00:00
$out [ 'starredCount' ] = ( int ) 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 ;
}
2018-01-04 04:13:08 +00:00
return new Response ( $out );
2017-07-07 12:13:03 +00:00
}
2017-05-19 03:03:33 +00:00
// delete a feed
2018-01-04 04:13:08 +00:00
protected function subscriptionRemove ( array $url , array $data ) : ResponseInterface {
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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 404 );
2017-05-19 03:03:33 +00:00
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-05-19 03:03:33 +00:00
}
// rename a feed
2018-01-04 04:13:08 +00:00
protected function subscriptionRename ( array $url , array $data ) : ResponseInterface {
2017-05-20 03:52:26 +00:00
try {
2017-10-20 00:35:45 +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
2018-01-04 04:13:08 +00:00
case 10239 : return new EmptyResponse ( 404 );
2017-05-21 14:10:36 +00:00
// name is invalid
case 10231 :
2018-01-04 04:13:08 +00:00
case 10232 : return new EmptyResponse ( 422 );
2017-05-21 14:10:36 +00:00
// other errors related to input
2018-01-04 04:13:08 +00:00
default : return new EmptyResponse ( 400 ); // @codeCoverageIgnore
2017-05-21 14:10:36 +00:00
}
2017-05-19 03:03:33 +00:00
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-05-20 03:52:26 +00:00
}
// move a feed to a folder
2018-01-04 04:13:08 +00:00
protected function subscriptionMove ( array $url , array $data ) : ResponseInterface {
2017-10-20 00:35:45 +00:00
// if no folder is specified this is an error
if ( ! isset ( $data [ 'folderId' ])) {
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 422 );
2017-05-19 03:03:33 +00:00
}
2017-05-20 03:52:26 +00:00
// perform the move
try {
2017-10-20 00:35:45 +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 02:25:45 +00:00
case 10239 : // subscription does not exist
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 404 );
2017-09-28 02:25:45 +00:00
case 10235 : // folder does not exist
case 10237 : // folder ID is invalid
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 422 );
2017-09-28 02:25:45 +00:00
default : // other errors related to input
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 400 ); // @codeCoverageIgnore
2017-05-21 14:10:36 +00:00
}
2017-05-19 03:03:33 +00:00
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-05-19 03:03:33 +00:00
}
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
2018-01-04 04:13:08 +00:00
protected function subscriptionMarkRead ( array $url , array $data ) : ResponseInterface {
2017-10-20 00:35:45 +00:00
if ( ! ValueInfo :: id ( $data [ 'newestItemId' ])) {
// if the item ID is invalid (i.e. not a positive integer), this is an error
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 422 );
2017-07-07 12:13:03 +00:00
}
2017-10-20 00:35:45 +00:00
// build the context
2020-12-20 22:42:28 +00:00
$c = ( new Context ) -> hidden ( false );
2017-10-20 00:35:45 +00:00
$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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 404 );
2017-07-07 12:13:03 +00:00
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-07-07 12:13:03 +00:00
}
2017-07-09 21:57:18 +00:00
// list articles and their properties
2018-01-04 04:13:08 +00:00
protected function articleList ( array $url , array $data ) : ResponseInterface {
2017-07-07 12:13:03 +00:00
// set the context options supplied by the client
2020-12-20 22:42:28 +00:00
$c = ( new Context ) -> hidden ( false );
2017-07-07 12:13:03 +00:00
// set the batch size
2017-10-20 00:35:45 +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
2019-04-04 15:22:50 +00:00
$reverse = ! $data [ 'oldestFirst' ];
2017-07-07 12:13:03 +00:00
// 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 00:35:45 +00:00
if ( $data [ 'offset' ] > 0 ) {
2019-04-04 15:22:50 +00:00
if ( $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 00:35:45 +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 00:35:45 +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 00:35:45 +00:00
if ( $data [ 'lastModified' ]) {
2017-11-17 22:52:00 +00:00
$c -> markedSince ( $data [ 'lastModified' ]);
2017-07-21 02:40:09 +00:00
}
2017-07-07 12:13:03 +00:00
// perform the fetch
try {
2018-12-05 21:55:14 +00:00
$items = Arsse :: $db -> articleList ( Arsse :: $user -> id , $c , [
" edition " ,
" guid " ,
" id " ,
" url " ,
" title " ,
" author " ,
" edited_date " ,
" content " ,
" media_type " ,
" media_url " ,
" subscription " ,
" unread " ,
" starred " ,
" modified_date " ,
" fingerprint " ,
2019-04-04 15:22:50 +00:00
], [ $reverse ? " edition desc " : " edition " ]);
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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 422 );
2017-07-07 12:13:03 +00:00
}
$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 ];
2018-01-04 04:13:08 +00:00
return new Response ( $out );
2017-07-07 12:13:03 +00:00
}
2017-07-09 21:57:18 +00:00
// mark all articles as read
2018-01-04 04:13:08 +00:00
protected function articleMarkReadAll ( array $url , array $data ) : ResponseInterface {
2017-10-20 00:35:45 +00:00
if ( ! ValueInfo :: id ( $data [ 'newestItemId' ])) {
// if the item ID is invalid (i.e. not a positive integer), this is an error
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 422 );
2017-07-07 12:13:03 +00:00
}
2017-10-20 00:35:45 +00:00
// build the context
2020-12-20 22:42:28 +00:00
$c = ( new Context ) -> hidden ( false );
2017-10-20 00:35:45 +00:00
$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 );
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-07-07 12:13:03 +00:00
}
2017-07-09 21:57:18 +00:00
// mark a single article as read
2018-01-04 04:13:08 +00:00
protected function articleMarkRead ( array $url , array $data ) : ResponseInterface {
2017-07-07 12:13:03 +00:00
// initialize the matching context
$c = new Context ;
$c -> edition (( int ) $url [ 1 ]);
// determine whether to mark read or unread
2019-01-11 15:38:06 +00:00
$set = ( $url [ 2 ] === " read " );
2017-07-07 12:13:03 +00:00
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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 404 );
2017-07-07 12:13:03 +00:00
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-07-07 12:13:03 +00:00
}
2017-07-09 21:57:18 +00:00
// mark a single article as read
2018-01-04 04:13:08 +00:00
protected function articleMarkStarred ( array $url , array $data ) : ResponseInterface {
2017-07-07 12:13:03 +00:00
// initialize the matching context
$c = new Context ;
$c -> article (( int ) $url [ 2 ]);
// determine whether to mark read or unread
2020-03-01 20:16:50 +00:00
$set = ( $url [ 3 ] === " star " );
2017-07-07 12:13:03 +00:00
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
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 404 );
2017-07-07 12:13:03 +00:00
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-07-07 12:13:03 +00:00
}
2017-07-09 21:57:18 +00:00
// mark an array of articles as read
2018-01-04 04:13:08 +00:00
protected function articleMarkReadMulti ( array $url , array $data ) : ResponseInterface {
2017-07-07 12:13:03 +00:00
// determine whether to mark read or unread
2020-03-01 20:16:50 +00:00
$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
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-07-07 12:13:03 +00:00
}
2017-07-09 21:57:18 +00:00
// mark an array of articles as starred
2018-01-04 04:13:08 +00:00
protected function articleMarkStarredMulti ( array $url , array $data ) : ResponseInterface {
2017-07-09 21:57:18 +00:00
// determine whether to mark starred or unstarred
2020-03-01 20:16:50 +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
}
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-07-07 12:13:03 +00:00
}
2017-07-15 20:44:06 +00:00
2018-01-04 04:13:08 +00:00
protected function userStatus ( array $url , array $data ) : ResponseInterface {
2018-10-28 17:59:09 +00:00
return new Response ([
2020-03-01 20:16:50 +00:00
'userId' => ( string ) Arsse :: $user -> id ,
'displayName' => ( string ) Arsse :: $user -> id ,
2017-07-15 20:44:06 +00:00
'lastLoginTimestamp' => time (),
2020-03-01 20:16:50 +00:00
'avatar' => null ,
2018-10-28 17:59:09 +00:00
]);
2017-07-15 20:44:06 +00:00
}
2018-01-04 04:13:08 +00:00
protected function cleanupBefore ( array $url , array $data ) : ResponseInterface {
2021-02-09 15:05:44 +00:00
if ( ! $this -> isAdmin ()) {
return new EmptyResponse ( 403 );
}
2017-08-02 22:27:04 +00:00
Service :: cleanupPre ();
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-07-15 20:44:06 +00:00
}
2018-01-04 04:13:08 +00:00
protected function cleanupAfter ( array $url , array $data ) : ResponseInterface {
2021-02-09 15:05:44 +00:00
if ( ! $this -> isAdmin ()) {
return new EmptyResponse ( 403 );
}
2017-08-18 02:36:15 +00:00
Service :: cleanupPost ();
2018-01-04 04:13:08 +00:00
return new EmptyResponse ( 204 );
2017-07-15 20:44:06 +00:00
}
// return the server version
2018-01-04 04:13:08 +00:00
protected function serverVersion ( array $url , array $data ) : ResponseInterface {
return new Response ([
2020-03-01 20:16:50 +00:00
'version' => self :: VERSION ,
2017-10-01 13:33:49 +00:00
'arsse_version' => Arsse :: VERSION ,
2017-07-15 20:44:06 +00:00
]);
}
2018-01-04 04:13:08 +00:00
protected function serverStatus ( array $url , array $data ) : ResponseInterface {
return new Response ([
2020-03-01 20:16:50 +00:00
'version' => self :: VERSION ,
2017-10-01 13:33:49 +00:00
'arsse_version' => Arsse :: VERSION ,
2020-03-01 20:16:50 +00:00
'warnings' => [
2017-08-02 22:27:04 +00:00
'improperlyConfiguredCron' => ! Service :: hasCheckedIn (),
2020-03-01 20:16:50 +00:00
'incorrectDbCharset' => ! Arsse :: $db -> driverCharsetAcceptable (),
],
2017-07-15 20:44:06 +00:00
]);
}
2017-08-29 14:50:31 +00:00
}