refactor: extract frontpage to template (#3130)

Also introduce usage of Response object
This commit is contained in:
Dag 2022-11-07 18:22:54 +01:00 committed by GitHub
parent fe59cbabc9
commit 2ef98b299f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 147 additions and 225 deletions

View file

@ -38,8 +38,7 @@ class ConnectivityAction implements ActionInterface
}
if (!isset($request['bridge'])) {
print render_template('connectivity.html.php');
return;
return render_template('connectivity.html.php');
}
$bridgeClassName = $this->bridgeFactory->sanitizeBridgeName($request['bridge']);
@ -48,7 +47,7 @@ class ConnectivityAction implements ActionInterface
throw new \InvalidArgumentException('Bridge name invalid!');
}
$this->reportBridgeConnectivity($bridgeClassName);
return $this->reportBridgeConnectivity($bridgeClassName);
}
private function reportBridgeConnectivity($bridgeClassName)
@ -80,7 +79,6 @@ class ConnectivityAction implements ActionInterface
$retVal['successful'] = false;
}
header('Content-Type: text/json');
print Json::encode($retVal);
return new Response(Json::encode($retVal), 200, ['Content-Type' => 'text/json']);
}
}

View file

@ -44,8 +44,8 @@ class DetectAction implements ActionInterface
$bridgeParams['bridge'] = $bridgeClassName;
$bridgeParams['format'] = $format;
header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
return;
$url = '?action=display&' . http_build_query($bridgeParams);
return new Response('', 301, ['Location' => $url]);
}
throw new \Exception('No bridge found for given URL: ' . $targetURL);

View file

@ -52,8 +52,7 @@ class DisplayAction implements ActionInterface
if (! Configuration::getConfig('cache', 'custom_timeout')) {
unset($request['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($request);
header('Location: ' . $uri, true, 301);
return;
return new Response('', 301, ['Location' => $uri]);
}
$cache_timeout = filter_var($request['_cache_timeout'], FILTER_VALIDATE_INT);
@ -116,8 +115,8 @@ class DisplayAction implements ActionInterface
if ($mtime <= $stime) {
// Cached data is older or same
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
return;
$lastModified2 = gmdate('D, d M Y H:i:s ', $mtime) . 'GMT';
return new Response('', 304, ['Last-Modified' => $lastModified2]);
}
}
@ -197,11 +196,12 @@ class DisplayAction implements ActionInterface
$format->setExtraInfos($infos);
$lastModified = $cache->getTime();
$format->setLastModified($lastModified);
$headers = [];
if ($lastModified) {
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $lastModified) . 'GMT');
$headers['Last-Modified'] = gmdate('D, d M Y H:i:s ', $lastModified) . 'GMT';
}
header('Content-Type: ' . $format->getMimeType() . '; charset=' . $format->getCharset());
print $format->stringify();
$headers['Content-Type'] = $format->getMimeType() . '; charset=' . $format->getCharset();
return new Response($format->stringify(), 200, $headers);
}
private static function createGithubIssueUrl($bridge, $e, string $message): string

View file

