Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public struct BrowserUserAgentPolicy: Sendable {

/// Resolves the browser identity policy for a top-level destination.
///
/// Google Sheets intentionally uses WebKit's default embedded identity.
/// Every web destination receives the current Safari-compatible identity.
/// Non-web destinations have no applicable user-agent policy.
///
/// - Parameter url: The destination of the top-level navigation.
Expand All @@ -68,16 +68,7 @@ public struct BrowserUserAgentPolicy: Sendable {
scheme == "http" || scheme == "https" else {
return .notApplicable
}
guard let host = url.host?.lowercased() else {
return .custom(safariCompatibleUserAgent)
}
let isSheetsHost = host == "sheets.google.com" || host == "spreadsheets.google.com"
let isSheetsPath = host == "docs.google.com"
&& url.path.split(separator: "/", omittingEmptySubsequences: true).first?
.lowercased() == "spreadsheets"
return isSheetsHost || isSheetsPath
? .webKitDefault
: .custom(safariCompatibleUserAgent)
return .custom(safariCompatibleUserAgent)
}

/// Parses a numeric dot-separated browser version.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ public enum BrowserUserAgentPolicyResolution: Equatable, Sendable {
/// Use the supplied custom user-agent identity for an HTTP or HTTPS destination.
case custom(String)

/// Use WebKit's default embedded identity for an HTTP or HTTPS destination.
case webKitDefault

/// User-agent identity does not apply to this non-web destination.
case notApplicable
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ struct BrowserUserAgentPolicyTests {
#expect(newerPolicy.safariCompatibleUserAgent.contains("Version/27.0 Safari/605.1.15"))
}

@Test func googleSheetsKeepsEmbeddedWebKitIdentity() {
@Test func googleSheetsReceivesCurrentSafariCompatibleIdentity() {
let sheetURL = URL(string: "https://docs.google.com/spreadsheets/d/example/edit")!
let sheetsRedirectURL = URL(string: "https://sheets.google.com/")!
let legacyRedirectURL = URL(string: "https://spreadsheets.google.com/")!
let expectedResolution = BrowserUserAgentPolicyResolution.custom(
policy.safariCompatibleUserAgent
)

#expect(policy.resolution(for: sheetURL) == .webKitDefault)
#expect(policy.resolution(for: sheetsRedirectURL) == .webKitDefault)
#expect(policy.resolution(for: legacyRedirectURL) == .webKitDefault)
#expect(policy.resolution(for: sheetURL) == expectedResolution)
#expect(policy.resolution(for: sheetsRedirectURL) == expectedResolution)
#expect(policy.resolution(for: legacyRedirectURL) == expectedResolution)
}

@Test func otherGoogleWorkspaceEditorsRemainSafariCompatible() {
Expand Down
3 changes: 1 addition & 2 deletions Sources/Panels/BrowserNavigationDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,7 @@ import WebKit
let replacedNavigation = activeMainFrameNavigation
let reportReplacementWillStart = willReplaceNavigationForUserAgentPolicy
return webView.restartNavigationForBrowserUserAgentPolicyIfNeeded(
for: navigationAction.request,
targetFrameIsMainFrame: navigationAction.targetFrame?.isMainFrame,
navigationAction,
decisionHandler: decisionHandler,
willRestart: {
reportReplacementWillStart?(webView, replacedNavigation)
Expand Down
3 changes: 1 addition & 2 deletions Sources/Panels/BrowserPopupWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -789,8 +789,7 @@ private class PopupUIDelegate: BrowserPDFPreviewActionUIDelegate {
}

return webView.restartNavigationForBrowserUserAgentPolicyIfNeeded(
for: navigationAction.request,
targetFrameIsMainFrame: navigationAction.targetFrame?.isMainFrame,
navigationAction,
decisionHandler: decisionHandler,
startReplacement: { restartRequest in
controller.requestNavigation(restartRequest, in: webView)
Expand Down
9 changes: 3 additions & 6 deletions Sources/Panels/WKWebView+BrowserUserAgentPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ extension WKWebView {
switch BrowserUserAgentPolicy.system.resolution(for: url) {
case .custom(let userAgent):
resolvedUserAgent = userAgent
case .webKitDefault:
resolvedUserAgent = nil
case .notApplicable:
guard currentUserAgent != nil else { return false }
customUserAgent = nil
Expand Down Expand Up @@ -44,15 +42,14 @@ extension WKWebView {

@MainActor
func restartNavigationForBrowserUserAgentPolicyIfNeeded(
for request: URLRequest,
targetFrameIsMainFrame: Bool?,
_ navigationAction: WKNavigationAction,
decisionHandler: (WKNavigationActionPolicy) -> Void,
willRestart: () -> Void = {},
startReplacement: (URLRequest) -> Void
) -> Bool {
guard let restartRequest = browserUserAgentPolicyRestartRequest(
for: request,
targetFrameIsMainFrame: targetFrameIsMainFrame
for: navigationAction.request,
targetFrameIsMainFrame: navigationAction.targetFrame?.isMainFrame
) else {
return false
}
Expand Down
66 changes: 28 additions & 38 deletions cmuxTests/BrowserUserAgentPolicyWebKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,25 @@ struct BrowserUserAgentPolicyWebKitTests {
#expect(webView.browserUserAgentPolicyRestartRequest(for: restartRequest) == nil)
}

@Test func restartRequestCanRestoreEmbeddedIdentityForSheets() throws {
@Test func restartRequestReplacesStaleSheetsIdentity() throws {
let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
webView.customUserAgent = "stale-agent"
var request = URLRequest(
url: URL(string: "https://docs.google.com/spreadsheets/d/example/edit")!
)
request.setValue("stale-agent", forHTTPHeaderField: "User-Agent")

let restartRequest = try #require(
webView.browserUserAgentPolicyRestartRequest(for: request)
)

#expect(restartRequest.value(forHTTPHeaderField: "User-Agent") == nil)
#expect(webView.customUserAgent == BrowserUserAgentPolicy.system.safariCompatibleUserAgent)
#expect(webView.browserUserAgentPolicyRestartRequest(for: restartRequest) == nil)
}

@Test func sheetsRequestUsesCurrentSafariForNetworkAndNavigatorIdentity() throws {
let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
webView.customUserAgent = BrowserUserAgentPolicy.system.safariCompatibleUserAgent
let request = URLRequest(
url: URL(string: "https://docs.google.com/spreadsheets/d/example/edit")!
)
Expand All @@ -37,18 +53,25 @@ struct BrowserUserAgentPolicyWebKitTests {
webView.browserUserAgentPolicyRestartRequest(for: request)
)

#expect(webView.customUserAgent?.isEmpty != false)
#expect(restartRequest.value(forHTTPHeaderField: "User-Agent") == nil)
#expect(webView.customUserAgent == BrowserUserAgentPolicy.system.safariCompatibleUserAgent)
#expect(webView.browserUserAgentPolicyRestartRequest(for: restartRequest) == nil)
}

@Test func emptyCustomUserAgentIsAlreadyEmbeddedIdentityForSheets() {
@Test func emptyCustomUserAgentIsNotAcceptedAsSheetsIdentity() throws {
let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
webView.customUserAgent = ""
let request = URLRequest(
url: URL(string: "https://docs.google.com/spreadsheets/d/example/edit")!
)

#expect(webView.browserUserAgentPolicyRestartRequest(for: request) == nil)
let restartRequest = try #require(
webView.browserUserAgentPolicyRestartRequest(for: request)
)

#expect(restartRequest.value(forHTTPHeaderField: "User-Agent") == nil)
#expect(webView.customUserAgent == BrowserUserAgentPolicy.system.safariCompatibleUserAgent)
#expect(webView.browserUserAgentPolicyRestartRequest(for: restartRequest) == nil)
}

@Test func restartRequestIgnoresSubframesAndNewWindowTargets() throws {
Expand All @@ -73,37 +96,4 @@ struct BrowserUserAgentPolicyWebKitTests {
#expect(webView.browserUserAgentPolicyRestartRequest(for: request) == nil)
#expect(webView.customUserAgent?.isEmpty != false)
}

@Test func sheetsDestinationNeverAllowsStaleSafariIdentity() throws {
let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
let request = URLRequest(
url: URL(string: "https://docs.google.com/spreadsheets/d/example/edit")!
)
var decisions: [WKNavigationActionPolicy] = []
var replacementRequests: [URLRequest] = []

webView.customUserAgent = BrowserUserAgentPolicy.system.safariCompatibleUserAgent
#expect(webView.restartNavigationForBrowserUserAgentPolicyIfNeeded(
for: request,
targetFrameIsMainFrame: true,
decisionHandler: { decisions.append($0) },
startReplacement: { replacementRequests.append($0) }
))
#expect(decisions == [.cancel])
#expect((webView.customUserAgent ?? "").isEmpty)
let replacementRequest = try #require(replacementRequests.first)

// If another load phase exposes the stale Safari identity again, the
// replacement must correct it instead of silently allowing a downgrade.
webView.customUserAgent = BrowserUserAgentPolicy.system.safariCompatibleUserAgent
#expect(webView.restartNavigationForBrowserUserAgentPolicyIfNeeded(
for: replacementRequest,
targetFrameIsMainFrame: true,
decisionHandler: { decisions.append($0) },
startReplacement: { replacementRequests.append($0) }
))
#expect(decisions == [.cancel, .cancel])
#expect(replacementRequests.count == 2)
#expect((webView.customUserAgent ?? "").isEmpty)
}
}