Skip to content

Commit

Permalink
prevent repeated loading attempts of broken feed icons
Browse files Browse the repository at this point in the history
apply timeout of 10 seconds when loading feed icons
  • Loading branch information
Niehztog committed Nov 12, 2023
1 parent a55ffd9 commit 76235cc
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 25 deletions.
104 changes: 80 additions & 24 deletions actions/FeedIconAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('~(?<!:)//~', '/', $imageUrl);
return str_replace(["\r", "\n"], '', $imageUrl);
}

private function writeCache(string $cacheKey, string $mimeType, string $imageData): void
{
$this->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];
}
}
12 changes: 11 additions & 1 deletion lib/BridgeCard.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down Expand Up @@ -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'];
}
}

0 comments on commit 76235cc

Please sign in to comment.