@ -5,91 +5,7 @@ final class FrontpageAction implements ActionInterface
public function execute(array $request)
{
$showInactive = (bool) ($request['show_inactive'] ?? null);
$totalBridges = 0;
$totalActiveBridges = 0;
$html = self::getHead()
. self::getHeader()
. self::getSearchbar()
. self::getBridges($showInactive, $totalBridges, $totalActiveBridges)
. self::getFooter($totalBridges, $totalActiveBridges, $showInactive);
print $html;
}
private static function getHead()
{
return <<<EOD
<!DOCTYPE html><html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="RSS-Bridge" />
<title>RSS-Bridge</title>
<link href="static/style.css" rel="stylesheet">
<link rel="icon" type="image/png" href="static/favicon.png">
<script src="static/rss-bridge.js"></script>
<script>
document.addEventListener('DOMContentLoaded', rssbridge_toggle_bridge);
</script>
</head>
<body onload="rssbridge_list_search()">
<div class="container">
EOD;
}
private static function getHeader()
{
$warning = '';
if (Debug::isEnabled()) {
if (!Debug::isSecure()) {
$warning .= <<<EOD
<section class="critical-warning">Warning : Debug mode is active from any location,
make sure only you can access RSS-Bridge.</section>
EOD;
} else {
$warning .= <<<EOD
<section class="warning">Warning : Debug mode is active from your IP address,
your requests will bypass the cache.</section>
EOD;
}
}
return <<<EOD
<header>
<div class="logo"></div>
{$warning}
</header>
EOD;
}
private static function getSearchbar()
{
$query = filter_input(INPUT_GET, 'q', \FILTER_SANITIZE_SPECIAL_CHARS);
return <<<EOD
<section class="searchbar">
<h3>Search</h3>
<input
type="text"
name="searchfield"
id="searchfield"
placeholder="Insert URL or bridge name"
onchange="rssbridge_list_search()"
onkeyup="rssbridge_list_search()"
value="{$query}"
>
</section>
EOD;
}
private static function getBridges($showInactive, &$totalBridges, &$totalActiveBridges)
{
$body = '';
$totalActiveBridges = 0;
$inactiveBridges = '';
$activeBridges = 0;
$bridgeFactory = new BridgeFactory();
$bridgeClassNames = $bridgeFactory->getBridgeClassNames();
@ -97,58 +13,22 @@ EOD;
$formatFactory = new FormatFactory();
$formats = $formatFactory->getFormatNames();
$totalBridges = count($bridgeClassNames);
$body = '';
foreach ($bridgeClassNames as $bridgeClassName) {
if ($bridgeFactory->isWhitelisted($bridgeClassName)) {
$body .= BridgeCard::displayBridgeCard($bridgeClassName, $formats);
$totalActiveBridges++;
$activeBridges++;
} elseif ($showInactive) {
$inactiveBridges .= BridgeCard::displayBridgeCard($bridgeClassName, $formats, false) . PHP_EOL;
$body .= BridgeCard::displayBridgeCard($bridgeClassName, $formats, false) . PHP_EOL;
}
}
$body .= $inactiveBridges;
return $body;
}
private static function getFooter($totalBridges, $totalActiveBridges, $showInactive)
{
$version = Configuration::getVersion();
$email = Configuration::getConfig('admin', 'email');
$admininfo = '';
if ($email) {
$admininfo = <<<EOD
<br />
<span>
You may email the administrator of this RSS-Bridge instance
at <a href="mailto:{$email}">{$email}</a>
</span>
EOD;
}
$inactive = '';
if ($totalActiveBridges !== $totalBridges) {
if ($showInactive) {
$inactive = '<a href="?show_inactive=0"><button class="small">Hide inactive bridges</button></a><br>';
} else {
$inactive = '<a href="?show_inactive=1"><button class="small">Show inactive bridges</button></a><br>';
}
}
return <<<EOD
<section class="footer">
<a href="https://github.com/rss-bridge/rss-bridge">RSS-Bridge ~ Public Domain</a><br>
<p class="version">{$version}</p>
{$totalActiveBridges}/{$totalBridges} active bridges.<br>
{$inactive}
{$admininfo}
</section>
</div>
</body></html>
EOD;
return render(__DIR__ . '/../templates/frontpage.html.php', [
'admin_email' => Configuration::getConfig('admin', 'email'),
'bridges' => $body,
'active_bridges' => $activeBridges,
'total_bridges' => count($bridgeClassNames),
'show_inactive' => $showInactive,
]);
}
}

View file

@ -36,10 +36,7 @@ class ListAction implements ActionInterface
'description' => $bridge->getDescription()
];
}
$list->total = count($list->bridges);
header('Content-Type: application/json');
print Json::encode($list);
return new Response(Json::encode($list), 200, ['Content-Type' => 'application/json']);
}
}

View file

@ -1,38 +0,0 @@
<?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class ActionFactory
{
private $folder;
public function __construct(string $folder = PATH_LIB_ACTIONS)
{
$this->folder = $folder;
}
/**
* @param string $name The name of the action e.g. "Display", "List", or "Connectivity"
*/
public function create(string $name): ActionInterface
{
$name = strtolower($name) . 'Action';
$name = implode(array_map('ucfirst', explode('-', $name)));
$filePath = $this->folder . $name . '.php';
if (!file_exists($filePath)) {
throw new \Exception('Invalid action');
}
$className = '\\' . $name;
return new $className();
}
}

View file

@ -22,7 +22,7 @@ interface ActionInterface
*
* Note: This function directly outputs data to the user.
*
* @return void
* @return ?string
*/
public function execute(array $request);
}

View file

@ -60,6 +60,7 @@ final class RssBridge
}
});
// Consider: ini_set('error_reporting', E_ALL & ~E_DEPRECATED);
date_default_timezone_set(Configuration::getConfig('system', 'timezone'));
$authenticationMiddleware = new AuthenticationMiddleware();
@ -73,9 +74,22 @@ final class RssBridge
}
}
$actionFactory = new ActionFactory();
$action = $request['action'] ?? 'Frontpage';
$action = $actionFactory->create($action);
$action->execute($request);
$actionName = $request['action'] ?? 'Frontpage';
$actionName = strtolower($actionName) . 'Action';
$actionName = implode(array_map('ucfirst', explode('-', $actionName)));
$filePath = __DIR__ . '/../actions/' . $actionName . '.php';
if (!file_exists($filePath)) {
throw new \Exception(sprintf('Invalid action: %s', $actionName));
}
$className = '\\' . $actionName;
$action = new $className();
$response = $action->execute($request);
if (is_string($response)) {
print $response;
} elseif ($response instanceof Response) {
$response->send();
}
}
}

