Sfoglia il codice sorgente

Improve embedded Indeed browser navigation for apply, sign-in, and external links.

Open company career sites in the default browser while keeping Indeed, Cloudflare, reCAPTCHA, and Google OAuth in the web view. Show the Google account chooser once per sign-in attempt without breaking the post-selection flow, block YouTube and other platform detours, and allow policy pages in-app.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 2 mesi fa
parent
commit
9e7949b128

+ 271 - 60
App for Indeed/Controllers/IndeedJobBrowserWindowController.swift

@@ -8,15 +8,25 @@ import WebKit
 
 /// Indeed job listing and apply flow in a `WKWebView`, embedded in the dashboard main panel or hosted in a window.
 final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelegate, WKUIDelegate {
+    /// Shared pool so Indeed / Cloudflare cookies persist across embedded browser sessions.
+    private static let sharedProcessPool = WKProcessPool()
+
     /// When set, a leading **Home** control calls this so the host can hide the embedded browser (same-window UX).
     var onDismissEmbedded: (() -> Void)?
     private let webView: WKWebView = {
         let configuration = WKWebViewConfiguration()
+        configuration.processPool = IndeedJobBrowserViewController.sharedProcessPool
+        configuration.websiteDataStore = .default()
         configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
+        if #available(macOS 11.0, *) {
+            configuration.defaultWebpagePreferences.allowsContentJavaScript = true
+        }
         return WKWebView(frame: .zero, configuration: configuration)
     }()
 
     private var pendingURL: URL?
+    /// Set when the user starts Google sign-in from Indeed; cleared after one `prompt=select_account` rewrite.
+    private var pendingGoogleAccountPicker = false
 
     private let backButton = NSButton()
     private let forwardButton = NSButton()
@@ -178,6 +188,9 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
     }
 
     func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+        if let host = webView.url?.host?.lowercased(), IndeedWebBrowsingPolicy.isIndeedHost(host) {
+            pendingGoogleAccountPicker = false
+        }
         updateNavigationButtons()
     }
 
@@ -191,44 +204,154 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
             return
         }
 
-        let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? true
-        if isMainFrame,
-           let url = navigationAction.request.url,
-           IndeedWebBrowsingPolicy.shouldForceGoogleAccountPicker(for: url) {
-            let pickerURL = IndeedWebBrowsingPolicy.urlForcingGoogleAccountPicker(url)
-            if pickerURL != url {
-                webView.load(URLRequest(url: pickerURL))
-                decisionHandler(.cancel)
-                return
-            }
+        if let url = navigationAction.request.url,
+           IndeedWebBrowsingPolicy.shouldOpenInSystemBrowser(url: url) {
+            NSWorkspace.shared.open(url)
+            decisionHandler(.cancel)
+            return
+        }
+
+        noteGoogleSignInStartedFromIndeed(navigationAction)
+        if let url = navigationAction.request.url,
+           applyGoogleAccountPickerIfNeeded(to: url, in: webView) {
+            decisionHandler(.cancel)
+            return
         }
 
         decisionHandler(.allow)
     }
 
-    /// Target=_blank / `window.open` without a frame: load in this view so apply flows stay in-app.
+    /// Target=_blank / `window.open` without a frame: Indeed stays in-app; company apply sites open in the default browser.
     func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
         guard navigationAction.targetFrame == nil,
               IndeedWebBrowsingPolicy.allows(navigationAction: navigationAction) else {
             return nil
         }
-        var request = navigationAction.request
-        if let url = request.url,
-           IndeedWebBrowsingPolicy.shouldForceGoogleAccountPicker(for: url) {
-            request = URLRequest(url: IndeedWebBrowsingPolicy.urlForcingGoogleAccountPicker(url))
+        if let url = navigationAction.request.url,
+           IndeedWebBrowsingPolicy.shouldOpenInSystemBrowser(url: url) {
+            NSWorkspace.shared.open(url)
+            return nil
+        }
+        noteGoogleSignInStartedFromIndeed(navigationAction)
+        if let url = navigationAction.request.url,
+           applyGoogleAccountPickerIfNeeded(to: url, in: webView) {
+            return nil
         }
-        webView.load(request)
+        webView.load(navigationAction.request)
         return nil
     }
 
