diff --git a/actions/FeedIconAction.php b/actions/FeedIconAction.php index c48a74f46f7..c296f582758 100644 --- a/actions/FeedIconAction.php +++ b/actions/FeedIconAction.php @@ -11,63 +11,119 @@ public function __construct() $this->bridgeFactory = new BridgeFactory(); } - public function execute(array $request) + public function execute(array $request): void { $bridgeClassName = $request['bridgeClassName'] ?? null; - if (!$bridgeClassName) { + if (!$bridgeClassName || !class_exists($bridgeClassName)) { $this->sendNotFoundResponse(); } $cacheKey = $this->buildCacheKey($bridgeClassName); - if ($cachedImageData = $this->cache->get($cacheKey)) { - $mimeType = $this->cache->get($cacheKey . 'mimeType'); - if ($mimeType) { - $this->sendImageResponse($mimeType, $cachedImageData); - } - } - - $bridge = $this->bridgeFactory->create($bridgeClassName); - $image_url = $bridge->getIcon(); + list( + 'imageData' => $imageData, + 'mimeType' => $mimeType + ) = $this->readCache($cacheKey); - if (!filter_var($image_url, FILTER_VALIDATE_URL)) { + if ($imageData && $mimeType) { + $this->sendImageResponse($mimeType, $imageData); + } + elseif('' === $imageData && '' === $mimeType) { + // empty values means that the image was broken and could not be loaded $this->sendNotFoundResponse(); } - $image_data = file_get_contents($image_url); + $bridge = $this->bridgeFactory->create($bridgeClassName); + $imageUrl = $bridge->getIcon(); + + $imageUrl = $this->cleanImageUrl($imageUrl); - if ($image_data === false) { + if (!filter_var($imageUrl, FILTER_VALIDATE_URL)) { + //store empty values to prevent further attempts to load broken image + $this->writeCache($cacheKey, '', ''); $this->sendNotFoundResponse(); } - $image_info = getimagesize($image_url); - if ($image_info === false) { + list( + 'imageData' => $imageData, + 'mimeType' => $mimeType + ) = $this->readImageData($imageUrl); + + if (false === $imageData || false === $mimeType) { + //store empty values to prevent further attempts to load broken image + $this->writeCache($cacheKey, '', ''); $this->sendNotFoundResponse(); } - $mimeType = $image_info['mime']; - $this->cache->set($cacheKey . 'mimeType', $mimeType); - $this->cache->set($cacheKey, $image_data); - - $this->sendImageResponse($mimeType, $image_data); + $this->writeCache($cacheKey, $mimeType, $imageData); + $this->sendImageResponse($mimeType, $imageData); } - private function sendNotFoundResponse() + private function sendNotFoundResponse(): void { header('HTTP/1.1 404 Not Found'); exit; } - private function sendImageResponse($mimeType, $imageData) + private function sendImageResponse(string $mimeType, string $imageData): void { header('Content-Type: ' . $mimeType); echo $imageData; exit; } - public function buildCacheKey($bridgeClassName): string + private function buildCacheKey(string $bridgeClassName): string { return md5($bridgeClassName . 'icon'); } + + private function cleanImageUrl(string $imageUrl): string + { + //negative look behind: replace duplicate slashes in path, but not in protocol part of uri + $imageUrl = preg_replace('~(?cache->set($cacheKey, ['imageData' => $imageData, 'mimeType' => $mimeType]); + } + + private function readCache(string $cacheKey): array + { + return (array)$this->cache->get($cacheKey); + } + + private function readImageData(string $imageUrl): array + { + $context = stream_context_create([ + 'http' => [ + 'timeout' => 10, + ], + ]); + + $handle = @fopen($imageUrl, 'r', false, $context); + + if ($handle === false) { + return ['imageData' => false, 'mimeType' => false]; + } + $metaData = stream_get_meta_data($handle); + $headers = $metaData['wrapper_data']; + + $contentType = false; + + foreach ($headers as $header) { + if (stripos($header, 'Content-Type:') === 0) { + $contentType = trim(substr($header, 13)); // 13 is the length of "Content-Type:" + break; + } + } + + $imageData = stream_get_contents($handle); + + fclose($handle); + + return ['imageData' => $imageData, 'mimeType' => $contentType]; + } } diff --git a/lib/BridgeCard.php b/lib/BridgeCard.php index c7fad94e2f6..e8e5577fc90 100644 --- a/lib/BridgeCard.php +++ b/lib/BridgeCard.php @@ -20,7 +20,7 @@ public static function displayBridgeCard($bridgeClassName, $formats, $isActive = $uri = $bridge->getURI(); $name = $bridge->getName(); - $icon = $_SERVER['PHP_SELF'] . '?action=FeedIcon&bridgeClassName=' . $bridgeClassName; + $icon = self::hasBrokenIcon($bridgeClassName) ? '' : $_SERVER['PHP_SELF'] . '?action=FeedIcon&bridgeClassName=' . $bridgeClassName; $description = $bridge->getDescription(); $parameters = $bridge->getParameters(); if (Configuration::getConfig('proxy', 'url') && Configuration::getConfig('proxy', 'by_bridge')) { @@ -371,4 +371,14 @@ private static function getCheckboxInput($entry, $id, $name) . ' />' . PHP_EOL; } + + private static function hasBrokenIcon(string $bridgeClassName): bool + { + $logger = RssBridge::getLogger(); + $cacheFactory = new CacheFactory($logger); + $cache = $cacheFactory->create(); + $cacheKey = md5($bridgeClassName . 'icon'); + $cachedImage = (array)$cache->get($cacheKey); + return isset($cachedImage['imageData']) && '' === $cachedImage['imageData'] && isset($cachedImage['mimeType']) && '' === $cachedImage['mimeType']; + } }