Skip to content

Commit da14f94

Browse files
Alex BanguAlex Bangu
authored andcommitted
Simplified the changes on this branch to make it easier to review
1 parent 3b2e6c1 commit da14f94

4 files changed

Lines changed: 321 additions & 4 deletions

File tree

firefox-ios/Client.xcodeproj/project.pbxproj

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2268,6 +2268,9 @@
22682268
EDF567A02C8B51DC00FDB09D /* SiteImageView in Frameworks */ = {isa = PBXBuildFile; productRef = EDF5679F2C8B51DC00FDB09D /* SiteImageView */; };
22692269
EDF567A22C8B51E100FDB09D /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = EDF567A12C8B51E100FDB09D /* Kingfisher */; };
22702270
EDFEE3F42DE670B8005ADE03 /* gleanProbes.xcfilelist in Resources */ = {isa = PBXBuildFile; fileRef = EDFEE3F32DE670B8005ADE03 /* gleanProbes.xcfilelist */; };
2271+
EEB965112FBCCDFC00D6C232 /* PageRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB9650E2FBCCDFC00D6C232 /* PageRoute.swift */; };
2272+
EEB965122FBCCDFC00D6C232 /* ReaderModeSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB9650F2FBCCDFC00D6C232 /* ReaderModeSchemeHandler.swift */; };
2273+
EEB965162FBCCE7B00D6C232 /* ReaderModeSchemeHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB965132FBCCE7B00D6C232 /* ReaderModeSchemeHandlerTests.swift */; };
22712274
F00CA4012F00000000000003 /* WorldCupMatchesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F00000000000004 /* WorldCupMatchesResponse.swift */; };
22722275
F00CA4012F00000000000005 /* WorldCupLiveResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F00000000000006 /* WorldCupLiveResponse.swift */; };
22732276
F00CA4012F00000000000007 /* WorldCupAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F00000000000008 /* WorldCupAPIClient.swift */; };
@@ -2277,9 +2280,9 @@
22772280
F00CA4012F00000000000013 /* WorldCupTeamsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F00000000000014 /* WorldCupTeamsResponse.swift */; };
22782281
F00CA4012F00000000000015 /* WorldCupLoadError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F00000000000016 /* WorldCupLoadError.swift */; };
22792282
F00CA4012F00000000000017 /* WorldCupBaseHostOverrideSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F00000000000018 /* WorldCupBaseHostOverrideSetting.swift */; };
2280-
F00CA4012F00000000000020 /* WorldCupFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F00000000000021 /* WorldCupFeed.swift */; };
2281-
F00CA4012F0000000000001E /* WorldCupPollIntervalOverrideSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F0000000000001F /* WorldCupPollIntervalOverrideSetting.swift */; };
22822283
F00CA4012F0000000000001C /* WorldCupPollingFetchStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F0000000000001D /* WorldCupPollingFetchStrategy.swift */; };
2284+
F00CA4012F0000000000001E /* WorldCupPollIntervalOverrideSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F0000000000001F /* WorldCupPollIntervalOverrideSetting.swift */; };
2285+
F00CA4012F00000000000020 /* WorldCupFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4012F00000000000021 /* WorldCupFeed.swift */; };
22832286
F00CA4022F00000000000001 /* WorldCupMatchesResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4022F00000000000002 /* WorldCupMatchesResponseTests.swift */; };
22842287
F00CA4022F00000000000003 /* WorldCupLiveResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4022F00000000000004 /* WorldCupLiveResponseTests.swift */; };
22852288
F00CA4022F00000000000005 /* WorldCupFetchStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00CA4022F00000000000006 /* WorldCupFetchStrategyTests.swift */; };
@@ -11660,6 +11663,9 @@
1166011663
EE994F2590D706FC0ADD4B42 /* ur */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ur; path = ur.lproj/AuthenticationManager.strings; sourceTree = "<group>"; };
1166111664
EEA34C528147F2E7C3AB52C8 /* mr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mr; path = mr.lproj/Localizable.strings; sourceTree = "<group>"; };
1166211665
EEAB4DF28099E1BEA98A0B00 /* an */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = an; path = an.lproj/ClearPrivateDataConfirm.strings; sourceTree = "<group>"; };
11666+
EEB9650E2FBCCDFC00D6C232 /* PageRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageRoute.swift; sourceTree = "<group>"; };
11667+
EEB9650F2FBCCDFC00D6C232 /* ReaderModeSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderModeSchemeHandler.swift; sourceTree = "<group>"; };
11668+
EEB965132FBCCE7B00D6C232 /* ReaderModeSchemeHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderModeSchemeHandlerTests.swift; sourceTree = "<group>"; };
1166311669
EEE8476797612D908D23E6BC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
1166411670
EEF342E5A045117853806115 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
1166511671
EF0444EEBEA6A45A3E0F654C /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/PrivateBrowsing.strings"; sourceTree = "<group>"; };
@@ -11680,9 +11686,9 @@
1168011686
F00CA4012F00000000000014 /* WorldCupTeamsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupTeamsResponse.swift; sourceTree = "<group>"; };
1168111687
F00CA4012F00000000000016 /* WorldCupLoadError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupLoadError.swift; sourceTree = "<group>"; };
1168211688
F00CA4012F00000000000018 /* WorldCupBaseHostOverrideSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupBaseHostOverrideSetting.swift; sourceTree = "<group>"; };
11683-
F00CA4012F00000000000021 /* WorldCupFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupFeed.swift; sourceTree = "<group>"; };
11684-
F00CA4012F0000000000001F /* WorldCupPollIntervalOverrideSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupPollIntervalOverrideSetting.swift; sourceTree = "<group>"; };
1168511689
F00CA4012F0000000000001D /* WorldCupPollingFetchStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupPollingFetchStrategy.swift; sourceTree = "<group>"; };
11690+
F00CA4012F0000000000001F /* WorldCupPollIntervalOverrideSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupPollIntervalOverrideSetting.swift; sourceTree = "<group>"; };
11691+
F00CA4012F00000000000021 /* WorldCupFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupFeed.swift; sourceTree = "<group>"; };
1168611692
F00CA4022F00000000000002 /* WorldCupMatchesResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupMatchesResponseTests.swift; sourceTree = "<group>"; };
1168711693
F00CA4022F00000000000004 /* WorldCupLiveResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupLiveResponseTests.swift; sourceTree = "<group>"; };
1168811694
F00CA4022F00000000000006 /* WorldCupFetchStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorldCupFetchStrategyTests.swift; sourceTree = "<group>"; };
@@ -16855,6 +16861,31 @@
1685516861
path = Sharing;
1685616862
sourceTree = "<group>";
1685716863
};
16864+
EEB965102FBCCDFC00D6C232 /* SchemeHandler */ = {
16865+
isa = PBXGroup;
16866+
children = (
16867+
EEB9650E2FBCCDFC00D6C232 /* PageRoute.swift */,
16868+
EEB9650F2FBCCDFC00D6C232 /* ReaderModeSchemeHandler.swift */,
16869+
);
16870+
path = SchemeHandler;
16871+
sourceTree = "<group>";
16872+
};
16873+
EEB965142FBCCE7B00D6C232 /* SchemeHandler */ = {
16874+
isa = PBXGroup;
16875+
children = (
16876+
EEB965132FBCCE7B00D6C232 /* ReaderModeSchemeHandlerTests.swift */,
16877+
);
16878+
path = SchemeHandler;
16879+
sourceTree = "<group>";
16880+
};
16881+
EEB965152FBCCE7B00D6C232 /* ReaderTests */ = {
16882+
isa = PBXGroup;
16883+
children = (
16884+
EEB965142FBCCE7B00D6C232 /* SchemeHandler */,
16885+
);
16886+
path = ReaderTests;
16887+
sourceTree = "<group>";
16888+
};
1685816889
F8324A082649A188007E4BFA /* CredentialProvider */ = {
1685916890
isa = PBXGroup;
1686016891
children = (
@@ -16972,6 +17003,7 @@
1697217003
F84B21D61A090F8100AAB793 /* ClientTests */ = {
1697317004
isa = PBXGroup;
1697417005
children = (
17006+
EEB965152FBCCE7B00D6C232 /* ReaderTests */,
1697517007
C24A0D2E2F3A7E7200BF08B7 /* KeyChainAppAttestKeyIDStoreTests.swift */,
1697617008
2151EF582F2AB962007B67A6 /* BrowserViewController */,
1697717009
218457C42F22A3FA00B4FF23 /* Downloads */,
@@ -17196,6 +17228,7 @@
1719617228
F84B21F51A0910F600AAB793 /* Reader */ = {
1719717229
isa = PBXGroup;
1719817230
children = (
17231+
EEB965102FBCCDFC00D6C232 /* SchemeHandler */,
1719917232
2178A69E2914546D002EC290 /* Resources */,
1720017233
2178A69D291453CC002EC290 /* View */,
1720117234
E4CD9E901A6897FB00318571 /* ReaderMode.swift */,
@@ -19204,6 +19237,8 @@
1920419237
8A2DAD4B2CC02AA00067ECD0 /* LabelButtonHeaderView.swift in Sources */,
1920519238
8A2DAD4D2CC02AA10067ECD0 /* LabelButtonHeaderCell.swift in Sources */,
1920619239
8A9B7A8A2F1A120100ABCDEF /* NewsAffordanceHeaderView.swift in Sources */,
19240+
EEB965112FBCCDFC00D6C232 /* PageRoute.swift in Sources */,
19241+
EEB965122FBCCDFC00D6C232 /* ReaderModeSchemeHandler.swift in Sources */,
1920719242
8A91D4112F7D6C7800A1B2C3 /* NewsTransitionHeaderCell.swift in Sources */,
1920819243
8A91D4132F7D6C7900A1B2C3 /* StoryCategoryPickerView.swift in Sources */,
1920919244
8AF347DE2CADD1B200624036 /* HomepageState.swift in Sources */,
@@ -20263,6 +20298,7 @@
2026320298
C28EA9822FA2554900AEC3AE /* WorldCupTelemetryTests.swift in Sources */,
2026420299
FF0003AB2F000002000BBBBB /* WorldCupMiddlewareTests.swift in Sources */,
2026520300
FF0003AD2F000004000BBBBB /* WorldCupCellFactoryTests.swift in Sources */,
20301+
EEB965162FBCCE7B00D6C232 /* ReaderModeSchemeHandlerTests.swift in Sources */,
2026620302
FF0004AB2F000002000BBBBB /* WorldCupSectionStateTests.swift in Sources */,
2026720303
FF0005AB2F000002000BBBBB /* MockWorldCupStore.swift in Sources */,
2026820304
EDC3D3552CB70A3F00C62DE3 /* OpenSearchEngineTests.swift in Sources */,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/
4+
5+
import Common
6+
import Foundation
7+
import Shared
8+
import WebEngine
9+
10+
/// Serves the reader-mode page document at `readermode://app/page?url=<encoded-article-url>`.
11+
///
12+
/// This initial version validates the incoming URL parameters. Content rendering
13+
/// (cache integration, readability extraction, error pages) will be added in
14+
/// FXIOS-15783.
15+
final class PageRoute: TinyRoute {
16+
private let profile: Profile
17+
18+
init(profile: Profile) {
19+
self.profile = profile
20+
}
21+
22+
// Always erros our for now, will actually handle in next PR
23+
func handle(url: URL, components: URLComponents) async throws -> TinyHTTPReply? {
24+
_ = try extractArticleURL(from: components)
25+
throw TinyRouterError.badResponse
26+
}
27+
28+
// MARK: - URL parsing
29+
30+
private func extractArticleURL(from components: URLComponents) throws -> URL {
31+
guard let raw = components.queryItems?.first(where: { $0.name == "url" })?.value else {
32+
throw TinyRouterError.missingParam("url")
33+
}
34+
guard let parsed = URL(string: raw), parsed.isWebPage() else {
35+
throw TinyRouterError.invalidParam("url", raw)
36+
}
37+
return parsed
38+
}
39+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/
4+
5+
import Common
6+
import Foundation
7+
import Shared
8+
import WebEngine
9+
import WebKit
10+
11+
/// `ReaderModeSchemeHandler` defines a custom URL scheme handler for a `WKWebView` that
12+
/// serves reader-mode pages and their assets. It replaces the legacy GCDWebServer-based
13+
/// reader-mode routes.
14+
///
15+
/// Request flow
16+
/// ------------
17+
/// 1. The browser navigates a tab to `readermode://app/...`.
18+
///
19+
/// 2. WebKit detects the `readermode://` scheme and creates a `WKURLSchemeTask`.
20+
///
21+
/// 3. WebKit calls `ReaderModeSchemeHandler.webView(_:start:)` passing in the task.
22+
///
23+
/// 4. The scheme handler:
24+
/// - reads the URL from `urlSchemeTask.request`
25+
/// - validates that the URL uses the correct scheme and host (`readermode` and `app`)
26+
/// - forwards the URL to `router.route(_:)` (TinyRouter)
27+
///
28+
/// 5. TinyRouter chooses a route handler based on the path. As the migration progresses
29+
/// the registered routes will be:
30+
/// - `/app/page` -> `PageRoute`
31+
/// - `/app/page-exists` -> (future)
32+
/// - `/app/styles/...` -> (future, static)
33+
/// - `/app/fonts/...` -> (future, static)
34+
///
35+
/// 6. `send(_:for:to:)` converts the `TinyHTTPReply` into an `HTTPURLResponse` and body
36+
/// and completes the `WKURLSchemeTask`.
37+
///
38+
/// 7. WebKit renders the response in the tab.
39+
final class ReaderModeSchemeHandler: NSObject, WKURLSchemeHandler {
40+
// These are plain string constants and need to be readable from non-MainActor contexts
41+
// (e.g. `PageRoute.buildReply`, which constructs the CSP off the main actor). The class
42+
// itself is @MainActor by virtue of conforming to `WKURLSchemeHandler`, which would
43+
// otherwise propagate isolation to these statics.
44+
45+
/// The custom scheme this handler is responsible for.
46+
nonisolated static let scheme = "readermode"
47+
48+
/// The host this handler expects for all reader-mode requests.
49+
nonisolated static let host = "app"
50+
51+
/// Canonical base URL for the reader page. Callers that need to construct a reader-mode
52+
/// URL (e.g. `URL.encodeReaderModeURL(_:)`) pass this in place of the legacy
53+
/// `WebServer.sharedInstance.baseReaderModeURL()`.
54+
nonisolated static let baseURL = "readermode://app/page"
55+
56+
private let router: TinyRouter
57+
private let logger: Logger
58+
private var requestTasks = [ObjectIdentifier: Task<Void, Never>]()
59+
60+
init(profile: Profile,
61+
logger: Logger = DefaultLogger.shared) {
62+
// `StaticFileRoute` is the same one Translations uses — given the path it strips
63+
// the leading slash, separates the resource name + extension, and looks the file up
64+
// via `Bundle.main.url(forResource:withExtension:)`. That covers the legacy paths
65+
// `Reader.html` already references (`/reader-mode/styles/Reader.css` and
66+
// `/reader-mode/fonts/*.otf`) without any template edits.
67+
self.router = TinyRouter()
68+
.register("page", PageRoute(profile: profile))
69+
.setDefault(StaticFileRoute())
70+
self.logger = logger
71+
super.init()
72+
}
73+
74+
/// Validates incoming requests and forwards them to the router.
75+
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
76+
let id = ObjectIdentifier(urlSchemeTask)
77+
let requestTask = Task { @MainActor in
78+
defer { requestTasks[id] = nil }
79+
do {
80+
let url = try validateRequest(urlSchemeTask)
81+
try Task.checkCancellation()
82+
let reply = try await router.route(url)
83+
try Task.checkCancellation()
84+
try send(reply, for: url, to: urlSchemeTask)
85+
} catch is CancellationError {
86+
self.logger.log("Reader-mode scheme task cancelled.",
87+
level: .debug,
88+
category: .library)
89+
} catch {
90+
urlSchemeTask.didFailWithError(mapError(error))
91+
}
92+
}
93+
requestTasks[id] = requestTask
94+
}
95+
96+
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
97+
let id = ObjectIdentifier(urlSchemeTask)
98+
requestTasks[id]?.cancel()
99+
requestTasks[id] = nil
100+
}
101+
102+
/// Bridges a `TinyHTTPReply` into the `WKURLSchemeTask` callbacks.
103+
private func send(_ reply: TinyHTTPReply, for url: URL, to task: WKURLSchemeTask) throws {
104+
guard let httpResponse = reply.httpResponse else {
105+
throw TinyRouterError.badResponse
106+
}
107+
task.didReceive(httpResponse)
108+
task.didReceive(reply.body)
109+
task.didFinish()
110+
}
111+
112+
/// Normalizes any thrown `Error` into a `TinyRouterError`.
113+
private func mapError(_ error: Error) -> TinyRouterError {
114+
if let tinyError = error as? TinyRouterError {
115+
return tinyError
116+
}
117+
return .unknown(String(describing: error))
118+
}
119+
120+
/// Validates an incoming request and returns a well-formed URL,
121+
/// or throws a typed error if the request is not acceptable.
122+
private func validateRequest(_ task: WKURLSchemeTask) throws -> URL {
123+
guard let url = task.request.url else { throw TinyRouterError.badURL }
124+
125+
guard url.scheme == Self.scheme else {
126+
throw TinyRouterError.unsupportedScheme(expected: Self.scheme, found: url.scheme)
127+
}
128+
129+
guard url.host == Self.host else {
130+
throw TinyRouterError.unsupportedHost(expected: Self.host, found: url.host)
131+
}
132+
133+
return url
134+
}
135+
}

0 commit comments

Comments
 (0)