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

Merge master

This commit is contained in:
J. King 2017-09-30 12:52:05 -04:00
parent 5488b994f7
commit 0a0aabe4ed
19 changed files with 135 additions and 75 deletions

31
.gitattributes vendored
View file

@ -1,22 +1,13 @@
# Auto detect text files and perform LF normalization * text=auto encoding=utf-8
* text=auto
# Custom for Visual Studio *.html diff=html
*.cs diff=csharp *.php diff=php
*.sln merge=union *.bat eol=crlf
*.csproj merge=union .gitignore -eol
*.vbproj merge=union
*.fsproj merge=union
*.dbproj merge=union
# Standard to msysgit
*.doc diff=astextplain tests/ export-ignore
*.DOC diff=astextplain .* export-ignore
*.docx diff=astextplain build.xml export-ignore
*.DOCX diff=astextplain composer.* export-ignore
*.dot diff=astextplain phpdoc.* export-ignore
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

53
.gitignore vendored
View file

@ -1,48 +1,45 @@
#dependencies
vendor/
#temp files # Temporary files and dependencies
vendor/
documentation/ documentation/
tests/coverage tests/coverage/
build/
arsse.db* arsse.db*
config.php config.php
.php_cs.cache .php_cs.cache
build
# Windows image file caches
# Windows files
Thumbs.db Thumbs.db
ehthumbs.db ehthumbs.db
# Folder config file
Desktop.ini Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/ $RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# ========================= # macOS files
# Operating System Files
# =========================
# OSX
# =========================
.DS_Store .DS_Store
.AppleDouble .AppleDouble
.LSOverride .LSOverride
# Icon must ends with two \r.
Icon Icon
# Thumbnails
._* ._*
# Files that might appear on external disk
.Spotlight-V100 .Spotlight-V100
.Trashes .Trashes
# archives
*.zip
*.7z
*.tar.gz
*.tgz
*.deb
*.rpm
*.dmg
*.cab
*.msi
*.msm
*.msp

12
CHANGELOG Normal file
View file

@ -0,0 +1,12 @@
Version 0.1.1 (2017-09-30)
==========================
Bug fixes:
- Perform feed discovery like NextCloud News does
- Respond correctly to HEAD requests
- Various minor fixes
Version 0.1.0 (2017-08-29)
==========================
Initial release

View file

@ -4,7 +4,7 @@ namespace JKingWeb\Arsse;
const BASE = __DIR__.DIRECTORY_SEPARATOR; const BASE = __DIR__.DIRECTORY_SEPARATOR;
const NS_BASE = __NAMESPACE__."\\"; const NS_BASE = __NAMESPACE__."\\";
const VERSION = "0.1.0"; const VERSION = "0.1.1";
require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php"; require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php";
ignore_user_abort(true); ignore_user_abort(true);

View file

@ -11,6 +11,7 @@
<include name="composer.*"/> <include name="composer.*"/>
<include name="arsse.php"/> <include name="arsse.php"/>
<include name="bootstrap.php"/> <include name="bootstrap.php"/>
<include name="CHANGELOG"/>
<include name="LICENSE"/> <include name="LICENSE"/>
<include name="README.md"/> <include name="README.md"/>
</fileset> </fileset>

View file

@ -10,3 +10,4 @@ fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param REQUEST_URI $request_uri; fastcgi_param REQUEST_URI $request_uri;
fastcgi_param HTTPS $https if_not_empty; fastcgi_param HTTPS $https if_not_empty;
fastcgi_param REMOTE_USER $remote_user;

View file

@ -84,7 +84,7 @@ USAGE_TEXT;
public function userAdd(string $user, string $password = null): int { public function userAdd(string $user, string $password = null): int {
$passwd = Arsse::$user->add($user, $password); $passwd = Arsse::$user->add($user, $password);
if (is_null($password)) { if (is_null($password)) {
echo $passwd; echo $passwd.\PHP_EOL;
} }
return 0; return 0;
} }

View file

