feat(favicon): Use feed-provided icon URL (<image><url>, Atom icon/logo, JSON Feed icon) (#8633)

* prefer feed.icon
Closes #5518

Changes proposed in this pull request:

- When a feed provides an icon URL (<image><url> in RSS 2.0/1.0, <atom:icon>/<atom:logo> in Atom, icon/favicon fields in JSON Feed), that URL is stored as a feedIconUrl attribute on the feed and used as the primary source for favicon downloads, instead of scraping the feed's website for <link rel="icon"> tags.
- If the feed-provided icon URL fails to return a valid image, the existing fallback chain (website HTML favicon search → /favicon.ico) is preserved.
Custom favicons uploaded by users always take priority and are never overridden.

How to test the feature manually:

1. Add an RSS feed that includes a <image><url> element (e.g. an RSSHub feed: `https://rsshub.app/youtube/channel/UC2cRwTuSWxxEtrRnT4lrlQA`). After actualization, confirm the feed's favicon matches the avatar image from the feed, not the Bilibili site favicon.
2. Add an Atom feed containing <atom:icon> or <atom:logo> Confirm the feed icon is used.
3. Add a JSON Feed (spec: icon field). Confirm icon is preferred over favicon when both are present.
4. Temporarily point a feed's <image><url> to a broken URL. Confirm FreshRSS falls back to the website favicon silently.
5. Upload a custom favicon for a feed, then actualize it. Confirm the custom favicon is not replaced.

<img width="470" height="317" alt="image" src="https://github.com/user-attachments/assets/17445154-d94c-44d6-b7e7-019bf24c5767" />

* fix(favicon): use htmlspecialchars_decode for feed image URL

* Decode quotes as well

* New function in our SimplePie fork
https://github.com/FreshRSS/simplepie/pull/73

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
This commit is contained in:
Bowen
2026-03-31 15:57:06 +08:00
committed by GitHub
parent 61f3151f7f
commit ae2d0d7fe8
7 changed files with 73 additions and 9 deletions

View File

@@ -744,6 +744,14 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
}
if ($simplePie != null) {
$feedImageUrl = htmlspecialchars_decode($simplePie->get_icon_url() ?? '', ENT_QUOTES);
$feedImageUrl = $feedImageUrl !== '' ? (FreshRSS_http_Util::checkUrl($feedImageUrl) ?: '') : '';
if ($feedImageUrl !== ($feed->attributeString('feedIconUrl') ?? '')) {
$feed->_attribute('feedIconUrl', $feedImageUrl !== '' ? $feedImageUrl : null);
$feed->resetFaviconHash();
$feedProperties['attributes'] = $feed->attributes();
}
if ($feed->name(true) === '') {
//HTML to HTML-PRE //ENT_COMPAT except '&'
$name = strtr(html_only_entity_decode($simplePie->get_title()), ['<' => '&lt;', '>' => '&gt;', '"' => '&quot;']);