Browse Source

Documentation tweaks and CS fixes

microsub
J. King 6 years ago
parent
commit
22cdc8916d
  1. 26
      README.md
  2. 2
      lib/AbstractException.php
  3. 8
      lib/Database.php
  4. 4
      lib/Db/ResultAggregate.php
  5. 6
      lib/REST/NextCloudNews/V1_2.php
  6. 14
      lib/REST/TinyTinyRSS/API.php
  7. 6
      lib/REST/TinyTinyRSS/Icon.php
  8. 3
      tests/Db/TestResultAggregate.php
  9. 3
      tests/Db/TestResultEmpty.php
  10. 20
      tests/REST/TinyTinyRSS/TestTinyTinyAPI.php
  11. 11
      tests/lib/Database/SeriesSubscription.php

26
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.
### <a name="proto-ncnv1"></a> 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
### <a name="proto-ttrss"></a> Tiny Tiny RSS

2
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,

8
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",
]);

4
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() {

6
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);
}
}

14
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);

6
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);
}
}
}
}

3
tests/Db/TestResultAggregate.php

@ -6,7 +6,6 @@ use JKingWeb\Arsse\Test\Result;
/** @covers \JKingWeb\Arsse\Db\ResultAggregate<extended> */
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());
}
}
}

3
tests/Db/TestResultEmpty.php

@ -4,7 +4,6 @@ namespace JKingWeb\Arsse;
/** @covers \JKingWeb\Arsse\Db\ResultEmpty<extended> */
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());
}
}
}

20
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;
}

11
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));
}
}

Loading…
Cancel
Save