diff --git a/README.md b/README.md index ec693a1d..078e3ba9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The Arsse is a news aggregator server which implements multiple synchronization protocols, including [version 1.2][NCNv1] of [NextCloud News](https://github.com/nextcloud/news)' protocol and the [Tiny Tiny RSS][TTRSS] protocol ([details](#proto)). Unlike most other aggregator servers, The Arsse does not include a Web front-end (though one is planned as a separate project), and it relies on existing protocols to maximize compatibility with existing clients. -At present the software should be considered in an "alpha" state: though its core subsystems are covered by unit tests and should be free of major bugs, not everything has been rigorously tested. Additionally, though the NextCloud News protocol is fully supported, many features one would expect from other similar software have yet to be implemented. Areas of future work include: +At present the software should be considered in an "alpha" state: though its core subsystems are covered by unit tests and should be free of major bugs, not everything has been rigorously tested. Additionally, many features one would expect from other similar software have yet to be implemented. Areas of future work include: - Support for more database engines (PostgreSQL, MySQL, MariaDB) - Providing more sync protocols (Google Reader, Fever, others) @@ -14,7 +14,7 @@ At present the software should be considered in an "alpha" state: though its cor The Arsse has the following requirements: - A Linux server utilizing systemd and Nginx (tested on Ubuntu 16.04) -- PHP 7.0.7 or newer with the following extensions: +- PHP 7.0.7 or later with the following extensions: - [intl](http://php.net/manual/en/book.intl.php), [json](http://php.net/manual/en/book.json.php), [hash](http://php.net/manual/en/book.hash.php), and [pcre](http://php.net/manual/en/book.pcre.php) - [dom](http://php.net/manual/en/book.dom.php), [simplexml](http://php.net/manual/en/book.simplexml.php), and [iconv](http://php.net/manual/en/book.iconv.php) (for picoFeed) - [sqlite3](http://php.net/manual/en/book.sqlite3.php) @@ -62,7 +62,7 @@ php ./arsse.php conf save-defaults "./config.defaults.php" ## License -The Arsse is made available under the permissive MIT license. See the `LICENSE` file included with the distribution or source code for exact legal text. Dependencies included in the distribution may be governed by other licenses. +The Arsse is made available under the permissive MIT license. See the `LICENSE` and `AUTHORS` files included with the distribution or source code for exact legal text and copyright holders. Dependencies included in the distribution may be governed by other licenses. ## Contributing @@ -76,11 +76,11 @@ Please refer to `CONTRIBUTING.md` for guidelines on contributing code to The Ars The Arsse does not guarantee it will handle type casting of input in the same way as reference implementations for its supported protocols. As a general rule, clients should endeavour to send only correct input. -The Arsse _does_, however, guarantee output to be of the same type. If it is not, this is [a bug][newIssue] and should be reported. +The Arsse does, however, guarantee _output_ to be of the same type. If it is not, this is [a bug][newIssue] and should be reported. #### Content sanitization -The Arsse makes use of the [picoFeed](https://github.com/miniflux/picoFeed/) newsfeed parsing library to sanitize article content. The exact sanitization rules may differ from those of reference implementations for protocols The Arsse supports. +The Arsse makes use of the [picoFeed](https://github.com/miniflux/picoFeed/) newsfeed parsing library to sanitize article content. The exact sanitization parameters may differ from those of reference implementations for protocols The Arsse supports. ### NextCloud News v1.2 @@ -97,18 +97,18 @@ As a general rule, The Arsse should yield the same output as the reference imple - When marking articles as starred the feed ID is ignored, as they are not needed to establish uniqueness - The feed updater ignores the `userId` parameter: feeds in The Arsse are deduplicated, and have no owner - The `/feeds/all` route lists only feeds which should be checked for updates, and it also returns all `userId` attributes as empty strings: feeds in The Arsse are deduplicated, and have no owner -- The updater console commands mentioned in the protocol specification are not implemented +- The updater console commands mentioned in the protocol specification are not implemented, as The Arsse does not implement the required NextCloud subsystems - The `lastLoginTimestamp` attribute of the user metadata is always the current time: The Arsse's implementation of the protocol is fully stateless #### Ambiguities -- [The protocol][NCNv1] does not specify an output character encoding, but the reference server uses UTF-8; The Arsse also uses UTF-8 -- The protocol specifies that GET parameters are treated "the same" as request body parameters; it does not specify what to do in cases where they conflict. The Arsse chooses to give GET parameters precedence -- The protocol does not define validity of folder and names other than to say that the empty string is invalid. The Arsse further considers any string composed only of whitesapce to be invalid -- The protocol does not specify a return code for bulk-marking operations without a `newestItemId` provided; The Arsse returns `422` -- The protocol does not specify what should be done when creating a feed in a folder which does not exist; the Arsse adds the feed to the root folder -- The protocol does not specify what should be done when moving a feed to a folder which does not exist; The Arsse return `422` -- The protocol does not specify what should be done when renaming a feed to an invalid title, nor what constitutes an invalid title; The Arsse uses the same rules as it does for folders, and returns `422` in cases of rejection +- NCN does not specify an output character encoding, but the reference server uses UTF-8; The Arsse also uses UTF-8 +- NCN specifies that GET parameters are treated "the same" as request body parameters; it does not specify what to do in cases where they conflict. The Arsse chooses to give GET parameters precedence +- NCN does not define validity of folder and names other than to say that the empty string is invalid. The Arsse further considers any string composed only of whitesapce to be invalid +- NCN does not specify a return code for bulk-marking operations without a `newestItemId` provided; The Arsse returns `422` +- NCN does not specify what should be done when creating a feed in a folder which does not exist; the Arsse adds the feed to the root folder +- NCN does not specify what should be done when moving a feed to a folder which does not exist; The Arsse return `422` +- NCN does not specify what should be done when renaming a feed to an invalid title, nor what constitutes an invalid title; The Arsse uses the same rules as it does for folders, and returns `422` in cases of rejection ### Tiny Tiny RSS diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 0806a135..a4310603 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; abstract class AbstractException extends \Exception { - const CODES = [ + const CODES = [ "Exception.uncoded" => -1, "Exception.unknown" => 10000, "Exception.constantUnknown" => 10001, diff --git a/lib/Database.php b/lib/Database.php index 7b684939..2323f5f9 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1004,17 +1004,17 @@ class Database { switch ($fields) { // NOTE: the cases all cascade into each other: a given verbosity level is always a superset of the previous one case self::LIST_FULL: // everything - $columns = array_merge($columns,[ + $columns = array_merge($columns, [ "(select note from arsse_marks where article is arsse_articles.id and subscription in (select sub from subscribed_feeds)) as note", ]); case self::LIST_TYPICAL: // conservative, plus content - $columns = array_merge($columns,[ + $columns = array_merge($columns, [ "content", "arsse_enclosures.url as media_url", // enclosures are potentially large due to data: URLs "arsse_enclosures.type as media_type", // FIXME: enclosures should eventually have their own fetch method ]); case self::LIST_CONSERVATIVE: // base metadata, plus anything that is not likely to be large text - $columns = array_merge($columns,[ + $columns = array_merge($columns, [ "arsse_articles.url as url", "arsse_articles.title as title", "(select coalesce(arsse_subscriptions.title,arsse_feeds.title) from arsse_feeds join arsse_subscriptions on arsse_subscriptions.feed is arsse_feeds.id where arsse_feeds.id is arsse_articles.feed) as subscription_title", @@ -1025,7 +1025,7 @@ class Database { "url_title_hash||':'||url_content_hash||':'||title_content_hash as fingerprint", ]); case self::LIST_MINIMAL: // base metadata (always included: required for context matching) - $columns = array_merge($columns,[ + $columns = array_merge($columns, [ // id, subscription, feed, modified_date, marked_date, unread, starred, edition "edited as edited_date", ]); diff --git a/lib/Db/ResultAggregate.php b/lib/Db/ResultAggregate.php index f934e176..84fb2822 100644 --- a/lib/Db/ResultAggregate.php +++ b/lib/Db/ResultAggregate.php @@ -16,7 +16,9 @@ class ResultAggregate extends AbstractResult { // actual public methods public function changes() { - return array_reduce($this->data, function($sum, $value) {return $sum + $value->changes();}, 0); + return array_reduce($this->data, function ($sum, $value) { + return $sum + $value->changes(); + }, 0); } public function lastId() { diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index 7b33af00..cf842d57 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/lib/REST/NextCloudNews/V1_2.php @@ -147,10 +147,10 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { } else { // otherwise return 405 throw new Exception405(implode(", ", array_keys($this->paths[$url]))); - } + } } else { // if the path is not supported, return 404 - throw new Exception404(); + throw new Exception404(); } } @@ -225,7 +225,7 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { ]); } else { // if the path is not supported, return 404 - return new Response(404); + return new Response(404); } } diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 4dd60dae..c6ba1e0b 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -287,7 +287,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { unset($cats[$catmap[$c['id']]]); } } - // do a third pass on categories, building a final category list + // do a third pass on categories, building a final category list foreach ($categories as $c) { // only include categories with unread articles if ($catCounts[$c['id']]) { @@ -333,7 +333,9 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { 'id' => "FEED:".self::FEED_ALL, 'bare_id' => self::FEED_ALL, 'icon' => "images/folder.png", - 'unread' => array_reduce($subs, function($sum, $value) {return $sum + $value['unread'];}, 0), // the sum of all feeds' unread is the total unread + 'unread' => array_reduce($subs, function ($sum, $value) { + return $sum + $value['unread']; + }, 0), // the sum of all feeds' unread is the total unread ], $tSpecial), array_merge([ // Fresh articles 'name' => Arsse::$lang->msg("API.TTRSS.Feed.Fresh"), @@ -392,7 +394,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { ]; $unread += ($l['articles'] - $l['read']); } - // if there are labels, all the label category, + // if there are labels, all the label category, if ($items) { $out[] = [ 'name' => Arsse::$lang->msg("API.TTRSS.Category.Labels"), @@ -682,7 +684,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { $archived = 0; // the archived feed is non-functional in the TT-RSS protocol itself // build the list; exclude anything with zero unread if requested if (!$unread || $starred) { - $out[] = [ + $out[] = [ 'id' => self::FEED_STARRED, 'title' => Arsse::$lang->msg("API.TTRSS.Feed.Starred"), 'unread' => (string) $starred, // output is a string in TTRSS @@ -1154,7 +1156,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { throw new Exception("INCORRECT_USAGE"); } $tr->commit(); - return ['status' => "OK", 'updated' => $out]; + return ['status' => "OK", 'updated' => $out]; } public function opGetArticle(array $data): array { @@ -1409,7 +1411,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { case "all_articles": // no context needed here break; - case "adaptive": + case "adaptive": // adaptive means "return only unread unless there are none, in which case return all articles" if ($c->unread !== false && Arsse::$db->articleCount(Arsse::$user->id, (clone $c)->unread(true))) { $c->unread(true); diff --git a/lib/REST/TinyTinyRSS/Icon.php b/lib/REST/TinyTinyRSS/Icon.php index b31587f2..aabb2c28 100644 --- a/lib/REST/TinyTinyRSS/Icon.php +++ b/lib/REST/TinyTinyRSS/Icon.php @@ -10,8 +10,6 @@ use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\REST\Response; class Icon extends \JKingWeb\Arsse\REST\AbstractHandler { - - public function __construct() { } @@ -25,7 +23,7 @@ class Icon extends \JKingWeb\Arsse\REST\AbstractHandler { $url = Arsse::$db->subscriptionFavicon((int) $match[1]); if ($url) { // strip out anything after literal line-end characters; this is to mitigate a potential header (e.g. cookie) injection from the URL - if (($pos = strpos($url, "\r")) !== FALSE || ($pos = strpos($url, "\n")) !== FALSE) { + if (($pos = strpos($url, "\r")) !== false || ($pos = strpos($url, "\n")) !== false) { $url = substr($url, 0, $pos); } return new Response(301, "", "", ["Location: $url"]); @@ -33,4 +31,4 @@ class Icon extends \JKingWeb\Arsse\REST\AbstractHandler { return new Response(404); } } -} \ No newline at end of file +} diff --git a/tests/Db/TestResultAggregate.php b/tests/Db/TestResultAggregate.php index 433ef428..a44121ea 100644 --- a/tests/Db/TestResultAggregate.php +++ b/tests/Db/TestResultAggregate.php @@ -6,7 +6,6 @@ use JKingWeb\Arsse\Test\Result; /** @covers \JKingWeb\Arsse\Db\ResultAggregate */ class TestResultAggregate extends Test\AbstractTest { - public function testGetChangeCountAndLastInsertId() { $in = [ new Result([], 3, 4), @@ -98,4 +97,4 @@ class TestResultAggregate extends Test\AbstractTest { ]; $this->assertEquals($rows, $test->getAll()); } -} \ No newline at end of file +} diff --git a/tests/Db/TestResultEmpty.php b/tests/Db/TestResultEmpty.php index 0a6cbe9f..65b1380b 100644 --- a/tests/Db/TestResultEmpty.php +++ b/tests/Db/TestResultEmpty.php @@ -4,7 +4,6 @@ namespace JKingWeb\Arsse; /** @covers \JKingWeb\Arsse\Db\ResultEmpty */ class TestResultEmpty extends Test\AbstractTest { - public function testGetChangeCountAndLastInsertId() { $r = new Db\ResultEmpty; $this->assertEquals(0, $r->changes()); @@ -34,4 +33,4 @@ class TestResultEmpty extends Test\AbstractTest { $rows = []; $this->assertEquals($rows, $test->getAll()); } -} \ No newline at end of file +} diff --git a/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php b/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php index 4d045469..69e6ef4c 100644 --- a/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php +++ b/tests/REST/TinyTinyRSS/TestTinyTinyAPI.php @@ -942,9 +942,9 @@ LONG_STRING; public function testAssignArticlesToALabel() { $list = [ - range(1,100), - range(1,50), - range(51,100), + range(1, 100), + range(1, 50), + range(51, 100), ]; $in = [ ['op' => "setArticleLabel", 'sid' => "PriestsOfSyrinx", 'label_id' => -2112, 'article_ids' => implode(",", $list[0])], @@ -1184,11 +1184,15 @@ LONG_STRING; } protected function filterFolders(int $id = null): array { - return array_filter($this->folders, function($value) use ($id) {return $value['parent']==$id;}); + return array_filter($this->folders, function ($value) use ($id) { + return $value['parent']==$id; + }); } protected function filterSubs(int $folder = null): array { - return array_filter($this->subscriptions, function($value) use ($folder) {return $value['folder']==$folder;}); + return array_filter($this->subscriptions, function ($value) use ($folder) { + return $value['folder']==$folder; + }); } protected function reduceFolders(int $id = null) : int { @@ -1196,7 +1200,11 @@ LONG_STRING; foreach ($this->filterFolders($id) as $f) { $out += $this->reduceFolders($f['id']); } - $out += array_reduce(array_filter($this->subscriptions, function($value) use ($id) {return $value['folder']==$id;}), function($sum, $value) {return $sum + $value['unread'];}, 0); + $out += array_reduce(array_filter($this->subscriptions, function ($value) use ($id) { + return $value['folder']==$id; + }), function ($sum, $value) { + return $sum + $value['unread']; + }, 0); return $out; } diff --git a/tests/lib/Database/SeriesSubscription.php b/tests/lib/Database/SeriesSubscription.php index 78f240e3..ddf0b3a3 100644 --- a/tests/lib/Database/SeriesSubscription.php +++ b/tests/lib/Database/SeriesSubscription.php @@ -289,7 +289,6 @@ trait SeriesSubscription { ], ]; $this->assertResult($exp, Arsse::$db->subscriptionList($this->user, 2)); - } public function testListSubscriptionsInAMissingFolder() { @@ -412,15 +411,15 @@ trait SeriesSubscription { $exp = "http://example.com/favicon.ico"; $this->assertSame($exp, Arsse::$db->subscriptionFavicon(1)); $this->assertSame($exp, Arsse::$db->subscriptionFavicon(2)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(3)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(4)); + $this->assertSame('', Arsse::$db->subscriptionFavicon(3)); + $this->assertSame('', Arsse::$db->subscriptionFavicon(4)); // authorization shouldn't have any bearing on this function Phake::when(Arsse::$user)->authorize->thenReturn(false); $this->assertSame($exp, Arsse::$db->subscriptionFavicon(1)); $this->assertSame($exp, Arsse::$db->subscriptionFavicon(2)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(3)); - $this->assertSame('', Arsse::$db->subscriptionFavicon(4)); + $this->assertSame('', Arsse::$db->subscriptionFavicon(3)); + $this->assertSame('', Arsse::$db->subscriptionFavicon(4)); // invalid IDs should simply return an empty string - $this->assertSame('', Arsse::$db->subscriptionFavicon(-2112)); + $this->assertSame('', Arsse::$db->subscriptionFavicon(-2112)); } }