mirror of
https://github.com/RSS-Bridge/rss-bridge.git
synced 2025-07-16 06:26:24 +02:00
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
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
42
lib/AuthenticationMiddleware.php
Normal file
42
lib/AuthenticationMiddleware.php
Normal file
@ -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 !']);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -22,6 +22,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
|
||||
*
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,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
|
||||
*
|
||||
@ -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>';
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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'],
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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', '<‌script', $html); // Disable scripts, but leave them visible.
|
||||
$html = str_replace('<iframe', '<‌iframe', $html);
|
||||
$html = str_replace('<link', '<‌link', $html);
|
||||
// We leave alone object and embed so that videos can play in RSS readers.
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -98,6 +98,15 @@ function sanitize(
|
||||
return $htmlContent;
|
||||
}
|
||||
|
||||
function sanitize_html(string $html): string
|
||||
{
|
||||
$html = str_replace('<script', '<‌script', $html); // Disable scripts, but leave them visible.
|
||||
$html = str_replace('<iframe', '<‌iframe', $html);
|
||||
$html = str_replace('<link', '<‌link', $html);
|
||||
// We leave alone object and embed so that videos can play in RSS readers.
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace background by image
|
||||
*
|
||||
|
@ -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/';
|
||||
|
||||
// Allow larger files for simple_html_dom
|
||||
const MAX_FILE_SIZE = 10000000;
|
||||
|
||||
// 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';
|
||||
|
||||
// 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 = [
|
||||
__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 = [
|
||||
|
123
lib/utils.php
Normal file
123
lib/utils.php
Normal file
@ -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';
|
||||
}
|
Reference in New Issue
Block a user