diff --git a/docs/en/030_Supported_Protocols/005_Miniflux.md b/docs/en/030_Supported_Protocols/005_Miniflux.md index c3a19211..c098aa1e 100644 --- a/docs/en/030_Supported_Protocols/005_Miniflux.md +++ b/docs/en/030_Supported_Protocols/005_Miniflux.md @@ -32,7 +32,7 @@ Miniflux version 2.0.27 is emulated, though not all features are implemented - Various error codes and messages differ due to significant implementation differences - `PUT` requests which return a body respond with `200 OK` rather than `201 Created` - The "All" category is treated specially (see below for details) -- Category names consisting only of whitespace are rejected along with the empty string +- Feed and category titles consisting only of whitespace are rejected along with the empty string - Filtering rules may not function identically (see below for details) - The `checked_at` field of feeds indicates when the feed was last updated rather than when it was last checked - Creating a feed with the `scrape` property set to `true` might not return scraped content for the initial synchronization diff --git a/lib/REST/Miniflux/V1.php b/lib/REST/Miniflux/V1.php index 2438494e..e987a25e 100644 --- a/lib/REST/Miniflux/V1.php +++ b/lib/REST/Miniflux/V1.php @@ -66,6 +66,34 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { 'entry_swipe' => ["swipe", true], 'stylesheet' => ["stylesheet", ""], ]; + /** A map between Miniflux's input properties and our input properties when modifiying feeds + * + * Miniflux also allows changing the following properties: + * + * - feed_url + * - username + * - password + * - user_agent + * - scraper_rules + * - rewrite_rules + * - disabled + * - ignore_http_cache + * - fetch_via_proxy + * + * These either do not apply because we have no cache or proxy, + * or cannot be changed because feeds are deduplicated and changing + * how they are fetched is not practical with our implementation. + * The properties are still checked for type and syntactic validity + * where practical, on the assumption Miniflux would also reject + * invalid values. + */ + protected const FEED_META_MAP = [ + 'title' => "title", + 'category_id' => "folder", + 'crawler' => "scrape", + 'keeplist_rules' => "keep_rule", + 'blocklist_rules' => "block_rule", + ]; protected const CALLS = [ // handler method Admin Path Body Query Required fields '/categories' => [ 'GET' => ["getCategories", false, false, false, false, []], @@ -745,6 +773,32 @@ class V1 extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(['feed_id' => $id], 201); } + protected function updateFeed(array $path, array $data): ResponseInterface { + $in = []; + foreach (self::FEED_META_MAP as $from => $to) { + if (isset($data[$from])) { + $in[$to] = $data[$from]; + } + } + if (isset($in['folder'])) { + $in['folder'] -= 1; + } + try { + Arsse::$db->subscriptionPropertiesSet(Arsse::$user->id, (int) $path[1], $in); + return $this->getFeed($path); + } catch (ExceptionInput $e) { + switch ($e->getCode()) { + case 10231: + case 10232: + return new ErrorResponse("InvalidTitle", 422); + case 10235: + return new ErrorResponse("MissingCategory", 422); + case 10239: + return new ErrorResponse("404", 404); + } + } + } + public static function tokenGenerate(string $user, string $label): string { // Miniflux produces tokens in base64url alphabet $t = str_replace(["+", "/"], ["-", "_"], base64_encode(random_bytes(self::TOKEN_LENGTH))); diff --git a/locale/en.php b/locale/en.php index 812a50c9..b44d7490 100644 --- a/locale/en.php +++ b/locale/en.php @@ -21,11 +21,12 @@ return [ 'API.Miniflux.Error.FetchOther' => 'Unable to fetch this resource', 'API.Miniflux.Error.FetchFormat' => 'Unsupported feed format', 'API.Miniflux.Error.DuplicateCategory' => 'This category already exists.', - 'API.Miniflux.Error.InvalidCategory' => 'Invalid category title "{title}"', + 'API.Miniflux.Error.InvalidCategory' => 'Invalid category title', 'API.Miniflux.Error.MissingCategory' => 'This category does not exist or does not belong to this user.', 'API.Miniflux.Error.InvalidElevation' => 'Only administrators can change permissions of standard users', 'API.Miniflux.Error.DuplicateUser' => 'The user name "{user}" already exists', 'API.Miniflux.Error.DuplicateFeed' => 'This feed already exists.', + 'API.Miniflux.Error.InvalidTitle' => 'Invalid feed title', 'API.TTRSS.Category.Uncategorized' => 'Uncategorized', 'API.TTRSS.Category.Special' => 'Special', diff --git a/tests/cases/REST/Miniflux/TestV1.php b/tests/cases/REST/Miniflux/TestV1.php index 9b5877d7..dbb0eff7 100644 --- a/tests/cases/REST/Miniflux/TestV1.php +++ b/tests/cases/REST/Miniflux/TestV1.php @@ -683,4 +683,33 @@ class TestV1 extends \JKingWeb\Arsse\Test\AbstractTest { [['feed_url' => "http://example.com/", 'category_id' => 1, 'blocklist_rules' => "A"], 2112, 44, true, true, new Response(['feed_id' => 44], 201)], ]; } + + /** @dataProvider provideFeedModifications */ + public function testModifyAFeed(array $in, array $data, $out, ResponseInterface $exp): void { + $this->h = \Phake::partialMock(V1::class); + \Phake::when($this->h)->getFeed->thenReturn(new Response($this->feedsOut[0])); + if ($out instanceof \Exception) { + \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenThrow($out); + } else { + \Phake::when(Arsse::$db)->subscriptionPropertiesSet->thenReturn($out); + } + $this->assertMessage($exp, $this->req("PUT", "/feeds/2112")); + } + + public function provideFeedModifications(): iterable { + self::clearData(); + $success = new Response($this->feedsOut[0]); + return [ + [[], [], true, $success], + [[], [], new ExceptionInput("subjectMissing"), new ErrorResponse("404", 404)], + [['title' => ""], ['title' => ""], new ExceptionInput("missing"), new ErrorResponse("InvalidTitle", 422)], + [['title' => " "], ['title' => " "], new ExceptionInput("whitespace"), new ErrorResponse("InvalidTitle", 422)], + [['title' => " "], ['title' => " "], new ExceptionInput("whitespace"), new ErrorResponse("InvalidTitle", 422)], + [['category_id' => 47], ['folder' => 46], new ExceptionInput("idMissing"), new ErrorResponse("MissingCategory", 422)], + [['crawler' => false], ['scrape' => false], true, $success], + [['keeplist_rules' => ""], ['keep_rule' => ""], true, $success], + [['blocklist_rules' => "ook"], ['block_rule' => "ook"], true, $success], + [['title' => "Ook!", 'crawler' => true], ['title' => "Ook!", 'scrape' => true], true, $success] + ]; + } }