@ -459,7 +459,7 @@ class Database {
} }
} }
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int { public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = "", bool $discover = true): int {
if (!Arsse::$user->authorize($user, __FUNCTION__)) { if (!Arsse::$user->authorize($user, __FUNCTION__)) {
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
} }
@ -470,7 +470,7 @@ class Database {
$feedID = $this->db->prepare('INSERT INTO arsse_feeds(url,username,password) values(?,?,?)', 'str', 'str', 'str')->run($url, $fetchUser, $fetchPassword)->lastId(); $feedID = $this->db->prepare('INSERT INTO arsse_feeds(url,username,password) values(?,?,?)', 'str', 'str', 'str')->run($url, $fetchUser, $fetchPassword)->lastId();
try { try {
// perform an initial update on the newly added feed // perform an initial update on the newly added feed
$this->feedUpdate($feedID, true); $this->feedUpdate($feedID, true, $discover);
} catch (\Throwable $e) { } catch (\Throwable $e) {
// if the update fails, delete the feed we just added // if the update fails, delete the feed we just added
$this->db->prepare('DELETE from arsse_feeds where id is ?', 'int')->run($feedID); $this->db->prepare('DELETE from arsse_feeds where id is ?', 'int')->run($feedID);
@ -601,7 +601,7 @@ class Database {
return array_column($feeds, 'id'); return array_column($feeds, 'id');
} }
public function feedUpdate($feedID, bool $throwError = false): bool { public function feedUpdate($feedID, bool $throwError = false, bool $discover = false): bool {
$tr = $this->db->begin(); $tr = $this->db->begin();
// check to make sure the feed exists // check to make sure the feed exists
if (!ValueInfo::id($feedID)) { if (!ValueInfo::id($feedID)) {
@ -617,7 +617,7 @@ class Database {
// here. When an exception is thrown it should update the database with the // here. When an exception is thrown it should update the database with the
// error instead of failing; if other exceptions are thrown, we should simply roll back // error instead of failing; if other exceptions are thrown, we should simply roll back
try { try {
$feed = new Feed((int) $feedID, $f['url'], (string) Date::transform($f['modified'], "http", "sql"), $f['etag'], $f['username'], $f['password'], $scrape); $feed = new Feed((int) $feedID, $f['url'], (string) Date::transform($f['modified'], "http", "sql"), $f['etag'], $f['username'], $f['password'], $scrape, $discover);
if (!$feed->modified) { if (!$feed->modified) {
// if the feed hasn't changed, just compute the next fetch time and record it // if the feed hasn't changed, just compute the next fetch time and record it
$this->db->prepare("UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ? WHERE id is ?", 'datetime', 'int')->run($feed->nextFetch, $feedID); $this->db->prepare("UPDATE arsse_feeds SET updated = CURRENT_TIMESTAMP, next_fetch = ? WHERE id is ?", 'datetime', 'int')->run($feed->nextFetch, $feedID);

View file

@ -21,7 +21,7 @@ class Feed {
public $newItems = []; public $newItems = [];
public $changedItems = []; public $changedItems = [];
public function __construct(int $feedID = null, string $url, string $lastModified = '', string $etag = '', string $username = '', string $password = '', bool $scrape = false) { public function __construct(int $feedID = null, string $url, string $lastModified = '', string $etag = '', string $username = '', string $password = '', bool $scrape = false, bool $discover = false) {
// set the configuration // set the configuration
$userAgent = Arsse::$conf->fetchUserAgentString ?? sprintf('Arsse/%s (%s %s; %s; https://code.jkingweb.ca/jking/arsse) PicoFeed (https://github.com/fguillot/picoFeed)', $userAgent = Arsse::$conf->fetchUserAgentString ?? sprintf('Arsse/%s (%s %s; %s; https://code.jkingweb.ca/jking/arsse) PicoFeed (https://github.com/fguillot/picoFeed)',
VERSION, // Arsse version VERSION, // Arsse version
@ -36,7 +36,7 @@ class Feed {
$this->config->setClientUserAgent($userAgent); $this->config->setClientUserAgent($userAgent);
$this->config->setGrabberUserAgent($userAgent); $this->config->setGrabberUserAgent($userAgent);
// fetch the feed // fetch the feed
$this->download($url, $lastModified, $etag, $username, $password); $this->download($url, $lastModified, $etag, $username, $password, $discover);
// format the HTTP Last-Modified date returned // format the HTTP Last-Modified date returned
$lastMod = $this->resource->getLastModified(); $lastMod = $this->resource->getLastModified();
if (strlen($lastMod)) { if (strlen($lastMod)) {
@ -65,10 +65,11 @@ class Feed {
$this->nextFetch = $this->computeNextFetch(); $this->nextFetch = $this->computeNextFetch();
} }
protected function download(string $url, string $lastModified = '', string $etag = '', string $username = '', string $password = ''): bool { protected function download(string $url, string $lastModified, string $etag, string $username, string $password, bool $discover): bool {
$action = $discover ? "discover" : "download";
try { try {
$this->reader = new Reader($this->config); $this->reader = new Reader($this->config);
$this->resource = $this->reader->download($url, $lastModified, $etag, $username, $password); $this->resource = $this->reader->$action($url, $lastModified, $etag, $username, $password);
} catch (PicoFeedException $e) { } catch (PicoFeedException $e) {
throw new Feed\Exception($url, $e); throw new Feed\Exception($url, $e);
} }
@ -361,13 +362,13 @@ class Feed {
protected function computeLastModified() { protected function computeLastModified() {
if (!$this->modified) { if (!$this->modified) {
return $this->lastModified; return $this->lastModified; // @codeCoverageIgnore
} }
$dates = $this->gatherDates(); $dates = $this->gatherDates();
if (sizeof($dates)) { if (sizeof($dates)) {
return Date::normalize($dates[0]); return Date::normalize($dates[0]);
} else { } else {
return null; return null; // @codeCoverageIgnore
} }
} }

View file

@ -43,8 +43,14 @@ class REST {
$req->refreshURL(); $req->refreshURL();
$class = $this->apis[$api]['class']; $class = $this->apis[$api]['class'];
$drv = new $class(); $drv = new $class();
if ($req->head) {
$res = $drv->dispatch($req);
$res->head = true;
return $res;
} else {
return $drv->dispatch($req); return $drv->dispatch($req);
} }
}
public function apiMatch(string $url, array $map): string { public function apiMatch(string $url, array $map): string {
// sort the API list so the longest URL prefixes come first // sort the API list so the longest URL prefixes come first

View file

@ -11,7 +11,7 @@ class Versions implements \JKingWeb\Arsse\REST\Handler {
public function dispatch(\JKingWeb\Arsse\REST\Request $req): Response { public function dispatch(\JKingWeb\Arsse\REST\Request $req): Response {
// if a method other than GET was used, this is an error // if a method other than GET was used, this is an error
if ($req->method != "GET") { if ($req->method != "GET") {
return new Response(405); return new Response(405, "", "", ["Allow: GET"]);
} }
if (preg_match("<^/?$>", $req->path)) { if (preg_match("<^/?$>", $req->path)) {
// if the request path is an empty string or just a slash, return the supported versions // if the request path is an empty string or just a slash, return the supported versions

View file

@ -4,6 +4,7 @@ namespace JKingWeb\Arsse\REST;
class Request { class Request {
public $method = "GET"; public $method = "GET";
public $head = false;
public $url = ""; public $url = "";
public $path =""; public $path ="";
public $paths = []; public $paths = [];
@ -26,6 +27,10 @@ class Request {
$this->url = $url; $this->url = $url;
$this->body = $body; $this->body = $body;
$this->type = $contentType; $this->type = $contentType;
if ($this->method=="HEAD") {
$this->head = true;
$this->method = "GET";
}
$this->refreshURL(); $this->refreshURL();
} }

View file

@ -9,6 +9,7 @@ class Response {
const T_XML = "application/xml"; const T_XML = "application/xml";
const T_TEXT = "text/plain"; const T_TEXT = "text/plain";
public $head = false;
public $code; public $code;
public $payload; public $payload;
public $type; public $type;
@ -24,15 +25,11 @@ class Response {
public function output() { public function output() {
if (!headers_sent()) { if (!headers_sent()) {
try { foreach ($this->fields as $field) {
$statusText = Arsse::$lang->msg("HTTP.Status.".$this->code); header($field);
} catch (\JKingWeb\Arsse\Lang\Exception $e) {
$statusText = "";
} }
header("Status: ".$this->code." ".$statusText);
$body = ""; $body = "";
if (!is_null($this->payload)) { if (!is_null($this->payload)) {
header("Content-Type: ".$this->type);
switch ($this->type) { switch ($this->type) {
case self::T_JSON: case self::T_JSON:
$body = (string) json_encode($this->payload, \JSON_PRETTY_PRINT); $body = (string) json_encode($this->payload, \JSON_PRETTY_PRINT);
@ -42,10 +39,21 @@ class Response {
break; break;
} }
} }
foreach ($this->fields as $field) { if (strlen($body)) {
header($field); header("Content-Type: ".$this->type);
header("Content-Length: ".strlen($body));
} elseif ($this->code==200) {
$this->code = 204;
} }
try {
$statusText = Arsse::$lang->msg("HTTP.Status.".$this->code);
} catch (\JKingWeb\Arsse\Lang\Exception $e) {
$statusText = "";
}
header("Status: ".$this->code." ".$statusText);
if (!$this->head) {
echo $body; echo $body;
}
} else { } else {
throw new REST\Exception("headersSent"); throw new REST\Exception("headersSent");
} }

View file

@ -133,6 +133,15 @@ class TestFeed extends Test\AbstractTest {
$this->assertSame($categories, $f->data->items[5]->categories); $this->assertSame($categories, $f->data->items[5]->categories);
} }
public function testDiscoverAFeedSuccessfully() {
$this->assertInstanceOf(Feed::class, new Feed(null, $this->base."Discovery/Valid", "", "", "", "", false, true));
}
public function testDiscoverAFeedUnsuccessfully() {
$this->assertException("subscriptionNotFound", "Feed");
new Feed(null, $this->base."Discovery/Invalid", "", "", "", "", false, true);
}
public function testParseEntityExpansionAttack() { public function testParseEntityExpansionAttack() {
$this->assertException("xmlEntity", "Feed"); $this->assertException("xmlEntity", "Feed");
new Feed(null, $this->base."Parsing/XEEAttack"); new Feed(null, $this->base."Parsing/XEEAttack");

View file

@ -26,7 +26,7 @@ class TestNCNVersionDiscovery extends Test\AbstractTest {
} }
public function testUseIncorrectMethod() { public function testUseIncorrectMethod() {
$exp = new Response(405); $exp = new Response(405, "", "", ["Allow: GET"]);
$h = new REST\NextCloudNews\Versions(); $h = new REST\NextCloudNews\Versions();
$req = new Request("POST", "/"); $req = new Request("POST", "/");
$res = $h->dispatch($req); $res = $h->dispatch($req);

View file

@ -0,0 +1,12 @@
<?php return [
'mime' => "application/rss+xml",
'content' => <<<MESSAGE_BODY
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Test feed</title>
<link>http://example.com/</link>
<description>Example newsfeed title</description>
</channel>
</rss>
MESSAGE_BODY
];

View file

@ -0,0 +1,8 @@
<?php return [
'mime' => "text/html",
'content' => <<<MESSAGE_BODY
<html>
<title>Example article</title>
</html>
MESSAGE_BODY
];

View file

@ -0,0 +1,9 @@
<?php return [
'mime' => "text/html",
'content' => <<<MESSAGE_BODY
<html>
<title>Example article</title>
<link rel="alternate" type="application/rss+xml" href="http://localhost:8000/Feed/Discovery/Feed">
</html>
MESSAGE_BODY
];

View file

@ -135,7 +135,7 @@ trait SeriesSubscription {
Phake::when(Arsse::$db)->feedUpdate->thenReturn(true); Phake::when(Arsse::$db)->feedUpdate->thenReturn(true);
$this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url)); $this->assertSame($subID, Arsse::$db->subscriptionAdd($this->user, $url));
Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd"); Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd");
Phake::verify(Arsse::$db)->feedUpdate($feedID, true); Phake::verify(Arsse::$db)->feedUpdate($feedID, true, true);
$state = $this->primeExpectations($this->data, [ $state = $this->primeExpectations($this->data, [
'arsse_feeds' => ['id','url','username','password'], 'arsse_feeds' => ['id','url','username','password'],
'arsse_subscriptions' => ['id','owner','feed'], 'arsse_subscriptions' => ['id','owner','feed'],
@ -153,7 +153,7 @@ trait SeriesSubscription {
Arsse::$db->subscriptionAdd($this->user, $url); Arsse::$db->subscriptionAdd($this->user, $url);
} catch (FeedException $e) { } catch (FeedException $e) {
Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd"); Phake::verify(Arsse::$user)->authorize($this->user, "subscriptionAdd");
Phake::verify(Arsse::$db)->feedUpdate($feedID, true); Phake::verify(Arsse::$db)->feedUpdate($feedID, true, true);
$state = $this->primeExpectations($this->data, [ $state = $this->primeExpectations($this->data, [
'arsse_feeds' => ['id','url','username','password'], 'arsse_feeds' => ['id','url','username','password'],
'arsse_subscriptions' => ['id','owner','feed'], 'arsse_subscriptions' => ['id','owner','feed'],