aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Dag <me@dvikan.no> 2022-08-06 22:46:28 +0200
committerGravatar GitHub <noreply@github.com> 2022-08-06 22:46:28 +0200
commit2bbce8ebef8cf4f88392431aabe84a15482dc933 (patch)
tree1f5027ca69b1dfa2364bd9319e8536b86a41e928
parentb042412416cc4ecc71c3f9c13239661a0dd588a6 (diff)
downloadrss-bridge-2bbce8ebef8cf4f88392431aabe84a15482dc933.tar.gz
rss-bridge-2bbce8ebef8cf4f88392431aabe84a15482dc933.tar.zst
rss-bridge-2bbce8ebef8cf4f88392431aabe84a15482dc933.zip
refactor: general code base refactor (#2950)
* refactor * fix: bug in previous refactor * chore: exclude phpcompat sniff due to bug in phpcompat * fix: do not leak absolute paths * refactor/fix: batch extensions checking, fix DOS issue
-rw-r--r--actions/ConnectivityAction.php75
-rw-r--r--actions/DetectAction.php22
-rw-r--r--actions/DisplayAction.php54
-rw-r--r--actions/ListAction.php18
-rw-r--r--bridges/FDroidRepoBridge.php40
-rw-r--r--bridges/PirateCommunityBridge.php4
-rw-r--r--bridges/RedditBridge.php4
-rw-r--r--bridges/WordPressBridge.php2
-rw-r--r--caches/FileCache.php67
-rw-r--r--caches/MemcachedCache.php36
-rw-r--r--caches/SQLiteCache.php38
-rw-r--r--composer.json1
-rw-r--r--config.default.ini.php8
-rw-r--r--docs/08_Format_API/02_FormatInterface.md2
-rw-r--r--docs/08_Format_API/03_FormatAbstract.md2
-rw-r--r--formats/AtomFormat.php34
-rw-r--r--formats/HtmlFormat.php67
-rw-r--r--formats/JsonFormat.php23
-rw-r--r--formats/MrssFormat.php33
-rw-r--r--formats/PlaintextFormat.php4
-rw-r--r--index.php22
-rw-r--r--lib/Authentication.php89
-rw-r--r--lib/AuthenticationMiddleware.php42
-rw-r--r--lib/BridgeAbstract.php52
-rw-r--r--lib/BridgeCard.php177
-rw-r--r--lib/BridgeFactory.php4
-rw-r--r--lib/BridgeList.php54
-rw-r--r--lib/CacheInterface.php2
-rw-r--r--lib/Configuration.php49
-rw-r--r--lib/Debug.php4
-rw-r--r--lib/FeedExpander.php32
-rw-r--r--lib/FeedItem.php22
-rw-r--r--lib/FormatAbstract.php29
-rw-r--r--lib/ParameterValidator.php10
-rw-r--r--lib/XPathAbstract.php36
-rw-r--r--lib/contents.php72
-rw-r--r--lib/error.php35
-rw-r--r--lib/html.php9
-rw-r--r--lib/rssbridge.php55
-rw-r--r--lib/utils.php123
-rw-r--r--phpcompatibility.xml2
-rw-r--r--phpcs.xml1
-rw-r--r--templates/connectivity.html.php29
-rw-r--r--templates/error.html.php3
-rw-r--r--tests/UtilsTest.php15
45 files changed, 677 insertions, 825 deletions
diff --git a/actions/ConnectivityAction.php b/actions/ConnectivityAction.php
index ac86fa1b..9ebd640c 100644
--- a/actions/ConnectivityAction.php
+++ b/actions/ConnectivityAction.php
@@ -28,23 +28,21 @@ class ConnectivityAction implements ActionInterface
public function __construct()
{
- $this->bridgeFactory = new \BridgeFactory();
+ $this->bridgeFactory = new BridgeFactory();
}
public function execute(array $request)
{
if (!Debug::isEnabled()) {
- returnError('This action is only available in debug mode!', 400);
+ throw new \Exception('This action is only available in debug mode!');
}
if (!isset($request['bridge'])) {
- $this->returnEntryPage();
+ print render_template('connectivity.html.php');
return;
}
- $bridgeName = $request['bridge'];
-
- $bridgeClassName = $this->bridgeFactory->sanitizeBridgeName($bridgeName);
+ $bridgeClassName = $this->bridgeFactory->sanitizeBridgeName($request['bridge']);
if ($bridgeClassName === null) {
throw new \InvalidArgumentException('Bridge name invalid!');
@@ -53,28 +51,12 @@ class ConnectivityAction implements ActionInterface
$this->reportBridgeConnectivity($bridgeClassName);
}
- /**
- * Generates a report about the bridge connectivity status and sends it back
- * to the user.
- *
- * The report is generated as Json-formatted string in the format
- * {
- * "bridge": "<bridge-name>",
- * "successful": true/false
- * }
- *
- * @param class-string<BridgeInterface> $bridgeClassName Name of the bridge to generate the report for
- * @return void
- */
private function reportBridgeConnectivity($bridgeClassName)
{
if (!$this->bridgeFactory->isWhitelisted($bridgeClassName)) {
- header('Content-Type: text/html');
- returnServerError('Bridge is not whitelisted!');
+ throw new \Exception('Bridge is not whitelisted!');
}
- header('Content-Type: text/json');
-
$retVal = [
'bridge' => $bridgeClassName,
'successful' => false,
@@ -82,16 +64,9 @@ class ConnectivityAction implements ActionInterface
];
$bridge = $this->bridgeFactory->create($bridgeClassName);
-
- if ($bridge === false) {
- echo json_encode($retVal);
- return;
- }
-
$curl_opts = [
CURLOPT_CONNECTTIMEOUT => 5
];
-
try {
$reply = getContents($bridge::URI, [], $curl_opts, true);
@@ -101,45 +76,11 @@ class ConnectivityAction implements ActionInterface
$retVal['http_code'] = 301;
}
}
- } catch (Exception $e) {
+ } catch (\Exception $e) {
$retVal['successful'] = false;
}
- echo json_encode($retVal);
- }
-
- private function returnEntryPage()
- {
- echo <<<EOD
-<!DOCTYPE html>
-
-<html>
- <head>
- <link rel="stylesheet" href="static/bootstrap.min.css">
- <link
- rel="stylesheet"
- href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
- integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
- crossorigin="anonymous">
- <link rel="stylesheet" href="static/connectivity.css">
- <script src="static/connectivity.js" type="text/javascript"></script>
- </head>
- <body>
- <div id="main-content" class="container">
- <div class="progress">
- <div class="progress-bar" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"></div>
- </div>
- <div id="status-message" class="sticky-top alert alert-primary alert-dismissible fade show" role="alert">
- <i id="status-icon" class="fas fa-sync"></i>
- <span>...</span>
- <button type="button" class="close" data-dismiss="alert" aria-label="Close" onclick="stopConnectivityChecks()">
- <span aria-hidden="true">&times;</span>
- </button>
- </div>
- <input type="text" class="form-control" id="search" onkeyup="search()" placeholder="Search for bridge..">
- </div>
- </body>
-</html>
-EOD;
+ header('Content-Type: text/json');
+ print Json::encode($retVal);
}
}
diff --git a/actions/DetectAction.php b/actions/DetectAction.php
index 1ed002af..71060bb8 100644
--- a/actions/DetectAction.php
+++ b/actions/DetectAction.php
@@ -16,13 +16,17 @@ class DetectAction implements ActionInterface
{
public function execute(array $request)
{
- $targetURL = $request['url']
- or returnClientError('You must specify a url!');
+ $targetURL = $request['url'] ?? null;
+ $format = $request['format'] ?? null;
- $format = $request['format']
- or returnClientError('You must specify a format!');
+ if (!$targetURL) {
+ throw new \Exception('You must specify a url!');
+ }
+ if (!$format) {
+ throw new \Exception('You must specify a format!');
+ }
- $bridgeFactory = new \BridgeFactory();
+ $bridgeFactory = new BridgeFactory();
foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
if (!$bridgeFactory->isWhitelisted($bridgeClassName)) {
@@ -31,10 +35,6 @@ class DetectAction implements ActionInterface
$bridge = $bridgeFactory->create($bridgeClassName);
- if ($bridge === false) {
- continue;
- }
-
$bridgeParams = $bridge->detectParameters($targetURL);
if (is_null($bridgeParams)) {
@@ -45,9 +45,9 @@ class DetectAction implements ActionInterface
$bridgeParams['format'] = $format;
header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
- exit;
+ return;
}
- returnClientError('No bridge found for given URL: ' . $targetURL);
+ throw new \Exception('No bridge found for given URL: ' . $targetURL);
}
}
diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php
index c2f536d1..e8912f09 100644
--- a/actions/DisplayAction.php
+++ b/actions/DisplayAction.php
@@ -16,7 +16,7 @@ class DisplayAction implements ActionInterface
{
public function execute(array $request)
{
- $bridgeFactory = new \BridgeFactory();
+ $bridgeFactory = new BridgeFactory();
$bridgeClassName = null;
if (isset($request['bridge'])) {
@@ -27,16 +27,14 @@ class DisplayAction implements ActionInterface
throw new \InvalidArgumentException('Bridge name invalid!');
}
- $format = $request['format']
- or returnClientError('You must specify a format!');
-
- // whitelist control
+ $format = $request['format'] ?? null;
+ if (!$format) {
+ throw new \Exception('You must specify a format!');
+ }
if (!$bridgeFactory->isWhitelisted($bridgeClassName)) {
- throw new \Exception('This bridge is not whitelisted', 401);
- die;
+ throw new \Exception('This bridge is not whitelisted');
}
- // Data retrieval
$bridge = $bridgeFactory->create($bridgeClassName);
$bridge->loadConfiguration();
@@ -47,14 +45,12 @@ class DisplayAction implements ActionInterface
define('NOPROXY', true);
}
- // Cache timeout
- $cache_timeout = -1;
if (array_key_exists('_cache_timeout', $request)) {
if (!CUSTOM_CACHE_TIMEOUT) {
unset($request['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($request);
header('Location: ' . $uri, true, 301);
- exit;
+ return;
}
$cache_timeout = filter_var($request['_cache_timeout'], FILTER_VALIDATE_INT);
@@ -93,7 +89,6 @@ class DisplayAction implements ActionInterface
)
);
- // Initialize cache
$cacheFactory = new CacheFactory();
$cache = $cacheFactory->create();
@@ -109,15 +104,17 @@ class DisplayAction implements ActionInterface
$mtime !== false
&& (time() - $cache_timeout < $mtime)
&& !Debug::isEnabled()
- ) { // Load cached data
+ ) {
+ // Load cached data
// Send "Not Modified" response if client supports it
// Implementation based on https://stackoverflow.com/a/10847262
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
- if ($mtime <= $stime) { // Cached data is older or same
+ if ($mtime <= $stime) {
+ // Cached data is older or same
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
- exit;
+ return;
}
}
@@ -125,27 +122,24 @@ class DisplayAction implements ActionInterface
if (isset($cached['items']) && isset($cached['extraInfos'])) {
foreach ($cached['items'] as $item) {
- $items[] = new \FeedItem($item);
+ $items[] = new FeedItem($item);
}
$infos = $cached['extraInfos'];
}
- } else { // Collect new data
+ } else {
+ // Collect new data
try {
$bridge->setDatas($bridge_params);
$bridge->collectData();
$items = $bridge->getItems();
- // Transform "legacy" items to FeedItems if necessary.
- // Remove this code when support for "legacy" items ends!
if (isset($items[0]) && is_array($items[0])) {
$feedItems = [];
-
foreach ($items as $item) {
- $feedItems[] = new \FeedItem($item);
+ $feedItems[] = new FeedItem($item);
}
-
$items = $feedItems;
}
@@ -158,18 +152,16 @@ class DisplayAction implements ActionInterface
} catch (\Throwable $e) {
error_log($e);
- if (logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) {
+ $errorCount = logBridgeError($bridge::NAME, $e->getCode());
+
+ if ($errorCount >= Configuration::getConfig('error', 'report_limit')) {
if (Configuration::getConfig('error', 'output') === 'feed') {
- $item = new \FeedItem();
+ $item = new FeedItem();
// Create "new" error message every 24 hours
$request['_error_time'] = urlencode((int)(time() / 86400));
- $message = sprintf(
- 'Bridge returned error %s! (%s)',
- $e->getCode(),
- $request['_error_time']
- );
+ $message = sprintf('Bridge returned error %s! (%s)', $e->getCode(), $request['_error_time']);
$item->setTitle($message);
$item->setURI(
@@ -205,8 +197,8 @@ class DisplayAction implements ActionInterface
}
$cache->saveData([
- 'items' => array_map(function ($i) {
- return $i->toArray();
+ 'items' => array_map(function (FeedItem $item) {
+ return $item->toArray();
}, $items),
'extraInfos' => $infos
]);
diff --git a/actions/ListAction.php b/actions/ListAction.php
index 5076b6dc..e2b0ccb9 100644
--- a/actions/ListAction.php
+++ b/actions/ListAction.php
@@ -16,27 +16,17 @@ class ListAction implements ActionInterface
{
public function execute(array $request)
{
- $list = new StdClass();
+ $list = new \stdClass();
$list->bridges = [];
$list->total = 0;
- $bridgeFactory = new \BridgeFactory();
+ $bridgeFactory = new BridgeFactory();
foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
$bridge = $bridgeFactory->create($bridgeClassName);
- if ($bridge === false) { // Broken bridge, show as inactive
- $list->bridges[$bridgeClassName] = [
- 'status' => 'inactive'
- ];
-
- continue;
- }
-
- $status = $bridgeFactory->isWhitelisted($bridgeClassName) ? 'active' : 'inactive';
-
$list->bridges[$bridgeClassName] = [
- 'status' => $status,
+ 'status' => $bridgeFactory->isWhitelisted($bridgeClassName) ? 'active' : 'inactive',
'uri' => $bridge->getURI(),
'donationUri' => $bridge->getDonationURI(),
'name' => $bridge->getName(),
@@ -50,6 +40,6 @@ class ListAction implements ActionInterface
$list->total = count($list->bridges);
header('Content-Type: application/json');
- echo json_encode($list, JSON_PRETTY_PRINT);
+ print Json::encode($list);
}
}
diff --git a/bridges/FDroidRepoBridge.php b/bridges/FDroidRepoBridge.php
index 74147310..7ce41baf 100644
--- a/bridges/FDroidRepoBridge.php
+++ b/bridges/FDroidRepoBridge.php
@@ -43,6 +43,25 @@ class FDroidRepoBridge extends BridgeAbstract
// Stores repo information
private $repo;
+ public function collectData()
+ {
+ if (!extension_loaded('zip')) {
+ throw new \Exception('FDroidRepoBridge requires the php-zip extension');
+ }
+
+ $this->repo = $this->getRepo();
+ switch ($this->queriedContext) {
+ case 'Latest Updates':
+ $this->getAllUpdates();
+ break;
+ case 'Follow Package':
+ $this->getPackage($this->getInput('package'));
+ break;
+ default:
+ returnServerError('Unimplemented Context (collectData)');
+ }
+ }
+
public function getURI()
{
if (empty($this->queriedContext)) {
@@ -70,21 +89,6 @@ class FDroidRepoBridge extends BridgeAbstract
}
}
- public function collectData()
- {
- $this->repo = $this->getRepo();
- switch ($this->queriedContext) {
- case 'Latest Updates':
- $this->getAllUpdates();
- break;
- case 'Follow Package':
- $this->getPackage($this->getInput('package'));
- break;
- default:
- returnServerError('Unimplemented Context (collectData)');
- }
- }
-
private function getRepo()
{
$url = $this->getURI();
@@ -95,9 +99,10 @@ class FDroidRepoBridge extends BridgeAbstract
file_put_contents($jar_loc, $jar);
// JAR files are specially formatted ZIP files
- $jar = new ZipArchive();
+ $jar = new \ZipArchive();
if ($jar->open($jar_loc) !== true) {
- returnServerError('Failed to extract archive');
+ unlink($jar_loc);
+ throw new \Exception('Failed to extract archive');
}
// Get file pointer to the relevant JSON inside
@@ -109,6 +114,7 @@ class FDroidRepoBridge extends BridgeAbstract
$data = json_decode(stream_get_contents($fp), true);
fclose($fp);
$jar->close();
+ unlink($jar_loc);
return $data;
}
diff --git a/bridges/PirateCommunityBridge.php b/bridges/PirateCommunityBridge.php
index a1a9d8f5..5a617b04 100644
--- a/bridges/PirateCommunityBridge.php
+++ b/bridges/PirateCommunityBridge.php
@@ -22,7 +22,9 @@ class PirateCommunityBridge extends BridgeAbstract
{
$parsed_url = parse_url($url);
- if ($parsed_url['host'] !== 'raymanpc.com') {
+ $host = $parsed_url['host'] ?? null;
+
+ if ($host !== 'raymanpc.com') {
return null;
}
diff --git a/bridges/RedditBridge.php b/bridges/RedditBridge.php
index e2303987..f02f46a2 100644
--- a/bridges/RedditBridge.php
+++ b/bridges/RedditBridge.php
@@ -71,7 +71,9 @@ class RedditBridge extends BridgeAbstract
{
$parsed_url = parse_url($url);
- if ($parsed_url['host'] != 'www.reddit.com' && $parsed_url['host'] != 'old.reddit.com') {
+ $host = $parsed_url['host'] ?? null;
+
+ if ($host != 'www.reddit.com' && $host != 'old.reddit.com') {
return null;
}
diff --git a/bridges/WordPressBridge.php b/bridges/WordPressBridge.php
index 5a80c398..ca004547 100644
--- a/bridges/WordPressBridge.php
+++ b/bridges/WordPressBridge.php
@@ -72,7 +72,7 @@ class WordPressBridge extends FeedExpander
} else {
$article_image = $article_image->getAttribute('data-lazy-src');
}
- $mime_type = getMimeType($article_image);
+ $mime_type = parse_mime_type($article_image);
if (strpos($mime_type, 'image') === false) {
$article_image .= '#.image'; // force image
}
diff --git a/caches/FileCache.php b/caches/FileCache.php
index 29f4d78b..fde7d18d 100644
--- a/caches/FileCache.php
+++ b/caches/FileCache.php
@@ -1,8 +1,5 @@
<?php
-/**
-* Cache with file system
-*/
class FileCache implements CacheInterface
{
protected $path;
@@ -11,10 +8,7 @@ class FileCache implements CacheInterface
public function __construct()
{
if (!is_writable(PATH_CACHE)) {
- returnServerError(
- 'RSS-Bridge does not have write permissions for '
- . PATH_CACHE . '!'
- );
+ throw new \Exception('The cache folder is not writeable');
}
}
@@ -23,20 +17,15 @@ class FileCache implements CacheInterface
if (file_exists($this->getCacheFile())) {
return unserialize(file_get_contents($this->getCacheFile()));
}
-
return null;
}
public function saveData($data)
{
- // Notice: We use plain serialize() here to reduce memory footprint on
- // large input data.
$writeStream = file_put_contents($this->getCacheFile(), serialize($data));
-
if ($writeStream === false) {
throw new \Exception('Cannot write the cache... Do you have the right permissions ?');
}
-
return $this;
}
@@ -46,7 +35,10 @@ class FileCache implements CacheInterface
clearstatcache(false, $cacheFile);
if (file_exists($cacheFile)) {
$time = filemtime($cacheFile);
- return ($time !== false) ? $time : null;
+ if ($time !== false) {
+ return $time;
+ }
+ return null;
}
return null;
@@ -55,28 +47,25 @@ class FileCache implements CacheInterface
public function purgeCache($seconds)
{
$cachePath = $this->getPath();
- if (file_exists($cachePath)) {
- $cacheIterator = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator($cachePath),
- RecursiveIteratorIterator::CHILD_FIRST
- );
-
- foreach ($cacheIterator as $cacheFile) {
- if (in_array($cacheFile->getBasename(), ['.', '..', '.gitkeep'])) {
- continue;
- } elseif ($cacheFile->isFile()) {
- if (filemtime($cacheFile->getPathname()) < time() - $seconds) {
- unlink($cacheFile->getPathname());
- }
+ if (!file_exists($cachePath)) {
+ return;
+ }
+ $cacheIterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($cachePath),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($cacheIterator as $cacheFile) {
+ if (in_array($cacheFile->getBasename(), ['.', '..', '.gitkeep'])) {
+ continue;
+ } elseif ($cacheFile->isFile()) {
+ if (filemtime($cacheFile->getPathname()) < time() - $seconds) {
+ unlink($cacheFile->getPathname());
}
}
}
}
- /**
- * Set scope
- * @return self
- */
public function setScope($scope)
{
if (is_null($scope) || !is_string($scope)) {
@@ -88,10 +77,6 @@ class FileCache implements CacheInterface
return $this;
}
- /**
- * Set key
- * @return self
- */
public function setKey($key)
{
if (!empty($key) && is_array($key)) {
@@ -107,10 +92,6 @@ class FileCache implements CacheInterface
return $this;
}
- /**
- * Return cache path (and create if not exist)
- * @return string Cache path
- */
private function getPath()
{
if (is_null($this->path)) {
@@ -119,26 +100,18 @@ class FileCache implements CacheInterface
if (!is_dir($this->path)) {
if (mkdir($this->path, 0755, true) !== true) {
- throw new \Exception('Unable to create ' . $this->path);
+ throw new \Exception('mkdir: Unable to create file cache folder');
}
}
return $this->path;
}
- /**
- * Get the file name use for cache store
- * @return string Path to the file cache
- */
private function getCacheFile()
{
return $this->getPath() . $this->getCacheName();
}
- /**
- * Determines file name for store the cache
- * return string
- */
private function getCacheName()
{
if (is_null($this->key)) {
diff --git a/caches/MemcachedCache.php b/caches/MemcachedCache.php
index 42a8ddad..c593ac79 100644
--- a/caches/MemcachedCache.php
+++ b/caches/MemcachedCache.php
@@ -12,29 +12,33 @@ class MemcachedCache implements CacheInterface
public function __construct()
{
if (!extension_loaded('memcached')) {
- returnServerError('"memcached" extension not loaded. Please check "php.ini"');
+ throw new \Exception('"memcached" extension not loaded. Please check "php.ini"');
}
$section = 'MemcachedCache';
$host = Configuration::getConfig($section, 'host');
$port = Configuration::getConfig($section, 'port');
+
if (empty($host) && empty($port)) {
- returnServerError('Configuration for ' . $section . ' missing. Please check your ' . FILE_CONFIG);
- } elseif (empty($host)) {
- returnServerError('"host" param is not set for ' . $section . '. Please check your ' . FILE_CONFIG);
- } elseif (empty($port)) {
- returnServerError('"port" param is not set for ' . $section . '. Please check your ' . FILE_CONFIG);
- } elseif (!ctype_digit($port)) {
- returnServerError('"port" param is invalid for ' . $section . '. Please check your ' . FILE_CONFIG);
+ throw new \Exception('Configuration for ' . $section . ' missing. Please check your ' . FILE_CONFIG);
+ }
+ if (empty($host)) {
+ throw new \Exception('"host" param is not set for ' . $section . '. Please check your ' . FILE_CONFIG);
+ }
+ if (empty($port)) {
+ throw new \Exception('"port" param is not set for ' . $section . '. Please check your ' . FILE_CONFIG);
+ }
+ if (!ctype_digit($port)) {
+ throw new \Exception('"port" param is invalid for ' . $section . '. Please check your ' . FILE_CONFIG);
}
$port = intval($port);
if ($port < 1 || $port > 65535) {
- returnServerError('"port" param is invalid for ' . $section . '. Please check your ' . FILE_CONFIG);
+ throw new \Exception('"port" param is invalid for ' . $section . '. Please check your ' . FILE_CONFIG);
}
- $conn = new Memcached();
+ $conn = new \Memcached();
$conn->addServer($host, $port) or returnServerError('Could not connect to memcached server');
$this->conn = $conn;
}
@@ -64,7 +68,7 @@ class MemcachedCache implements CacheInterface
$result = $this->conn->set($this->getCacheKey(), $object_to_save, $this->expiration);
if ($result === false) {
- returnServerError('Cannot write the cache to memcached server');
+ throw new \Exception('Cannot write the cache to memcached server');
}
$this->time = $time;
@@ -87,20 +91,12 @@ class MemcachedCache implements CacheInterface
$this->expiration = $duration;
}
- /**
- * Set scope
- * @return self
- */
public function setScope($scope)
{
$this->scope = $scope;
return $this;
}
- /**
- * Set key
- * @return self
- */
public function setKey($key)
{
if (!empty($key) && is_array($key)) {
@@ -119,7 +115,7 @@ class MemcachedCache implements CacheInterface
private function getCacheKey()
{
if (is_null($this->key)) {
- returnServerError('Call "setKey" first!');
+ throw new \Exception('Call "setKey" first!');
}
return 'rss_bridge_cache_' . hash('md5', $this->scope . $this->key . 'A');
diff --git a/caches/SQLiteCache.php b/caches/SQLiteCache.php
index 2f798714..339e4119 100644
--- a/caches/SQLiteCache.php
+++ b/caches/SQLiteCache.php
@@ -13,38 +13,32 @@ class SQLiteCache implements CacheInterface
public function __construct()
{
if (!extension_loaded('sqlite3')) {
- print render('error.html.php', ['message' => '"sqlite3" extension not loaded. Please check "php.ini"']);
- exit;
+ throw new \Exception('"sqlite3" extension not loaded. Please check "php.ini"');
}
if (!is_writable(PATH_CACHE)) {
- returnServerError(
- 'RSS-Bridge does not have write permissions for '
- . PATH_CACHE . '!'
- );
+ throw new \Exception('The cache folder is not writable');
}
$section = 'SQLiteCache';
$file = Configuration::getConfig($section, 'file');
if (empty($file)) {
- $message = sprintf('Configuration for %s missing. Please check your %s', $section, FILE_CONFIG);
- print render('error.html.php', ['message' => $message]);
- exit;
+ throw new \Exception(sprintf('Configuration for %s missing.', $section));
}
+
if (dirname($file) == '.') {
$file = PATH_CACHE . $file;
} elseif (!is_dir(dirname($file))) {
- $message = sprintf('Invalid configuration for %s. Please check your %s', $section, FILE_CONFIG);
- print render('error.html.php', ['message' => $message]);
- exit;
+ throw new \Exception(sprintf('Invalid configuration for %s', $section));
}
if (!is_file($file)) {
- $this->db = new SQLite3($file);
+ // The instantiation creates the file
+ $this->db = new \SQLite3($file);
$this->db->enableExceptions(true);
$this->db->exec("CREATE TABLE storage ('key' BLOB PRIMARY KEY, 'value' BLOB, 'updated' INTEGER)");
} else {
- $this->db = new SQLite3($file);
+ $this->db = new \SQLite3($file);
$this->db->enableExceptions(true);
}
$this->db->busyTimeout(5000);
@@ -55,8 +49,8 @@ class SQLiteCache implements CacheInterface
$Qselect = $this->db->prepare('SELECT value FROM storage WHERE key = :key');
$Qselect->bindValue(':key', $this->getCacheKey());
$result = $Qselect->execute();
- if ($result instanceof SQLite3Result) {
- $data = $result->fetchArray(SQLITE3_ASSOC);
+ if ($result instanceof \SQLite3Result) {
+ $data = $result->fetchArray(\SQLITE3_ASSOC);
if (isset($data['value'])) {
return unserialize($data['value']);
}
@@ -81,7 +75,7 @@ class SQLiteCache implements CacheInterface
$Qselect = $this->db->prepare('SELECT updated FROM storage WHERE key = :key');
$Qselect->bindValue(':key', $this->getCacheKey());
$result = $Qselect->execute();
- if ($result instanceof SQLite3Result) {
+ if ($result instanceof \SQLite3Result) {
$data = $result->fetchArray(SQLITE3_ASSOC);
if (isset($data['updated'])) {
return $data['updated'];
@@ -98,10 +92,6 @@ class SQLiteCache implements CacheInterface
$Qdelete->execute();
}
- /**
- * Set scope
- * @return self
- */
public function setScope($scope)
{
if (is_null($scope) || !is_string($scope)) {
@@ -112,10 +102,6 @@ class SQLiteCache implements CacheInterface
return $this;
}
- /**
- * Set key
- * @return self
- */
public function setKey($key)
{
if (!empty($key) && is_array($key)) {
@@ -131,8 +117,6 @@ class SQLiteCache implements CacheInterface
return $this;
}
- ////////////////////////////////////////////////////////////////////////////
-
private function getCacheKey()
{
if (is_null($this->key)) {
diff --git a/composer.json b/composer.json
index 4a63e72e..2c0c5038 100644
--- a/composer.json
+++ b/composer.json
@@ -37,6 +37,7 @@
"suggest": {
"ext-memcached": "Allows to use memcached as cache type",
"ext-sqlite3": "Allows to use an SQLite database for caching",
+ "ext-zip": "Required for FDroidRepoBridge",
"ext-dom": "Allows to use some bridges based on XPath expressions"
},
"autoload-dev": {
diff --git a/config.default.ini.php b/config.default.ini.php
index b7f2677b..7ea40bc2 100644
--- a/config.default.ini.php
+++ b/config.default.ini.php
@@ -65,12 +65,10 @@ by_bridge = false
; false = disabled (default)
enable = false
-; The username for authentication. Insert this name when prompted for login.
-username = ""
+username = "admin"
-; The password for authentication. Insert this password when prompted for login.
-; Use a strong password to prevent others from guessing your login!
-password = ""
+; This default password is public knowledge. Replace it.
+password = "7afbf648a369b261"
[error]
diff --git a/docs/08_Format_API/02_FormatInterface.md b/docs/08_Format_API/02_FormatInterface.md
index 88448c38..f216092c 100644
--- a/docs/08_Format_API/02_FormatInterface.md
+++ b/docs/08_Format_API/02_FormatInterface.md
@@ -82,7 +82,7 @@ getExtraInfos(): array
The `getMimeType` function returns the expected [MIME type](https://en.wikipedia.org/wiki/Media_type#Common_examples) of the format's output.
```PHP
-getMimeType(): string
+parse_mime_type(): string
```
# Template
diff --git a/docs/08_Format_API/03_FormatAbstract.md b/docs/08_Format_API/03_FormatAbstract.md
index 8cf0418f..cb206b85 100644
--- a/docs/08_Format_API/03_FormatAbstract.md
+++ b/docs/08_Format_API/03_FormatAbstract.md
@@ -9,5 +9,5 @@ The `FormatAbstract` class implements the [`FormatInterface`](../08_Format_API/0
The `sanitizeHtml` function receives an HTML formatted string and returns the string with disabled `<script>`, `<iframe>` and `<link>` tags.
```PHP
-sanitizeHtml(string $html): string
+sanitize_html(string $html): string
```
diff --git a/formats/AtomFormat.php b/formats/AtomFormat.php
index 5f564266..c611226f 100644
--- a/formats/AtomFormat.php
+++ b/formats/AtomFormat.php
@@ -18,17 +18,21 @@ class AtomFormat extends FormatAbstract
public function stringify()
{
- $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
- $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
- $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
- $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
+ $https = $_SERVER['HTTPS'] ?? null;
+ $urlPrefix = $https === 'on' ? 'https://' : 'http://';
+ $urlHost = $_SERVER['HTTP_HOST'] ?? '';
+ $urlRequest = $_SERVER['REQUEST_URI'] ?? '';
$feedUrl = $urlPrefix . $urlHost . $urlRequest;
$extraInfos = $this->getExtraInfos();
- $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
+ if (empty($extraInfos['uri'])) {
+ $uri = REPOSITORY;
+ } else {
+ $uri = $extraInfos['uri'];
+ }
- $document = new DomDocument('1.0', $this->getCharset());
+ $document = new \DomDocument('1.0', $this->getCharset());
$document->formatOutput = true;
$feed = $document->createElementNS(self::ATOM_NS, 'feed');
$document->appendChild($feed);
@@ -44,10 +48,10 @@ class AtomFormat extends FormatAbstract
$id->appendChild($document->createTextNode($feedUrl));
$uriparts = parse_url($uri);
- if (!empty($extraInfos['icon'])) {
- $iconUrl = $extraInfos['icon'];
- } else {
+ if (empty($extraInfos['icon'])) {
$iconUrl = $uriparts['scheme'] . '://' . $uriparts['host'] . '/favicon.ico';
+ } else {
+ $iconUrl = $extraInfos['icon'];
}
$icon = $document->createElement('icon');
$feed->appendChild($icon);
@@ -94,11 +98,13 @@ class AtomFormat extends FormatAbstract
$entryID = 'urn:sha1:' . $item->getUid();
}
- if (empty($entryID)) { // Fallback to provided URI
+ if (empty($entryID)) {
+ // Fallback to provided URI
$entryID = $entryUri;
}
- if (empty($entryID)) { // Fallback to title and content
+ if (empty($entryID)) {
+ // Fallback to title and content
$entryID = 'urn:sha1:' . hash('sha1', $entryTitle . $entryContent);
}
@@ -126,7 +132,7 @@ class AtomFormat extends FormatAbstract
$title->setAttribute('type', 'html');
$title->appendChild($document->createTextNode($entryTitle));
- $entryTimestamp = gmdate(DATE_ATOM, $entryTimestamp);
+ $entryTimestamp = gmdate(\DATE_ATOM, $entryTimestamp);
$published = $document->createElement('published');
$entry->appendChild($published);
$published->appendChild($document->createTextNode($entryTimestamp));
@@ -157,14 +163,14 @@ class AtomFormat extends FormatAbstract
$content = $document->createElement('content');
$content->setAttribute('type', 'html');
- $content->appendChild($document->createTextNode($this->sanitizeHtml($entryContent)));
+ $content->appendChild($document->createTextNode(sanitize_html($entryContent)));
$entry->appendChild($content);
foreach ($item->getEnclosures() as $enclosure) {
$entryEnclosure = $document->createElement('link');
$entry->appendChild($entryEnclosure);
$entryEnclosure->setAttribute('rel', 'enclosure');
- $entryEnclosure->setAttribute('type', getMimeType($enclosure));
+ $entryEnclosure->setAttribute('type', parse_mime_type($enclosure));
$entryEnclosure->setAttribute('href', $enclosure);
}
diff --git a/formats/HtmlFormat.php b/formats/HtmlFormat.php
index d9ab65ef..6c916de6 100644
--- a/formats/HtmlFormat.php
+++ b/formats/HtmlFormat.php
@@ -7,9 +7,9 @@ class HtmlFormat extends FormatAbstract
public function stringify()
{
$extraInfos = $this->getExtraInfos();
- $title = htmlspecialchars($extraInfos['name']);
- $uri = htmlspecialchars($extraInfos['uri']);
- $donationUri = htmlspecialchars($extraInfos['donationUri']);
+ $title = e($extraInfos['name']);
+ $uri = e($extraInfos['uri']);
+ $donationUri = e($extraInfos['donationUri']);
$donationsAllowed = Configuration::getConfig('admin', 'donations');
// Dynamically build buttons for all formats (except HTML)
@@ -19,32 +19,39 @@ class HtmlFormat extends FormatAbstract
$links = '';
foreach ($formatFactory->getFormatNames() as $format) {
- if (strcasecmp($format, 'HTML') === 0) {
+ if ($format === 'Html') {
continue;
}
- $query = str_ireplace('format=Html', 'format=' . $format, htmlentities($_SERVER['QUERY_STRING']));
- $buttons .= $this->buildButton($format, $query) . PHP_EOL;
+ $queryString = $_SERVER['QUERY_STRING'];
+ $query = str_ireplace('format=Html', 'format=' . $format, htmlentities($queryString));
+ $buttons .= sprintf('<a href="./?%s"><button class="rss-feed">%s</button></a>', $query, $format) . "\n";
$mime = $formatFactory->create($format)->getMimeType();
- $links .= $this->buildLink($format, $query, $mime) . PHP_EOL;
+ $links .= sprintf('<link href="./?%s" title="%s" rel="alternate" type="%s">', $query, $format, $mime) . "\n";
}
if ($donationUri !== '' && $donationsAllowed) {
- $buttons .= '<a href="'
- . $donationUri
- . '" target="_blank"><button class="highlight">Donate to maintainer</button></a>'
- . PHP_EOL;
- $links .= '<link href="'
- . $donationUri
- . ' target="_blank"" title="Donate to Maintainer" rel="alternate">'
- . PHP_EOL;
+ $str = sprintf(
+ '<a href="%s" target="_blank"><button class="highlight">Donate to maintainer</button></a>',
+ $donationUri
+ );
+ $buttons .= $str;
+ $str1 = sprintf(
+ '<link href="%s target="_blank"" title="Donate to Maintainer" rel="alternate">',
+ $donationUri
+ );
+ $links .= $str1;
}
$entries = '';
foreach ($this->getItems() as $item) {
- $entryAuthor = $item->getAuthor() ? '<br /><p class="author">by: ' . $item->getAuthor() . '</p>' : '';
- $entryTitle = $this->sanitizeHtml(strip_tags($item->getTitle()));
+ if ($item->getAuthor()) {
+ $entryAuthor = sprintf('<br /><p class="author">by: %s</p>', $item->getAuthor());
+ } else {
+ $entryAuthor = '';
+ }
+ $entryTitle = sanitize_html(strip_tags($item->getTitle()));
$entryUri = $item->getURI() ?: $uri;
$entryDate = '';
@@ -58,9 +65,8 @@ class HtmlFormat extends FormatAbstract
$entryContent = '';
if ($item->getContent()) {
- $entryContent = '<div class="content">'
- . $this->sanitizeHtml($item->getContent())
- . '</div>';
+ $str2 = sprintf('<div class="content">%s</div>', sanitize_html($item->getContent()));
+ $entryContent = $str2;
}
$entryEnclosures = '';
@@ -69,7 +75,7 @@ class HtmlFormat extends FormatAbstract
foreach ($item->getEnclosures() as $enclosure) {
$template = '<li class="enclosure"><a href="%s" rel="noopener noreferrer nofollow">%s</a></li>';
- $url = $this->sanitizeHtml($enclosure);
+ $url = sanitize_html($enclosure);
$anchorText = substr($url, strrpos($url, '/') + 1);
$entryEnclosures .= sprintf($template, $url, $anchorText);
@@ -84,7 +90,7 @@ class HtmlFormat extends FormatAbstract
foreach ($item->getCategories() as $category) {
$entryCategories .= '<li class="category">'
- . $this->sanitizeHtml($category)
+ . sanitize_html($category)
. '</li>';
}
@@ -106,8 +112,6 @@ EOD;
}
$charset = $this->getCharset();
-
- /* Data are prepared, now let's begin the "MAGIE !!!" */
$toReturn = <<<EOD
<!DOCTYPE html>
<html>
@@ -136,19 +140,4 @@ EOD;
$toReturn = mb_convert_encoding($toReturn, $this->getCharset(), 'UTF-8');
return $toReturn;
}
-
- private function buildButton($format, $query)
- {
- return <<<EOD
-<a href="./?{$query}"><button class="rss-feed">{$format}</button></a>
-EOD;
- }
-
- private function buildLink($format, $query, $mime)
- {
- return <<<EOD
-<link href="./?{$query}" title="{$format}" rel="alternate" type="{$mime}">
-
-EOD;
- }
}
diff --git a/formats/JsonFormat.php b/formats/JsonFormat.php
index 3b2a29ab..bb9e81a2 100644
--- a/formats/JsonFormat.php
+++ b/formats/JsonFormat.php
@@ -25,10 +25,10 @@ class JsonFormat extends FormatAbstract
public function stringify()
{
- $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
- $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
- $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
- $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
+ $https = $_SERVER['HTTPS'] ?? null;
+ $urlPrefix = $https === 'on' ? 'https://' : 'http://';
+ $urlHost = $_SERVER['HTTP_HOST'] ?? '';
+ $urlRequest = $_SERVER['REQUEST_URI'] ?? '';
$extraInfos = $this->getExtraInfos();
@@ -52,7 +52,7 @@ class JsonFormat extends FormatAbstract
$entryTitle = $item->getTitle();
$entryUri = $item->getURI();
$entryTimestamp = $item->getTimestamp();
- $entryContent = $item->getContent() ? $this->sanitizeHtml($item->getContent()) : '';
+ $entryContent = $item->getContent() ? sanitize_html($item->getContent()) : '';
$entryEnclosures = $item->getEnclosures();
$entryCategories = $item->getCategories();
@@ -76,13 +76,13 @@ class JsonFormat extends FormatAbstract
];
}
if (!empty($entryTimestamp)) {
- $entry['date_modified'] = gmdate(DATE_ATOM, $entryTimestamp);
+ $entry['date_modified'] = gmdate(\DATE_ATOM, $entryTimestamp);
}
if (!empty($entryUri)) {
$entry['url'] = $entryUri;
}
if (!empty($entryContent)) {
- if ($this->isHTML($entryContent)) {
+ if (is_html($entryContent)) {
$entry['content_html'] = $entryContent;
} else {
$entry['content_text'] = $entryContent;
@@ -93,7 +93,7 @@ class JsonFormat extends FormatAbstract
foreach ($entryEnclosures as $enclosure) {
$entry['attachments'][] = [
'url' => $enclosure,
- 'mime_type' => getMimeType($enclosure)
+ 'mime_type' => parse_mime_type($enclosure)
];
}
}
@@ -121,13 +121,8 @@ class JsonFormat extends FormatAbstract
* So consider this a hack.
* Switch to JSON_INVALID_UTF8_IGNORE when PHP 7.2 is the latest platform requirement.
*/
- $json = json_encode($data, JSON_PRETTY_PRINT | JSON_PARTIAL_OUTPUT_ON_ERROR);
+ $json = json_encode($data, \JSON_PRETTY_PRINT | \JSON_PARTIAL_OUTPUT_ON_ERROR);
return $json;
}
-
- private function isHTML($text)
- {
- return (strlen(strip_tags($text)) != strlen($text));
- }
}
diff --git a/formats/MrssFormat.php b/formats/MrssFormat.php
index 45c2181f..f4067b73 100644
--- a/formats/MrssFormat.php
+++ b/formats/MrssFormat.php
@@ -33,22 +33,28 @@ class MrssFormat extends FormatAbstract
protected const MRSS_NS = 'http://search.yahoo.com/mrss/';
const ALLOWED_IMAGE_EXT = [
- '.gif', '.jpg', '.png'
+ '.gif',
+ '.jpg',
+ '.png',
];
public function stringify()
{
- $urlPrefix = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
- $urlHost = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
- $urlPath = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : '';
- $urlRequest = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
+ $https = $_SERVER['HTTPS'] ?? null;
+ $urlPrefix = $https == 'on' ? 'https://' : 'http://';
+ $urlHost = $_SERVER['HTTP_HOST'] ?? '';
+ $urlRequest = $_SERVER['REQUEST_URI'] ?? '';
$feedUrl = $urlPrefix . $urlHost . $urlRequest;
$extraInfos = $this->getExtraInfos();
- $uri = !empty($extraInfos['uri']) ? $extraInfos['uri'] : REPOSITORY;
+ if (empty($extraInfos['uri'])) {
+ $uri = REPOSITORY;
+ } else {
+ $uri = $extraInfos['uri'];
+ }
- $document = new DomDocument('1.0', $this->getCharset());
+ $document = new \DomDocument('1.0', $this->getCharset());
$document->formatOutput = true;
$feed = $document->createElement('rss');
$document->appendChild($feed);
@@ -103,16 +109,18 @@ class MrssFormat extends FormatAbstract
$itemTimestamp = $item->getTimestamp();
$itemTitle = $item->getTitle();
$itemUri = $item->getURI();
- $itemContent = $item->getContent() ? $this->sanitizeHtml($item->getContent()) : '';
+ $itemContent = $item->getContent() ? sanitize_html($item->getContent()) : '';
$entryID = $item->getUid();
$isPermaLink = 'false';
- if (empty($entryID) && !empty($itemUri)) { // Fallback to provided URI
+ if (empty($entryID) && !empty($itemUri)) {
+ // Fallback to provided URI
$entryID = $itemUri;
$isPermaLink = 'true';
}
- if (empty($entryID)) { // Fallback to title and content
+ if (empty($entryID)) {
+ // Fallback to title and content
$entryID = hash('sha1', $itemTitle . $itemContent);
}
@@ -139,7 +147,7 @@ class MrssFormat extends FormatAbstract
if (!empty($itemTimestamp)) {
$entryPublished = $document->createElement('pubDate');
$entry->appendChild($entryPublished);
- $entryPublished->appendChild($document->createTextNode(gmdate(DATE_RFC2822, $itemTimestamp)));
+ $entryPublished->appendChild($document->createTextNode(gmdate(\DATE_RFC2822, $itemTimestamp)));
}
if (!empty($itemContent)) {
@@ -152,10 +160,9 @@ class MrssFormat extends FormatAbstract
$entryEnclosure = $document->createElementNS(self::MRSS_NS, 'content');
$entry->appendChild($entryEnclosure);
$entryEnclosure->setAttribute('url', $enclosure);
- $entryEnclosure->setAttribute('type', getMimeType($enclosure));
+ $entryEnclosure->setAttribute('type', parse_mime_type($enclosure));
}
- $entryCategories = '';
foreach ($item->getCategories() as $category) {
$entryCategory = $document->createElement('category');
$entry->appendChild($entryCategory);
diff --git a/formats/PlaintextFormat.php b/formats/PlaintextFormat.php
index a1e125c7..c8c4e9d6 100644
--- a/formats/PlaintextFormat.php
+++ b/formats/PlaintextFormat.php
@@ -1,9 +1,5 @@
<?php
-/**
-* Plaintext
-* Returns $this->items as raw php data.
-*/
class PlaintextFormat extends FormatAbstract
{
const MIME_TYPE = 'text/plain';
diff --git a/index.php b/index.php
index 60ece16a..c17d74ad 100644
--- a/index.php
+++ b/index.php
@@ -2,16 +2,19 @@
require_once __DIR__ . '/lib/rssbridge.php';
-Configuration::verifyInstallation();
-Configuration::loadConfiguration();
+try {
+ Configuration::verifyInstallation();
+ Configuration::loadConfiguration();
-date_default_timezone_set(Configuration::getConfig('system', 'timezone'));
+ date_default_timezone_set(Configuration::getConfig('system', 'timezone'));
-define('CUSTOM_CACHE_TIMEOUT', Configuration::getConfig('cache', 'custom_timeout'));
+ define('CUSTOM_CACHE_TIMEOUT', Configuration::getConfig('cache', 'custom_timeout'));
-Authentication::showPromptIfNeeded();
+ $authenticationMiddleware = new AuthenticationMiddleware();
+ if (Configuration::getConfig('authentication', 'enable')) {
+ $authenticationMiddleware();
+ }
-try {
if (isset($argv)) {
parse_str(implode('&', array_slice($argv, 1)), $cliArgs);
$request = $cliArgs;
@@ -20,12 +23,7 @@ try {
}
foreach ($request as $key => $value) {
if (! is_string($value)) {
- http_response_code(400);
- print render('error.html.php', [
- 'title' => '400 Bad Request',
- 'message' => "Query parameter \"$key\" is not a string.",
- ]);
- exit(1);
+ throw new \Exception("Query parameter \"$key\" is not a string.");
}
}
diff --git a/lib/Authentication.php b/lib/Authentication.php
deleted file mode 100644
index 172836b2..00000000
--- a/lib/Authentication.php
+++ /dev/null
@@ -1,89 +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
- */
-
-/**
- * Authentication module for RSS-Bridge.
- *
- * This class implements an authentication module for RSS-Bridge, utilizing the
- * HTTP authentication capabilities of PHP.
- *
- * _Notice_: Authentication via HTTP does not prevent users from accessing files
- * on your server. If your server supports `.htaccess`, you should globally restrict
- * access to files instead.
- *
- * @link https://php.net/manual/en/features.http-auth.php HTTP authentication with PHP
- * @link https://httpd.apache.org/docs/2.4/howto/htaccess.html Apache HTTP Server
- * Tutorial: .htaccess files
- *
- * @todo Configuration parameters should be stored internally instead of accessing
- * the configuration class directly.
- * @todo Add functions to detect if a user is authenticated or not. This can be
- * utilized for limiting access to authorized users only.
- */
-class Authentication
-{
- /**
- * Throw an exception when trying to create a new instance of this class.
- * Use {@see Authentication::showPromptIfNeeded()} instead!
- *
- * @throws \LogicException if called.
- */
- public function __construct()
- {
- throw new \LogicException('Use ' . __CLASS__ . '::showPromptIfNeeded()!');
- }
-
- /**
- * Requests the user for login credentials if necessary.
- *
- * Responds to an authentication request or returns the `WWW-Authenticate`
- * header if authentication is enabled in the configuration of RSS-Bridge
- * (`[authentication] enable = true`).
- *
- * @return void
- */
- public static function showPromptIfNeeded()
- {
- if (Configuration::getConfig('authentication', 'enable') === true) {
- if (!Authentication::verifyPrompt()) {
- header('WWW-Authenticate: Basic realm="RSS-Bridge"', true, 401);
- $message = 'Please authenticate in order to access this instance !';
- print $message;
- exit;
- }
- }
- }
-
- /**
- * Verifies if an authentication request was received and compares the
- * provided username and password to the configuration of RSS-Bridge
- * (`[authentication] username` and `[authentication] password`).
- *
- * @return bool True if authentication succeeded.
- */
- public static function verifyPrompt()
- {
- if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
- if (
- Configuration::getConfig('authentication', 'username') === $_SERVER['PHP_AUTH_USER']
- && Configuration::getConfig('authentication', 'password') === $_SERVER['PHP_AUTH_PW']
- ) {
- return true;
- } else {
- error_log('[RSS-Bridge] Failed authentication attempt from ' . $_SERVER['REMOTE_ADDR']);
- }
- }
- return false;
- }
-}
diff --git a/lib/AuthenticationMiddleware.php b/lib/AuthenticationMiddleware.php
new file mode 100644
index 00000000..430f977c
--- /dev/null
+++ b/lib/AuthenticationMiddleware.php
@@ -0,0 +1,42 @@
+<?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
+ */
+
+final class AuthenticationMiddleware
+{
+ public function __invoke(): void
+ {
+ $user = $_SERVER['PHP_AUTH_USER'] ?? null;
+ $password = $_SERVER['PHP_AUTH_PW'] ?? null;
+
+ if ($user === null || $password === null) {
+ $this->renderAuthenticationDialog();
+ exit;
+ }
+ if (
+ Configuration::getConfig('authentication', 'username') === $user
+ && Configuration::getConfig('authentication', 'password') === $password
+ ) {
+ return;
+ }
+ $this->renderAuthenticationDialog();
+ exit;
+ }
+
+ private function renderAuthenticationDialog(): void
+ {
+ http_response_code(401);
+ header('WWW-Authenticate: Basic realm="RSS-Bridge"');
+ print render('error.html.php', ['message' => 'Please authenticate in order to access this instance !']);
+ }
+}
diff --git a/lib/BridgeAbstract.php b/lib/BridgeAbstract.php
index 16e057c7..e91c8ae9 100644
--- a/lib/BridgeAbstract.php
+++ b/lib/BridgeAbstract.php
@@ -12,19 +12,6 @@
* @link https://github.com/rss-bridge/rss-bridge
*/
-/**
- * An abstract class for bridges
- *
- * This class implements {@see BridgeInterface} with most common functions in
- * order to reduce code duplication. Bridges should inherit from this class
- * instead of implementing the interface manually.
- *
- * @todo Move constants to the interface (this is supported by PHP)
- * @todo Change visibility of constants to protected
- * @todo Return `self` on more functions to allow chaining
- * @todo Add specification for PARAMETERS ()
- * @todo Add specification for $items
- */
abstract class BridgeAbstract implements BridgeInterface
{
/**
@@ -107,7 +94,7 @@ abstract class BridgeAbstract implements BridgeInterface
*
* @var array
*/
- protected $items = [];
+ protected array $items = [];
/**
* Holds the list of input parameters used by the bridge
@@ -117,7 +104,7 @@ abstract class BridgeAbstract implements BridgeInterface
*
* @var array
*/
- protected $inputs = [];
+ protected array $inputs = [];
/**
* Holds the name of the queried context
@@ -233,7 +220,7 @@ abstract class BridgeAbstract implements BridgeInterface
if (empty(static::PARAMETERS)) {
if (!empty($inputs)) {
- returnClientError('Invalid parameters value(s)');
+ throw new \Exception('Invalid parameters value(s)');
}
return;
@@ -249,10 +236,7 @@ abstract class BridgeAbstract implements BridgeInterface
$validator->getInvalidParameters()
);
- returnClientError(
- 'Invalid parameters value(s): '
- . implode(', ', $parameters)
- );
+ throw new \Exception(sprintf('Invalid parameters value(s): %s', implode(', ', $parameters)));
}
// Guess the context from input data
@@ -261,9 +245,9 @@ abstract class BridgeAbstract implements BridgeInterface
}
if (is_null($this->queriedContext)) {
- returnClientError('Required parameter(s) missing');
+ throw new \Exception('Required parameter(s) missing');
} elseif ($this->queriedContext === false) {
- returnClientError('Mixed context parameters');
+ throw new \Exception('Mixed context parameters');
}
$this->setInputs($inputs, $this->queriedContext);
@@ -289,10 +273,7 @@ abstract class BridgeAbstract implements BridgeInterface
}
if (isset($optionValue['required']) && $optionValue['required'] === true) {
- returnServerError(
- 'Missing configuration option: '
- . $optionName
- );
+ throw new \Exception(sprintf('Missing configuration option: %s', $optionName));
} elseif (isset($optionValue['defaultValue'])) {
$this->configuration[$optionName] = $optionValue['defaultValue'];
}
@@ -314,17 +295,11 @@ abstract class BridgeAbstract implements BridgeInterface
}
/**
- * Returns the value for the selected configuration
- *
- * @param string $input The option name
- * @return mixed|null The option value or null if the input is not defined
+ * Get bridge configuration value
*/
public function getOption($name)
{
- if (!isset($this->configuration[$name])) {
- return null;
- }
- return $this->configuration[$name];
+ return $this->configuration[$name] ?? null;
}
/** {@inheritdoc} */
@@ -392,9 +367,8 @@ abstract class BridgeAbstract implements BridgeInterface
&& $urlMatches[3] === $bridgeUriMatches[3]
) {
return [];
- } else {
- return null;
}
+ return null;
}
/**
@@ -404,13 +378,13 @@ abstract class BridgeAbstract implements BridgeInterface
* @param int $duration Cache duration (optional, default: 24 hours)
* @return mixed Cached value or null if the key doesn't exist or has expired
*/
- protected function loadCacheValue($key, $duration = 86400)
+ protected function loadCacheValue($key, int $duration = 86400)
{
$cacheFactory = new CacheFactory();
$cache = $cacheFactory->create();
// Create class name without the namespace part
- $scope = (new ReflectionClass($this))->getShortName();
+ $scope = (new \ReflectionClass($this))->getShortName();
$cache->setScope($scope);
$cache->setKey($key);
if ($cache->getTime() < time() - $duration) {
@@ -430,7 +404,7 @@ abstract class BridgeAbstract implements BridgeInterface
$cacheFactory = new CacheFactory();
$cache = $cacheFactory->create();
- $scope = (new ReflectionClass($this))->getShortName();
+ $scope = (new \ReflectionClass($this))->getShortName();
$cache->setScope($scope);
$cache->setKey($key);
$cache->saveData($value);
diff --git a/lib/BridgeCard.php b/lib/BridgeCard.php
index dbb2a23d..900671ca 100644
--- a/lib/BridgeCard.php
+++ b/lib/BridgeCard.php
@@ -23,6 +23,90 @@
final class BridgeCard
{
/**
+ * Gets a single bridge card
+ *
+ * @param class-string<BridgeInterface> $bridgeClassName The bridge name
+ * @param array $formats A list of formats
+ * @param bool $isActive Indicates if the bridge is active or not
+ * @return string The bridge card
+ */
+ public static function displayBridgeCard($bridgeClassName, $formats, $isActive = true)
+ {
+ $bridgeFactory = new BridgeFactory();
+
+ $bridge = $bridgeFactory->create($bridgeClassName);
+
+ $isHttps = strpos($bridge->getURI(), 'https') === 0;
+
+ $uri = $bridge->getURI();
+ $name = $bridge->getName();
+ $icon = $bridge->getIcon();
+ $description = $bridge->getDescription();
+ $parameters = $bridge->getParameters();
+ if (Configuration::getConfig('proxy', 'url') && Configuration::getConfig('proxy', 'by_bridge')) {
+ $parameters['global']['_noproxy'] = [
+ 'name' => 'Disable proxy (' . (Configuration::getConfig('proxy', 'name') ?: Configuration::getConfig('proxy', 'url')) . ')',
+ 'type' => 'checkbox'
+ ];
+ }
+
+ if (CUSTOM_CACHE_TIMEOUT) {
+ $parameters['global']['_cache_timeout'] = [
+ 'name' => 'Cache timeout in seconds',
+ 'type' => 'number',
+ 'defaultValue' => $bridge->getCacheTimeout()
+ ];
+ }
+
+ $card = <<<CARD
+ <section id="bridge-{$bridgeClassName}" data-ref="{$name}">
+ <h2><a href="{$uri}">{$name}</a></h2>
+ <p class="description">{$description}</p>
+ <input type="checkbox" class="showmore-box" id="showmore-{$bridgeClassName}" />
+ <label class="showmore" for="showmore-{$bridgeClassName}">Show more</label>
+CARD;
+
+ // If we don't have any parameter for the bridge, we print a generic form to load it.
+ if (count($parameters) === 0) {
+ $card .= self::getForm($bridgeClassName, $formats, $isActive, $isHttps);
+
+ // Display form with cache timeout and/or noproxy options (if enabled) when bridge has no parameters
+ } elseif (count($parameters) === 1 && array_key_exists('global', $parameters)) {
+ $card .= self::getForm($bridgeClassName, $formats, $isActive, $isHttps, '', $parameters['global']);
+ } else {
+ foreach ($parameters as $parameterName => $parameter) {
+ if (!is_numeric($parameterName) && $parameterName === 'global') {
+ continue;
+ }
+
+ if (array_key_exists('global', $parameters)) {
+ $parameter = array_merge($parameter, $parameters['global']);
+ }
+
+ if (!is_numeric($parameterName)) {
+ $card .= '<h5>' . $parameterName . '</h5>' . PHP_EOL;
+ }
+
+ $card .= self::getForm($bridgeClassName, $formats, $isActive, $isHttps, $parameterName, $parameter);
+ }
+ }
+
+ $card .= sprintf('<label class="showless" for="showmore-%s">Show less</label>', $bridgeClassName);
+ if ($bridge->getDonationURI() !== '' && Configuration::getConfig('admin', 'donations')) {
+ $card .= sprintf(
+ '<p class="maintainer">%s ~ <a href="%s">Donate</a></p>',
+ $bridge->getMaintainer(),
+ $bridge->getDonationURI()
+ );
+ } else {
+ $card .= sprintf('<p class="maintainer">%s</p>', $bridge->getMaintainer());
+ }
+ $card .= '</section>';
+
+ return $card;
+ }
+
+ /**
* Get the form header for a bridge card
*
* @param class-string<BridgeInterface> $bridgeClassName The bridge name
@@ -38,9 +122,7 @@ final class BridgeCard
EOD;
if (!empty($parameterName)) {
- $form .= <<<EOD
- <input type="hidden" name="context" value="{$parameterName}" />
-EOD;
+ $form .= sprintf('<input type="hidden" name="context" value="%s" />', $parameterName);
}
if (!$isHttps) {
@@ -293,93 +375,4 @@ This bridge is not fetching its content through a secure connection</div>';
. ' />'
. PHP_EOL;
}
-
- /**
- * Gets a single bridge card
- *
- * @param class-string<BridgeInterface> $bridgeClassName The bridge name
- * @param array $formats A list of formats
- * @param bool $isActive Indicates if the bridge is active or not
- * @return string The bridge card
- */
- public static function displayBridgeCard($bridgeClassName, $formats, $isActive = true)
- {
- $bridgeFactory = new \BridgeFactory();
-
- $bridge = $bridgeFactory->create($bridgeClassName);
-
- if ($bridge == false) {
- return '';
- }
-
- $isHttps = strpos($bridge->getURI(), 'https') === 0;
-
- $uri = $bridge->getURI();
- $name = $bridge->getName();
- $icon = $bridge->getIcon();
- $description = $bridge->getDescription();
- $parameters = $bridge->getParameters();
- $donationUri = $bridge->getDonationURI();
- $maintainer = $bridge->getMaintainer();
-
- $donationsAllowed = Configuration::getConfig('admin', 'donations');
-
- if (Configuration::getConfig('proxy', 'url') && Configuration::getConfig('proxy', 'by_bridge')) {
- $parameters['global']['_noproxy'] = [
- 'name' => 'Disable proxy (' . (Configuration::getConfig('proxy', 'name') ?: Configuration::getConfig('proxy', 'url')) . ')',
- 'type' => 'checkbox'
- ];
- }
-
- if (CUSTOM_CACHE_TIMEOUT) {
- $parameters['global']['_cache_timeout'] = [
- 'name' => 'Cache timeout in seconds',
- 'type' => 'number',
- 'defaultValue' => $bridge->getCacheTimeout()
- ];
- }
-
- $card = <<<CARD
- <section id="bridge-{$bridgeClassName}" data-ref="{$name}">
- <h2><a href="{$uri}">{$name}</a></h2>
- <p class="description">{$description}</p>
- <input type="checkbox" class="showmore-box" id="showmore-{$bridgeClassName}" />
- <label class="showmore" for="showmore-{$bridgeClassName}">Show more</label>
-CARD;
-
- // If we don't have any parameter for the bridge, we print a generic form to load it.
- if (count($parameters) === 0) {
- $card .= self::getForm($bridgeClassName, $formats, $isActive, $isHttps);
-
- // Display form with cache timeout and/or noproxy options (if enabled) when bridge has no parameters
- } elseif (count($parameters) === 1 && array_key_exists('global', $parameters)) {
- $card .= self::getForm($bridgeClassName, $formats, $isActive, $isHttps, '', $parameters['global']);
- } else {
- foreach ($parameters as $parameterName => $parameter) {
- if (!is_numeric($parameterName) && $parameterName === 'global') {
- continue;
- }
-
- if (array_key_exists('global', $parameters)) {
- $parameter = array_merge($parameter, $parameters['global']);
- }
-
- if (!is_numeric($parameterName)) {
- $card .= '<h5>' . $parameterName . '</h5>' . PHP_EOL;
- }
-
- $card .= self::getForm($bridgeClassName, $formats, $isActive, $isHttps, $parameterName, $parameter);
- }
- }
-
- $card .= '<label class="showless" for="showmore-' . $bridgeClassName . '">Show less</label>';
- if ($donationUri !== '' && $donationsAllowed) {
- $card .= '<p class="maintainer">' . $maintainer . ' ~ <a href="' . $donationUri . '">Donate</a></p>';
- } else {
- $card .= '<p class="maintainer">' . $maintainer . '</p>';
- }
- $card .= '</section>';
-
- return $card;
- }
}
diff --git a/lib/BridgeFactory.php b/lib/BridgeFactory.php
index 34602476..e525452b 100644
--- a/lib/BridgeFactory.php
+++ b/lib/BridgeFactory.php
@@ -27,7 +27,8 @@ final class BridgeFactory
} else {
$contents = '';
}
- if ($contents === '*') { // Whitelist all bridges
+ if ($contents === '*') {
+ // Whitelist all bridges
$this->whitelist = $this->getBridgeClassNames();
} else {
foreach (explode("\n", $contents) as $bridgeName) {
@@ -97,7 +98,6 @@ final class BridgeFactory
return $this->getBridgeClassNames()[$index];
}
- Debug::log('Invalid bridge name specified: "' . $name . '"!');
return null;
}
}
diff --git a/lib/BridgeList.php b/lib/BridgeList.php
index f8d0b1a1..41b1f267 100644
--- a/lib/BridgeList.php
+++ b/lib/BridgeList.php
@@ -23,6 +23,28 @@
final class BridgeList
{
/**
+ * Create the entire home page
+ *
+ * @param bool $showInactive Inactive bridges are displayed on the home page,
+ * if enabled.
+ * @return string The home page
+ */
+ public static function create($showInactive = true)
+ {
+ $totalBridges = 0;
+ $totalActiveBridges = 0;
+
+ return '<!DOCTYPE html><html lang="en">'
+ . BridgeList::getHead()
+ . '<body onload="search()">'
+ . BridgeList::getHeader()
+ . BridgeList::getSearchbar()
+ . BridgeList::getBridges($showInactive, $totalBridges, $totalActiveBridges)
+ . BridgeList::getFooter($totalBridges, $totalActiveBridges, $showInactive)
+ . '</body></html>';
+ }
+
+ /**
* Get the document head
*
* @return string The document head
@@ -65,7 +87,7 @@ EOD;
$totalActiveBridges = 0;
$inactiveBridges = '';
- $bridgeFactory = new \BridgeFactory();
+ $bridgeFactory = new BridgeFactory();
$bridgeClassNames = $bridgeFactory->getBridgeClassNames();
$formatFactory = new FormatFactory();
@@ -126,7 +148,7 @@ EOD;
*/
private static function getSearchbar()
{
- $query = filter_input(INPUT_GET, 'q', FILTER_SANITIZE_SPECIAL_CHARS);
+ $query = filter_input(INPUT_GET, 'q', \FILTER_SANITIZE_SPECIAL_CHARS);
return <<<EOD
<section class="searchbar">
@@ -167,10 +189,10 @@ EOD;
$inactive = '';
if ($totalActiveBridges !== $totalBridges) {
- if (!$showInactive) {
- $inactive = '<a href="?show_inactive=1"><button class="small">Show inactive bridges</button></a><br>';
- } else {
+ 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>';
}
}
@@ -184,26 +206,4 @@ EOD;
</section>
EOD;
}
-
- /**
- * Create the entire home page
- *
- * @param bool $showInactive Inactive bridges are displayed on the home page,
- * if enabled.
- * @return string The home page
- */
- public static function create($showInactive = true)
- {
- $totalBridges = 0;
- $totalActiveBridges = 0;
-
- return '<!DOCTYPE html><html lang="en">'
- . BridgeList::getHead()
- . '<body onload="search()">'
- . BridgeList::getHeader()
- . BridgeList::getSearchbar()
- . BridgeList::getBridges($showInactive, $totalBridges, $totalActiveBridges)
- . BridgeList::getFooter($totalBridges, $totalActiveBridges, $showInactive)
- . '</body></html>';
- }
}
diff --git a/lib/CacheInterface.php b/lib/CacheInterface.php
index 67cee681..1c3b89ca 100644
--- a/lib/CacheInterface.php
+++ b/lib/CacheInterface.php
@@ -55,7 +55,7 @@ interface CacheInterface
/**
* Returns the timestamp for the curent cache data
*
- * @return int Timestamp or null
+ * @return ?int Timestamp
*/
public function getTime();
diff --git a/lib/Configuration.php b/lib/Configuration.php
index 141f3d88..9f8b76bc 100644
--- a/lib/Configuration.php
+++ b/lib/Configuration.php
@@ -41,14 +41,8 @@ final class Configuration
*/
private static $config = null;
- /**
- * Throw an exception when trying to create a new instance of this class.
- *
- * @throws \LogicException if called.
- */
- public function __construct()
+ private function __construct()
{
- throw new \LogicException('Can\'t create object of this class!');
}
/**
@@ -61,43 +55,45 @@ final class Configuration
*/
public static function verifyInstallation()
{
- // Check PHP version
- // PHP Supported Versions: https://www.php.net/supported-versions.php
- if (version_compare(PHP_VERSION, '7.4.0') === -1) {
+ if (version_compare(\PHP_VERSION, '7.4.0') === -1) {
self::reportError('RSS-Bridge requires at least PHP version 7.4.0!');
}
- // Extensions check
+ $errors = [];
// OpenSSL: https://www.php.net/manual/en/book.openssl.php
if (!extension_loaded('openssl')) {
- self::reportError('"openssl" extension not loaded. Please check "php.ini"');
+ $errors[] = 'openssl extension not loaded';
}
// libxml: https://www.php.net/manual/en/book.libxml.php
if (!extension_loaded('libxml')) {
- self::reportError('"libxml" extension not loaded. Please check "php.ini"');
+ $errors[] = 'libxml extension not loaded';
}
// Multibyte String (mbstring): https://www.php.net/manual/en/book.mbstring.php
if (!extension_loaded('mbstring')) {
- self::reportError('"mbstring" extension not loaded. Please check "php.ini"');
+ $errors[] = 'mbstring extension not loaded';
}
// SimpleXML: https://www.php.net/manual/en/book.simplexml.php
if (!extension_loaded('simplexml')) {
- self::reportError('"simplexml" extension not loaded. Please check "php.ini"');
+ $errors[] = 'simplexml extension not loaded';
}
// Client URL Library (curl): https://www.php.net/manual/en/book.curl.php
// Allow RSS-Bridge to run without curl module in CLI mode without root certificates
if (!extension_loaded('curl') && !(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo')))) {
- self::reportError('"curl" extension not loaded. Please check "php.ini"');
+ $errors[] = 'curl extension not loaded';
}
// JavaScript Object Notation (json): https://www.php.net/manual/en/book.json.php
if (!extension_loaded('json')) {
- self::reportError('"json" extension not loaded. Please check "php.ini"');
+ $errors[] = 'json extension not loaded';
+ }
+
+ if ($errors) {
+ throw new \Exception(sprintf('Configuration error: %s', implode(', ', $errors)));
}
}
@@ -192,11 +188,11 @@ final class Configuration
self::reportConfigurationError('authentication', 'enable', 'Is not a valid Boolean');
}
- if (!is_string(self::getConfig('authentication', 'username'))) {
+ if (!self::getConfig('authentication', 'username')) {
self::reportConfigurationError('authentication', 'username', 'Is not a valid string');
}
- if (!is_string(self::getConfig('authentication', 'password'))) {
+ if (! self::getConfig('authentication', 'password')) {
self::reportConfigurationError('authentication', 'password', 'Is not a valid string');
}
@@ -250,7 +246,7 @@ final class Configuration
*/
public static function getVersion()
{
- $headFile = PATH_ROOT . '.git/HEAD';
+ $headFile = __DIR__ . '/../.git/HEAD';
// '@' is used to mute open_basedir warning
if (@is_readable($headFile)) {
@@ -295,19 +291,8 @@ final class Configuration
self::reportError($report);
}
- /**
- * Reports an error message to the user and ends execution
- *
- * @param string $message The error message
- *
- * @return void
- */
private static function reportError($message)
{
- http_response_code(500);
- print render('error.html.php', [
- 'message' => "Configuration error: $message",
- ]);
- exit;
+ throw new \Exception(sprintf('Configuration error: %s', $message));
}
}
diff --git a/lib/Debug.php b/lib/Debug.php
index 0b05a20d..316d5595 100644
--- a/lib/Debug.php
+++ b/lib/Debug.php
@@ -64,8 +64,8 @@ class Debug
{
static $firstCall = true; // Initialized on first call
- if ($firstCall && file_exists(PATH_ROOT . 'DEBUG')) {
- $debug_whitelist = trim(file_get_contents(PATH_ROOT . 'DEBUG'));
+ if ($firstCall && file_exists(__DIR__ . '/../DEBUG')) {
+ $debug_whitelist = trim(file_get_contents(__DIR__ . '/../DEBUG'));
self::$enabled = empty($debug_whitelist) || in_array(
$_SERVER['REMOTE_ADDR'],
diff --git a/lib/FeedExpander.php b/lib/FeedExpander.php
index 685108b9..052bce82 100644
--- a/lib/FeedExpander.php
+++ b/lib/FeedExpander.php
@@ -85,10 +85,10 @@ abstract class FeedExpander extends BridgeAbstract
public function collectExpandableDatas($url, $maxItems = -1)
{
if (empty($url)) {
- returnServerError('There is no $url for this RSS expander');
+ throw new \Exception('There is no $url for this RSS expander');
}
- Debug::log('Loading from ' . $url);
+ Debug::log(sprintf('Loading from %s', $url));
/* Notice we do not use cache here on purpose:
* we want a fresh view of the RSS stream each time
@@ -100,8 +100,7 @@ abstract class FeedExpander extends BridgeAbstract
'*/*',
];
$httpHeaders = ['Accept: ' . implode(', ', $mimeTypes)];
- $content = getContents($url, $httpHeaders)
- or returnServerError('Could not request ' . $url);
+ $content = getContents($url, $httpHeaders);
$rssContent = simplexml_load_string(trim($content));
if ($rssContent === false) {
@@ -127,8 +126,7 @@ abstract class FeedExpander extends BridgeAbstract
break;
default:
Debug::log('Unknown feed format/version');
- returnServerError('The feed format is unknown!');
- break;
+ throw new \Exception('The feed format is unknown!');
}
return $this;
@@ -151,7 +149,7 @@ abstract class FeedExpander extends BridgeAbstract
{
$this->loadRss2Data($rssContent->channel[0]);
foreach ($rssContent->item as $item) {
- Debug::log('parsing item ' . var_export($item, true));
+ Debug::log(sprintf('Parsing item %s', var_export($item, true)));
$tmp_item = $this->parseItem($item);
if (!empty($tmp_item)) {
$this->items[] = $tmp_item;
@@ -453,33 +451,39 @@ abstract class FeedExpander extends BridgeAbstract
switch ($this->feedType) {
case self::FEED_TYPE_RSS_1_0:
return $this->parseRss1Item($item);
- break;
case self::FEED_TYPE_RSS_2_0:
return $this->parseRss2Item($item);
- break;
case self::FEED_TYPE_ATOM_1_0:
return $this->parseATOMItem($item);
- break;
default:
- returnClientError('Unknown version ' . $this->getInput('version') . '!');
+ throw new \Exception(sprintf('Unknown version %s!', $this->getInput('version')));
}
}
/** {@inheritdoc} */
public function getURI()
{
- return !empty($this->uri) ? $this->uri : parent::getURI();
+ if (!empty($this->uri)) {
+ return $this->uri;
+ }
+ return parent::getURI();
}
/** {@inheritdoc} */
public function getName()
{
- return !empty($this->title) ? $this->title : parent::getName();
+ if (!empty($this->title)) {
+ return $this->title;
+ }
+ return parent::getName();
}
/** {@inheritdoc} */
public function getIcon()
{
- return !empty($this->icon) ? $this->icon : parent::getIcon();
+ if (!empty($this->icon)) {
+ return $this->icon;
+ }
+ return parent::getIcon();
}
}
diff --git a/lib/FeedItem.php b/lib/FeedItem.php
index 4bf9492c..4435e0e0 100644
--- a/lib/FeedItem.php
+++ b/lib/FeedItem.php
@@ -329,10 +329,10 @@ class FeedItem
$content = (string)$content;
}
- if (!is_string($content)) {
- Debug::log('Content must be a string!');
- } else {
+ if (is_string($content)) {
$this->content = $content;
+ } else {
+ Debug::log('Content must be a string!');
}
return $this;
@@ -361,11 +361,9 @@ class FeedItem
*/
public function setEnclosures($enclosures)
{
- $this->enclosures = []; // Clear previous data
+ $this->enclosures = [];
- if (!is_array($enclosures)) {
- Debug::log('Enclosures must be an array!');
- } else {
+ if (is_array($enclosures)) {
foreach ($enclosures as $enclosure) {
if (
!filter_var(
@@ -379,6 +377,8 @@ class FeedItem
$this->enclosures[] = $enclosure;
}
}
+ } else {
+ Debug::log('Enclosures must be an array!');
}
return $this;
@@ -407,11 +407,9 @@ class FeedItem
*/
public function setCategories($categories)
{
- $this->categories = []; // Clear previous data
+ $this->categories = [];
- if (!is_array($categories)) {
- Debug::log('Categories must be an array!');
- } else {
+ if (is_array($categories)) {
foreach ($categories as $category) {
if (!is_string($category)) {
Debug::log('Category must be a string!');
@@ -419,6 +417,8 @@ class FeedItem
$this->categories[] = $category;
}
}
+ } else {
+ Debug::log('Categories must be an array!');
}
return $this;
diff --git a/lib/FormatAbstract.php b/lib/FormatAbstract.php
index 7a4c6c92..3289d651 100644
--- a/lib/FormatAbstract.php
+++ b/lib/FormatAbstract.php
@@ -63,7 +63,10 @@ abstract class FormatAbstract implements FormatInterface
{
$charset = $this->charset;
- return is_null($charset) ? static::DEFAULT_CHARSET : $charset;
+ if (is_null($charset)) {
+ return static::DEFAULT_CHARSET;
+ }
+ return $charset;
}
/**
@@ -93,7 +96,7 @@ abstract class FormatAbstract implements FormatInterface
public function getItems()
{
if (!is_array($this->items)) {
- throw new \LogicException('Feed the ' . get_class($this) . ' with "setItems" method before !');
+ throw new \LogicException(sprintf('Feed the %s with "setItems" method before !', get_class($this)));
}
return $this->items;
@@ -126,26 +129,4 @@ abstract class FormatAbstract implements FormatInterface
return $this->extraInfos;
}
-
- /**
- * Sanitize HTML while leaving it functional.
- *
- * Keeps HTML as-is (with clickable hyperlinks) while reducing annoying and
- * potentially dangerous things.
- *
- * @param string $html The HTML content
- * @return string The sanitized HTML content
- *
- * @todo This belongs into `html.php`
- * @todo Maybe switch to http://htmlpurifier.org/
- * @todo Maybe switch to http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php
- */
- protected function sanitizeHtml(string $html): string
- {
- $html = str_replace('<script', '<&zwnj;script', $html); // Disable scripts, but leave them visible.
- $html = str_replace('<iframe', '<&zwnj;iframe', $html);
- $html = str_replace('<link', '<&zwnj;link', $html);
- // We leave alone object and embed so that videos can play in RSS readers.
- return $html;
- }
}
diff --git a/lib/ParameterValidator.php b/lib/ParameterValidator.php
index a903ff8d..31934432 100644
--- a/lib/ParameterValidator.php
+++ b/lib/ParameterValidator.php
@@ -35,7 +35,7 @@ class ParameterValidator
{
$this->invalid[] = [
'name' => $name,
- 'reason' => $reason
+ 'reason' => $reason,
];
}
@@ -216,7 +216,7 @@ class ParameterValidator
if (array_key_exists('global', $parameters)) {
$notInContext = array_diff_key($notInContext, $parameters['global']);
}
- if (sizeof($notInContext) > 0) {
+ if (count($notInContext) > 0) {
continue;
}
@@ -246,7 +246,8 @@ class ParameterValidator
unset($queriedContexts['global']);
switch (array_sum($queriedContexts)) {
- case 0: // Found no match, is there a context without parameters?
+ case 0:
+ // Found no match, is there a context without parameters?
if (isset($data['context'])) {
return $data['context'];
}
@@ -256,7 +257,8 @@ class ParameterValidator
}
}
return null;
- case 1: // Found unique match
+ case 1:
+ // Found unique match
return array_search(true, $queriedContexts);
default:
return false;
diff --git a/lib/XPathAbstract.php b/lib/XPathAbstract.php
index 686addf4..b78511fc 100644
--- a/lib/XPathAbstract.php
+++ b/lib/XPathAbstract.php
@@ -341,10 +341,10 @@ abstract class XPathAbstract extends BridgeAbstract
/**
* Should provide the feeds title
*
- * @param DOMXPath $xpath
+ * @param \DOMXPath $xpath
* @return string
*/
- protected function provideFeedTitle(DOMXPath $xpath)
+ protected function provideFeedTitle(\DOMXPath $xpath)
{
$title = $xpath->query($this->getParam('feed_title'));
if (count($title) === 1) {
@@ -355,10 +355,10 @@ abstract class XPathAbstract extends BridgeAbstract
/**
* Should provide the URL of the feed's favicon
*
- * @param DOMXPath $xpath
+ * @param \DOMXPath $xpath
* @return string
*/
- protected function provideFeedIcon(DOMXPath $xpath)
+ protected function provideFeedIcon(\DOMXPath $xpath)
{
$icon = $xpath->query($this->getParam('feed_icon'));
if (count($icon) === 1) {
@@ -369,10 +369,10 @@ abstract class XPathAbstract extends BridgeAbstract
/**
* Should provide the feed's items.
*
- * @param DOMXPath $xpath
- * @return DOMNodeList
+ * @param \DOMXPath $xpath
+ * @return \DOMNodeList
*/
- protected function provideFeedItems(DOMXPath $xpath)
+ protected function provideFeedItems(\DOMXPath $xpath)
{
return @$xpath->query($this->getParam('item'));
}
@@ -381,13 +381,13 @@ abstract class XPathAbstract extends BridgeAbstract
{
$this->feedUri = $this->getParam('url');
- $webPageHtml = new DOMDocument();
+ $webPageHtml = new \DOMDocument();
libxml_use_internal_errors(true);
$webPageHtml->loadHTML($this->provideWebsiteContent());
libxml_clear_errors();
libxml_use_internal_errors(false);
- $xpath = new DOMXPath($webPageHtml);
+ $xpath = new \DOMXPath($webPageHtml);
$this->feedName = $this->provideFeedTitle($xpath);
$this->feedIcon = $this->provideFeedIcon($xpath);
@@ -398,7 +398,7 @@ abstract class XPathAbstract extends BridgeAbstract
}
foreach ($entries as $entry) {
- $item = new \FeedItem();
+ $item = new FeedItem();
foreach (['title', 'content', 'uri', 'author', 'timestamp', 'enclosures', 'categories'] as $param) {
$expression = $this->getParam($param);
if ('' === $expression) {
@@ -408,7 +408,7 @@ abstract class XPathAbstract extends BridgeAbstract
//can be a string or DOMNodeList, depending on the expression result
$typedResult = @$xpath->evaluate($expression, $entry);
if (
- $typedResult === false || ($typedResult instanceof DOMNodeList && count($typedResult) === 0)
+ $typedResult === false || ($typedResult instanceof \DOMNodeList && count($typedResult) === 0)
|| (is_string($typedResult) && strlen(trim($typedResult)) === 0)
) {
continue;
@@ -571,19 +571,19 @@ abstract class XPathAbstract extends BridgeAbstract
*/
protected function getItemValueOrNodeValue($typedResult)
{
- if ($typedResult instanceof DOMNodeList) {
+ if ($typedResult instanceof \DOMNodeList) {
$item = $typedResult->item(0);
- if ($item instanceof DOMElement) {
+ if ($item instanceof \DOMElement) {
return trim($item->nodeValue);
- } elseif ($item instanceof DOMAttr) {
+ } elseif ($item instanceof \DOMAttr) {
return trim($item->value);
- } elseif ($item instanceof DOMText) {
+ } elseif ($item instanceof \DOMText) {
return trim($item->wholeText);
}
} elseif (is_string($typedResult) && strlen($typedResult) > 0) {
return trim($typedResult);
}
- returnServerError('Unknown type of XPath expression result.');
+ throw new \Exception('Unknown type of XPath expression result.');
}
/**
@@ -605,8 +605,8 @@ abstract class XPathAbstract extends BridgeAbstract
* @param FeedItem $item
* @return string|null
*/
- protected function generateItemId(\FeedItem $item)
+ protected function generateItemId(FeedItem $item)
{
- return null; //auto generation
+ return null;
}
}
diff --git a/lib/contents.php b/lib/contents.php
index 5b39bb66..c1eb5ad5 100644
--- a/lib/contents.php
+++ b/lib/contents.php
@@ -1,9 +1,5 @@
<?php
-final class HttpException extends \Exception
-{
-}
-
// todo: move this somewhere useful, possibly into a function
const RSSBRIDGE_HTTP_STATUS_CODES = [
'100' => 'Continue',
@@ -128,7 +124,8 @@ function getContents(
}
$cache->saveData($result['body']);
break;
- case 304: // Not Modified
+ case 304:
+ // Not Modified
$response['content'] = $cache->loadData();
break;
default:
@@ -379,68 +376,3 @@ function getSimpleHTMLDOMCached(
$defaultSpanText
);
}
-
-/**
- * Determines the MIME type from a URL/Path file extension.
- *
- * _Remarks_:
- *
- * * The built-in functions `mime_content_type` and `fileinfo` require fetching
- * remote contents.
- * * A caller can hint for a MIME type by appending `#.ext` to the URL (i.e. `#.image`).
- *
- * Based on https://stackoverflow.com/a/1147952
- *
- * @param string $url The URL or path to the file.
- * @return string The MIME type of the file.
- */
-function getMimeType($url)
-{
- static $mime = null;
-
- if (is_null($mime)) {
- // Default values, overriden by /etc/mime.types when present
- $mime = [
- 'jpg' => 'image/jpeg',
- 'gif' => 'image/gif',
- 'png' => 'image/png',
- 'image' => 'image/*',
- 'mp3' => 'audio/mpeg',
- ];
- // '@' is used to mute open_basedir warning, see issue #818
- if (@is_readable('/etc/mime.types')) {
- $file = fopen('/etc/mime.types', 'r');
- while (($line = fgets($file)) !== false) {
- $line = trim(preg_replace('/#.*/', '', $line));
- if (!$line) {
- continue;
- }
- $parts = preg_split('/\s+/', $line);
- if (count($parts) == 1) {
- continue;
- }
- $type = array_shift($parts);
- foreach ($parts as $part) {
- $mime[$part] = $type;
- }
- }
- fclose($file);
- }
- }
-
- if (strpos($url, '?') !== false) {
- $url_temp = substr($url, 0, strpos($url, '?'));
- if (strpos($url, '#') !== false) {
- $anchor = substr($url, strpos($url, '#'));
- $url_temp .= $anchor;
- }
- $url = $url_temp;
- }
-
- $ext = strtolower(pathinfo($url, PATHINFO_EXTENSION));
- if (!empty($mime[$ext])) {
- return $mime[$ext];
- }
-
- return 'application/octet-stream';
-}
diff --git a/lib/error.php b/lib/error.php
index 1a3d294d..f6322567 100644
--- a/lib/error.php
+++ b/lib/error.php
@@ -64,7 +64,7 @@ function logBridgeError($bridgeName, $code)
$cache->purgeCache(86400); // 24 hours
if ($report = $cache->loadData()) {
- $report = json_decode($report, true);
+ $report = Json::decode($report);
$report['time'] = time();
$report['count']++;
} else {
@@ -75,38 +75,7 @@ function logBridgeError($bridgeName, $code)
];
}
- $cache->saveData(json_encode($report));
+ $cache->saveData(Json::encode($report));
return $report['count'];
}
-
-function create_sane_stacktrace(\Throwable $e): array
-{
- $frames = array_reverse($e->getTrace());
- $frames[] = [
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- ];
- $stackTrace = [];
- foreach ($frames as $i => $frame) {
- $file = $frame['file'] ?? '(no file)';
- $line = $frame['line'] ?? '(no line)';
- $stackTrace[] = sprintf(
- '#%s %s:%s',
- $i,
- trim_path_prefix($file),
- $line,
- );
- }
- return $stackTrace;
-}
-
-/**
- * Trim path prefix for privacy/security reasons
- *
- * Example: "/var/www/rss-bridge/index.php" => "index.php"
- */
-function trim_path_prefix(string $filePath): string
-{
- return mb_substr($filePath, mb_strlen(dirname(__DIR__)) + 1);
-}
diff --git a/lib/html.php b/lib/html.php
index 4a4bea1b..693504b0 100644
--- a/lib/html.php
+++ b/lib/html.php
@@ -98,6 +98,15 @@ function sanitize(
return $htmlContent;
}
+function sanitize_html(string $html): string
+{
+ $html = str_replace('<script', '<&zwnj;script', $html); // Disable scripts, but leave them visible.
+ $html = str_replace('<iframe', '<&zwnj;iframe', $html);
+ $html = str_replace('<link', '<&zwnj;link', $html);
+ // We leave alone object and embed so that videos can play in RSS readers.
+ return $html;
+}
+
/**
* Replace background by image
*
diff --git a/lib/rssbridge.php b/lib/rssbridge.php
index d900c910..b1261ffd 100644
--- a/lib/rssbridge.php
+++ b/lib/rssbridge.php
@@ -13,55 +13,56 @@
*/
/** Path to the root folder of RSS-Bridge (where index.php is located) */
-define('PATH_ROOT', __DIR__ . '/../');
-
-/** Path to the core library */
-define('PATH_LIB', PATH_ROOT . 'lib/');
-
-/** Path to the vendor library */
-define('PATH_LIB_VENDOR', PATH_ROOT . 'vendor/');
+const PATH_ROOT = __DIR__ . '/../';
/** Path to the bridges library */
-define('PATH_LIB_BRIDGES', PATH_ROOT . 'bridges/');
+const PATH_LIB_BRIDGES = __DIR__ . '/../bridges/';
/** Path to the formats library */
-define('PATH_LIB_FORMATS', PATH_ROOT . 'formats/');
+const PATH_LIB_FORMATS = __DIR__ . '/../formats/';
/** Path to the caches library */
-define('PATH_LIB_CACHES', PATH_ROOT . 'caches/');
+const PATH_LIB_CACHES = __DIR__ . '/../caches/';
/** Path to the actions library */
-define('PATH_LIB_ACTIONS', PATH_ROOT . 'actions/');
+const PATH_LIB_ACTIONS = __DIR__ . '/../actions/';
/** Path to the cache folder */
-define('PATH_CACHE', PATH_ROOT . 'cache/');
+const PATH_CACHE = __DIR__ . '/../cache/';
/** Path to the whitelist file */
-define('WHITELIST', PATH_ROOT . 'whitelist.txt');
+const WHITELIST = __DIR__ . '/../whitelist.txt';
/** Path to the default whitelist file */
-define('WHITELIST_DEFAULT', PATH_ROOT . 'whitelist.default.txt');
+const WHITELIST_DEFAULT = __DIR__ . '/../whitelist.default.txt';
/** Path to the configuration file */
-define('FILE_CONFIG', PATH_ROOT . 'config.ini.php');
+const FILE_CONFIG = __DIR__ . '/../config.ini.php';
/** Path to the default configuration file */
-define('FILE_CONFIG_DEFAULT', PATH_ROOT . 'config.default.ini.php');
+const FILE_CONFIG_DEFAULT = __DIR__ . '/../config.default.ini.php';
/** URL to the RSS-Bridge repository */
-define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
+const REPOSITORY = 'https://github.com/RSS-Bridge/rss-bridge/';
-// Files
-require_once PATH_LIB . 'html.php';
-require_once PATH_LIB . 'error.php';
-require_once PATH_LIB . 'contents.php';
-require_once PATH_LIB . 'php8backports.php';
+// Allow larger files for simple_html_dom
+const MAX_FILE_SIZE = 10000000;
-// Vendor
-define('MAX_FILE_SIZE', 10000000); /* Allow larger files for simple_html_dom */
-require_once PATH_LIB_VENDOR . 'parsedown/Parsedown.php';
-require_once PATH_LIB_VENDOR . 'php-urljoin/src/urljoin.php';
-require_once PATH_LIB_VENDOR . 'simplehtmldom/simple_html_dom.php';
+// Files
+$files = [
+ __DIR__ . '/../lib/html.php',
+ __DIR__ . '/../lib/error.php',
+ __DIR__ . '/../lib/contents.php',
+ __DIR__ . '/../lib/php8backports.php',
+ __DIR__ . '/../lib/utils.php',
+ // Vendor
+ __DIR__ . '/../vendor/parsedown/Parsedown.php',
+ __DIR__ . '/../vendor/php-urljoin/src/urljoin.php',
+ __DIR__ . '/../vendor/simplehtmldom/simple_html_dom.php',
+];
+foreach ($files as $file) {
+ require_once $file;
+}
spl_autoload_register(function ($className) {
$folders = [
diff --git a/lib/utils.php b/lib/utils.php
new file mode 100644
index 00000000..312591f4
--- /dev/null
+++ b/lib/utils.php
@@ -0,0 +1,123 @@
+<?php
+
+final class HttpException extends \Exception
+{
+}
+
+final class Json
+{
+ public static function encode($value): string
+ {
+ $flags = JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
+ return \json_encode($value, $flags);
+ }
+
+ public static function decode(string $json, bool $assoc = true)
+ {
+ return \json_decode($json, $assoc, 512, JSON_THROW_ON_ERROR);
+ }
+}
+
+function create_sane_stacktrace(\Throwable $e): array
+{
+ $frames = array_reverse($e->getTrace());
+ $frames[] = [
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ];
+ $stackTrace = [];
+ foreach ($frames as $i => $frame) {
+ $file = $frame['file'] ?? '(no file)';
+ $line = $frame['line'] ?? '(no line)';
+ $stackTrace[] = sprintf(
+ '#%s %s:%s',
+ $i,
+ trim_path_prefix($file),
+ $line,
+ );
+ }
+ return $stackTrace;
+}
+
+/**
+ * Trim path prefix for privacy/security reasons
+ *
+ * Example: "/var/www/rss-bridge/index.php" => "index.php"
+ */
+function trim_path_prefix(string $filePath): string
+{
+ return mb_substr($filePath, mb_strlen(dirname(__DIR__)) + 1);
+}
+
+/**
+ * This is buggy because strip tags removes a lot that isn't html
+ */
+function is_html(string $text): bool
+{
+ return strlen(strip_tags($text)) !== strlen($text);
+}
+
+/**
+ * Determines the MIME type from a URL/Path file extension.
+ *
+ * _Remarks_:
+ *
+ * * The built-in functions `mime_content_type` and `fileinfo` require fetching
+ * remote contents.
+ * * A caller can hint for a MIME type by appending `#.ext` to the URL (i.e. `#.image`).
+ *
+ * Based on https://stackoverflow.com/a/1147952
+ *
+ * @param string $url The URL or path to the file.
+ * @return string The MIME type of the file.
+ */
+function parse_mime_type($url)
+{
+ static $mime = null;
+
+ if (is_null($mime)) {
+ // Default values, overriden by /etc/mime.types when present
+ $mime = [
+ 'jpg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'png' => 'image/png',
+ 'image' => 'image/*',
+ 'mp3' => 'audio/mpeg',
+ ];
+ // '@' is used to mute open_basedir warning, see issue #818
+ if (@is_readable('/etc/mime.types')) {
+ $file = fopen('/etc/mime.types', 'r');
+ while (($line = fgets($file)) !== false) {
+ $line = trim(preg_replace('/#.*/', '', $line));
+ if (!$line) {
+ continue;
+ }
+ $parts = preg_split('/\s+/', $line);
+ if (count($parts) == 1) {
+ continue;
+ }
+ $type = array_shift($parts);
+ foreach ($parts as $part) {
+ $mime[$part] = $type;
+ }
+ }
+ fclose($file);
+ }
+ }
+
+ if (strpos($url, '?') !== false) {
+ $url_temp = substr($url, 0, strpos($url, '?'));
+ if (strpos($url, '#') !== false) {
+ $anchor = substr($url, strpos($url, '#'));
+ $url_temp .= $anchor;
+ }
+ $url = $url_temp;
+ }
+
+ $ext = strtolower(pathinfo($url, PATHINFO_EXTENSION));
+ if (!empty($mime[$ext])) {
+ return $mime[$ext];
+ }
+
+ return 'application/octet-stream';
+}
diff --git a/phpcompatibility.xml b/phpcompatibility.xml
index 2726bfcb..f2268d7f 100644
--- a/phpcompatibility.xml
+++ b/phpcompatibility.xml
@@ -10,6 +10,8 @@
-->
<config name="testVersion" value="7.4-"/>
<rule ref="PHPCompatibility">
+ <!-- This sniff is very overzealous and inaccurate, so we'll disable it -->
+ <exclude name="PHPCompatibility.Extensions.RemovedExtensions"/>
</rule>
</ruleset>
diff --git a/phpcs.xml b/phpcs.xml
index 599fc9ca..d937f3c9 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -9,6 +9,7 @@
<rule ref="PSR12">
<exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace"/>
+ <exclude name="PSR1.Classes.ClassDeclaration.MultipleClasses"/>
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols"/>
<exclude name="PSR12.Properties.ConstantVisibility.NotFound"/>
</rule>
diff --git a/templates/connectivity.html.php b/templates/connectivity.html.php
new file mode 100644
index 00000000..c00e8177
--- /dev/null
+++ b/templates/connectivity.html.php
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <link rel="stylesheet" href="static/bootstrap.min.css">
+ <link
+ rel="stylesheet"
+ href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
+ integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
+ crossorigin="anonymous">
+ <link rel="stylesheet" href="static/connectivity.css">
+ <script src="static/connectivity.js" type="text/javascript"></script>
+</head>
+<body>
+<div id="main-content" class="container">
+ <div class="progress">
+ <div class="progress-bar" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"></div>
+ </div>
+ <div id="status-message" class="sticky-top alert alert-primary alert-dismissible fade show" role="alert">
+ <i id="status-icon" class="fas fa-sync"></i>
+ <span>...</span>
+ <button type="button" class="close" data-dismiss="alert" aria-label="Close" onclick="stopConnectivityChecks()">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <input type="text" class="form-control" id="search" onkeyup="search()" placeholder="Search for bridge..">
+</div>
+</body>
+</html> \ No newline at end of file
diff --git a/templates/error.html.php b/templates/error.html.php
index 12f77b0b..2473e5f2 100644
--- a/templates/error.html.php
+++ b/templates/error.html.php
@@ -10,6 +10,9 @@
<br>
<?php if (isset($stacktrace)): ?>
+ <h2>Stacktrace</h2>
+ <br>
+
<?php foreach ($stacktrace as $frame) : ?>
<code>
<?= e($frame) ?>
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index 996886a7..3dd389c6 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -16,4 +16,19 @@ final class UtilsTest extends TestCase
$this->assertSame('foo', truncate('foo', 4));
$this->assertSame('fo[...]', truncate('foo', 2, '[...]'));
}
+
+ public function testFileCache()
+ {
+ $sut = new \FileCache();
+ $sut->setScope('scope');
+ $sut->purgeCache(-1);
+ $sut->setKey(['key']);
+
+ $this->assertNull($sut->loadData());
+
+ $sut->saveData('data');
+ $this->assertSame('data', $sut->loadData());
+ $this->assertIsNumeric($sut->getTime());
+ $sut->purgeCache(-1);
+ }
}