-    /// Desktop Safari UA helps Indeed serve a full desktop apply experience.
-    private static let desktopSafariLikeUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15"
+    private func noteGoogleSignInStartedFromIndeed(_ navigationAction: WKNavigationAction) {
+        guard let destination = navigationAction.request.url else { return }
+        let sourceHost = navigationAction.sourceFrame.request.url?.host?.lowercased()
+            ?? navigationAction.request.mainDocumentURL?.host?.lowercased()
+            ?? webView.url?.host?.lowercased()
+        guard let sourceHost, IndeedWebBrowsingPolicy.isIndeedHost(sourceHost) else { return }
+        guard IndeedWebBrowsingPolicy.isGoogleSignInEntryURL(destination) else { return }
+        pendingGoogleAccountPicker = true
+    }
+
+    @discardableResult
+    private func applyGoogleAccountPickerIfNeeded(to url: URL, in webView: WKWebView) -> Bool {
+        guard pendingGoogleAccountPicker,
+              IndeedWebBrowsingPolicy.shouldApplyGoogleAccountPicker(to: url) else {
+            return false
+        }
+        pendingGoogleAccountPicker = false
+        let pickerURL = IndeedWebBrowsingPolicy.urlAddingGoogleAccountPickerPrompt(url)
+        guard pickerURL != url else { return false }
+        webView.load(URLRequest(url: pickerURL))
+        return true
+    }
+
+    func webView(
+        _ webView: WKWebView,
+        runJavaScriptAlertPanelWithMessage message: String,
+        initiatedByFrame frame: WKFrameInfo,
+        completionHandler: @escaping () -> Void
+    ) {
+        let alert = NSAlert()
+        alert.messageText = message
+        alert.addButton(withTitle: NSLocalizedString("OK", comment: "Web alert dismiss"))
+        alert.runModal()
+        completionHandler()
+    }
+
+    func webView(
+        _ webView: WKWebView,
+        runJavaScriptConfirmPanelWithMessage message: String,
+        initiatedByFrame frame: WKFrameInfo,
+        completionHandler: @escaping (Bool) -> Void
+    ) {
+        let alert = NSAlert()
+        alert.messageText = message
+        alert.addButton(withTitle: NSLocalizedString("OK", comment: "Web confirm accept"))
+        alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Web confirm cancel"))
+        completionHandler(alert.runModal() == .alertFirstButtonReturn)
+    }
+
+    func webView(
+        _ webView: WKWebView,
+        runJavaScriptTextInputPanelWithPrompt prompt: String,
+        defaultText: String?,
+        initiatedByFrame frame: WKFrameInfo,
+        completionHandler: @escaping (String?) -> Void
+    ) {
+        let alert = NSAlert()
+        alert.messageText = prompt
+        alert.addButton(withTitle: NSLocalizedString("OK", comment: "Web prompt accept"))
+        alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Web prompt cancel"))
+        let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24))
+        field.stringValue = defaultText ?? ""
+        alert.accessoryView = field
+        guard alert.runModal() == .alertFirstButtonReturn else {
+            completionHandler(nil)
+            return
+        }
+        completionHandler(field.stringValue)
+    }
+
+    /// Safari-like UA without the app name suffix — Cloudflare / Indeed bot checks reject `App for Indeed/…` in the UA string.
+    private static let desktopSafariLikeUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15"
 }
 
-// MARK: - Google sign-in only (no Gmail, Help hub, Search, etc.)
+// MARK: - Embedded browsing policy
 