View file

@ -44,6 +44,38 @@ final class Response
'504' => 'Gateway Timeout',
'505' => 'HTTP Version Not Supported'
];
private string $body;
private int $code;
private array $headers;
public function __construct(
string $body = '',
int $code = 200,
array $headers = []
) {
$this->body = $body;
$this->code = $code;
$this->headers = $headers;
}
public function getBody()
{
return $this->body;
}
public function getHeaders()
{
return $this->headers;
}
public function send(): void
{
http_response_code($this->code);
foreach ($this->headers as $name => $value) {
header(sprintf('%s: %s', $name, $value));
}
print $this->body;
}
}
/**

View file

@ -78,12 +78,12 @@ header > div.logo {
margin: auto;
}
header > section.warning {
section.warning {
background-color: #ffc600;
color: #5f5f5f;
}
header > section.critical-warning {
section.critical-warning {
background-color: #cf3e3e;
font-weight: bold;
color: white;

View file

@ -0,0 +1,62 @@
<script src="static/rss-bridge.js"></script>
<script>
document.addEventListener('DOMContentLoaded', rssbridge_toggle_bridge);
document.addEventListener('DOMContentLoaded', rssbridge_list_search);
</script>
<?php if (Debug::isEnabled()): ?>
<?php if (!Debug::isSecure()): ?>
<section class="critical-warning">
Warning : Debug mode is active from any location,
make sure only you can access RSS-Bridge.
</section>
<?php else: ?>
<section class="warning">
Warning : Debug mode is active from your IP address,
your requests will bypass the cache.
</section>
<?php endif; ?>
<?php endif; ?>
<section class="searchbar">
<h3>Search</h3>
<input
type="text"
name="searchfield"
id="searchfield"
placeholder="Insert URL or bridge name"
onchange="rssbridge_list_search()"
onkeyup="rssbridge_list_search()"
value=""
>
</section>
<?= raw($bridges) ?>
<section class="footer">
<a href="https://github.com/rss-bridge/rss-bridge">RSS-Bridge ~ Public Domain</a><br>
<p class="version"><?= e(Configuration::getVersion()) ?></p>
<?= $active_bridges ?>/<?= $total_bridges ?> active bridges.<br>
<?php if ($active_bridges !== $total_bridges): ?>
<?php if ($show_inactive): ?>
<a href="?show_inactive=0">
<button class="small">Hide inactive bridges</button>
</a>
<br>
<?php else: ?>
<a href="?show_inactive=1">
<button class="small">Show inactive bridges</button>
</a>
<br>
<?php endif; ?>
<?php endif; ?>
<?php if ($admin_email): ?>
<span>
You may email the administrator of this RSS-Bridge instance at
<a href="mailto:<?= e($admin_email) ?>"><?= e($admin_email) ?></a>
</span>
<?php endif; ?>
</section>

View file

@ -2,36 +2,26 @@
namespace RssBridge\Tests\Actions;
use ActionFactory;
use BridgeFactory;
use PHPUnit\Framework\TestCase;
class ListActionTest extends TestCase
{
private $data;
/**
* @runInSeparateProcess
* @requires function xdebug_get_headers
*/
public function testHeaders()
{
$this->initAction();
$this->assertContains(
'Content-Type: application/json',
xdebug_get_headers()
);
$action = new \ListAction();
$response = $action->execute([]);
$headers = $response->getHeaders();
$this->assertSame($headers['Content-Type'], 'application/json');
}
/**
* @runInSeparateProcess
*/
public function testOutput()
{
$this->initAction();
$action = new \ListAction();
$response = $action->execute([]);
$data = $response->getBody();
$items = json_decode($this->data, true);
$items = json_decode($data, true);
$this->assertNotNull($items, 'invalid JSON output: ' . json_last_error_msg());
@ -77,17 +67,4 @@ class ListActionTest extends TestCase
$this->assertContains($bridge['status'], $allowedStatus, 'Invalid status value');
}
}
private function initAction()
{
$actionFactory = new ActionFactory();
$action = $actionFactory->create('list');
ob_start();
$action->execute([]);
$this->data = ob_get_contents();
ob_clean();
ob_end_flush();
}
}