1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 21:22:40 +00:00

Implement TTRSS feed icons; fixes #121

This introduces a data model function of unusual privilege: it can retrieve favicon URLs for any subscription, regardless of user ID. This is a single-purpose hack and its use should be avoided if at all possible.
This commit is contained in:
J. King 2017-11-10 12:02:59 -05:00
parent ea986f5032
commit ea08bbb87b
6 changed files with 120 additions and 7 deletions

View file

@ -629,6 +629,10 @@ class Database {
return $out; return $out;
} }
public function subscriptionFavicon(int $id): string {
return (string) $this->db->prepare("SELECT favicon from arsse_feeds join arsse_subscriptions on feed is arsse_feeds.id where arsse_subscriptions.id is ?", "int")->run($id)->getValue();
}
protected function subscriptionValidateId(string $user, $id, bool $subject = false): array { protected function subscriptionValidateId(string $user, $id, bool $subject = false): array {
if (!ValueInfo::id($id)) { if (!ValueInfo::id($id)) {
throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "feed", 'type' => "int > 0"]); throw new Db\ExceptionInput("typeViolation", ["action" => $this->caller(), "field" => "feed", 'type' => "int > 0"]);

View file

@ -21,14 +21,19 @@ class REST {
'strip' => '/tt-rss/api/', 'strip' => '/tt-rss/api/',
'class' => REST\TinyTinyRSS\API::class, 'class' => REST\TinyTinyRSS\API::class,
], ],
'ttrss_icon' => [ // Tiny Tiny RSS feed icons
'match' => '/tt-rss/feed-icons/',
'strip' => '/tt-rss/feed-icons/',
'class' => REST\TinyTinyRSS\Icon::class,
],
// Other candidates: // Other candidates:
// NextCloud News v2 https://github.com/nextcloud/news/blob/master/docs/externalapi/External-Api.md
// Feedbin v1 https://github.com/feedbin/feedbin-api/commit/86da10aac5f1a57531a6e17b08744e5f9e7db8a9
// Feedbin v2 https://github.com/feedbin/feedbin-api
// Fever https://feedafever.com/api
// Google Reader http://feedhq.readthedocs.io/en/latest/api/index.html // Google Reader http://feedhq.readthedocs.io/en/latest/api/index.html
// Fever https://feedafever.com/api
// Feedbin v2 https://github.com/feedbin/feedbin-api
// Feedbin v1 https://github.com/feedbin/feedbin-api/commit/86da10aac5f1a57531a6e17b08744e5f9e7db8a9
// Miniflux https://github.com/miniflux/miniflux/blob/master/docs/json-rpc-api.markdown // Miniflux https://github.com/miniflux/miniflux/blob/master/docs/json-rpc-api.markdown
// CommaFeed https://www.commafeed.com/api/ // CommaFeed https://www.commafeed.com/api/
// NextCloud News v2 https://github.com/nextcloud/news/blob/master/docs/externalapi/External-Api.md
// Selfoss https://github.com/SSilence/selfoss/wiki/Restful-API-for-Apps-or-any-other-external-access // Selfoss https://github.com/SSilence/selfoss/wiki/Restful-API-for-Apps-or-any-other-external-access
// BirdReader https://github.com/glynnbird/birdreader/blob/master/API.md // BirdReader https://github.com/glynnbird/birdreader/blob/master/API.md
// Proprietary (centralized) entities: // Proprietary (centralized) entities:

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\REST\TinyTinyRSS;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\REST\Response;
class Icon extends \JKingWeb\Arsse\REST\AbstractHandler {
public function __construct() {
}
public function dispatch(\JKingWeb\Arsse\REST\Request $req): Response {
if ($req->method != "GET") {
// only GET requests are allowed
return new Response(405, "", "", ["Allow: GET"]);
} elseif (!preg_match("<^(\d+)\.ico$>", $req->url, $match) || !((int) $match[1])) {
return new Response(404);
}
$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) {
$url = substr($url, 0, $pos);
}
return new Response(301, "", "", ["Location: $url"]);
} else {
return new Response(404);
}
}
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse;
use JKingWeb\Arsse\REST\Request;
use JKingWeb\Arsse\REST\Response;
use JKingWeb\Arsse\Test\Result;
use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\Context;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\Transaction;
use JKingWeb\Arsse\REST\TinyTinyRSS\API;
use Phake;
/** @covers \JKingWeb\Arsse\REST\TinyTinyRSS\Icon<extended> */
class TestTinyTinyIcon extends Test\AbstractTest {
protected $h;
public function setUp() {
$this->clearData();
Arsse::$conf = new Conf();
// create a mock user manager
// create a mock database interface
Arsse::$db = Phake::mock(Database::class);
$this->h = new REST\TinyTinyRSS\Icon();
}
public function tearDown() {
$this->clearData();
}
public function testRetrieveFavion() {
Phake::when(Arsse::$db)->subscriptionFavicon->thenReturn("");
Phake::when(Arsse::$db)->subscriptionFavicon(42)->thenReturn("http://example.com/favicon.ico");
Phake::when(Arsse::$db)->subscriptionFavicon(2112)->thenReturn("http://example.net/logo.png");
Phake::when(Arsse::$db)->subscriptionFavicon(1337)->thenReturn("http://example.org/icon.gif\r\nLocation: http://bad.example.com/");
// these requests should succeed
$exp = new Response(301, "", "", ["Location: http://example.com/favicon.ico"]);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "42.ico")));
$exp = new Response(301, "", "", ["Location: http://example.net/logo.png"]);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "2112.ico")));
$exp = new Response(301, "", "", ["Location: http://example.org/icon.gif"]);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "1337.ico")));
// these requests should fail
$exp = new Response(404);
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "ook.ico")));
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "ook")));
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "47.ico")));
$this->assertEquals($exp, $this->h->dispatch(new Request("GET", "2112.png")));
// only GET is allowed
$exp = new Response(405, "", "", ["Allow: GET"]);
$this->assertEquals($exp, $this->h->dispatch(new Request("PUT", "2112.ico")));
}
}

