2017-09-25 03:32:21 +00:00
< ? php
declare ( strict_types = 1 );
namespace JKingWeb\Arsse\REST\TinyTinyRSS ;
2017-10-03 14:43:09 +00:00
use JKingWeb\Arsse\Feed ;
2017-09-25 03:32:21 +00:00
use JKingWeb\Arsse\Arsse ;
use JKingWeb\Arsse\User ;
use JKingWeb\Arsse\Service ;
2017-10-07 16:46:05 +00:00
use JKingWeb\Arsse\Misc\Date ;
2017-09-25 03:32:21 +00:00
use JKingWeb\Arsse\Misc\Context ;
2017-10-01 02:15:55 +00:00
use JKingWeb\Arsse\Misc\ValueInfo ;
2017-09-25 03:32:21 +00:00
use JKingWeb\Arsse\AbstractException ;
use JKingWeb\Arsse\Db\ExceptionInput ;
use JKingWeb\Arsse\Feed\Exception as FeedException ;
use JKingWeb\Arsse\REST\Response ;
2017-10-01 02:15:55 +00:00
/*
Protocol difference so far :
2017-10-11 16:55:50 +00:00
- Handling of incorrect Content - Type and / or HTTP method is different
2017-10-01 02:15:55 +00:00
- TT - RSS accepts whitespace - only names ; we do not
- TT - RSS allows two folders to share the same name under the same parent ; we do not
- Session lifetime is much shorter by default ( does TT - RSS even expire sessions ? )
2017-10-05 21:42:12 +00:00
- Categories and feeds will always be sorted alphabetically ( the protocol does not allow for clients to re - order )
2017-10-11 16:55:50 +00:00
- The " Archived " virtual feed is non - functional ( the protocol does not allow archiving )
- The " Published " virtual feed is non - functional ( this will not be implemented in the near term )
2017-10-01 02:15:55 +00:00
*/
2017-09-25 03:32:21 +00:00
class API extends \JKingWeb\Arsse\REST\AbstractHandler {
const LEVEL = 14 ;
const VERSION = " 17.4 " ;
2017-10-11 16:55:50 +00:00
const LABEL_OFFSET = 1024 ;
2017-10-15 16:47:07 +00:00
const VALID_INPUT = [
'op' => " str " ,
'sid' => " str " ,
'user' => " str " ,
'password' => " str " ,
'include_empty' => " bool " ,
'unread_only' => " bool " ,
'enable_nested' => " bool " ,
'caption' => " str " ,
'parent_id' => " int " ,
'category_id' => " int " ,
'feed_url' => " str " ,
'login' => " str " ,
'feed_id' => " int " ,
'article_id' => " int " ,
'label_id' => " int " ,
'article_ids' => " str " ,
'assign' => " bool " ,
'is_cat' => " bool " ,
'cat_id' => " int " ,
'limit' => " int " ,
'offset' => " int " ,
'include_nested' => " bool " ,
'skip' => " int " ,
'filter' => " str " ,
'show_excerpt' => " bool " ,
'show_content' => " bool " ,
'view_mode' => " str " ,
'include_attachments' => " bool " ,
'since_id' => " int " ,
'order_by' => " str " ,
'sanitize' => " bool " ,
'force_update' => " bool " ,
'has_sandbox' => " bool " ,
'include_header' => " bool " ,
'search' => " str " ,
'search_mode' => " str " ,
'match_on' => " str " ,
'mode' => " int " ,
'field' => " int " ,
'data' => " str " ,
];
2017-09-25 12:15:39 +00:00
const FATAL_ERR = [
2017-10-15 16:47:07 +00:00
'seq' => null ,
'status' => 1 ,
2017-09-25 12:15:39 +00:00
'content' => [ 'error' => " NOT_LOGGED_IN " ],
];
2017-09-25 03:32:21 +00:00
public function __construct () {
}
public function dispatch ( \JKingWeb\Arsse\REST\Request $req ) : Response {
if ( $req -> method != " POST " ) {
// only POST requests are allowed
2017-09-25 12:15:39 +00:00
return new Response ( 405 , self :: FATAL_ERR , " application/json " , [ " Allow: POST " ]);
2017-09-25 03:32:21 +00:00
}
if ( $req -> body ) {
// only JSON entities are allowed
if ( ! preg_match ( " <^application/json \ b|^ $ > " , $req -> type )) {
2017-09-25 12:15:39 +00:00
return new Response ( 415 , self :: FATAL_ERR , " application/json " , [ 'Accept: application/json' ]);
2017-09-25 03:32:21 +00:00
}
$data = @ json_decode ( $req -> body , true );
if ( json_last_error () != \JSON_ERROR_NONE || ! is_array ( $data )) {
// non-JSON input indicates an error
2017-09-25 12:15:39 +00:00
return new Response ( 400 , self :: FATAL_ERR );
2017-09-25 03:32:21 +00:00
}
// layer input over defaults
$data = array_merge ([
'seq' => 0 ,
'op' => " " ,
'sid' => null ,
], $data );
try {
2017-10-11 16:55:50 +00:00
if ( strtolower (( string ) $data [ 'op' ]) != " login " ) {
// unless logging in, a session identifier is required
2017-09-25 03:32:21 +00:00
$this -> resumeSession ( $data [ 'sid' ]);
}
$method = " op " . ucfirst ( $data [ 'op' ]);
if ( ! method_exists ( $this , $method )) {
// because method names are supposed to be case insensitive, we need to try a bit harder to match
$method = strtolower ( $method );
$map = get_class_methods ( $this );
$map = array_combine ( array_map ( " strtolower " , $map ), $map );
2017-09-28 14:16:24 +00:00
if ( ! array_key_exists ( $method , $map )) {
2017-09-25 03:32:21 +00:00
// if the method really doesn't exist, throw an exception
throw new Exception ( " UNKNWON_METHOD " , [ 'method' => $data [ 'op' ]]);
}
// otherwise retrieve the correct camelCase and continue
$method = $map [ $method ];
}
return new Response ( 200 , [
'seq' => $data [ 'seq' ],
'status' => 0 ,
'content' => $this -> $method ( $data ),
]);
} catch ( Exception $e ) {
return new Response ( 200 , [
'seq' => $data [ 'seq' ],
'status' => 1 ,
'content' => $e -> getData (),
]);
} catch ( AbstractException $e ) {
return new Response ( 500 );
}
} else {
// absence of a request body indicates an error
2017-09-25 12:15:39 +00:00
return new Response ( 400 , self :: FATAL_ERR );
2017-09-25 03:32:21 +00:00
}
}
protected function resumeSession ( $id ) : bool {
try {
// verify the supplied session is valid
$s = Arsse :: $db -> sessionResume (( string ) $id );
} catch ( \JKingWeb\Arsse\User\ExceptionSession $e ) {
// if not throw an exception
throw new Exception ( " NOT_LOGGED_IN " );
}
// resume the session (currently only the user name)
Arsse :: $user -> id = $s [ 'user' ];
return true ;
}
2017-09-25 14:08:37 +00:00
public function opGetApiLevel ( array $data ) : array {
2017-09-25 03:32:21 +00:00
return [ 'level' => self :: LEVEL ];
}
public function opGetVersion ( array $data ) : array {
return [
'version' => self :: VERSION ,
2017-10-02 19:42:15 +00:00
'arsse_version' => Arsse :: VERSION ,
2017-09-25 03:32:21 +00:00
];
}
2017-09-25 14:08:37 +00:00
public function opLogin ( array $data ) : array {
2017-09-25 03:32:21 +00:00
if ( isset ( $data [ 'user' ]) && isset ( $data [ 'password' ]) && Arsse :: $user -> auth ( $data [ 'user' ], $data [ 'password' ])) {
$id = Arsse :: $db -> sessionCreate ( $data [ 'user' ]);
return [
2017-09-28 14:16:24 +00:00
'session_id' => $id ,
2017-09-25 03:32:21 +00:00
'api_level' => self :: LEVEL
];
} else {
throw new Exception ( " LOGIN_ERROR " );
}
}
public function opLogout ( array $data ) : array {
Arsse :: $db -> sessionDestroy ( Arsse :: $user -> id , $data [ 'sid' ]);
return [ 'status' => " OK " ];
}
public function opIsLoggedIn ( array $data ) : array {
// session validity is already checked by the dispatcher, so we need only return true
return [ 'status' => true ];
}
2017-09-27 02:45:54 +00:00
2017-10-11 16:55:50 +00:00
public function opGetConfig ( array $data ) : array {
return [
'icons_dir' => " feed-icons " ,
'icons_url' => " feed-icons " ,
'daemon_is_running' => Service :: hasCheckedIn (),
'num_feeds' => Arsse :: $db -> subscriptionCount ( Arsse :: $user -> id ),
];
}
public function opGetUnread ( array $data ) : array {
// simply sum the unread count of each subscription
$out = 0 ;
foreach ( Arsse :: $db -> subscriptionList ( Arsse :: $user -> id ) as $sub ) {
$out += $sub [ 'unread' ];
}
return [ 'unread' => $out ];
}
public function opGetCounters ( array $data ) : array {
$user = Arsse :: $user -> id ;
$starred = Arsse :: $db -> articleStarred ( $user );
$fresh = Arsse :: $db -> articleCount ( $user , ( new Context ) -> unread ( true ) -> modifiedSince ( Date :: sub ( " PT24H " )));
$countAll = 0 ;
$countSubs = 0 ;
$feeds = [];
$labels = [];
// do a first pass on categories: add the ID to a lookup table and set the unread counter to zero
$categories = Arsse :: $db -> folderList ( $user ) -> getAll ();
$catmap = [];
for ( $a = 0 ; $a < sizeof ( $categories ); $a ++ ) {
$catmap [( int ) $categories [ $a ][ 'id' ]] = $a ;
$categories [ $a ][ 'counter' ] = 0 ;
}
// add the "Uncategorized" and "Labels" virtual categories to the list
$catmap [ 0 ] = sizeof ( $categories );
$categories [] = [ 'id' => 0 , 'name' => Arsse :: $lang -> msg ( " API.TTRSS.Category.Uncategorized " ), 'parent' => 0 , 'children' => 0 , 'counter' => 0 ];
$catmap [ - 2 ] = sizeof ( $categories );
$categories [] = [ 'id' => - 2 , 'name' => Arsse :: $lang -> msg ( " API.TTRSS.Category.Labels " ), 'parent' => 0 , 'children' => 0 , 'counter' => 0 ];
// prepare data for each subscription; we also add unread counts for their host categories
foreach ( Arsse :: $db -> subscriptionList ( $user ) as $f ) {
if ( $f [ 'unread' ]) {
// add the feed to the list of feeds
$feeds [] = [ 'id' => $f [ 'id' ], 'updated' => Date :: transform ( $f [ 'updated' ], " iso8601 " , " sql " ), 'counter' => $f [ 'unread' ], 'has_img' => ( int ) ( strlen (( string ) $f [ 'favicon' ]) > 0 )];
// add the feed's unread count to the global unread count
$countAll += $f [ 'unread' ];
// add the feed's unread count to its category unread count
$categories [ $catmap [( int ) $f [ 'folder' ]]][ 'counter' ] += $f [ 'unread' ];
}
// increment the global feed count
$countSubs += 1 ;
}
// prepare data for each non-empty label
foreach ( Arsse :: $db -> labelList ( $user , false ) as $l ) {
$unread = $l [ 'articles' ] - $l [ 'read' ];
$labels [] = [ 'id' => $this -> labelOut ( $l [ 'id' ]), 'counter' => $unread , 'auxcounter' => $l [ 'articles' ]];
$categories [ $catmap [ - 2 ]][ 'counter' ] += $unread ;
}
// do a second pass on categories, summing descendant unread counts for ancestors, pruning categories with no unread, and building a final category list
$cats = [];
while ( $categories ) {
foreach ( $categories as $c ) {
if ( $c [ 'children' ]) {
// only act on leaf nodes
continue ;
}
if ( $c [ 'parent' ]) {
// if the category has a parent, add its counter to the parent's counter, and decrement the parent's child count
$categories [ $catmap [ $c [ 'parent' ]]][ 'counter' ] += $c [ 'counter' ];
$categories [ $catmap [ $c [ 'parent' ]]][ 'children' ] -= 1 ;
}
if ( $c [ 'counter' ]) {
// if the category's counter is non-zero, add the category to the output list
$cats [] = [ 'id' => $c [ 'id' ], 'kind' => " cat " , 'counter' => $c [ 'counter' ]];
}
// remove the category from the input list
unset ( $categories [ $catmap [ $c [ 'id' ]]]);
}
}
// prepare data for the virtual feeds and other counters
$special = [
[ 'id' => " global-unread " , 'counter' => $countAll ], //this should not count archived articles, but we do not have an archive
[ 'id' => " subscribed-feeds " , 'counter' => $countSubs ],
[ 'id' => 0 , 'counter' => 0 , 'auxcounter' => 0 ], // Archived articles
[ 'id' => - 1 , 'counter' => $starred [ 'unread' ], 'auxcounter' => $starred [ 'total' ]], // Starred articles
[ 'id' => - 2 , 'counter' => 0 , 'auxcounter' => 0 ], // Published articles
[ 'id' => - 3 , 'counter' => $fresh , 'auxcounter' => 0 ], // Fresh articles
[ 'id' => - 4 , 'counter' => $countAll , 'auxcounter' => 0 ], // All articles
];
return array_merge ( $special , $labels , $feeds , $cats );
}
2017-10-07 16:46:05 +00:00
public function opGetCategories ( array $data ) : array {
// normalize input
$all = isset ( $data [ 'include_empty' ]) ? ValueInfo :: bool ( $data [ 'include_empty' ], false ) : false ;
$read = ! ( isset ( $data [ 'unread_only' ]) ? ValueInfo :: bool ( $data [ 'unread_only' ], false ) : false );
$deep = ! ( isset ( $data [ 'enable_nested' ]) ? ValueInfo :: bool ( $data [ 'enable_nested' ], false ) : false );
$user = Arsse :: $user -> id ;
2017-10-11 16:55:50 +00:00
// for each category, add the ID to a lookup table, set the number of unread to zero, and assign an increasing order index
2017-10-07 16:46:05 +00:00
$cats = Arsse :: $db -> folderList ( $user , null , $deep ) -> getAll ();
$map = [];
for ( $a = 0 ; $a < sizeof ( $cats ); $a ++ ) {
$map [ $cats [ $a ][ 'id' ]] = $a ;
$cats [ $a ][ 'unread' ] = 0 ;
$cats [ $a ][ 'order' ] = $a + 1 ;
}
// add the "Uncategorized", "Special", and "Labels" virtual categories to the list
$map [ 0 ] = sizeof ( $cats );
$cats [] = [ 'id' => 0 , 'name' => Arsse :: $lang -> msg ( " API.TTRSS.Category.Uncategorized " ), 'children' => 0 , 'unread' => 0 , 'feeds' => 0 ];
$map [ - 1 ] = sizeof ( $cats );
$cats [] = [ 'id' => - 1 , 'name' => Arsse :: $lang -> msg ( " API.TTRSS.Category.Special " ), 'children' => 0 , 'unread' => 0 , 'feeds' => 6 ];
$map [ - 2 ] = sizeof ( $cats );
$cats [] = [ 'id' => - 2 , 'name' => Arsse :: $lang -> msg ( " API.TTRSS.Category.Labels " ), 'children' => 0 , 'unread' => 0 , 'feeds' => 0 ];
// for each subscription, add the unread count to its category, and increment the category's feed count
$subs = Arsse :: $db -> subscriptionList ( $user );
foreach ( $subs as $sub ) {
// note we use top_folder if we're in "nested" mode
$f = $map [( int ) ( $deep ? $sub [ 'folder' ] : $sub [ 'top_folder' ])];
$cats [ $f ][ 'unread' ] += $sub [ 'unread' ];
2017-10-11 16:55:50 +00:00
if ( ! $cats [ $f ][ 'id' ]) {
$cats [ $f ][ 'feeds' ] += 1 ;
}
2017-10-07 16:46:05 +00:00
}
// for each label, add the unread count to the labels category, and increment the labels category's feed count
$labels = Arsse :: $db -> labelList ( $user );
$f = $map [ - 2 ];
foreach ( $labels as $label ) {
$cats [ $f ][ 'unread' ] += $label [ 'articles' ] - $label [ 'read' ];
$cats [ $f ][ 'feeds' ] += 1 ;
}
// get the unread counts for the special feeds
// FIXME: this is pretty inefficient
$f = $map [ - 1 ];
2017-10-11 16:55:50 +00:00
$cats [ $f ][ 'unread' ] += Arsse :: $db -> articleStarred ( $user )[ 'unread' ]; // starred
2017-10-07 16:46:05 +00:00
$cats [ $f ][ 'unread' ] += Arsse :: $db -> articleCount ( $user , ( new Context ) -> unread ( true ) -> modifiedSince ( Date :: sub ( " PT24H " ))); // fresh
if ( ! $read ) {
// if we're only including unread entries, remove any categories with zero unread items (this will by definition also exclude empties)
$count = sizeof ( $cats );
for ( $a = 0 ; $a < $count ; $a ++ ) {
if ( ! $cats [ $a ][ 'unread' ]) {
unset ( $cats [ $a ]);
}
}
$cats = array_values ( $cats );
} elseif ( ! $all ) {
// otherwise if we're not including empty entries, remove categories with no children and no feeds
$count = sizeof ( $cats );
for ( $a = 0 ; $a < $count ; $a ++ ) {
if (( $cats [ $a ][ 'children' ] + $cats [ $a ][ 'feeds' ]) < 1 ) {
unset ( $cats [ $a ]);
}
}
$cats = array_values ( $cats );
}
// transform the result and return
$out = [];
for ( $a = 0 ; $a < sizeof ( $cats ); $a ++ ) {
$out [] = $this -> fieldMapNames ( $cats [ $a ], [
'id' => " id " ,
'title' => " name " ,
'unread' => " unread " ,
'order_id' => " order " ,
]);
}
return $out ;
}
2017-09-27 02:45:54 +00:00
public function opAddCategory ( array $data ) {
$in = [
'name' => isset ( $data [ 'caption' ]) ? $data [ 'caption' ] : " " ,
'parent' => isset ( $data [ 'parent_id' ]) ? $data [ 'parent_id' ] : null ,
];
if ( ! $in [ 'parent' ]) {
$in [ 'parent' ] = null ;
}
try {
return Arsse :: $db -> folderAdd ( Arsse :: $user -> id , $in );
} catch ( ExceptionInput $e ) {
switch ( $e -> getCode ()) {
2017-10-01 02:15:55 +00:00
case 10236 : // folder already exists
// retrieve the ID of the existing folder; duplicating a folder silently returns the existing one
2017-09-27 02:45:54 +00:00
$folders = Arsse :: $db -> folderList ( Arsse :: $user -> id , $in [ 'parent' ], false );
foreach ( $folders as $folder ) {
if ( $folder [ 'name' ] == $in [ 'name' ]) {
return ( int ) $folder [ 'id' ];
}
}
2017-10-03 14:43:09 +00:00
return false ; // @codeCoverageIgnore
2017-10-01 02:15:55 +00:00
case 10235 : // parent folder does not exist; this returns false as an ID
return false ;
default : // other errors related to input
throw new Exception ( " INCORRECT_USAGE " );
2017-09-27 02:45:54 +00:00
}
}
}
2017-10-01 02:15:55 +00:00
public function opRemoveCategory ( array $data ) {
if ( ! isset ( $data [ 'category_id' ]) || ! ValueInfo :: id ( $data [ 'category_id' ])) {
// if the folder is invalid, throw an error
throw new Exception ( " INCORRECT_USAGE " );
}
try {
// attempt to remove the folder
Arsse :: $db -> folderRemove ( Arsse :: $user -> id , ( int ) $data [ 'category_id' ]);
} catch ( ExceptionInput $e ) {
// ignore all errors
}
return null ;
}
public function opMoveCategory ( array $data ) {
if ( ! isset ( $data [ 'category_id' ]) || ! ValueInfo :: id ( $data [ 'category_id' ]) || ! isset ( $data [ 'parent_id' ]) || ! ValueInfo :: id ( $data [ 'parent_id' ], true )) {
// if the folder or parent is invalid, throw an error
throw new Exception ( " INCORRECT_USAGE " );
}
$in = [
'parent' => ( int ) $data [ 'parent_id' ],
];
try {
// try to move the folder
Arsse :: $db -> folderPropertiesSet ( Arsse :: $user -> id , ( int ) $data [ 'category_id' ], $in );
} catch ( ExceptionInput $e ) {
// ignore all errors
}
return null ;
}
public function opRenameCategory ( array $data ) {
if ( ! isset ( $data [ 'category_id' ]) || ! ValueInfo :: id ( $data [ 'category_id' ]) || ! isset ( $data [ 'caption' ])) {
// if the folder is invalid, throw an error
throw new Exception ( " INCORRECT_USAGE " );
}
$info = ValueInfo :: str ( $data [ 'caption' ]);
if ( ! ( $info & ValueInfo :: VALID ) || ( $info & ValueInfo :: EMPTY ) || ( $info & ValueInfo :: WHITE )) {
// if the folder name is invalid, throw an error
throw new Exception ( " INCORRECT_USAGE " );
}
$in = [
'name' => ( string ) $data [ 'caption' ],
];
try {
// try to rename the folder
Arsse :: $db -> folderPropertiesSet ( Arsse :: $user -> id , ( int ) $data [ 'category_id' ], $in );
} catch ( ExceptionInput $e ) {
// ignore all errors
}
return null ;
}
2017-10-03 14:43:09 +00:00
protected function feedError ( FeedException $e ) : array {
// N.B.: we don't return code 4 (multiple feeds discovered); we simply pick the first feed discovered
switch ( $e -> getCode ()) {
case 10502 : // invalid URL
return [ 'code' => 2 , 'message' => $e -> getMessage ()];
case 10521 : // no feeds discovered
return [ 'code' => 3 , 'message' => $e -> getMessage ()];
case 10511 :
case 10512 :
case 10522 : // malformed data
return [ 'code' => 6 , 'message' => $e -> getMessage ()];
default : // unable to download
return [ 'code' => 5 , 'message' => $e -> getMessage ()];
}
}
public function opSubscribeToFeed ( array $data ) : array {
if ( ! isset ( $data [ 'feed_url' ]) || ! ( ValueInfo :: str ( $data [ 'feed_url' ]) & ValueInfo :: VALID )) {
// if the feed URL is invalid, throw an error
throw new Exception ( " INCORRECT_USAGE " );
}
// normalize input data
if (
( isset ( $data [ 'category_id' ]) && ! ValueInfo :: id ( $data [ 'category_id' ], true )) ||
( isset ( $data [ 'login' ]) && ! ( ValueInfo :: str ( $data [ 'login' ]) & ValueInfo :: VALID )) ||
( isset ( $data [ 'password' ]) && ! ( ValueInfo :: str ( $data [ 'password' ]) & ValueInfo :: VALID ))
) {
// if the category is not a valid ID or the feed username or password are not convertible to strings, also throw an error
throw new Exception ( " INCORRECT_USAGE " );
}
$url = ( string ) $data [ 'feed_url' ];
$folder = isset ( $data [ 'category_id' ]) ? ( int ) $data [ 'category_id' ] : null ;
$fetchUser = isset ( $data [ 'login' ]) ? ( string ) $data [ 'login' ] : " " ;
$fetchPassword = isset ( $data [ 'password' ]) ? ( string ) $data [ 'password' ] : " " ;
// check to make sure the requested folder exists before doing anything else, if one is specified
if ( $folder ) {
try {
Arsse :: $db -> folderPropertiesGet ( Arsse :: $user -> id , $folder );
} catch ( ExceptionInput $e ) {
// folder does not exist: TT-RSS is a bit weird in this case and returns a feed ID of 0. It checks the feed first, but we do not
return [ 'code' => 1 , 'feed_id' => 0 ];
}
}
try {
$id = Arsse :: $db -> subscriptionAdd ( Arsse :: $user -> id , $url , $fetchUser , $fetchPassword );
} catch ( ExceptionInput $e ) {
// subscription already exists; retrieve the existing ID and return that with the correct code
for ( $triedDiscovery = 0 ; $triedDiscovery <= 1 ; $triedDiscovery ++ ) {
$subs = Arsse :: $db -> subscriptionList ( Arsse :: $user -> id );
$id = false ;
foreach ( $subs as $sub ) {
if ( $sub [ 'url' ] === $url ) {
$id = ( int ) $sub [ 'id' ];
break ;
}
}
if ( $id ) {
break ;
} elseif ( ! $triedDiscovery ) {
// if we didn't find the ID we perform feed discovery for the next iteration; this is pretty messy: discovery ends up being done twice because it was already done in $db->subscriptionAdd()
try {
$url = Feed :: discover ( $url , $fetchUser , $fetchPassword );
} catch ( FeedException $e ) {
// feed errors (handled above)
return $this -> feedError ( $e );
}
}
}
return [ 'code' => 0 , 'feed_id' => $id ];
} catch ( FeedException $e ) {
// feed errors (handled above)
return $this -> feedError ( $e );
}
// if all went well, move the new subscription to the requested folder (if one was requested)
try {
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , $id , [ 'folder' => $folder ]);
} catch ( ExceptionInput $e ) {
// ignore errors
}
return [ 'code' => 1 , 'feed_id' => $id ];
}
2017-10-01 02:15:55 +00:00
public function opUnsubscribeFeed ( array $data ) : array {
if ( ! isset ( $data [ 'feed_id' ]) || ! ValueInfo :: id ( $data [ 'feed_id' ])) {
// if the feed is invalid, throw an error
throw new Exception ( " FEED_NOT_FOUND " );
}
try {
// attempt to remove the feed
Arsse :: $db -> subscriptionRemove ( Arsse :: $user -> id , ( int ) $data [ 'feed_id' ]);
} catch ( ExceptionInput $e ) {
throw new Exception ( " FEED_NOT_FOUND " );
}
return [ 'status' => " OK " ];
}
public function opMoveFeed ( array $data ) {
if ( ! isset ( $data [ 'feed_id' ]) || ! ValueInfo :: id ( $data [ 'feed_id' ]) || ! isset ( $data [ 'category_id' ]) || ! ValueInfo :: id ( $data [ 'category_id' ], true )) {
// if the feed or folder is invalid, throw an error
throw new Exception ( " INCORRECT_USAGE " );
}
$in = [
'folder' => ( int ) $data [ 'category_id' ],
];
try {
// try to move the feed
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , ( int ) $data [ 'feed_id' ], $in );
} catch ( ExceptionInput $e ) {
// ignore all errors
}
return null ;
}
public function opRenameFeed ( array $data ) {
if ( ! isset ( $data [ 'feed_id' ]) || ! ValueInfo :: id ( $data [ 'feed_id' ]) || ! isset ( $data [ 'caption' ])) {
2017-10-03 20:14:37 +00:00
// if the feed is invalid or there is no caption, throw an error
2017-10-01 02:15:55 +00:00
throw new Exception ( " INCORRECT_USAGE " );
}
$info = ValueInfo :: str ( $data [ 'caption' ]);
if ( ! ( $info & ValueInfo :: VALID ) || ( $info & ValueInfo :: EMPTY ) || ( $info & ValueInfo :: WHITE )) {
// if the feed name is invalid, throw an error
throw new Exception ( " INCORRECT_USAGE " );
}
$in = [
'name' => ( string ) $data [ 'caption' ],
];
try {
// try to rename the feed
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , ( int ) $data [ 'feed_id' ], $in );
} catch ( ExceptionInput $e ) {
// ignore all errors
}
return null ;
}
2017-10-03 16:43:46 +00:00
2017-10-03 20:14:37 +00:00
public function opUpdateFeed ( array $data ) : array {
if ( ! isset ( $data [ 'feed_id' ]) || ! ValueInfo :: id ( $data [ 'feed_id' ])) {
// if the feed is invalid, throw an error
throw new Exception ( " INCORRECT_USAGE " );
}
try {
Arsse :: $db -> feedUpdate ( Arsse :: $db -> subscriptionPropertiesGet ( Arsse :: $user -> id , ( int ) $data [ 'feed_id' ])[ 'feed' ]);
} catch ( ExceptionInput $e ) {
throw new Exception ( " FEED_NOT_FOUND " );
}
return [ 'status' => " OK " ];
}
2017-10-05 21:42:12 +00:00
protected function labelIn ( $id ) : int {
2017-10-11 16:55:50 +00:00
if ( ! ( ValueInfo :: int ( $id ) & ValueInfo :: NEG ) || $id > ( - 1 - self :: LABEL_OFFSET )) {
2017-10-05 21:42:12 +00:00
throw new Exception ( " INCORRECT_USAGE " );
}
2017-10-11 16:55:50 +00:00
return ( abs ( $id ) - self :: LABEL_OFFSET );
2017-10-05 21:42:12 +00:00
}
protected function labelOut ( int $id ) : int {
2017-10-11 16:55:50 +00:00
return ( $id * - 1 - self :: LABEL_OFFSET );
2017-10-05 21:42:12 +00:00
}
2017-10-13 21:05:06 +00:00
public function opGetLabels ( array $data ) : array {
// this function doesn't complain about invalid article IDs
$article = ( isset ( $data [ 'article_id' ]) && ValueInfo :: id ( $data [ 'article_id' ])) ? ( int ) $data [ 'article_id' ] : 0 ;
try {
$list = $article ? Arsse :: $db -> articleLabelsGet ( Arsse :: $user -> id , $article ) : [];
} catch ( ExceptionInput $e ) {
$list = [];
}
$out = [];
foreach ( Arsse :: $db -> labelList ( Arsse :: $user -> id ) as $l ) {
$out [] = [
'id' => $this -> labelOut ( $l [ 'id' ]),
'caption' => $l [ 'name' ],
'fg_color' => " " ,
'bg_color' => " " ,
'checked' => in_array ( $l [ 'id' ], $list ),
];
}
return $out ;
}
2017-10-05 21:42:12 +00:00
public function opAddLabel ( array $data ) {
$in = [
'name' => isset ( $data [ 'caption' ]) ? $data [ 'caption' ] : " " ,
];
try {
return $this -> labelOut ( Arsse :: $db -> labelAdd ( Arsse :: $user -> id , $in ));
} catch ( ExceptionInput $e ) {
switch ( $e -> getCode ()) {
case 10236 : // label already exists
// retrieve the ID of the existing label; duplicating a label silently returns the existing one
return $this -> labelOut ( Arsse :: $db -> labelPropertiesGet ( Arsse :: $user -> id , $in [ 'name' ], true )[ 'id' ]);
default : // other errors related to input
throw new Exception ( " INCORRECT_USAGE " );
}
}
}
public function opRemoveLabel ( array $data ) {
// normalize the label ID; missing or invalid IDs are rejected
$id = $this -> labelIn ( isset ( $data [ 'label_id' ]) ? $data [ 'label_id' ] : 0 );
try {
// attempt to remove the label
Arsse :: $db -> labelRemove ( Arsse :: $user -> id , $id );
} catch ( ExceptionInput $e ) {
// ignore all errors
}
return null ;
}
public function opRenameLabel ( array $data ) {
// normalize input; missing or invalid IDs are rejected
$id = $this -> labelIn ( isset ( $data [ 'label_id' ]) ? $data [ 'label_id' ] : 0 );
$name = isset ( $data [ 'caption' ]) ? $data [ 'caption' ] : " " ;
try {
// try to rename the folder
Arsse :: $db -> labelPropertiesSet ( Arsse :: $user -> id , $id , [ 'name' => $name ]);
} catch ( ExceptionInput $e ) {
if ( $e -> getCode () == 10237 ) {
// if the supplied ID was invalid, report an error; other errors are to be ignored
throw new Exception ( " INCORRECT_USAGE " );
}
}
return null ;
}
2017-10-15 16:47:07 +00:00
public function opSetArticleLabel ( array $data ) : array {
if ( ! isset ( $data [ 'article_ids' ]) || ! isset ( $data [ 'label_id' ])) {
throw new Exception ( " INCORRECT_USAGE " );
}
$label = $this -> labelIn ( $data [ 'label_id' ]);
$articles = explode ( " , " , ( string ) $data [ 'article_ids' ]);
$assign = ValueInfo :: bool ( isset ( $data [ 'assign' ]) ? $data [ 'assign' ] : null , false );
}
2017-09-28 14:16:24 +00:00
}