createBridgeClassName($request['bridge'] ?? ''); $format = $request['format'] ?? null; if (!$format) { throw new \Exception('You must specify a format!'); } if (!$bridgeFactory->isEnabled($bridgeClassName)) { throw new \Exception('This bridge is not whitelisted'); } $formatFactory = new FormatFactory(); $format = $formatFactory->create($format); $bridge = $bridgeFactory->create($bridgeClassName); $bridge->loadConfiguration(); $noproxy = $request['_noproxy'] ?? null; if ( Configuration::getConfig('proxy', 'url') && Configuration::getConfig('proxy', 'by_bridge') && $noproxy ) { // This const is only used once in getContents() define('NOPROXY', true); } $cacheTimeout = $request['_cache_timeout'] ?? null; if (Configuration::getConfig('cache', 'custom_timeout') && $cacheTimeout) { $cacheTimeout = (int) $cacheTimeout; } else { // At this point the query argument might still be in the url but it won't be used $cacheTimeout = $bridge->getCacheTimeout(); } // Remove parameters that don't concern bridges $bridge_params = array_diff_key( $request, array_fill_keys( [ 'action', 'bridge', 'format', '_noproxy', '_cache_timeout', '_error_time' ], '' ) ); // Remove parameters that don't concern caches $cache_params = array_diff_key( $request, array_fill_keys( [ 'action', 'format', '_noproxy', '_cache_timeout', '_error_time' ], '' ) ); $cache = RssBridge::getCache(); $cache->setScope(''); $cache->setKey($cache_params); // This cache purge will basically delete all cache items older than 24h, regardless of scope and key $cache->purgeCache(86400); $items = []; $infos = []; $mtime = $cache->getTime(); if ( $mtime && (time() - $cacheTimeout < $mtime) && !Debug::isEnabled() ) { // At this point we found the feed in the cache and debug mode is disabled if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { // The client wants to know if the feed has changed since its last check $stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); if ($mtime <= $stime) { $lastModified2 = gmdate('D, d M Y H:i:s ', $mtime) . 'GMT'; return new Response('', 304, ['Last-Modified' => $lastModified2]); } } // Load the feed from cache and prepare it $cached = $cache->loadData(); if (isset($cached['items']) && isset($cached['extraInfos'])) { foreach ($cached['items'] as $item) { $items[] = new FeedItem($item); } $infos = $cached['extraInfos']; } } else { // At this point we did NOT find the feed in the cache or debug mode is enabled. try { $bridge->setDatas($bridge_params); $bridge->collectData(); $items = $bridge->getItems(); if (isset($items[0]) && is_array($items[0])) { $feedItems = []; foreach ($items as $item) { $feedItems[] = new FeedItem($item); } $items = $feedItems; } $infos = [ 'name' => $bridge->getName(), 'uri' => $bridge->getURI(), 'donationUri' => $bridge->getDonationURI(), 'icon' => $bridge->getIcon() ]; } catch (\Exception $e) { if ($e instanceof HttpException) { Logger::warning(sprintf('Exception in DisplayAction(%s): %s', $bridgeClassName, create_sane_exception_message($e))); if ($e->getCode() === 429) { return new Response('503 Service Unavailable', 503); } } else { Logger::error(sprintf('Exception in DisplayAction(%s): %s', $bridgeClassName, create_sane_exception_message($e)), ['e' => $e]); } // Emit error only if we are passed the error report limit $errorCount = self::logBridgeError($bridge->getName(), $e->getCode()); if ($errorCount >= Configuration::getConfig('error', 'report_limit')) { if (Configuration::getConfig('error', 'output') === 'feed') { // Emit the error as a feed item in a feed so that feed readers can pick it up $items[] = $this->createFeedItemFromException($e, $bridge); } elseif (Configuration::getConfig('error', 'output') === 'http') { throw $e; } } } // Unfortunately need to set scope and key again because they might be modified $cache->setScope(''); $cache->setKey($cache_params); $cache->saveData([ 'items' => array_map(function (FeedItem $item) { return $item->toArray(); }, $items), 'extraInfos' => $infos ]); } $format->setItems($items); $format->setExtraInfos($infos); $lastModified = $cache->getTime(); $format->setLastModified($lastModified); $headers = []; if ($lastModified) { $headers['Last-Modified'] = gmdate('D, d M Y H:i:s ', $lastModified) . 'GMT'; } $headers['Content-Type'] = $format->getMimeType() . '; charset=' . $format->getCharset(); return new Response($format->stringify(), 200, $headers); } private function createFeedItemFromException($e, BridgeInterface $bridge): FeedItem { $item = new FeedItem(); // Create a unique identifier every 24 hours $uniqueIdentifier = urlencode((int)(time() / 86400)); $itemTitle = sprintf('Bridge returned error %s! (%s)', $e->getCode(), $uniqueIdentifier); $item->setTitle($itemTitle); $item->setURI(get_current_url()); $item->setTimestamp(time()); // Create an item identifier for feed readers e.g. "staysafetv twitch videos_19389" $item->setUid($bridge->getName() . '_' . $uniqueIdentifier); $content = render_template(__DIR__ . '/../templates/bridge-error.html.php', [ 'error' => render_template(__DIR__ . '/../templates/error.html.php', ['e' => $e]), 'searchUrl' => self::createGithubSearchUrl($bridge), 'issueUrl' => self::createGithubIssueUrl($bridge, $e, create_sane_exception_message($e)), 'maintainer' => $bridge->getMaintainer(), ]); $item->setContent($content); return $item; } private static function logBridgeError($bridgeName, $code) { $cache = RssBridge::getCache(); $cache->setScope('error_reporting'); $cache->setkey([$bridgeName . '_' . $code]); if ($report = $cache->loadData()) { $report = Json::decode($report); $report['time'] = time(); $report['count']++; } else { $report = [ 'error' => $code, 'time' => time(), 'count' => 1, ]; } $cache->saveData(Json::encode($report)); return $report['count']; } private static function createGithubIssueUrl($bridge, $e, string $message): string { return sprintf('https://github.com/RSS-Bridge/rss-bridge/issues/new?%s', http_build_query([ 'title' => sprintf('%s failed with error %s', $bridge->getName(), $e->getCode()), 'body' => sprintf( "```\n%s\n\n%s\n\nQuery string: %s\nVersion: %s\nOs: %s\nPHP version: %s\n```", $message, implode("\n", trace_to_call_points(trace_from_exception($e))), $_SERVER['QUERY_STRING'] ?? '', Configuration::getVersion(), PHP_OS_FAMILY, phpversion() ?: 'Unknown' ), 'labels' => 'Bridge-Broken', 'assignee' => $bridge->getMaintainer(), ])); } 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()) ); } }