2019-02-06 20:34:51 +03:00
|
|
|
<?php
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-06-22 19:30:37 +03:00
|
|
|
class DisplayAction implements ActionInterface
|
|
|
|
{
|
2023-07-24 00:05:35 +03:00
|
|
|
private CacheInterface $cache;
|
2023-09-21 23:05:55 +03:00
|
|
|
private Logger $logger;
|
2024-08-29 23:48:59 +03:00
|
|
|
private BridgeFactory $bridgeFactory;
|
2023-09-21 23:05:55 +03:00
|
|
|
|
2024-08-29 23:48:59 +03:00
|
|
|
public function __construct(
|
|
|
|
CacheInterface $cache,
|
|
|
|
Logger $logger,
|
|
|
|
BridgeFactory $bridgeFactory
|
|
|
|
) {
|
|
|
|
$this->cache = $cache;
|
|
|
|
$this->logger = $logger;
|
|
|
|
$this->bridgeFactory = $bridgeFactory;
|
2023-09-21 23:05:55 +03:00
|
|
|
}
|
2023-07-24 00:05:35 +03:00
|
|
|
|
2024-08-07 01:21:06 +03:00
|
|
|
public function __invoke(Request $request): Response
|
2019-02-06 20:34:51 +03:00
|
|
|
{
|
2024-01-25 18:06:24 +03:00
|
|
|
$bridgeName = $request->get('bridge');
|
|
|
|
$format = $request->get('format');
|
|
|
|
$noproxy = $request->get('_noproxy');
|
|
|
|
|
|
|
|
$cacheKey = 'http_' . json_encode($request->toArray());
|
2023-07-24 00:05:35 +03:00
|
|
|
/** @var Response $cachedResponse */
|
2023-09-10 22:50:15 +03:00
|
|
|
$cachedResponse = $this->cache->get($cacheKey);
|
|
|
|
if ($cachedResponse) {
|
2024-08-22 01:33:35 +03:00
|
|
|
$ifModifiedSince = $request->server('HTTP_IF_MODIFIED_SINCE');
|
2023-09-10 22:50:15 +03:00
|
|
|
$lastModified = $cachedResponse->getHeader('last-modified');
|
|
|
|
if ($ifModifiedSince && $lastModified) {
|
|
|
|
$lastModified = new \DateTimeImmutable($lastModified);
|
|
|
|
$lastModifiedTimestamp = $lastModified->getTimestamp();
|
|
|
|
$modifiedSince = strtotime($ifModifiedSince);
|
2024-08-22 01:33:35 +03:00
|
|
|
// TODO: \DateTimeImmutable can be compared directly
|
2023-09-10 22:50:15 +03:00
|
|
|
if ($lastModifiedTimestamp <= $modifiedSince) {
|
|
|
|
$modificationTimeGMT = gmdate('D, d M Y H:i:s ', $lastModifiedTimestamp);
|
|
|
|
return new Response('', 304, ['last-modified' => $modificationTimeGMT . 'GMT']);
|
|
|
|
}
|
|
|
|
}
|
2024-03-31 22:02:55 +03:00
|
|
|
return $cachedResponse->withHeader('rss-bridge', 'This is a cached response');
|
2023-07-24 00:05:35 +03:00
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2023-09-10 22:50:15 +03:00
|
|
|
if (!$bridgeName) {
|
2023-09-23 19:54:14 +03:00
|
|
|
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Missing bridge parameter']), 400);
|
2023-09-10 22:50:15 +03:00
|
|
|
}
|
2024-08-29 23:48:59 +03:00
|
|
|
$bridgeClassName = $this->bridgeFactory->createBridgeClassName($bridgeName);
|
2023-08-01 20:35:15 +03:00
|
|
|
if (!$bridgeClassName) {
|
2023-09-23 19:54:14 +03:00
|
|
|
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Bridge not found']), 404);
|
2023-08-01 20:35:15 +03:00
|
|
|
}
|
2024-01-25 18:06:24 +03:00
|
|
|
|
2022-08-06 23:46:28 +03:00
|
|
|
if (!$format) {
|
2023-09-23 19:54:14 +03:00
|
|
|
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a format']), 400);
|
2022-08-06 23:46:28 +03:00
|
|
|
}
|
2024-08-29 23:48:59 +03:00
|
|
|
if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
|
2023-09-23 19:54:14 +03:00
|
|
|
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'This bridge is not whitelisted']), 400);
|
2019-02-06 20:34:51 +03:00
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2024-08-18 20:11:11 +03:00
|
|
|
// Disable proxy (if enabled and per user's request)
|
2023-07-08 18:06:33 +03:00
|
|
|
if (
|
|
|
|
Configuration::getConfig('proxy', 'url')
|
|
|
|
&& Configuration::getConfig('proxy', 'by_bridge')
|
|
|
|
&& $noproxy
|
|
|
|
) {
|
|
|
|
// This const is only used once in getContents()
|
2019-02-06 20:34:51 +03:00
|
|
|
define('NOPROXY', true);
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2024-08-29 23:48:59 +03:00
|
|
|
$bridge = $this->bridgeFactory->create($bridgeClassName);
|
2023-09-10 22:50:15 +03:00
|
|
|
|
|
|
|
$response = $this->createResponse($request, $bridge, $format);
|
|
|
|
|
|
|
|
if ($response->getCode() === 200) {
|
2024-01-25 18:06:24 +03:00
|
|
|
$ttl = $request->get('_cache_timeout');
|
2023-09-10 22:50:15 +03:00
|
|
|
if (Configuration::getConfig('cache', 'custom_timeout') && $ttl) {
|
|
|
|
$ttl = (int) $ttl;
|
|
|
|
} else {
|
|
|
|
$ttl = $bridge->getCacheTimeout();
|
|
|
|
}
|
|
|
|
$this->cache->set($cacheKey, $response, $ttl);
|
|
|
|
}
|
|
|
|
|
2023-12-13 23:56:14 +03:00
|
|
|
if (in_array($response->getCode(), [403, 429, 503])) {
|
|
|
|
// Cache these responses for about ~20 mins on average
|
|
|
|
$this->cache->set($cacheKey, $response, 60 * 15 + rand(1, 60 * 10));
|
2019-02-06 20:34:51 +03:00
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2023-09-10 22:50:15 +03:00
|
|
|
if ($response->getCode() === 500) {
|
|
|
|
$this->cache->set($cacheKey, $response, 60 * 15);
|
|
|
|
}
|
2023-12-13 23:56:14 +03:00
|
|
|
|
2023-09-10 22:50:15 +03:00
|
|
|
if (rand(1, 100) === 2) {
|
|
|
|
$this->cache->prune();
|
|
|
|
}
|
2023-12-13 23:56:14 +03:00
|
|
|
|
2023-09-10 22:50:15 +03:00
|
|
|
return $response;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2024-03-31 04:38:42 +03:00
|
|
|
private function createResponse(Request $request, BridgeAbstract $bridge, string $format)
|
2023-09-10 22:50:15 +03:00
|
|
|
{
|
2019-02-06 20:34:51 +03:00
|
|
|
$items = [];
|
2023-07-19 06:05:49 +03:00
|
|
|
|
2023-09-10 22:50:15 +03:00
|
|
|
try {
|
|
|
|
$bridge->loadConfiguration();
|
|
|
|
// Remove parameters that don't concern bridges
|
2024-01-25 15:03:00 +03:00
|
|
|
$remove = [
|
2024-01-25 20:20:02 +03:00
|
|
|
'token',
|
2024-01-25 15:03:00 +03:00
|
|
|
'action',
|
|
|
|
'bridge',
|
|
|
|
'format',
|
|
|
|
'_noproxy',
|
|
|
|
'_cache_timeout',
|
|
|
|
'_error_time',
|
|
|
|
'_', // Some RSS readers add a cache-busting parameter (_=<timestamp>) to feed URLs, detect and ignore them.
|
|
|
|
];
|
2024-01-25 18:06:24 +03:00
|
|
|
$requestArray = $request->toArray();
|
|
|
|
$input = array_diff_key($requestArray, array_fill_keys($remove, ''));
|
2023-10-01 20:23:30 +03:00
|
|
|
$bridge->setInput($input);
|
2023-09-10 22:50:15 +03:00
|
|
|
$bridge->collectData();
|
2024-07-31 20:25:51 +03:00
|
|
|
$items = $bridge->getItems();
|
2024-08-08 03:13:04 +03:00
|
|
|
} catch (\Throwable $e) {
|
|
|
|
if ($e instanceof RateLimitException) {
|
|
|
|
// These are internally generated by bridges
|
|
|
|
$this->logger->info(sprintf('RateLimitException in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
|
|
|
|
return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 429);
|
|
|
|
}
|
2023-09-10 22:50:15 +03:00
|
|
|
if ($e instanceof HttpException) {
|
|
|
|
// Reproduce (and log) these responses regardless of error output and report limit
|
|
|
|
if ($e->getCode() === 429) {
|
2023-09-21 23:05:55 +03:00
|
|
|
$this->logger->info(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
|
2023-09-23 19:54:14 +03:00
|
|
|
return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 429);
|
2022-11-18 23:36:06 +03:00
|
|
|
}
|
2023-09-10 22:50:15 +03:00
|
|
|
if ($e->getCode() === 503) {
|
2023-09-21 23:05:55 +03:00
|
|
|
$this->logger->info(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
|
2023-09-23 19:54:14 +03:00
|
|
|
return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 503);
|
2023-07-24 00:05:35 +03:00
|
|
|
}
|
2023-09-10 22:50:15 +03:00
|
|
|
}
|
2023-09-21 23:05:55 +03:00
|
|
|
$this->logger->error(sprintf('Exception in DisplayAction(%s)', $bridge->getShortName()), ['e' => $e]);
|
2023-09-20 03:45:48 +03:00
|
|
|
$errorOutput = Configuration::getConfig('error', 'output');
|
|
|
|
$reportLimit = Configuration::getConfig('error', 'report_limit');
|
2023-09-10 22:50:15 +03:00
|
|
|
$errorCount = 1;
|
|
|
|
if ($reportLimit > 1) {
|
|
|
|
$errorCount = $this->logBridgeError($bridge->getName(), $e->getCode());
|
|
|
|
}
|
|
|
|
// Let clients know about the error if we are passed the report limit
|
|
|
|
if ($errorCount >= $reportLimit) {
|
|
|
|
if ($errorOutput === 'feed') {
|
|
|
|
// Render the exception as a feed item
|
2024-08-07 19:56:27 +03:00
|
|
|
$items = [$this->createFeedItemFromException($e, $bridge)];
|
2023-09-10 22:50:15 +03:00
|
|
|
} elseif ($errorOutput === 'http') {
|
2023-09-23 19:54:14 +03:00
|
|
|
return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 500);
|
2023-09-10 22:50:15 +03:00
|
|
|
} elseif ($errorOutput === 'none') {
|
|
|
|
// Do nothing (produces an empty feed)
|
2019-10-31 20:40:51 +03:00
|
|
|
}
|
2019-02-06 20:34:51 +03:00
|
|
|
}
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2024-03-31 04:38:42 +03:00
|
|
|
$formatFactory = new FormatFactory();
|
|
|
|
$format = $formatFactory->create($format);
|
|
|
|
|
2022-07-08 21:39:13 +03:00
|
|
|
$format->setItems($items);
|
2024-08-08 04:43:26 +03:00
|
|
|
$format->setFeed($bridge->getFeed());
|
2023-09-10 22:50:15 +03:00
|
|
|
$now = time();
|
|
|
|
$format->setLastModified($now);
|
|
|
|
$headers = [
|
|
|
|
'last-modified' => gmdate('D, d M Y H:i:s ', $now) . 'GMT',
|
2024-08-23 18:34:06 +03:00
|
|
|
'content-type' => $format->getMimeType() . '; charset=UTF-8',
|
2023-09-10 22:50:15 +03:00
|
|
|
];
|
2024-08-23 18:34:06 +03:00
|
|
|
$body = $format->render();
|
|
|
|
|
|
|
|
// This is supposed to remove non-utf8 byte sequences, but I'm unsure if it works
|
|
|
|
ini_set('mbstring.substitute_character', 'none');
|
|
|
|
$body = mb_convert_encoding($body, 'UTF-8', 'UTF-8');
|
|
|
|
|
|
|
|
return new Response($body, 200, $headers);
|
2022-07-08 21:39:13 +03:00
|
|
|
}
|
|
|
|
|
2024-08-08 04:43:26 +03:00
|
|
|
private function createFeedItemFromException($e, BridgeAbstract $bridge): array
|
2023-02-03 00:53:01 +03:00
|
|
|
{
|
2024-08-08 04:43:26 +03:00
|
|
|
$item = [];
|
2023-02-03 00:53:01 +03:00
|
|
|
|
|
|
|
// Create a unique identifier every 24 hours
|
|
|
|
$uniqueIdentifier = urlencode((int)(time() / 86400));
|
2023-09-25 22:18:48 +03:00
|
|
|
$title = sprintf('Bridge returned error %s! (%s)', $e->getCode(), $uniqueIdentifier);
|
2024-08-08 04:43:26 +03:00
|
|
|
|
|
|
|
$item['title'] = $title;
|
|
|
|
$item['uri'] = get_current_url();
|
|
|
|
$item['timestamp'] = time();
|
2023-02-03 00:53:01 +03:00
|
|
|
|
2023-06-02 21:22:09 +03:00
|
|
|
// Create an item identifier for feed readers e.g. "staysafetv twitch videos_19389"
|
2024-08-08 04:43:26 +03:00
|
|
|
$item['uid'] = $bridge->getName() . '_' . $uniqueIdentifier;
|
2023-02-03 00:53:01 +03:00
|
|
|
|
|
|
|
$content = render_template(__DIR__ . '/../templates/bridge-error.html.php', [
|
2023-09-23 19:54:14 +03:00
|
|
|
'error' => render_template(__DIR__ . '/../templates/exception.html.php', ['e' => $e]),
|
2023-02-03 00:53:01 +03:00
|
|
|
'searchUrl' => self::createGithubSearchUrl($bridge),
|
2024-08-22 01:33:35 +03:00
|
|
|
'issueUrl' => self::createGithubIssueUrl($bridge, $e),
|
2023-02-03 00:53:01 +03:00
|
|
|
'maintainer' => $bridge->getMaintainer(),
|
|
|
|
]);
|
2024-08-08 04:43:26 +03:00
|
|
|
$item['content'] = $content;
|
|
|
|
|
2023-02-03 00:53:01 +03:00
|
|
|
return $item;
|
|
|
|
}
|
|
|
|
|
2023-07-24 00:05:35 +03:00
|
|
|
private function logBridgeError($bridgeName, $code)
|
2023-02-03 00:53:01 +03:00
|
|
|
{
|
2023-12-13 23:56:14 +03:00
|
|
|
// todo: it's not really necessary to json encode $report
|
2023-09-10 22:50:15 +03:00
|
|
|
$cacheKey = 'error_reporting_' . $bridgeName . '_' . $code;
|
|
|
|
$report = $this->cache->get($cacheKey);
|
2023-07-24 00:05:35 +03:00
|
|
|
if ($report) {
|
2023-02-03 00:53:01 +03:00
|
|
|
$report = Json::decode($report);
|
|
|
|
$report['time'] = time();
|
|
|
|
$report['count']++;
|
|
|
|
} else {
|
|
|
|
$report = [
|
|
|
|
'error' => $code,
|
|
|
|
'time' => time(),
|
|
|
|
'count' => 1,
|
|
|
|
];
|
|
|
|
}
|
2023-09-10 22:50:15 +03:00
|
|
|
$ttl = 86400 * 5;
|
|
|
|
$this->cache->set($cacheKey, Json::encode($report), $ttl);
|
2023-02-03 00:53:01 +03:00
|
|
|
return $report['count'];
|
|
|
|
}
|
|
|
|
|
2024-08-22 01:33:35 +03:00
|
|
|
private static function createGithubIssueUrl(BridgeAbstract $bridge, \Throwable $e): string
|
2022-07-08 21:39:13 +03:00
|
|
|
{
|
2024-07-31 22:57:33 +03:00
|
|
|
$maintainer = $bridge->getMaintainer();
|
|
|
|
if (str_contains($maintainer, ',')) {
|
|
|
|
$maintainers = explode(',', $maintainer);
|
|
|
|
} else {
|
|
|
|
$maintainers = [$maintainer];
|
|
|
|
}
|
|
|
|
$maintainers = array_map('trim', $maintainers);
|
|
|
|
|
2024-08-22 01:33:35 +03:00
|
|
|
$queryString = $_SERVER['QUERY_STRING'] ?? '';
|
2024-07-31 22:57:33 +03:00
|
|
|
$query = [
|
|
|
|
'title' => $bridge->getName() . ' failed with: ' . $e->getMessage(),
|
2022-07-08 21:39:13 +03:00
|
|
|
'body' => sprintf(
|
2024-07-31 22:57:33 +03:00
|
|
|
"```\n%s\n\n%s\n\nQuery string: %s\nVersion: %s\nOs: %s\nPHP version: %s\n```\nMaintainer: @%s",
|
2024-08-22 01:33:35 +03:00
|
|
|
create_sane_exception_message($e),
|
2022-10-16 18:55:43 +03:00
|
|
|
implode("\n", trace_to_call_points(trace_from_exception($e))),
|
2024-08-22 01:33:35 +03:00
|
|
|
$queryString,
|
2022-07-08 21:39:13 +03:00
|
|
|
Configuration::getVersion(),
|
2022-08-03 18:05:13 +03:00
|
|
|
PHP_OS_FAMILY,
|
2024-07-31 22:57:33 +03:00
|
|
|
phpversion() ?: 'Unknown',
|
|
|
|
implode(', @', $maintainers),
|
2022-07-08 21:39:13 +03:00
|
|
|
),
|
|
|
|
'labels' => 'Bridge-Broken',
|
2024-07-31 22:57:33 +03:00
|
|
|
'assignee' => $maintainer[0],
|
|
|
|
];
|
|
|
|
|
|
|
|
return 'https://github.com/RSS-Bridge/rss-bridge/issues/new?' . http_build_query($query);
|
2022-07-08 21:39:13 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
private static function createGithubSearchUrl($bridge): string
|
|
|
|
{
|
|
|
|
return sprintf(
|
|
|
|
'https://github.com/RSS-Bridge/rss-bridge/issues?q=%s',
|
|
|
|
urlencode('is:issue is:open ' . $bridge->getName())
|
|
|
|
);
|
2019-02-06 20:34:51 +03:00
|
|
|
}
|
|
|
|
}
|