View file

@ -44,6 +44,7 @@ trait SeriesSubscription {
'username' => "str", 'username' => "str",
'password' => "str", 'password' => "str",
'next_fetch' => "datetime", 'next_fetch' => "datetime",
'favicon' => "str",
], ],
'rows' => [] // filled in the series setup 'rows' => [] // filled in the series setup
], ],
@ -104,9 +105,9 @@ trait SeriesSubscription {
public function setUpSeries() { public function setUpSeries() {
$this->data['arsse_feeds']['rows'] = [ $this->data['arsse_feeds']['rows'] = [
[1,"http://example.com/feed1", "Ook", "", "",strtotime("now")], [1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),''],
[2,"http://example.com/feed2", "Eek", "", "",strtotime("now - 1 hour")], [2,"http://example.com/feed2", "Eek", "", "",strtotime("now - 1 hour"),'http://example.com/favicon.ico'],
[3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour")], [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),''],
]; ];
// initialize a partial mock of the Database object to later manipulate the feedUpdate method // initialize a partial mock of the Database object to later manipulate the feedUpdate method
Arsse::$db = Phake::partialMock(Database::class, $this->drv); Arsse::$db = Phake::partialMock(Database::class, $this->drv);
@ -402,4 +403,20 @@ trait SeriesSubscription {
$this->assertException("notAuthorized", "User", "ExceptionAuthz"); $this->assertException("notAuthorized", "User", "ExceptionAuthz");
Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['folder' => null]); Arsse::$db->subscriptionPropertiesSet($this->user, 1, ['folder' => null]);
} }
public function testRetrieveTheFaviconOfASubscription() {
$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));
// 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));
// invalid IDs should simply return an empty string
$this->assertSame('', Arsse::$db->subscriptionFavicon(-2112));
}
} }

View file

@ -78,6 +78,7 @@
<file>REST/NextCloudNews/TestNCNVersionDiscovery.php</file> <file>REST/NextCloudNews/TestNCNVersionDiscovery.php</file>
<file>REST/NextCloudNews/TestNCNV1_2.php</file> <file>REST/NextCloudNews/TestNCNV1_2.php</file>
<file>REST/TinyTinyRSS/TestTinyTinyAPI.php</file> <file>REST/TinyTinyRSS/TestTinyTinyAPI.php</file>
<file>REST/TinyTinyRSS/TestTinyTinyIcon.php</file>
</testsuite> </testsuite>
<testsuite name="Refresh service"> <testsuite name="Refresh service">
<file>Service/TestService.php</file> <file>Service/TestService.php</file>