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:
parent
ea986f5032
commit
ea08bbb87b
6 changed files with 120 additions and 7 deletions
|
@ -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"]);
|
||||||
|
|
13
lib/REST.php
13
lib/REST.php
|
@ -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:
|
||||||
|
|
32
lib/REST/TinyTinyRSS/Icon.php
Normal file
32
lib/REST/TinyTinyRSS/Icon.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
tests/REST/TinyTinyRSS/TestTinyTinyIcon.php
Normal file
54
tests/REST/TinyTinyRSS/TestTinyTinyIcon.php
Normal 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")));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue