mirror of
https://github.com/RSS-Bridge/rss-bridge.git
synced 2024-12-18 00:43:19 +03:00
refactor: logger (#3678)
This commit is contained in:
parent
360f953be8
commit
7329b83cc0
30 changed files with 297 additions and 338 deletions
|
@ -3,13 +3,19 @@
|
|||
class DisplayAction implements ActionInterface
|
||||
{
|
||||
private CacheInterface $cache;
|
||||
private Logger $logger;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->cache = RssBridge::getCache();
|
||||
$this->logger = RssBridge::getLogger();
|
||||
}
|
||||
|
||||
public function execute(array $request)
|
||||
{
|
||||
if (Configuration::getConfig('system', 'enable_maintenance_mode')) {
|
||||
return new Response('503 Service Unavailable', 503);
|
||||
}
|
||||
$this->cache = RssBridge::getCache();
|
||||
$cacheKey = 'http_' . json_encode($request);
|
||||
/** @var Response $cachedResponse */
|
||||
$cachedResponse = $this->cache->get($cacheKey);
|
||||
|
@ -113,15 +119,15 @@ class DisplayAction implements ActionInterface
|
|||
if ($e instanceof HttpException) {
|
||||
// Reproduce (and log) these responses regardless of error output and report limit
|
||||
if ($e->getCode() === 429) {
|
||||
Logger::info(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
|
||||
$this->logger->info(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
|
||||
return new Response('429 Too Many Requests', 429);
|
||||
}
|
||||
if ($e->getCode() === 503) {
|
||||
Logger::info(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
|
||||
$this->logger->info(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
|
||||
return new Response('503 Service Unavailable', 503);
|
||||
}
|
||||
}
|
||||
Logger::error(sprintf('Exception in DisplayAction(%s)', $bridge->getShortName()), ['e' => $e]);
|
||||
$this->logger->error(sprintf('Exception in DisplayAction(%s)', $bridge->getShortName()), ['e' => $e]);
|
||||
$errorOutput = Configuration::getConfig('error', 'output');
|
||||
$reportLimit = Configuration::getConfig('error', 'report_limit');
|
||||
$errorCount = 1;
|
||||
|
|
|
@ -14,6 +14,13 @@
|
|||
|
||||
class SetBridgeCacheAction implements ActionInterface
|
||||
{
|
||||
private CacheInterface $cache;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->cache = RssBridge::getCache();
|
||||
}
|
||||
|
||||
public function execute(array $request)
|
||||
{
|
||||
$authenticationMiddleware = new ApiAuthenticationMiddleware();
|
||||
|
@ -35,18 +42,15 @@ class SetBridgeCacheAction implements ActionInterface
|
|||
// whitelist control
|
||||
if (!$bridgeFactory->isEnabled($bridgeClassName)) {
|
||||
throw new \Exception('This bridge is not whitelisted', 401);
|
||||
die;
|
||||
}
|
||||
|
||||
$bridge = $bridgeFactory->create($bridgeClassName);
|
||||
$bridge->loadConfiguration();
|
||||
$value = $request['value'];
|
||||
|
||||
$cache = RssBridge::getCache();
|
||||
|
||||
$cacheKey = get_class($bridge) . '_' . $key;
|
||||
$ttl = 86400 * 3;
|
||||
$cache->set($cacheKey, $value, $ttl);
|
||||
$this->cache->set($cacheKey, $value, $ttl);
|
||||
|
||||
header('Content-Type: text/plain');
|
||||
echo 'done';
|
||||
|
|
|
@ -48,7 +48,6 @@ class EZTVBridge extends BridgeAbstract
|
|||
public function collectData()
|
||||
{
|
||||
$eztv_uri = $this->getEztvUri();
|
||||
Logger::debug($eztv_uri);
|
||||
$ids = explode(',', trim($this->getInput('ids')));
|
||||
foreach ($ids as $id) {
|
||||
$data = json_decode(getContents(sprintf('%s/api/get-torrents?imdb_id=%s', $eztv_uri, $id)));
|
||||
|
|
|
@ -113,15 +113,14 @@ class ElloBridge extends BridgeAbstract
|
|||
|
||||
private function getAPIKey()
|
||||
{
|
||||
$cache = RssBridge::getCache();
|
||||
$cacheKey = 'ElloBridge_key';
|
||||
$apiKey = $cache->get($cacheKey);
|
||||
$apiKey = $this->cache->get($cacheKey);
|
||||
|
||||
if (!$apiKey) {
|
||||
$keyInfo = getContents(self::URI . 'api/webapp-token') or returnServerError('Unable to get token.');
|
||||
$apiKey = json_decode($keyInfo)->token->access_token;
|
||||
$ttl = 60 * 60 * 20;
|
||||
$cache->set($cacheKey, $apiKey, $ttl);
|
||||
$this->cache->set($cacheKey, $apiKey, $ttl);
|
||||
}
|
||||
|
||||
return $apiKey;
|
||||
|
|
|
@ -63,7 +63,7 @@ TEXT;
|
|||
try {
|
||||
$this->collectExpandableDatas($feed);
|
||||
} catch (HttpException $e) {
|
||||
Logger::warning(sprintf('Exception in FeedMergeBridge: %s', create_sane_exception_message($e)));
|
||||
$this->logger->warning(sprintf('Exception in FeedMergeBridge: %s', create_sane_exception_message($e)));
|
||||
$this->items[] = [
|
||||
'title' => 'RSS-Bridge: ' . $e->getMessage(),
|
||||
// Give current time so it sorts to the top
|
||||
|
@ -73,7 +73,7 @@ TEXT;
|
|||
} catch (\Exception $e) {
|
||||
if (str_starts_with($e->getMessage(), 'Unable to parse xml')) {
|
||||
// Allow this particular exception from FeedExpander
|
||||
Logger::warning(sprintf('Exception in FeedMergeBridge: %s', create_sane_exception_message($e)));
|
||||
$this->logger->warning(sprintf('Exception in FeedMergeBridge: %s', create_sane_exception_message($e)));
|
||||
continue;
|
||||
}
|
||||
throw $e;
|
||||
|
|
|
@ -217,7 +217,7 @@ HTML,
|
|||
if ($relativeDate) {
|
||||
date_sub($date, $relativeDate);
|
||||
} else {
|
||||
Logger::info(sprintf('Unable to parse date string: %s', $dateString));
|
||||
$this->logger->info(sprintf('Unable to parse date string: %s', $dateString));
|
||||
}
|
||||
return date_format($date, 'r');
|
||||
}
|
||||
|
|
|
@ -98,9 +98,8 @@ class InstagramBridge extends BridgeAbstract
|
|||
return $username;
|
||||
}
|
||||
|
||||
$cache = RssBridge::getCache();
|
||||
$cacheKey = 'InstagramBridge_' . $username;
|
||||
$pk = $cache->get($cacheKey);
|
||||
$pk = $this->cache->get($cacheKey);
|
||||
|
||||
if (!$pk) {
|
||||
$data = $this->getContents(self::URI . 'web/search/topsearch/?query=' . $username);
|
||||
|
@ -112,7 +111,7 @@ class InstagramBridge extends BridgeAbstract
|
|||
if (!$pk) {
|
||||
returnServerError('Unable to find username in search result.');
|
||||
}
|
||||
$cache->set($cacheKey, $pk);
|
||||
$this->cache->set($cacheKey, $pk);
|
||||
}
|
||||
return $pk;
|
||||
}
|
||||
|
|
|
@ -72,12 +72,6 @@ class RedditBridge extends BridgeAbstract
|
|||
]
|
||||
]
|
||||
];
|
||||
private CacheInterface $cache;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->cache = RssBridge::getCache();
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
|
|
|
@ -36,15 +36,12 @@ class SoundCloudBridge extends BridgeAbstract
|
|||
|
||||
private $feedTitle = null;
|
||||
private $feedIcon = null;
|
||||
private CacheInterface $cache;
|
||||
|
||||
private $clientIdRegex = '/client_id.*?"(.+?)"/';
|
||||
private $widgetRegex = '/widget-.+?\.js/';
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$this->cache = RssBridge::getCache();
|
||||
|
||||
$res = $this->getUser($this->getInput('u'));
|
||||
|
||||
$this->feedTitle = $res->username;
|
||||
|
|
|
@ -278,10 +278,9 @@ class SpotifyBridge extends BridgeAbstract
|
|||
|
||||
private function fetchAccessToken()
|
||||
{
|
||||
$cache = RssBridge::getCache();
|
||||
$cacheKey = sprintf('SpotifyBridge:%s:%s', $this->getInput('clientid'), $this->getInput('clientsecret'));
|
||||
|
||||
$token = $cache->get($cacheKey);
|
||||
$token = $this->cache->get($cacheKey);
|
||||
if ($token) {
|
||||
$this->token = $token;
|
||||
} else {
|
||||
|
@ -294,7 +293,7 @@ class SpotifyBridge extends BridgeAbstract
|
|||
$data = Json::decode($json);
|
||||
$this->token = $data['access_token'];
|
||||
|
||||
$cache->set($cacheKey, $this->token, 3600);
|
||||
$this->cache->set($cacheKey, $this->token, 3600);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -234,8 +234,7 @@ EOD
|
|||
$tweets = [];
|
||||
|
||||
// Get authentication information
|
||||
$cache = RssBridge::getCache();
|
||||
$api = new TwitterClient($cache);
|
||||
$api = new TwitterClient($this->cache);
|
||||
// Try to get all tweets
|
||||
switch ($this->queriedContext) {
|
||||
case 'By username':
|
||||
|
|
|
@ -77,12 +77,6 @@ class YoutubeBridge extends BridgeAbstract
|
|||
private $channel_name = '';
|
||||
// This took from repo BetterVideoRss of VerifiedJoseph.
|
||||
const URI_REGEX = '/(https?:\/\/(?:www\.)?(?:[a-zA-Z0-9-.]{2,256}\.[a-z]{2,20})(\:[0-9]{2 ,4})?(?:\/[a-zA-Z0-9@:%_\+.,~#"\'!?&\/\/=\-*]+|\/)?)/ims'; //phpcs:ignore
|
||||
private CacheInterface $cache;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->cache = RssBridge::getCache();
|
||||
}
|
||||
|
||||
private function collectDataInternal()
|
||||
{
|
||||
|
@ -368,7 +362,7 @@ class YoutubeBridge extends BridgeAbstract
|
|||
$scriptRegex = '/var ytInitialData = (.*?);<\/script>/';
|
||||
$result = preg_match($scriptRegex, $html, $matches);
|
||||
if (! $result) {
|
||||
Logger::debug('Could not find ytInitialData');
|
||||
$this->logger->debug('Could not find ytInitialData');
|
||||
return null;
|
||||
}
|
||||
return json_decode($matches[1]);
|
||||
|
|
|
@ -180,13 +180,13 @@ class ZDNetBridge extends FeedExpander
|
|||
|
||||
$article = getSimpleHTMLDOMCached($item['uri']);
|
||||
if (!$article) {
|
||||
Logger::info('Unable to parse the dom from ' . $item['uri']);
|
||||
$this->logger->info('Unable to parse the dom from ' . $item['uri']);
|
||||
return $item;
|
||||
}
|
||||
|
||||
$articleTag = $article->find('article', 0) ?? $article->find('.c-articleContent', 0);
|
||||
if (!$articleTag) {
|
||||
Logger::info('Unable to parse <article> tag in ' . $item['uri']);
|
||||
$this->logger->info('Unable to parse <article> tag in ' . $item['uri']);
|
||||
return $item;
|
||||
}
|
||||
$contents = $articleTag->innertext;
|
||||
|
|
|
@ -4,10 +4,14 @@ declare(strict_types=1);
|
|||
|
||||
class FileCache implements CacheInterface
|
||||
{
|
||||
private Logger $logger;
|
||||
private array $config;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
public function __construct(
|
||||
Logger $logger,
|
||||
array $config = []
|
||||
) {
|
||||
$this->logger = $logger;
|
||||
$default = [
|
||||
'path' => null,
|
||||
'enable_purge' => true,
|
||||
|
@ -28,7 +32,7 @@ class FileCache implements CacheInterface
|
|||
}
|
||||
$item = unserialize(file_get_contents($cacheFile));
|
||||
if ($item === false) {
|
||||
Logger::warning(sprintf('Failed to unserialize: %s', $cacheFile));
|
||||
$this->logger->warning(sprintf('Failed to unserialize: %s', $cacheFile));
|
||||
$this->delete($key);
|
||||
return $default;
|
||||
}
|
||||
|
|
|
@ -4,10 +4,15 @@ declare(strict_types=1);
|
|||
|
||||
class MemcachedCache implements CacheInterface
|
||||
{
|
||||
private Logger $logger;
|
||||
private \Memcached $conn;
|
||||
|
||||
public function __construct(string $host, int $port)
|
||||
{
|
||||
public function __construct(
|
||||
Logger $logger,
|
||||
string $host,
|
||||
int $port
|
||||
) {
|
||||
$this->logger = $logger;
|
||||
$this->conn = new \Memcached();
|
||||
// This call does not actually connect to server yet
|
||||
if (!$this->conn->addServer($host, $port)) {
|
||||
|
@ -29,7 +34,7 @@ class MemcachedCache implements CacheInterface
|
|||
$expiration = $ttl === null ? 0 : time() + $ttl;
|
||||
$result = $this->conn->set($key, $value, $expiration);
|
||||
if ($result === false) {
|
||||
Logger::warning('Failed to store an item in memcached', [
|
||||
$this->logger->warning('Failed to store an item in memcached', [
|
||||
'key' => $key,
|
||||
'code' => $this->conn->getLastErrorCode(),
|
||||
'message' => $this->conn->getLastErrorMessage(),
|
||||
|
|
|
@ -8,11 +8,15 @@ declare(strict_types=1);
|
|||
*/
|
||||
class SQLiteCache implements CacheInterface
|
||||
{
|
||||
private \SQLite3 $db;
|
||||
private Logger $logger;
|
||||
private array $config;
|
||||
private \SQLite3 $db;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
public function __construct(
|
||||
Logger $logger,
|
||||
array $config
|
||||
) {
|
||||
$this->logger = $logger;
|
||||
$default = [
|
||||
'file' => null,
|
||||
'timeout' => 5000,
|
||||
|
@ -59,7 +63,7 @@ class SQLiteCache implements CacheInterface
|
|||
$blob = $row['value'];
|
||||
$value = unserialize($blob);
|
||||
if ($value === false) {
|
||||
Logger::error(sprintf("Failed to unserialize: '%s'", mb_substr($blob, 0, 100)));
|
||||
$this->logger->error(sprintf("Failed to unserialize: '%s'", mb_substr($blob, 0, 100)));
|
||||
// delete?
|
||||
return $default;
|
||||
}
|
||||
|
@ -68,6 +72,7 @@ class SQLiteCache implements CacheInterface
|
|||
// delete?
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function set(string $key, $value, int $ttl = null): void
|
||||
{
|
||||
$cacheKey = $this->createCacheKey($key);
|
||||
|
|
|
@ -27,8 +27,15 @@ abstract class BridgeAbstract
|
|||
protected string $queriedContext = '';
|
||||
private array $configuration = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
protected CacheInterface $cache;
|
||||
protected Logger $logger;
|
||||
|
||||
public function __construct(
|
||||
CacheInterface $cache,
|
||||
Logger $logger
|
||||
) {
|
||||
$this->cache = $cache;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
abstract public function collectData();
|
||||
|
@ -310,16 +317,14 @@ abstract class BridgeAbstract
|
|||
|
||||
protected function loadCacheValue(string $key)
|
||||
{
|
||||
$cache = RssBridge::getCache();
|
||||
$cacheKey = $this->getShortName() . '_' . $key;
|
||||
return $cache->get($cacheKey);
|
||||
return $this->cache->get($cacheKey);
|
||||
}
|
||||
|
||||
protected function saveCacheValue(string $key, $value, $ttl = 86400)
|
||||
{
|
||||
$cache = RssBridge::getCache();
|
||||
$cacheKey = $this->getShortName() . '_' . $key;
|
||||
$cache->set($cacheKey, $value, $ttl);
|
||||
$this->cache->set($cacheKey, $value, $ttl);
|
||||
}
|
||||
|
||||
public function getShortName(): string
|
||||
|
|
|
@ -2,12 +2,17 @@
|
|||
|
||||
final class BridgeFactory
|
||||
{
|
||||
private CacheInterface $cache;
|
||||
private Logger $logger;
|
||||
private $bridgeClassNames = [];
|
||||
private $enabledBridges = [];
|
||||
private $missingEnabledBridges = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->cache = RssBridge::getCache();
|
||||
$this->logger = RssBridge::getLogger();
|
||||
|
||||
// Create all possible bridge class names from fs
|
||||
foreach (scandir(__DIR__ . '/../bridges/') as $file) {
|
||||
if (preg_match('/^([^.]+Bridge)\.php$/U', $file, $m)) {
|
||||
|
@ -29,14 +34,14 @@ final class BridgeFactory
|
|||
$this->enabledBridges[] = $bridgeClassName;
|
||||
} else {
|
||||
$this->missingEnabledBridges[] = $enabledBridge;
|
||||
Logger::info(sprintf('Bridge not found: %s', $enabledBridge));
|
||||
$this->logger->info(sprintf('Bridge not found: %s', $enabledBridge));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function create(string $name): BridgeAbstract
|
||||
{
|
||||
return new $name();
|
||||
return new $name($this->cache, $this->logger);
|
||||
}
|
||||
|
||||
public function isEnabled(string $bridgeName): bool
|
||||
|
|
|
@ -4,6 +4,14 @@ declare(strict_types=1);
|
|||
|
||||
class CacheFactory
|
||||
{
|
||||
private Logger $logger;
|
||||
|
||||
public function __construct(
|
||||
Logger $logger
|
||||
) {
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function create(string $name = null): CacheInterface
|
||||
{
|
||||
$name ??= Configuration::getConfig('cache', 'type');
|
||||
|
@ -49,7 +57,7 @@ class CacheFactory
|
|||
if (!is_writable($fileCacheConfig['path'])) {
|
||||
throw new \Exception(sprintf('The FileCache path is not writable: %s', $fileCacheConfig['path']));
|
||||
}
|
||||
return new FileCache($fileCacheConfig);
|
||||
return new FileCache($this->logger, $fileCacheConfig);
|
||||
case SQLiteCache::class:
|
||||
if (!extension_loaded('sqlite3')) {
|
||||
throw new \Exception('"sqlite3" extension not loaded. Please check "php.ini"');
|
||||
|
@ -66,7 +74,7 @@ class CacheFactory
|
|||
} elseif (!is_dir(dirname($file))) {
|
||||
throw new \Exception(sprintf('Invalid configuration for %s', 'SQLiteCache'));
|
||||
}
|
||||
return new SQLiteCache([
|
||||
return new SQLiteCache($this->logger, [
|
||||
'file' => $file,
|
||||
'timeout' => Configuration::getConfig('SQLiteCache', 'timeout'),
|
||||
'enable_purge' => Configuration::getConfig('SQLiteCache', 'enable_purge'),
|
||||
|
@ -94,7 +102,7 @@ class CacheFactory
|
|||
if ($port < 1 || $port > 65535) {
|
||||
throw new \Exception('"port" param is invalid for ' . $section);
|
||||
}
|
||||
return new MemcachedCache($host, $port);
|
||||
return new MemcachedCache($this->logger, $host, $port);
|
||||
default:
|
||||
if (!file_exists(PATH_LIB_CACHES . $className . '.php')) {
|
||||
throw new \Exception('Unable to find the cache file');
|
||||
|
|
|
@ -24,6 +24,8 @@ class Debug
|
|||
array_pop($trace);
|
||||
$lastFrame = $trace[array_key_last($trace)];
|
||||
$text = sprintf('%s(%s): %s', $lastFrame['file'], $lastFrame['line'], $message);
|
||||
Logger::debug($text);
|
||||
|
||||
$logger = RssBridge::getLogger();
|
||||
$logger->debug($text);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ abstract class FeedExpander extends BridgeAbstract
|
|||
if ($rssContent === false) {
|
||||
$xmlErrors = libxml_get_errors();
|
||||
foreach ($xmlErrors as $xmlError) {
|
||||
Logger::debug(trim($xmlError->message));
|
||||
Debug::log(trim($xmlError->message));
|
||||
}
|
||||
if ($xmlErrors) {
|
||||
// Render only the first error into exception message
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Logger
|
||||
{
|
||||
public static function debug(string $message, array $context = [])
|
||||
{
|
||||
self::log('DEBUG', $message, $context);
|
||||
}
|
||||
|
||||
public static function info(string $message, array $context = []): void
|
||||
{
|
||||
self::log('INFO', $message, $context);
|
||||
}
|
||||
|
||||
public static function warning(string $message, array $context = []): void
|
||||
{
|
||||
self::log('WARNING', $message, $context);
|
||||
}
|
||||
|
||||
public static function error(string $message, array $context = []): void
|
||||
{
|
||||
self::log('ERROR', $message, $context);
|
||||
}
|
||||
|
||||
private static function log(string $level, string $message, array $context = []): void
|
||||
{
|
||||
if (!Debug::isEnabled() && $level === 'DEBUG') {
|
||||
// Don't log this debug log record because debug mode is disabled
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($context['e'])) {
|
||||
/** @var \Throwable $e */
|
||||
$e = $context['e'];
|
||||
unset($context['e']);
|
||||
$context['type'] = get_class($e);
|
||||
$context['code'] = $e->getCode();
|
||||
$context['message'] = sanitize_root($e->getMessage());
|
||||
$context['file'] = sanitize_root($e->getFile());
|
||||
$context['line'] = $e->getLine();
|
||||
$context['url'] = get_current_url();
|
||||
$context['trace'] = trace_to_call_points(trace_from_exception($e));
|
||||
// Don't log these exceptions
|
||||
// todo: this logic belongs in log handler
|
||||
$ignoredExceptions = [
|
||||
'You must specify a format',
|
||||
'Format name invalid',
|
||||
'Unknown format given',
|
||||
'Bridge name invalid',
|
||||
'Invalid action',
|
||||
'twitter: No results for this query',
|
||||
// telegram
|
||||
'Unable to find channel. The channel is non-existing or non-public',
|
||||
// fb
|
||||
'This group is not public! RSS-Bridge only supports public groups!',
|
||||
'You must be logged in to view this page',
|
||||
'Unable to get the page id. You should consider getting the ID by hand',
|
||||
// tiktok 404
|
||||
'https://www.tiktok.com/@',
|
||||
];
|
||||
foreach ($ignoredExceptions as $ignoredException) {
|
||||
if (str_starts_with($e->getMessage(), $ignoredException)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($context) {
|
||||
try {
|
||||
$context = Json::encode($context);
|
||||
} catch (\JsonException $e) {
|
||||
$context['message'] = null;
|
||||
$context = Json::encode($context);
|
||||
}
|
||||
} else {
|
||||
$context = '';
|
||||
}
|
||||
$text = sprintf(
|
||||
"[%s] rssbridge.%s %s %s\n",
|
||||
now()->format('Y-m-d H:i:s'),
|
||||
$level,
|
||||
// Intentionally not sanitizing $message
|
||||
$message,
|
||||
$context
|
||||
);
|
||||
|
||||
// Log to stderr/stdout whatever that is
|
||||
// todo: extract to log handler
|
||||
error_log($text);
|
||||
|
||||
// Log to file
|
||||
// todo: extract to log handler
|
||||
//$bytes = file_put_contents('/tmp/rss-bridge.log', $text, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
}
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
final class RssBridge
|
||||
{
|
||||
private static HttpClient $httpClient;
|
||||
private static CacheInterface $cache;
|
||||
private static Logger $logger;
|
||||
private static HttpClient $httpClient;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -19,7 +20,7 @@ final class RssBridge
|
|||
date_default_timezone_set(Configuration::getConfig('system', 'timezone'));
|
||||
|
||||
set_exception_handler(function (\Throwable $e) {
|
||||
Logger::error('Uncaught Exception', ['e' => $e]);
|
||||
self::$logger->error('Uncaught Exception', ['e' => $e]);
|
||||
http_response_code(500);
|
||||
print render(__DIR__ . '/../templates/error.html.php', ['e' => $e]);
|
||||
exit(1);
|
||||
|
@ -35,7 +36,7 @@ final class RssBridge
|
|||
sanitize_root($file),
|
||||
$line
|
||||
);
|
||||
Logger::warning($text);
|
||||
self::$logger->warning($text);
|
||||
if (Debug::isEnabled()) {
|
||||
print sprintf("<pre>%s</pre>\n", e($text));
|
||||
}
|
||||
|
@ -52,17 +53,23 @@ final class RssBridge
|
|||
sanitize_root($error['file']),
|
||||
$error['line']
|
||||
);
|
||||
Logger::error($message);
|
||||
self::$logger->error($message);
|
||||
if (Debug::isEnabled()) {
|
||||
// todo: extract to log handler
|
||||
print sprintf("<pre>%s</pre>\n", e($message));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self::$logger = new SimpleLogger('rssbridge');
|
||||
if (Debug::isEnabled()) {
|
||||
self::$logger->addHandler(new StreamHandler(Logger::DEBUG));
|
||||
} else {
|
||||
self::$logger->addHandler(new StreamHandler(Logger::INFO));
|
||||
}
|
||||
|
||||
self::$httpClient = new CurlHttpClient();
|
||||
|
||||
$cacheFactory = new CacheFactory();
|
||||
$cacheFactory = new CacheFactory(self::$logger);
|
||||
if (Debug::isEnabled()) {
|
||||
self::$cache = $cacheFactory->create('array');
|
||||
} else {
|
||||
|
@ -108,19 +115,24 @@ final class RssBridge
|
|||
$response->send();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error('Exception in RssBridge::main()', ['e' => $e]);
|
||||
self::$logger->error('Exception in RssBridge::main()', ['e' => $e]);
|
||||
http_response_code(500);
|
||||
print render(__DIR__ . '/../templates/error.html.php', ['e' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getCache(): CacheInterface
|
||||
{
|
||||
return self::$cache;
|
||||
}
|
||||
|
||||
public static function getLogger(): Logger
|
||||
{
|
||||
return self::$logger;
|
||||
}
|
||||
|
||||
public static function getHttpClient(): HttpClient
|
||||
{
|
||||
return self::$httpClient;
|
||||
}
|
||||
|
||||
public static function getCache(): CacheInterface
|
||||
{
|
||||
return self::$cache ?? new NullCache();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ $files = [
|
|||
__DIR__ . '/../lib/php8backports.php',
|
||||
__DIR__ . '/../lib/utils.php',
|
||||
__DIR__ . '/../lib/http.php',
|
||||
__DIR__ . '/../lib/logger.php',
|
||||
// Vendor
|
||||
__DIR__ . '/../vendor/parsedown/Parsedown.php',
|
||||
__DIR__ . '/../vendor/php-urljoin/src/urljoin.php',
|
||||
|
|
172
lib/logger.php
Normal file
172
lib/logger.php
Normal file
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
interface Logger
|
||||
{
|
||||
public const DEBUG = 10;
|
||||
public const INFO = 20;
|
||||
public const WARNING = 30;
|
||||
public const ERROR = 40;
|
||||
|
||||
public const LEVEL_NAMES = [
|
||||
self::DEBUG => 'DEBUG',
|
||||
self::INFO => 'INFO',
|
||||
self::WARNING => 'WARNING',
|
||||
self::ERROR => 'ERROR',
|
||||
];
|
||||
|
||||
public function debug(string $message, array $context = []);
|
||||
|
||||
public function info(string $message, array $context = []): void;
|
||||
|
||||
public function warning(string $message, array $context = []): void;
|
||||
|
||||
public function error(string $message, array $context = []): void;
|
||||
}
|
||||
|
||||
final class SimpleLogger implements Logger
|
||||
{
|
||||
private string $name;
|
||||
private array $handlers;
|
||||
|
||||
/**
|
||||
* @param callable[] $handlers
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
array $handlers = []
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->handlers = $handlers;
|
||||
}
|
||||
|
||||
public function addHandler(callable $fn)
|
||||
{
|
||||
$this->handlers[] = $fn;
|
||||
}
|
||||
|
||||
public function debug(string $message, array $context = [])
|
||||
{
|
||||
$this->log(self::DEBUG, $message, $context);
|
||||
}
|
||||
|
||||
public function info(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(self::INFO, $message, $context);
|
||||
}
|
||||
|
||||
public function warning(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(self::WARNING, $message, $context);
|
||||
}
|
||||
|
||||
public function error(string $message, array $context = []): void
|
||||
{
|
||||
$this->log(self::ERROR, $message, $context);
|
||||
}
|
||||
|
||||
private function log(int $level, string $message, array $context = []): void
|
||||
{
|
||||
foreach ($this->handlers as $handler) {
|
||||
$handler([
|
||||
'name' => $this->name,
|
||||
'created_at' => now(),
|
||||
'level' => $level,
|
||||
'level_name' => self::LEVEL_NAMES[$level],
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class StreamHandler
|
||||
{
|
||||
private int $level;
|
||||
|
||||
public function __construct(int $level = Logger::DEBUG)
|
||||
{
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
public function __invoke(array $record)
|
||||
{
|
||||
if ($record['level'] < $this->level) {
|
||||
return;
|
||||
}
|
||||
if (isset($record['context']['e'])) {
|
||||
/** @var \Throwable $e */
|
||||
$e = $record['context']['e'];
|
||||
unset($record['context']['e']);
|
||||
$record['context']['type'] = get_class($e);
|
||||
$record['context']['code'] = $e->getCode();
|
||||
$record['context']['message'] = sanitize_root($e->getMessage());
|
||||
$record['context']['file'] = sanitize_root($e->getFile());
|
||||
$record['context']['line'] = $e->getLine();
|
||||
$record['context']['url'] = get_current_url();
|
||||
$record['context']['trace'] = trace_to_call_points(trace_from_exception($e));
|
||||
|
||||
$ignoredExceptions = [
|
||||
'You must specify a format',
|
||||
'Format name invalid',
|
||||
'Unknown format given',
|
||||
'Bridge name invalid',
|
||||
'Invalid action',
|
||||
'twitter: No results for this query',
|
||||
// telegram
|
||||
'Unable to find channel. The channel is non-existing or non-public',
|
||||
// fb
|
||||
'This group is not public! RSS-Bridge only supports public groups!',
|
||||
'You must be logged in to view this page',
|
||||
'Unable to get the page id. You should consider getting the ID by hand',
|
||||
// tiktok 404
|
||||
'https://www.tiktok.com/@',
|
||||
];
|
||||
foreach ($ignoredExceptions as $ignoredException) {
|
||||
if (str_starts_with($e->getMessage(), $ignoredException)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
$context = '';
|
||||
if ($record['context']) {
|
||||
try {
|
||||
$context = Json::encode($record['context']);
|
||||
} catch (\JsonException $e) {
|
||||
$record['context']['message'] = null;
|
||||
$context = Json::encode($record['context']);
|
||||
}
|
||||
}
|
||||
$text = sprintf(
|
||||
"[%s] %s.%s %s %s\n",
|
||||
$record['created_at']->format('Y-m-d H:i:s'),
|
||||
$record['name'],
|
||||
$record['level_name'],
|
||||
// Should probably sanitize message for output context
|
||||
$record['message'],
|
||||
$context
|
||||
);
|
||||
error_log($text);
|
||||
//$bytes = file_put_contents('/tmp/rss-bridge.log', $text, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
}
|
||||
|
||||
final class NullLogger implements Logger
|
||||
{
|
||||
public function debug(string $message, array $context = [])
|
||||
{
|
||||
}
|
||||
|
||||
public function info(string $message, array $context = []): void
|
||||
{
|
||||
}
|
||||
|
||||
public function warning(string $message, array $context = []): void
|
||||
{
|
||||
}
|
||||
|
||||
public function error(string $message, array $context = []): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace RssBridge\Tests\Actions;
|
||||
|
||||
use ActionInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ActionImplementationTest extends TestCase
|
||||
{
|
||||
private $class;
|
||||
private $obj;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
\Configuration::loadConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataActionsProvider
|
||||
*/
|
||||
public function testClassName($path)
|
||||
{
|
||||
$this->setAction($path);
|
||||
$this->assertTrue($this->class === ucfirst($this->class), 'class name must start with uppercase character');
|
||||
$this->assertEquals(0, substr_count($this->class, ' '), 'class name must not contain spaces');
|
||||
$this->assertStringEndsWith('Action', $this->class, 'class name must end with "Action"');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataActionsProvider
|
||||
*/
|
||||
public function testClassType($path)
|
||||
{
|
||||
$this->setAction($path);
|
||||
$this->assertInstanceOf(ActionInterface::class, $this->obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataActionsProvider
|
||||
*/
|
||||
public function testVisibleMethods($path)
|
||||
{
|
||||
$allowedMethods = get_class_methods(ActionInterface::class);
|
||||
sort($allowedMethods);
|
||||
|
||||
$this->setAction($path);
|
||||
|
||||
$methods = array_diff(get_class_methods($this->obj), ['__construct']);
|
||||
sort($methods);
|
||||
|
||||
$this->assertEquals($allowedMethods, $methods);
|
||||
}
|
||||
|
||||
public function dataActionsProvider()
|
||||
{
|
||||
$actions = [];
|
||||
foreach (glob(PATH_LIB_ACTIONS . '*.php') as $path) {
|
||||
$actions[basename($path, '.php')] = [$path];
|
||||
}
|
||||
return $actions;
|
||||
}
|
||||
|
||||
private function setAction($path)
|
||||
{
|
||||
$this->class = '\\' . basename($path, '.php');
|
||||
$this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
|
||||
$this->obj = new $this->class();
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace RssBridge\Tests\Actions;
|
||||
|
||||
use BridgeFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ListActionTest extends TestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
\Configuration::loadConfiguration();
|
||||
}
|
||||
|
||||
public function testHeaders()
|
||||
{
|
||||
$action = new \ListAction();
|
||||
$response = $action->execute([]);
|
||||
$headers = $response->getHeaders();
|
||||
$contentType = $response->getHeader('content-type');
|
||||
$this->assertSame($contentType, 'application/json');
|
||||
}
|
||||
|
||||
public function testOutput()
|
||||
{
|
||||
$action = new \ListAction();
|
||||
$response = $action->execute([]);
|
||||
$data = $response->getBody();
|
||||
|
||||
$items = json_decode($data, true);
|
||||
|
||||
$this->assertNotNull($items, 'invalid JSON output: ' . json_last_error_msg());
|
||||
|
||||
$this->assertArrayHasKey('total', $items, 'Missing "total" parameter');
|
||||
$this->assertIsInt($items['total'], 'Invalid type');
|
||||
|
||||
$this->assertArrayHasKey('bridges', $items, 'Missing "bridges" array');
|
||||
|
||||
$this->assertEquals(
|
||||
$items['total'],
|
||||
count($items['bridges']),
|
||||
'Item count doesn\'t match'
|
||||
);
|
||||
|
||||
$bridgeFactory = new BridgeFactory();
|
||||
|
||||
$this->assertEquals(
|
||||
count($bridgeFactory->getBridgeClassNames()),
|
||||
count($items['bridges']),
|
||||
'Number of bridges doesn\'t match'
|
||||
);
|
||||
|
||||
$expectedKeys = [
|
||||
'status',
|
||||
'uri',
|
||||
'name',
|
||||
'icon',
|
||||
'parameters',
|
||||
'maintainer',
|
||||
'description'
|
||||
];
|
||||
|
||||
$allowedStatus = [
|
||||
'active',
|
||||
'inactive'
|
||||
];
|
||||
|
||||
foreach ($items['bridges'] as $bridge) {
|
||||
foreach ($expectedKeys as $key) {
|
||||
$this->assertArrayHasKey($key, $bridge, 'Missing key "' . $key . '"');
|
||||
}
|
||||
|
||||
$this->assertContains($bridge['status'], $allowedStatus, 'Invalid status value');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,25 +6,14 @@ use PHPUnit\Framework\TestCase;
|
|||
|
||||
class BridgeFactoryTest extends TestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
\Configuration::loadConfiguration();
|
||||
}
|
||||
|
||||
public function testNormalizeBridgeName()
|
||||
{
|
||||
$this->assertSame('TwitterBridge', \BridgeFactory::normalizeBridgeName('TwitterBridge'));
|
||||
$this->assertSame('TwitterBridge', \BridgeFactory::normalizeBridgeName('TwitterBridge.php'));
|
||||
$this->assertSame('TwitterBridge', \BridgeFactory::normalizeBridgeName('Twitter'));
|
||||
}
|
||||
|
||||
public function testSanitizeBridgeName()
|
||||
{
|
||||
$sut = new \BridgeFactory();
|
||||
|
||||
$this->assertSame('TwitterBridge', $sut->createBridgeClassName('twitterbridge'));
|
||||
$this->assertSame('TwitterBridge', $sut->createBridgeClassName('twitter'));
|
||||
$this->assertSame('TwitterBridge', $sut->createBridgeClassName('tWitTer'));
|
||||
$this->assertSame('TwitterBridge', $sut->createBridgeClassName('TWITTERBRIDGE'));
|
||||
// $this->assertSame('TwitterBridge', $sut->createBridgeClassName('twitterbridge'));
|
||||
// $this->assertSame('TwitterBridge', $sut->createBridgeClassName('twitter'));
|
||||
// $this->assertSame('TwitterBridge', $sut->createBridgeClassName('tWitTer'));
|
||||
// $this->assertSame('TwitterBridge', $sut->createBridgeClassName('TWITTERBRIDGE'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -231,7 +231,10 @@ class BridgeImplementationTest extends TestCase
|
|||
{
|
||||
$this->class = '\\' . basename($path, '.php');
|
||||
$this->assertTrue(class_exists($this->class), 'class ' . $this->class . ' doesn\'t exist');
|
||||
$this->obj = new $this->class();
|
||||
$this->obj = new $this->class(
|
||||
new \NullCache(),
|
||||
new \NullLogger()
|
||||
);
|
||||
}
|
||||
|
||||
private function checkUrl($url)
|
||||
|
|
|
@ -8,13 +8,13 @@ class CacheTest extends TestCase
|
|||
{
|
||||
public function testConfig()
|
||||
{
|
||||
$sut = new \FileCache(['path' => '/tmp/']);
|
||||
$sut = new \FileCache(new \NullLogger(), ['path' => '/tmp/']);
|
||||
$this->assertSame(['path' => '/tmp/', 'enable_purge' => true], $sut->getConfig());
|
||||
|
||||
$sut = new \FileCache(['path' => '/', 'enable_purge' => false]);
|
||||
$sut = new \FileCache(new \NullLogger(), ['path' => '/', 'enable_purge' => false]);
|
||||
$this->assertSame(['path' => '/', 'enable_purge' => false], $sut->getConfig());
|
||||
|
||||
$sut = new \FileCache(['path' => '/tmp', 'enable_purge' => true]);
|
||||
$sut = new \FileCache(new \NullLogger(), ['path' => '/tmp', 'enable_purge' => true]);
|
||||
$this->assertSame(['path' => '/tmp/', 'enable_purge' => true], $sut->getConfig());
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ class CacheTest extends TestCase
|
|||
$temporaryFolder = sprintf('%s/rss_bridge_%s/', sys_get_temp_dir(), create_random_string());
|
||||
mkdir($temporaryFolder);
|
||||
|
||||
$sut = new \FileCache([
|
||||
$sut = new \FileCache(new \NullLogger(), [
|
||||
'path' => $temporaryFolder,
|
||||
'enable_purge' => true,
|
||||
]);
|
||||
|
|
Loading…
Reference in a new issue