From a760bf2ded3eba8c671a2d48f6487aaaaf177cfe Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 9 Feb 2021 09:37:31 -0500 Subject: [PATCH] Implement "t" and "f" booleans in TT-RSS --- CHANGELOG | 1 + lib/REST/AbstractHandler.php | 13 --- lib/REST/NextcloudNews/V1_2.php | 12 +++ lib/REST/TinyTinyRSS/API.php | 122 +++++++++++++---------- tests/cases/REST/TinyTinyRSS/TestAPI.php | 4 +- 5 files changed, 86 insertions(+), 66 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f41bf0f..ba7040e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ Bug fixes: - Do not return null as subscription unread count - Explicitly forbid U+003A COLON and control characters in usernames, for compatibility with RFC 7617 +- Accept "t" and "f" as booleans in Tiny Tiny RSS Version 0.8.5 (2020-10-27) ========================== diff --git a/lib/REST/AbstractHandler.php b/lib/REST/AbstractHandler.php index 7103c34..2dadfa9 100644 --- a/lib/REST/AbstractHandler.php +++ b/lib/REST/AbstractHandler.php @@ -8,7 +8,6 @@ namespace JKingWeb\Arsse\REST; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Misc\Date; -use JKingWeb\Arsse\Misc\ValueInfo; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; @@ -46,16 +45,4 @@ abstract class AbstractHandler implements Handler { } return $data; } - - 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; - } } diff --git a/lib/REST/NextcloudNews/V1_2.php b/lib/REST/NextcloudNews/V1_2.php index 57d1e73..2b14cbd 100644 --- a/lib/REST/NextcloudNews/V1_2.php +++ b/lib/REST/NextcloudNews/V1_2.php @@ -136,6 +136,18 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { return implode("/", $path); } + 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; + } + protected function chooseCall(string $url, string $method) { // // normalize the URL path: change any IDs to 1 for easier comparison $url = $this->normalizePathIds($url); diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 0d4d12a..11b983d 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -12,7 +12,7 @@ use JKingWeb\Arsse\Service; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Context\Context; use JKingWeb\Arsse\Misc\Date; -use JKingWeb\Arsse\Misc\ValueInfo; +use JKingWeb\Arsse\Misc\ValueInfo as V; use JKingWeb\Arsse\AbstractException; use JKingWeb\Arsse\ExceptionType; use JKingWeb\Arsse\Db\ExceptionInput; @@ -46,41 +46,41 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // valid input protected const ACCEPTED_TYPES = ["application/json", "text/json"]; protected const VALID_INPUT = [ - 'op' => ValueInfo::T_STRING, // the function ("operation") to perform - 'sid' => ValueInfo::T_STRING, // session ID - 'seq' => ValueInfo::T_INT, // request number from client - 'user' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // user name for `login` - 'password' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // password for `login` or remote password for `subscribeToFeed` - 'include_empty' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include empty items in `getFeedTree` and `getCategories` - 'unread_only' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to exclude items without unread articles in `getCategories` and `getFeeds` - 'enable_nested' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to NOT show subcategories in `getCategories - 'include_nested' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include subcategories in `getFeeds` and the articles thereof in `getHeadlines` - 'caption' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // name for categories, feed, and labels - 'parent_id' => ValueInfo::T_INT, // parent category for `addCategory` and `moveCategory` - 'category_id' => ValueInfo::T_INT, // parent category for `subscribeToFeed` and `moveFeed`, and subject for category-modification functions - 'cat_id' => ValueInfo::T_INT, // parent category for `getFeeds` - 'label_id' => ValueInfo::T_INT, // label ID in label-related functions - 'feed_url' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // URL of feed in `subscribeToFeed` - 'login' => ValueInfo::T_STRING | ValueInfo::M_STRICT, // remote user name in `subscribeToFeed` - 'feed_id' => ValueInfo::T_INT, // feed, label, or category ID for various functions - 'is_cat' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether 'feed_id' refers to a category - 'article_id' => ValueInfo::T_MIXED, // single article ID in `getLabels`; one or more (comma-separated) article IDs in `getArticle` - 'article_ids' => ValueInfo::T_STRING, // one or more (comma-separated) article IDs in `updateArticle` and `setArticleLabel` - 'assign' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to assign or clear (false) a label in `setArticleLabel` - 'limit' => ValueInfo::T_INT, // maximum number of records returned in `getFeeds`, `getHeadlines`, and `getCompactHeadlines` - 'offset' => ValueInfo::T_INT, // number of records to skip in `getFeeds`, for pagination - 'skip' => ValueInfo::T_INT, // number of records to skip in `getHeadlines` and `getCompactHeadlines`, for pagination - 'show_excerpt' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article excerpts in `getHeadlines` - 'show_content' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article content in `getHeadlines` - 'include_attachments' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to include article enclosures in `getHeadlines` - 'view_mode' => ValueInfo::T_STRING, // various filters for `getHeadlines` - 'since_id' => ValueInfo::T_INT, // cut-off article ID for `getHeadlines` and `getCompactHeadlines; returns only higher article IDs when specified - 'order_by' => ValueInfo::T_STRING, // sort order for `getHeadlines` - 'include_header' => ValueInfo::T_BOOL | ValueInfo::M_DROP, // whether to attach a header to the results of `getHeadlines` - 'search' => ValueInfo::T_STRING, // search string for `getHeadlines` - 'field' => ValueInfo::T_INT, // which state to change in `updateArticle` - 'mode' => ValueInfo::T_MIXED, // whether to set, clear, or toggle the selected state in `updateArticle` (integer), or whether to ignore a certain recent timeframe in `catchupFeed` (string) - 'data' => ValueInfo::T_STRING, // note text in `updateArticle` if setting a note + 'op' => V::T_STRING, // the function ("operation") to perform + 'sid' => V::T_STRING, // session ID + 'seq' => V::T_INT, // request number from client + 'user' => V::T_STRING | V::M_STRICT, // user name for `login` + 'password' => V::T_STRING | V::M_STRICT, // password for `login` or remote password for `subscribeToFeed` + 'include_empty' => V::T_BOOL | V::M_DROP, // whether to include empty items in `getFeedTree` and `getCategories` + 'unread_only' => V::T_BOOL | V::M_DROP, // whether to exclude items without unread articles in `getCategories` and `getFeeds` + 'enable_nested' => V::T_BOOL | V::M_DROP, // whether to NOT show subcategories in `getCategories + 'include_nested' => V::T_BOOL | V::M_DROP, // whether to include subcategories in `getFeeds` and the articles thereof in `getHeadlines` + 'caption' => V::T_STRING | V::M_STRICT, // name for categories, feed, and labels + 'parent_id' => V::T_INT, // parent category for `addCategory` and `moveCategory` + 'category_id' => V::T_INT, // parent category for `subscribeToFeed` and `moveFeed`, and subject for category-modification functions + 'cat_id' => V::T_INT, // parent category for `getFeeds` + 'label_id' => V::T_INT, // label ID in label-related functions + 'feed_url' => V::T_STRING | V::M_STRICT, // URL of feed in `subscribeToFeed` + 'login' => V::T_STRING | V::M_STRICT, // remote user name in `subscribeToFeed` + 'feed_id' => V::T_INT, // feed, label, or category ID for various functions + 'is_cat' => V::T_BOOL | V::M_DROP, // whether 'feed_id' refers to a category + 'article_id' => V::T_MIXED, // single article ID in `getLabels`; one or more (comma-separated) article IDs in `getArticle` + 'article_ids' => V::T_STRING, // one or more (comma-separated) article IDs in `updateArticle` and `setArticleLabel` + 'assign' => V::T_BOOL | V::M_DROP, // whether to assign or clear (false) a label in `setArticleLabel` + 'limit' => V::T_INT, // maximum number of records returned in `getFeeds`, `getHeadlines`, and `getCompactHeadlines` + 'offset' => V::T_INT, // number of records to skip in `getFeeds`, for pagination + 'skip' => V::T_INT, // number of records to skip in `getHeadlines` and `getCompactHeadlines`, for pagination + 'show_excerpt' => V::T_BOOL | V::M_DROP, // whether to include article excerpts in `getHeadlines` + 'show_content' => V::T_BOOL | V::M_DROP, // whether to include article content in `getHeadlines` + 'include_attachments' => V::T_BOOL | V::M_DROP, // whether to include article enclosures in `getHeadlines` + 'view_mode' => V::T_STRING, // various filters for `getHeadlines` + 'since_id' => V::T_INT, // cut-off article ID for `getHeadlines` and `getCompactHeadlines; returns only higher article IDs when specified + 'order_by' => V::T_STRING, // sort order for `getHeadlines` + 'include_header' => V::T_BOOL | V::M_DROP, // whether to attach a header to the results of `getHeadlines` + 'search' => V::T_STRING, // search string for `getHeadlines` + 'field' => V::T_INT, // which state to change in `updateArticle` + 'mode' => V::T_MIXED, // whether to set, clear, or toggle the selected state in `updateArticle` (integer), or whether to ignore a certain recent timeframe in `catchupFeed` (string) + 'data' => V::T_STRING, // note text in `updateArticle` if setting a note ]; protected const VIEW_MODES = ["all_articles", "adaptive", "unread", "marked", "has_note", "published"]; // generic error construct @@ -156,6 +156,26 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } } + protected function normalizeInput(array $data): array { + $out = []; + foreach (self::VALID_INPUT as $key => $type) { + if (isset($data[$key])) { + // TT-RSS accepts "t" and "f" as booleans + if ($type === V::T_BOOL | V::M_DROP) { + if ($data[$key] === "t") { + $data[$key] = true; + } elseif ($data[$key] === "f") { + $data[$key] = false; + } + } + $out[$key] = V::normalize($data[$key], $type, "unix"); + } else { + $out[$key] = null; + } + } + return $out; + } + protected function resumeSession(string $id): bool { // if HTTP authentication was successful and sessions are not enforced, proceed unconditionally if (isset(Arsse::$user->id) && !Arsse::$conf->userSessionEnforced) { @@ -589,7 +609,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opRemoveCategory(array $data) { - if (!ValueInfo::id($data['category_id'])) { + if (!V::id($data['category_id'])) { // if the folder is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -603,7 +623,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opMoveCategory(array $data) { - if (!ValueInfo::id($data['category_id']) || !ValueInfo::id($data['parent_id'], true)) { + if (!V::id($data['category_id']) || !V::id($data['parent_id'], true)) { // if the folder or parent is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -620,8 +640,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opRenameCategory(array $data) { - $info = ValueInfo::str($data['caption']); - if (!ValueInfo::id($data['category_id']) || !($info & ValueInfo::VALID) || ($info & ValueInfo::EMPTY) || ($info & ValueInfo::WHITE)) { + $info = V::str($data['caption']); + if (!V::id($data['category_id']) || !($info & V::VALID) || ($info & V::EMPTY) || ($info & V::WHITE)) { // if the folder or its new name are invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -646,7 +666,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { $offset = $data['offset'] ?? 0; $nested = $data['include_nested'] ?? false; // if a special category was selected, nesting does not apply - if (!ValueInfo::id($cat)) { + if (!V::id($cat)) { $nested = false; // if the All, Special, or Labels category was selected, pagination also does not apply if (in_array($cat, [self::CAT_ALL, self::CAT_SPECIAL, self::CAT_LABELS])) { @@ -820,7 +840,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opSubscribeToFeed(array $data): array { - if (!$data['feed_url'] || !ValueInfo::id($data['category_id'], true)) { + if (!$data['feed_url'] || !V::id($data['category_id'], true)) { // if the feed URL or the category ID is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -887,7 +907,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opMoveFeed(array $data) { - if (!ValueInfo::id($data['feed_id']) || !isset($data['category_id']) || !ValueInfo::id($data['category_id'], true)) { + if (!V::id($data['feed_id']) || !isset($data['category_id']) || !V::id($data['category_id'], true)) { // if the feed or folder is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -904,8 +924,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opRenameFeed(array $data) { - $info = ValueInfo::str($data['caption']); - if (!ValueInfo::id($data['feed_id']) || !($info & ValueInfo::VALID) || ($info & ValueInfo::EMPTY) || ($info & ValueInfo::WHITE)) { + $info = V::str($data['caption']); + if (!V::id($data['feed_id']) || !($info & V::VALID) || ($info & V::EMPTY) || ($info & V::WHITE)) { // if the feed ID or name is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -922,7 +942,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } public function opUpdateFeed(array $data): array { - if (!isset($data['feed_id']) || !ValueInfo::id($data['feed_id'])) { + if (!isset($data['feed_id']) || !V::id($data['feed_id'])) { // if the feed is invalid, throw an error throw new Exception("INCORRECT_USAGE"); } @@ -935,7 +955,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } protected function labelIn($id, bool $throw = true): int { - if (!(ValueInfo::int($id) & ValueInfo::NEG) || $id > (-1 - self::LABEL_OFFSET)) { + if (!(V::int($id) & V::NEG) || $id > (-1 - self::LABEL_OFFSET)) { if ($throw) { throw new Exception("INCORRECT_USAGE"); } else { @@ -951,7 +971,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opGetLabels(array $data): array { // this function doesn't complain about invalid article IDs - $article = ValueInfo::id($data['article_id']) ? $data['article_id'] : 0; + $article = V::id($data['article_id']) ? $data['article_id'] : 0; try { $list = $article ? Arsse::$db->articleLabelsGet(Arsse::$user->id, $article) : []; } catch (ExceptionInput $e) { @@ -1112,8 +1132,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opUpdateArticle(array $data): array { // normalize input - $articles = array_filter(ValueInfo::normalize(explode(",", (string) $data['article_ids']), ValueInfo::T_INT | ValueInfo::M_ARRAY), [ValueInfo::class, "id"]); - $data['mode'] = ValueInfo::normalize($data['mode'], ValueInfo::T_INT); + $articles = array_filter(V::normalize(explode(",", (string) $data['article_ids']), V::T_INT | V::M_ARRAY), [V::class, "id"]); + $data['mode'] = V::normalize($data['mode'], V::T_INT); if (!$articles) { // if there are no valid articles this is an error throw new Exception("INCORRECT_USAGE"); @@ -1185,7 +1205,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { public function opGetArticle(array $data): array { // normalize input - $articles = array_filter(ValueInfo::normalize(explode(",", (string) $data['article_id']), ValueInfo::T_INT | ValueInfo::M_ARRAY), [ValueInfo::class, "id"]); + $articles = array_filter(V::normalize(explode(",", (string) $data['article_id']), V::T_INT | V::M_ARRAY), [V::class, "id"]); if (!$articles) { // if there are no valid articles this is an error throw new Exception("INCORRECT_USAGE"); diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index 139f7dc..cac71dd 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -1333,7 +1333,7 @@ LONG_STRING; [['feed_id' => 0, 'is_cat' => true], (clone $c)->folderShallow(0)], [['feed_id' => 0, 'is_cat' => true, 'mode' => "bogus"], (clone $c)->folderShallow(0)], [['feed_id' => -1], (clone $c)->starred(true)], - [['feed_id' => -1, 'is_cat' => true], null], + [['feed_id' => -1, 'is_cat' => "t"], null], [['feed_id' => -3], (clone $c)->modifiedSince(Date::sub("PT24H", self::NOW))], [['feed_id' => -3, 'mode' => "1day"], (clone $c)->modifiedSince(Date::sub("PT24H", self::NOW))->notModifiedSince(Date::sub("PT24H", self::NOW))], // this is a nonsense query, but it's what TT-RSS appearsto do [['feed_id' => -3, 'is_cat' => true], null], @@ -1342,7 +1342,7 @@ LONG_STRING; [['feed_id' => -2, 'is_cat' => true, 'mode' => "all"], (clone $c)->labelled(true)], [['feed_id' => -4], $c], [['feed_id' => -4, 'is_cat' => true], null], - [['feed_id' => -6], null], + [['feed_id' => -6, 'is_cat' => "f"], null], [['feed_id' => -2112], (clone $c)->label(1088)], [['feed_id' => 42, 'is_cat' => true], (clone $c)->folder(42)], [['feed_id' => 42, 'is_cat' => true, 'mode' => "1week"], (clone $c)->folder(42)->notModifiedSince(Date::sub("P1W", self::NOW))],