-/// Limits embedded browsing on Google-owned hosts to OAuth / sign-in, while leaving Indeed and third-party apply sites unrestricted.
+/// Keeps Indeed and apply helpers (Cloudflare, Google sign-in, reCAPTCHA) in-app; opens company career sites in the system browser.
 enum IndeedWebBrowsingPolicy {
+    /// Company career sites and other non-Indeed apply links (e.g. “Apply on company site”).
+    static func shouldOpenInSystemBrowser(url: URL) -> Bool {
+        guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
+            return false
+        }
+        guard let host = url.host?.lowercased() else { return false }
+        if shouldRemainInEmbeddedWebView(url: url) { return false }
+        if isRestrictedPlatformHost(host) { return false }
+        return true
+    }
+
+    /// URLs that must load inside the embedded `WKWebView` (not the default browser).
+    static func shouldRemainInEmbeddedWebView(url: URL) -> Bool {
+        guard let scheme = url.scheme?.lowercased() else { return false }
+        if scheme == "about" || scheme == "blob" || scheme == "data" { return true }
+        guard scheme == "http" || scheme == "https" else { return false }
+        guard let host = url.host?.lowercased() else { return false }
+
+        if isIndeedHost(host) { return true }
+        if isAllowedGoogleSignInHost(host, path: url.path) { return true }
+        if isGooglePolicyOrLegalPage(host: host, path: url.path) { return true }
+        if isGoogleRecaptchaHost(host, path: url.path) { return true }
+        if isCloudflareChallengeHost(host) { return true }
+        if isHCaptchaHost(host) { return true }
+        return false
+    }
+
+    static func isIndeedHost(_ host: String) -> Bool {
+        if host == "indeed.com" { return true }
+        if host.hasPrefix("indeed.") { return true }
+        return host.contains(".indeed.")
+    }
+
     static func allows(navigationAction: WKNavigationAction) -> Bool {
         guard let url = navigationAction.request.url else { return true }
         let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? true
@@ -244,10 +367,9 @@ enum IndeedWebBrowsingPolicy {
         guard scheme == "http" || scheme == "https" else { return false }
         guard let host = url.host?.lowercased() else { return false }
 
-        if !isGooglePropertyHost(host) {
-            return true
-        }
-        return isAllowedGoogleSignInHost(host, path: url.path)
+        if shouldRemainInEmbeddedWebView(url: url) { return true }
+        if isRestrictedPlatformHost(host) { return false }
+        return true
     }
 
     static func allowsSubframeNavigation(to url: URL) -> Bool {
@@ -256,14 +378,79 @@ enum IndeedWebBrowsingPolicy {
         guard scheme == "http" || scheme == "https" else { return false }
         guard let host = url.host?.lowercased() else { return false }
 
+        if isYouTubeRelatedHost(host) {
+            return false
+        }
         if !isGooglePropertyHost(host) {
             return true
         }
         if isAllowedGoogleSignInHost(host, path: url.path) {
             return true
         }
-        let path = url.path.lowercased()
-        if host == "www.google.com", path.contains("/recaptcha") {
+        if isGooglePolicyOrLegalPage(host: host, path: url.path) {
+            return true
+        }
+        if isGoogleRecaptchaHost(host, path: url.path) {
+            return true
+        }
+        if isCloudflareChallengeHost(host) {
+            return true
+        }
+        return isHCaptchaHost(host)
+    }
+
+    /// Google, YouTube, and similar — stay in the sign-in flow; never open in the system browser.
+    private static func isRestrictedPlatformHost(_ host: String) -> Bool {
+        isGooglePropertyHost(host) || isYouTubeRelatedHost(host)
+    }
+
+    private static func isYouTubeRelatedHost(_ host: String) -> Bool {
+        if host == "youtu.be" { return true }
+        if host == "youtube.com" || host.hasSuffix(".youtube.com") { return true }
+        if host.contains("youtube") { return true }
+        return false
+    }
+
+    /// Privacy / Terms / Help links on the Google account chooser (not YouTube, Gmail, or Search).
+    private static func isGooglePolicyOrLegalPage(host: String, path: String) -> Bool {
+        if host == "policies.google.com" || host == "privacy.google.com" {
+            return true
+        }
+        if host == "support.google.com" {
+            let pathLower = path.lowercased()
+            return pathLower.contains("/accounts")
+        }
+        if host == "www.google.com" || host == "google.com" {
+            let pathLower = path.lowercased()
+            return pathLower.contains("/policies")
+                || pathLower.contains("/privacy")
+                || pathLower.contains("/terms")
+                || pathLower.contains("/intl/")
+        }
+        return false
+    }
+
+    /// Cloudflare Turnstile / bot check during Indeed apply (`challenges.cloudflare.com`, …).
+    private static func isCloudflareChallengeHost(_ host: String) -> Bool {
+        if host == "cloudflare.com" || host.hasSuffix(".cloudflare.com") { return true }
+        return false
+    }
+
+    /// hCaptcha widget hosts used on some apply flows.
+    private static func isHCaptchaHost(_ host: String) -> Bool {
+        host == "hcaptcha.com" || host.hasSuffix(".hcaptcha.com")
+    }
+
+    /// Google reCAPTCHA during Indeed Easy Apply (`recaptcha.net`, `google.com/recaptcha`, `gstatic.com`, …).
+    private static func isGoogleRecaptchaHost(_ host: String, path: String) -> Bool {
+        if host == "recaptcha.net" || host.hasSuffix(".recaptcha.net") {
+            return true
+        }
+        let pathLower = path.lowercased()
+        if host == "recaptcha.google.com" || host.hasPrefix("recaptcha.google.") {
+            return true
+        }
+        if host == "google.com" || host.hasSuffix(".google.com"), pathLower.contains("recaptcha") {
             return true
         }
         if host == "gstatic.com" || host.hasSuffix(".gstatic.com") {
@@ -283,25 +470,72 @@ enum IndeedWebBrowsingPolicy {
         return false
     }
 
-    /// True for Google OAuth / sign-in navigations where we inject `prompt=select_account`.
-    static func shouldForceGoogleAccountPicker(for url: URL) -> Bool {
+    /// Hosts used during “Sign in with Google” — not Help Center, Gmail, Drive, Search, or the apps launcher.
+    private static func isAllowedGoogleSignInHost(_ host: String, path: String) -> Bool {
+        if host == "accounts.google.com" || host.hasPrefix("accounts.google.") {
+            return true
+        }
+        if host == "signin.google.com" {
+            return true
+        }
+        if host == "oauth.googleusercontent.com" {
+            return true
+        }
+        if host == "googleusercontent.com" || host.hasSuffix(".googleusercontent.com") {
+            return true
+        }
+        if host == "apis.google.com" {
+            return true
+        }
+        if isGooglePolicyOrLegalPage(host: host, path: path) {
+            return true
+        }
+        if host == "myaccount.google.com" {
+            return isGoogleSignInRelatedPath(path)
+        }
+        if host == "www.google.com" || host == "google.com" {
+            return isGoogleSignInRelatedPath(path)
+        }
+        return false
+    }
+
+    private static func isGoogleSignInRelatedPath(_ path: String) -> Bool {
+        let pathLower = path.lowercased()
+        return pathLower.contains("/signin")
+            || pathLower.contains("/accounts")
+            || pathLower.contains("servicelogin")
+            || pathLower.contains("/oauth")
+            || pathLower.contains("/gsi")
+            || pathLower.contains("/device")
+            || pathLower.contains("/security")
+    }
+
+    /// First Google OAuth hop after leaving Indeed (`/o/oauth2/.../auth`, account chooser, …).
+    static func isGoogleSignInEntryURL(_ url: URL) -> Bool {
         guard let host = url.host?.lowercased(),
               host == "accounts.google.com" || host.hasPrefix("accounts.google.") else {
             return false
         }
         let path = url.path.lowercased()
-        if path.contains("oauth")
-            || path.contains("accountchooser")
-            || path.contains("/gsi/")
-            || path.hasPrefix("/signin/") {
-            return true
-        }
+        if path.contains("/oauth2/") && path.contains("auth") { return true }
+        if path.contains("accountchooser") { return true }
+        return false
+    }
+
+    /// Whether to inject `prompt=select_account` once for this OAuth attempt.
+    static func shouldApplyGoogleAccountPicker(to url: URL) -> Bool {
+        guard isGoogleSignInEntryURL(url) else { return false }
+        let path = url.path.lowercased()
+        if path.contains("/signin/oauth/") || path.contains("/signin/v2/") { return false }
+
         let query = url.query?.lowercased() ?? ""
-        return query.contains("client_id=")
+        if query.contains("select_account") { return false }
+        if query.contains("authuser=") { return false }
+        return true
     }
 
-    /// Rewrites Google sign-in URLs so the account chooser is always shown (not silent reuse of the last account).
-    static func urlForcingGoogleAccountPicker(_ url: URL) -> URL {
+    /// Adds `prompt=select_account` so Google shows the account chooser instead of silent sign-in.
+    static func urlAddingGoogleAccountPickerPrompt(_ url: URL) -> URL {
         guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }
         var items = components.queryItems ?? []
 
@@ -321,27 +555,4 @@ enum IndeedWebBrowsingPolicy {
         components.queryItems = items.isEmpty ? nil : items
         return components.url ?? url
     }
-
-    /// Hosts used during “Sign in with Google” — not Help Center, Gmail, Drive, Search, or the apps launcher.
-    private static func isAllowedGoogleSignInHost(_ host: String, path: String) -> Bool {
-        if host == "accounts.google.com" || host.hasPrefix("accounts.google.") {
-            return true
-        }
-        if host == "signin.google.com" {
-            return true
-        }
-        if host == "oauth.googleusercontent.com" {
-            return true
-        }
-        if host == "googleusercontent.com" || host.hasSuffix(".googleusercontent.com") {
-            return true
-        }
-        if host == "policies.google.com" || host == "privacy.google.com" {
-            return true
-        }
-        if host == "apis.google.com" {
-            return true
-        }
-        return false
-    }
 }