2017-04-01 19:42:10 +00:00
< ? php
declare ( strict_types = 1 );
namespace JKingWeb\Arsse\REST\NextCloudNews ;
2017-04-02 03:06:52 +00:00
use JKingWeb\Arsse\Data ;
2017-05-19 03:03:33 +00:00
use JKingWeb\Arsse\User ;
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-04-03 01:34:30 +00:00
function __construct () {
}
function dispatch ( \JKingWeb\Arsse\REST\Request $req ) : Response {
// try to authenticate
if ( ! Data :: $user -> authHTTP ()) return new Response ( 401 , " " , " " , [ 'WWW-Authenticate: Basic realm="NextCloud News API v1-2"' ]);
// only accept GET, POST, PUT, or DELETE
if ( ! in_array ( $req -> method , [ " GET " , " POST " , " PUT " , " DELETE " ])) return new Response ( 405 , " " , " " , [ 'Allow: GET, POST, PUT, DELETE' ]);
// normalize the input
if ( $req -> body ) {
// if the entity body is not JSON according to content type, return "415 Unsupported Media Type"
if ( ! preg_match ( " <^application/json \ b|^ $ > " , $req -> type )) return new Response ( 415 , " " , " " , [ 'Accept: application/json' ]);
try {
$data = json_decode ( $req -> body , true );
} catch ( \Throwable $e ) {
// 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-04-07 01:41:21 +00:00
$data = array_merge ( $data , $req -> 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 ()]);
}
if ( ! method_exists ( $this , $func )) return new Response ( 501 );
// dispatch
try {
Data :: $db -> dateFormatDefault ( " unix " );
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 " ],
],
'cleanup' => [],
'version' => [
'' => [ 'GET' => " versionReport " ],
],
'status' => [],
'user' => [],
];
// the first path element is the overall scope of the request
$scope = $url [ 0 ];
2017-05-20 12:57:24 +00:00
// any URL components which are only digits should be replaced with "#", 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-05-20 12:57:24 +00:00
if ( $this -> validateId ( $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
if ( ! array_key_exists ( $scope , $choices )) throw new Exception501 ();
// 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-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-04-03 01:34:30 +00:00
$folders = Data :: $db -> folderList ( Data :: $user -> id , null , false ) -> getAll ();
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 {
$folder = Data :: $db -> folderAdd ( Data :: $user -> id , $data );
} 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
default : return new Response ( 400 );
}
}
$folder = Data :: $db -> folderPropertiesGet ( Data :: $user -> id , $folder );
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-05-20 03:52:26 +00:00
Data :: $db -> folderRemove ( Data :: $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
if ( ! sizeof ( $data )) return new Response ( 422 );
// perform the edit
try {
2017-05-20 03:52:26 +00:00
Data :: $db -> folderPropertiesSet ( Data :: $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
default : return new Response ( 400 );
}
}
return new Response ( 204 );
}
2017-04-03 01:49:37 +00:00
2017-04-06 15:02:47 +00:00
// return the server version
2017-05-20 03:52:26 +00:00
protected function versionReport ( array $url , array $data ) : Response {
2017-04-03 01:49:37 +00:00
return new Response ( 200 , [ 'version' => \JKingWeb\Arsse\VERSION ]);
}
2017-05-19 03:03:33 +00:00
protected function feedTranslate ( array $feed , bool $overwrite = false ) : array {
// cast values
$feed = $this -> mapFieldTypes ( $feed , [
2017-06-01 22:12:08 +00:00
'top_folder' => " int " ,
'pinned' => " bool " ,
2017-05-19 03:03:33 +00:00
]);
// map fields to proper names
$feed = $this -> mapFieldNames ( $feed , [
'source' => " link " ,
'favicon' => " faviconLink " ,
2017-06-01 22:12:08 +00:00
'top_folder' => " folderId " ,
2017-05-19 03:03:33 +00:00
'unread' => " unreadCount " ,
'order_type' => " ordering " ,
'err_count' => " updateErrorCount " ,
'err_msg' => " lastUpdateError " ,
], $overwrite );
2017-06-01 22:12:08 +00:00
// remove the true folder since the protocol does not support nesting
unset ( $feed [ 'folder' ]);
2017-05-19 03:03:33 +00:00
return $feed ;
}
// return list of feeds for the logged-in user
2017-05-20 03:52:26 +00:00
protected function subscriptionList ( array $url , array $data ) : Response {
$subs = Data :: $db -> subscriptionList ( Data :: $user -> id );
$out = [];
foreach ( $subs as $sub ) {
$sub = $this -> feedTranslate ( $sub );
$out [] = $sub ;
}
$out = [ 'feeds' => $out ];
$out [ 'starredCount' ] = Data :: $db -> articleStarredCount ( Data :: $user -> id );
2017-05-21 14:10:36 +00:00
$newest = Data :: $db -> editionLatest ( Data :: $user -> id );
2017-05-20 03:52:26 +00:00
if ( $newest ) $out [ 'newestItemId' ] = $newest ;
return new Response ( 200 , $out );
}
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
if ( Data :: $user -> rightsGet ( Data :: $user -> id ) == User :: RIGHTS_NONE ) return new Response ( 403 );
// list stale feeds which should be checked for updates
$feeds = Data :: $db -> feedListStale ();
$out = [];
foreach ( $feeds as $feed ) {
// since in our implementation feeds don't belong the users, the 'userId' field will always be an empty string
$out [] = [ 'id' => $feed , 'userId' => " " ];
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 {
// perform an update of a single feed
if ( ! array_key_exists ( " feedId " , $data )) return new Response ( 422 );
if ( ! $this -> validateId ( $data [ 'feedId' ])) return new Response ( 404 );
try {
Data :: $db -> feedUpdate (( int ) $data [ 'feedId' ]);
} catch ( ExceptionInput $e ) {
return new Response ( 404 );
}
return new Response ( 200 );
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-05-19 03:03:33 +00:00
if ( ! array_key_exists ( " url " , $data )) {
$url = " " ;
} else {
$url = $data [ 'url' ];
}
// normalize the folder ID, if specified
if ( ! array_key_exists ( " folderId " , $data )) {
$folder = null ;
} else {
$folder = $data [ 'folderId' ];
2017-05-20 03:52:26 +00:00
$folder = $folder ? $folder : null ;
2017-05-19 03:03:33 +00:00
}
// try to add the feed
$tr = Data :: $db -> begin ();
try {
$id = Data :: $db -> subscriptionAdd ( Data :: $user -> id , $url );
} 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 {
Data :: $db -> subscriptionPropertiesSet ( Data :: $user -> id , $id , [ 'folder' => $folder ]);
} catch ( ExceptionInput $e ) {}
}
$tr -> commit ();
// fetch the feed's metadata and format it appropriately
$feed = Data :: $db -> subscriptionPropertiesGet ( Data :: $user -> id , $id );
$feed = $this -> feedTranslate ( $feed );
$out = [ 'feeds' => [ $feed ]];
$newest = Data :: $db -> editionLatest ( Data :: $user -> id , [ 'subscription' => $id ]);
if ( $newest ) $out [ 'newestItemId' ] = $newest ;
return new Response ( 200 , $out );
}
// 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-05-20 03:52:26 +00:00
Data :: $db -> subscriptionRemove ( Data :: $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 = [];
if ( array_key_exists ( " feedTitle " , $data )) {
$in [ 'title' ] = $data [ 'feedTitle' ];
2017-05-20 03:52:26 +00:00
} else {
return new Response ( 422 );
}
// perform the renaming
try {
Data :: $db -> subscriptionPropertiesSet ( Data :: $user -> id , ( int ) $url [ 1 ], $in );
} 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
default : return new Response ( 400 );
}
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 {
// normalize input for move and rename
$in = [];
2017-05-19 03:03:33 +00:00
if ( array_key_exists ( " folderId " , $data )) {
$folder = $data [ 'folderId' ];
if ( ! $this -> validateId ( $folder )) return new Response ( 422 );
if ( ! $folder ) $folder = null ;
$in [ 'folder' ] = $folder ;
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 {
Data :: $db -> subscriptionPropertiesSet ( Data :: $user -> id , ( int ) $url [ 1 ], $in );
} 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
default : return new Response ( 400 );
}
2017-05-19 03:03:33 +00:00
}
return new Response ( 204 );
}
2017-04-01 19:42:10 +00:00
}