[GridView] Fetch Open Graph images with the FreshRSS HTTP client (#559)

The optional Open Graph thumbnail fetching downloaded article pages with
file_get_contents() on an http(s) URL. Since FreshRSS 1.30.0, every stream
wrapper except file:// and php:// is unregistered at startup, so that call
always fails and no og:image / twitter:image is ever found, both when
entries are inserted and through the asynchronous AJAX endpoint.

Use FreshRSS_http_Util::httpGet() instead, like the core favicon lookup.
It goes through cURL and also applies the system proxy and cURL options,
the internal-host (SSRF) checks, Retry-After handling and the HTML cache.
The previous 5-second timeout is kept, and relative image URLs are now
resolved against the URL reached after redirects.

FreshRSS_http_Util::httpGet() exists since FreshRSS 1.28.0, so raise the
declared compatibility accordingly and bump the version to 1.1.2.
This commit is contained in:
NopeCharon authored and GitHub committed 2026-10-27 08:26:41 +01:00
1 parent 0a0c2c2655
commit bee3194ea0
3 files changed
+13 -26

No files matched your search

+1 -1
View File
@@ -60,7 +60,7 @@ Your grid view preference is saved locally and persists across sessions.
## Requirements
- FreshRSS 1.20.0 or later
- FreshRSS 1.28.0 or later
## Responsive Breakpoints
+10 -23
View File
@@ -430,43 +430,30 @@ final class GridViewExtension extends Minz_Extension {
/**
* Fetch a URL and extract the og:image meta tag.
*
* @param string $url The article URL to fetch
* @param non-empty-string $url The article URL to fetch
* @return string|null The og:image URL or null if not found
*/
private function extractOgImage(string $url): ?string {
$ctx = stream_context_create([
'http' => [
'timeout' => 5,
'method' => 'GET',
'header' => "User-Agent: FreshRSS/GridView\r\n",
'follow_location' => 1,
'max_redirects' => 3,
'ignore_errors' => true,
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
// Only fetch the first 50 KB — og:image is in <head>
$html = @file_get_contents($url, false, $ctx, 0, 50000);
if ($html === false || $html === '') {
// Use the FreshRSS HTTP client, as FreshRSS 1.30+ disables the http(s) stream wrappers
['body' => $html, 'effective_url' => $effectiveUrl, 'fail' => $fail] = FreshRSS_http_Util::httpGet($url,
cachePath: CACHE_PATH . '/' . sha1($url) . '.html', type: 'html', attributes: ['timeout' => 5]);
if ($fail || $html === '') {
return null;
}
$pageUrl = $effectiveUrl ?: $url;
// Try og:image first, then twitter:image (both attribute orderings)
if (preg_match('/<meta\s[^>]*property=["\']og:image["\'][^>]*content=["\']([^"\']+)["\']/i', $html, $matches)) {
return $this->normalizeImageUrl($matches[1], $url);
return $this->normalizeImageUrl($matches[1], $pageUrl);
}
if (preg_match('/<meta\s[^>]*content=["\']([^"\']+)["\'][^>]*property=["\']og:image["\']/i', $html, $matches)) {
return $this->normalizeImageUrl($matches[1], $url);
return $this->normalizeImageUrl($matches[1], $pageUrl);
}
if (preg_match('/<meta\s[^>]*name=["\']twitter:image["\'][^>]*content=["\']([^"\']+)["\']/i', $html, $matches)) {
return $this->normalizeImageUrl($matches[1], $url);
return $this->normalizeImageUrl($matches[1], $pageUrl);
}
if (preg_match('/<meta\s[^>]*content=["\']([^"\']+)["\'][^>]*name=["\']twitter:image["\']/i', $html, $matches)) {
return $this->normalizeImageUrl($matches[1], $url);
return $this->normalizeImageUrl($matches[1], $pageUrl);
}
return null;
+2 -2
View File
@@ -2,8 +2,8 @@
"name": "Grid View",
"author": "Marius Stroe <https://github.com/mstroe>",
"description": "A grid/card view for displaying feeds with images, configurable 2-4 columns, FreshRSS native share integration, and browser reader mode support",
"version": "1.1.1",
"version": "1.1.2",
"entrypoint": "GridView",
"type": "user",
"compatibility": "1.25.0"
"compatibility": "1.28.0"
}