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;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!ValueInfo::id($id)) {
|
||||
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/',
|
||||
'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:
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// BirdReader https://github.com/glynnbird/birdreader/blob/master/API.md
|
||||
// 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",
|
||||
'password' => "str",
|
||||
'next_fetch' => "datetime",
|
||||
'favicon' => "str",
|
||||
],
|
||||
'rows' => [] // filled in the series setup
|
||||
],
|
||||
|
@ -104,9 +105,9 @@ trait SeriesSubscription {
|
|||
|
||||
public function setUpSeries() {
|
||||
$this->data['arsse_feeds']['rows'] = [
|
||||
[1,"http://example.com/feed1", "Ook", "", "",strtotime("now")],
|
||||
[2,"http://example.com/feed2", "Eek", "", "",strtotime("now - 1 hour")],
|
||||
[3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour")],
|
||||
[1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),''],
|
||||
[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"),''],
|
||||
];
|
||||
// initialize a partial mock of the Database object to later manipulate the feedUpdate method
|
||||
Arsse::$db = Phake::partialMock(Database::class, $this->drv);
|
||||
|
@ -402,4 +403,20 @@ trait SeriesSubscription {
|
|||
$this->assertException("notAuthorized", "User", "ExceptionAuthz");
|
||||
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/TestNCNV1_2.php</file>
|
||||
<file>REST/TinyTinyRSS/TestTinyTinyAPI.php</file>
|
||||
<file>REST/TinyTinyRSS/TestTinyTinyIcon.php</file>
|
||||
</testsuite>
|
||||
<testsuite name="Refresh service">
|
||||
<file>Service/TestService.php</file>
|
||||
|
|
Loading…
Reference in a new issue