Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions Modules/FeedFinder/Sources/FeedFinder/FeedFinder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,32 @@ public final class FeedFinder {
}
}

extension FeedFinder {

/// True when `feedURLString` is on the same host as `pageURLString` and its
/// path equals or is nested under the page's path. The page must have a
/// non-root path — a root request (e.g. example.com/) gets no on-path
/// preference, preserving the existing <head>-feed behavior for whole sites.
static func feedURLString(_ feedURLString: String, isUnderRequestedPageURLString pageURLString: String) -> Bool {
guard let feedURL = URL(string: feedURLString), let pageURL = URL(string: pageURLString),
let feedHost = feedURL.host(), let pageHost = pageURL.host() else {
return false
}
func normalizedHost(_ host: String) -> String {
let lowered = host.lowercased(with: localeForLowercasing)
return lowered.hasPrefix("www.") ? String(lowered.dropFirst("www.".count)) : lowered
}
guard normalizedHost(feedHost) == normalizedHost(pageHost) else {
return false
}
let pagePath = pageURL.path.hasSuffix("/") ? String(pageURL.path.dropLast()) : pageURL.path
guard pagePath.count > 1 else {
return false
}
return feedURL.path == pagePath || feedURL.path.hasPrefix(pagePath + "/")
}
}

private extension FeedFinder {

static func addFeedSpecifier(_ feedSpecifier: FeedSpecifier, feedSpecifiers: inout [String: FeedSpecifier]) {
Expand Down Expand Up @@ -180,6 +206,23 @@ private extension FeedFinder {
}
}

// A body-linked feed whose URL lives *under* the requested page's path
// (e.g. /blog/feed for a /blog request) is more specific than a
// site-wide feed declared in <head> (e.g. a network master feed). Prefer
// it: validate the on-path body candidates and, if any are real feeds,
// return those instead of the <head> feed. Without this, any <head> feed
// unconditionally suppressed every body feed, so a page-specific feed was
// discarded before it could be considered. (#5299)
let onPathToDownload = feedSpecifiersToDownload.filter {
Self.feedURLString($0.urlString, isUnderRequestedPageURLString: urlString)
}
if !onPathToDownload.isEmpty {
let onPathFeeds = await downloadFeedSpecifiers(onPathToDownload, feedSpecifiers: [:])
if !onPathFeeds.isEmpty {
return (onPathFeeds, .candidates)
}
}

if didFindFeedInHTMLHead {
return (Set(feedSpecifiers.values), .htmlHead)
} else if feedSpecifiersToDownload.isEmpty {
Expand Down
31 changes: 31 additions & 0 deletions Modules/FeedFinder/Tests/FeedFinderTests/FeedFinderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,35 @@ final class FeedFinderTests: XCTestCase {
let feedFinder = FeedFinder()
XCTAssertNotNil(feedFinder)
}

// Covers #5299: a page-specific feed linked in the body (e.g. /blog/feed)
// must be recognized as "under" the requested page path so it can be
// preferred over a site-wide <head> feed.
func testFeedURLIsUnderRequestedPagePath() {
func isUnder(_ feed: String, _ page: String) -> Bool {
FeedFinder.feedURLString(feed, isUnderRequestedPageURLString: page)
}

// The bug case: /blog/feed is under the requested /blog page…
XCTAssertTrue(isUnder("https://www.relay.fm/blog/feed", "https://www.relay.fm/blog"))
// …while the site-wide feed at a different path is not on-path.
XCTAssertFalse(isUnder("http://relay.fm/master/feed", "https://www.relay.fm/blog"))

// Host comparison is www- and case-insensitive.
XCTAssertTrue(isUnder("https://relay.fm/blog/feed", "https://www.relay.fm/blog"))
XCTAssertTrue(isUnder("https://WWW.Relay.FM/blog/feed", "https://www.relay.fm/blog"))

// A different host is never on-path.
XCTAssertFalse(isUnder("https://example.com/blog/feed", "https://www.relay.fm/blog"))

// A root request gets no on-path preference (whole-site Find Feed unchanged).
XCTAssertFalse(isUnder("https://relay.fm/feed", "https://relay.fm/"))
XCTAssertFalse(isUnder("https://relay.fm/feed", "https://relay.fm"))

// A sibling that merely shares a path prefix is not nested under it.
XCTAssertFalse(isUnder("https://relay.fm/blogger/feed", "https://relay.fm/blog"))

// An exact path match counts as on-path.
XCTAssertTrue(isUnder("https://relay.fm/blog", "https://relay.fm/blog"))
}
}