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 ;
2017-10-20 13:54:08 +00:00
use JKingWeb\Arsse\ExceptionType ;
2017-09-25 03:32:21 +00:00
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 = [
2017-10-20 13:54:08 +00:00
'op' => ValueInfo :: T_STRING ,
'sid' => ValueInfo :: T_STRING ,
'seq' => ValueInfo :: T_INT ,
'user' => ValueInfo :: T_STRING | ValueInfo :: M_STRICT ,
'password' => ValueInfo :: T_STRING | ValueInfo :: M_STRICT ,
'include_empty' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'unread_only' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'enable_nested' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'caption' => ValueInfo :: T_STRING | ValueInfo :: M_STRICT ,
'parent_id' => ValueInfo :: T_INT ,
'category_id' => ValueInfo :: T_INT ,
'feed_url' => ValueInfo :: T_STRING | ValueInfo :: M_STRICT ,
'login' => ValueInfo :: T_STRING | ValueInfo :: M_STRICT ,
'feed_id' => ValueInfo :: T_INT ,
'article_id' => ValueInfo :: T_INT ,
'label_id' => ValueInfo :: T_INT ,
'article_ids' => ValueInfo :: T_STRING ,
'assign' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'is_cat' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'cat_id' => ValueInfo :: T_INT ,
'limit' => ValueInfo :: T_INT ,
'offset' => ValueInfo :: T_INT ,
'include_nested' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'skip' => ValueInfo :: T_INT ,
'filter' => ValueInfo :: T_STRING ,
'show_excerpt' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'show_content' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'view_mode' => ValueInfo :: T_STRING ,
'include_attachments' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'since_id' => ValueInfo :: T_INT ,
'order_by' => ValueInfo :: T_STRING ,
'sanitize' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'force_update' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'has_sandbox' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'include_header' => ValueInfo :: T_BOOL | ValueInfo :: M_DROP ,
'search' => ValueInfo :: T_STRING ,
'search_mode' => ValueInfo :: T_STRING ,
'match_on' => ValueInfo :: T_STRING ,
'mode' => ValueInfo :: T_INT ,
'field' => ValueInfo :: T_INT ,
'data' => ValueInfo :: T_STRING ,
2017-10-20 22:17:47 +00:00
'pref_name' => ValueInfo :: T_STRING ,
2017-10-15 16:47:07 +00:00
];
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
}
try {
2017-10-20 13:54:08 +00:00
// normalize input
try {
$data [ 'seq' ] = isset ( $data [ 'seq' ]) ? $data [ 'seq' ] : 0 ;
$data = $this -> normalizeInput ( $data , self :: VALID_INPUT , " unix " );
2017-10-20 23:02:42 +00:00
} catch ( ExceptionType $e ) {
2017-10-20 13:54:08 +00:00
throw new Exception ( " INCORRECT_USAGE " );
}
2017-10-11 16:55:50 +00:00
if ( strtolower (( string ) $data [ 'op' ]) != " login " ) {
// unless logging in, a session identifier is required
2017-10-20 13:54:08 +00:00
$this -> resumeSession (( string ) $data [ 'sid' ]);
2017-09-25 03:32:21 +00:00
}
$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
}
}
2017-10-20 13:54:08 +00:00
protected function resumeSession ( string $id ) : bool {
2017-09-25 03:32:21 +00:00
try {
// verify the supplied session is valid
2017-10-20 13:54:08 +00:00
$s = Arsse :: $db -> sessionResume ( $id );
2017-09-25 03:32:21 +00:00
} 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-10-20 13:54:08 +00:00
if ( Arsse :: $user -> auth (( string ) $data [ 'user' ], ( string ) $data [ 'password' ])) {
2017-09-25 03:32:21 +00:00
$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
2017-10-20 13:54:08 +00:00
$all = $data [ 'include_empty' ] ? ? false ;
$read = ! ( $data [ 'unread_only' ] ? ? false );
$deep = ! ( $data [ 'enable_nested' ] ? ? false );
2017-10-07 16:46:05 +00:00
$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 = [
2017-10-20 13:54:08 +00:00
'name' => $data [ 'caption' ],
'parent' => $data [ 'parent_id' ],
2017-09-27 02:45:54 +00:00
];
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 ) {
2017-10-20 13:54:08 +00:00
if ( ! ValueInfo :: id ( $data [ 'category_id' ])) {
2017-10-01 02:15:55 +00:00
// 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' ]);
2017-10-20 23:02:42 +00:00
} catch ( ExceptionInput $e ) {
2017-10-01 02:15:55 +00:00
// ignore all errors
}
return null ;
}
public function opMoveCategory ( array $data ) {
2017-10-20 13:54:08 +00:00
if ( ! ValueInfo :: id ( $data [ 'category_id' ]) || ! ValueInfo :: id ( $data [ 'parent_id' ], true )) {
2017-10-01 02:15:55 +00:00
// 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 );
2017-10-20 23:02:42 +00:00
} catch ( ExceptionInput $e ) {
2017-10-01 02:15:55 +00:00
// ignore all errors
}
return null ;
}
public function opRenameCategory ( array $data ) {
$info = ValueInfo :: str ( $data [ 'caption' ]);
2017-10-20 13:54:08 +00:00
if ( ! ValueInfo :: id ( $data [ 'category_id' ]) || ! ( $info & ValueInfo :: VALID ) || ( $info & ValueInfo :: EMPTY ) || ( $info & ValueInfo :: WHITE )) {
// if the folder or its new name are invalid, throw an error
2017-10-01 02:15:55 +00:00
throw new Exception ( " INCORRECT_USAGE " );
}
$in = [
2017-10-20 13:54:08 +00:00
'name' => $data [ 'caption' ],
2017-10-01 02:15:55 +00:00
];
try {
// try to rename the folder
2017-10-20 13:54:08 +00:00
Arsse :: $db -> folderPropertiesSet ( Arsse :: $user -> id , $data [ 'category_id' ], $in );
2017-10-20 23:02:42 +00:00
} catch ( ExceptionInput $e ) {
2017-10-01 02:15:55 +00:00
// 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 ()) {
2017-10-20 23:02:42 +00:00
case 10502 : // invalid URL
2017-10-03 14:43:09 +00:00
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 {
2017-10-20 13:54:08 +00:00
if ( ! $data [ 'feed_url' ] || ! ValueInfo :: id ( $data [ 'category_id' ], true )) {
// if the feed URL or the category ID is invalid, throw an error
2017-10-03 14:43:09 +00:00
throw new Exception ( " INCORRECT_USAGE " );
}
$url = ( string ) $data [ 'feed_url' ];
2017-10-20 13:54:08 +00:00
$folder = ( int ) $data [ 'category_id' ];
$fetchUser = ( string ) $data [ 'login' ];
$fetchPassword = ( string ) $data [ 'password' ];
2017-10-03 14:43:09 +00:00
// 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 );
2017-10-20 23:02:42 +00:00
} catch ( FeedException $e ) {
2017-10-03 14:43:09 +00:00
// 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 {
try {
// attempt to remove the feed
Arsse :: $db -> subscriptionRemove ( Arsse :: $user -> id , ( int ) $data [ 'feed_id' ]);
2017-10-20 23:02:42 +00:00
} catch ( ExceptionInput $e ) {
2017-10-01 02:15:55 +00:00
throw new Exception ( " FEED_NOT_FOUND " );
}
return [ 'status' => " OK " ];
}
public function opMoveFeed ( array $data ) {
2017-10-20 13:54:08 +00:00
if ( ! ValueInfo :: id ( $data [ 'feed_id' ]) || ! isset ( $data [ 'category_id' ]) || ! ValueInfo :: id ( $data [ 'category_id' ], true )) {
2017-10-01 02:15:55 +00:00
// if the feed or folder is invalid, throw an error
throw new Exception ( " INCORRECT_USAGE " );
}
$in = [
2017-10-20 13:54:08 +00:00
'folder' => $data [ 'category_id' ],
2017-10-01 02:15:55 +00:00
];
try {
// try to move the feed
2017-10-20 13:54:08 +00:00
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , $data [ 'feed_id' ], $in );
2017-10-20 23:02:42 +00:00
} catch ( ExceptionInput $e ) {
2017-10-01 02:15:55 +00:00
// ignore all errors
}
return null ;
}
public function opRenameFeed ( array $data ) {
$info = ValueInfo :: str ( $data [ 'caption' ]);
2017-10-20 13:54:08 +00:00
if ( ! ValueInfo :: id ( $data [ 'feed_id' ]) || ! ( $info & ValueInfo :: VALID ) || ( $info & ValueInfo :: EMPTY ) || ( $info & ValueInfo :: WHITE )) {
// if the feed ID or name is invalid, throw an error
2017-10-01 02:15:55 +00:00
throw new Exception ( " INCORRECT_USAGE " );
}
$in = [
2017-10-20 13:54:08 +00:00
'name' => $data [ 'caption' ],
2017-10-01 02:15:55 +00:00
];
try {
// try to rename the feed
2017-10-20 13:54:08 +00:00
Arsse :: $db -> subscriptionPropertiesSet ( Arsse :: $user -> id , $data [ 'feed_id' ], $in );
2017-10-20 23:02:42 +00:00
} catch ( ExceptionInput $e ) {
2017-10-01 02:15:55 +00:00
// 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 {
2017-10-20 13:54:08 +00:00
Arsse :: $db -> feedUpdate ( Arsse :: $db -> subscriptionPropertiesGet ( Arsse :: $user -> id , $data [ 'feed_id' ])[ 'feed' ]);
2017-10-20 23:02:42 +00:00
} catch ( ExceptionInput $e ) {
2017-10-03 20:14:37 +00:00
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
2017-10-20 13:54:08 +00:00
$article = ValueInfo :: id ( $data [ 'article_id' ]) ? $data [ 'article_id' ] : 0 ;
2017-10-13 21:05:06 +00:00
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 ),
];
2017-10-20 23:02:42 +00:00
}
2017-10-13 21:05:06 +00:00
return $out ;
}
2017-10-05 21:42:12 +00:00
public function opAddLabel ( array $data ) {
$in = [
2017-10-20 13:54:08 +00:00
'name' => ( string ) $data [ 'caption' ],
2017-10-05 21:42:12 +00:00
];
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
2017-10-20 13:54:08 +00:00
$id = $this -> labelIn ( $data [ 'label_id' ]);
2017-10-05 21:42:12 +00:00
try {
// attempt to remove the label
Arsse :: $db -> labelRemove ( Arsse :: $user -> id , $id );
2017-10-20 23:02:42 +00:00
} catch ( ExceptionInput $e ) {
2017-10-05 21:42:12 +00:00
// ignore all errors
}
return null ;
}
public function opRenameLabel ( array $data ) {
// normalize input; missing or invalid IDs are rejected
2017-10-20 13:54:08 +00:00
$id = $this -> labelIn ( $data [ 'label_id' ]);
$name = ( string ) $data [ 'caption' ];
2017-10-05 21:42:12 +00:00
try {
// try to rename the folder
Arsse :: $db -> labelPropertiesSet ( Arsse :: $user -> id , $id , [ 'name' => $name ]);
2017-10-20 23:02:42 +00:00
} catch ( ExceptionInput $e ) {
2017-10-05 21:42:12 +00:00
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 {
$label = $this -> labelIn ( $data [ 'label_id' ]);
2017-10-20 13:54:08 +00:00
$articles = explode ( " , " , $data [ 'article_ids' ]);
$assign = $data [ 'assign' ] ? ? false ;
2017-10-20 22:17:47 +00:00
$out = 0 ;
$in = array_chunk ( $data [ 'article_ids' ], 50 );
for ( $a = 0 ; $a < sizeof ( $in ); $a ++ ) {
// initialize the matching context
$c = new Context ;
$c -> articles ( $in [ $a ]);
try {
$out += Arsse :: $db -> labelArticlesSet ( Arsse :: $user -> id , $label , $c , ! $assign );
} catch ( ExceptionInput $e ) {
}
}
return [ 'status' => " OK " , 'updated' => $out ];
2017-10-15 16:47:07 +00:00
}
2017-09-28 14:16:24 +00:00
}