Преглед изворни кода

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 недеља
родитељ
комит
9e7949b128
1 измењених фајлова са 271 додато и 60 уклоњено
  1. 271 60
      App for Indeed/Controllers/IndeedJobBrowserWindowController.swift

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

@@ -8,15 +8,25 @@ import WebKit
8
 
8
 
9
 /// Indeed job listing and apply flow in a `WKWebView`, embedded in the dashboard main panel or hosted in a window.
9
 /// Indeed job listing and apply flow in a `WKWebView`, embedded in the dashboard main panel or hosted in a window.
10
 final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelegate, WKUIDelegate {
10
 final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelegate, WKUIDelegate {
11
+    /// Shared pool so Indeed / Cloudflare cookies persist across embedded browser sessions.
12
+    private static let sharedProcessPool = WKProcessPool()
13
+
11
     /// When set, a leading **Home** control calls this so the host can hide the embedded browser (same-window UX).
14
     /// When set, a leading **Home** control calls this so the host can hide the embedded browser (same-window UX).
12
     var onDismissEmbedded: (() -> Void)?
15
     var onDismissEmbedded: (() -> Void)?
13
     private let webView: WKWebView = {
16
     private let webView: WKWebView = {
14
         let configuration = WKWebViewConfiguration()
17
         let configuration = WKWebViewConfiguration()
18
+        configuration.processPool = IndeedJobBrowserViewController.sharedProcessPool
19
+        configuration.websiteDataStore = .default()
15
         configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
20
         configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
21
+        if #available(macOS 11.0, *) {
22
+            configuration.defaultWebpagePreferences.allowsContentJavaScript = true
23
+        }
16
         return WKWebView(frame: .zero, configuration: configuration)
24
         return WKWebView(frame: .zero, configuration: configuration)
17
     }()
25
     }()
18
 
26
 
19
     private var pendingURL: URL?
27
     private var pendingURL: URL?
28
+    /// Set when the user starts Google sign-in from Indeed; cleared after one `prompt=select_account` rewrite.
29
+    private var pendingGoogleAccountPicker = false
20
 
30
 
21
     private let backButton = NSButton()
31
     private let backButton = NSButton()
22
     private let forwardButton = NSButton()
32
     private let forwardButton = NSButton()
@@ -178,6 +188,9 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
178
     }
188
     }
179
 
189
 
180
     func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
190
     func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
191
+        if let host = webView.url?.host?.lowercased(), IndeedWebBrowsingPolicy.isIndeedHost(host) {
192
+            pendingGoogleAccountPicker = false
193
+        }
181
         updateNavigationButtons()
194
         updateNavigationButtons()
182
     }
195
     }
183
 
196
 
@@ -191,44 +204,154 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
191
             return
204
             return
192
         }
205
         }
193
 
206
 
194
-        let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? true
195
-        if isMainFrame,
196
-           let url = navigationAction.request.url,
197
-           IndeedWebBrowsingPolicy.shouldForceGoogleAccountPicker(for: url) {
198
-            let pickerURL = IndeedWebBrowsingPolicy.urlForcingGoogleAccountPicker(url)
199
-            if pickerURL != url {
200
-                webView.load(URLRequest(url: pickerURL))
201
-                decisionHandler(.cancel)
202
-                return
203
-            }
207
+        if let url = navigationAction.request.url,
208
+           IndeedWebBrowsingPolicy.shouldOpenInSystemBrowser(url: url) {
209
+            NSWorkspace.shared.open(url)
210
+            decisionHandler(.cancel)
211
+            return
212
+        }
213
+
214
+        noteGoogleSignInStartedFromIndeed(navigationAction)
215
+        if let url = navigationAction.request.url,
216
+           applyGoogleAccountPickerIfNeeded(to: url, in: webView) {
217
+            decisionHandler(.cancel)
218
+            return
204
         }
219
         }
205
 
220
 
206
         decisionHandler(.allow)
221
         decisionHandler(.allow)
207
     }
222
     }
208
 
223
 
209
-    /// Target=_blank / `window.open` without a frame: load in this view so apply flows stay in-app.
224
+    /// Target=_blank / `window.open` without a frame: Indeed stays in-app; company apply sites open in the default browser.
210
     func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
225
     func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
211
         guard navigationAction.targetFrame == nil,
226
         guard navigationAction.targetFrame == nil,
212
               IndeedWebBrowsingPolicy.allows(navigationAction: navigationAction) else {
227
               IndeedWebBrowsingPolicy.allows(navigationAction: navigationAction) else {
213
             return nil
228
             return nil
214
         }
229
         }
215
-        var request = navigationAction.request
216
-        if let url = request.url,
217
-           IndeedWebBrowsingPolicy.shouldForceGoogleAccountPicker(for: url) {
218
-            request = URLRequest(url: IndeedWebBrowsingPolicy.urlForcingGoogleAccountPicker(url))
230
+        if let url = navigationAction.request.url,
231
+           IndeedWebBrowsingPolicy.shouldOpenInSystemBrowser(url: url) {
232
+            NSWorkspace.shared.open(url)
233
+            return nil
234
+        }
235
+        noteGoogleSignInStartedFromIndeed(navigationAction)
236
+        if let url = navigationAction.request.url,
237
+           applyGoogleAccountPickerIfNeeded(to: url, in: webView) {
238
+            return nil
219
         }
239
         }
220
-        webView.load(request)
240
+        webView.load(navigationAction.request)
221
         return nil
241
         return nil
222
     }
242
     }
223
 
243
 
224
-    /// Desktop Safari UA helps Indeed serve a full desktop apply experience.
225
-    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"
244
+    private func noteGoogleSignInStartedFromIndeed(_ navigationAction: WKNavigationAction) {
245
+        guard let destination = navigationAction.request.url else { return }
246
+        let sourceHost = navigationAction.sourceFrame.request.url?.host?.lowercased()
247
+            ?? navigationAction.request.mainDocumentURL?.host?.lowercased()
248
+            ?? webView.url?.host?.lowercased()
249
+        guard let sourceHost, IndeedWebBrowsingPolicy.isIndeedHost(sourceHost) else { return }
250
+        guard IndeedWebBrowsingPolicy.isGoogleSignInEntryURL(destination) else { return }
251
+        pendingGoogleAccountPicker = true
252
+    }
253
+
254
+    @discardableResult
255
+    private func applyGoogleAccountPickerIfNeeded(to url: URL, in webView: WKWebView) -> Bool {
256
+        guard pendingGoogleAccountPicker,
257
+              IndeedWebBrowsingPolicy.shouldApplyGoogleAccountPicker(to: url) else {
258
+            return false
259
+        }
260
+        pendingGoogleAccountPicker = false
261
+        let pickerURL = IndeedWebBrowsingPolicy.urlAddingGoogleAccountPickerPrompt(url)
262
+        guard pickerURL != url else { return false }
263
+        webView.load(URLRequest(url: pickerURL))
264
+        return true
265
+    }
266
+
267
+    func webView(
268
+        _ webView: WKWebView,
269
+        runJavaScriptAlertPanelWithMessage message: String,
270
+        initiatedByFrame frame: WKFrameInfo,
271
+        completionHandler: @escaping () -> Void
272
+    ) {
273
+        let alert = NSAlert()
274
+        alert.messageText = message
275
+        alert.addButton(withTitle: NSLocalizedString("OK", comment: "Web alert dismiss"))
276
+        alert.runModal()
277
+        completionHandler()
278
+    }
279
+
280
+    func webView(
281
+        _ webView: WKWebView,
282
+        runJavaScriptConfirmPanelWithMessage message: String,
283
+        initiatedByFrame frame: WKFrameInfo,
284
+        completionHandler: @escaping (Bool) -> Void
285
+    ) {
286
+        let alert = NSAlert()
287
+        alert.messageText = message
288
+        alert.addButton(withTitle: NSLocalizedString("OK", comment: "Web confirm accept"))
289
+        alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Web confirm cancel"))
290
+        completionHandler(alert.runModal() == .alertFirstButtonReturn)
291
+    }
292
+
293
+    func webView(
294
+        _ webView: WKWebView,
295
+        runJavaScriptTextInputPanelWithPrompt prompt: String,
296
+        defaultText: String?,
297
+        initiatedByFrame frame: WKFrameInfo,
298
+        completionHandler: @escaping (String?) -> Void
299
+    ) {
300
+        let alert = NSAlert()
301
+        alert.messageText = prompt
302
+        alert.addButton(withTitle: NSLocalizedString("OK", comment: "Web prompt accept"))
303
+        alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Web prompt cancel"))
304
+        let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24))
305
+        field.stringValue = defaultText ?? ""
306
+        alert.accessoryView = field
307
+        guard alert.runModal() == .alertFirstButtonReturn else {
308
+            completionHandler(nil)
309
+            return
310
+        }
311
+        completionHandler(field.stringValue)
312
+    }
313
+
314
+    /// Safari-like UA without the app name suffix — Cloudflare / Indeed bot checks reject `App for Indeed/…` in the UA string.
315
+    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"
226
 }
316
 }
227
 
317
 
228
-// MARK: - Google sign-in only (no Gmail, Help hub, Search, etc.)
318
+// MARK: - Embedded browsing policy
229
 
319
 
230
-/// Limits embedded browsing on Google-owned hosts to OAuth / sign-in, while leaving Indeed and third-party apply sites unrestricted.
320
+/// Keeps Indeed and apply helpers (Cloudflare, Google sign-in, reCAPTCHA) in-app; opens company career sites in the system browser.
231
 enum IndeedWebBrowsingPolicy {
321
 enum IndeedWebBrowsingPolicy {
322
+    /// Company career sites and other non-Indeed apply links (e.g. “Apply on company site”).
323
+    static func shouldOpenInSystemBrowser(url: URL) -> Bool {
324
+        guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
325
+            return false
326
+        }
327
+        guard let host = url.host?.lowercased() else { return false }
328
+        if shouldRemainInEmbeddedWebView(url: url) { return false }
329
+        if isRestrictedPlatformHost(host) { return false }
330
+        return true
331
+    }
332
+
333
+    /// URLs that must load inside the embedded `WKWebView` (not the default browser).
334
+    static func shouldRemainInEmbeddedWebView(url: URL) -> Bool {
335
+        guard let scheme = url.scheme?.lowercased() else { return false }
336
+        if scheme == "about" || scheme == "blob" || scheme == "data" { return true }
337
+        guard scheme == "http" || scheme == "https" else { return false }
338
+        guard let host = url.host?.lowercased() else { return false }
339
+
340
+        if isIndeedHost(host) { return true }
341
+        if isAllowedGoogleSignInHost(host, path: url.path) { return true }
342
+        if isGooglePolicyOrLegalPage(host: host, path: url.path) { return true }
343
+        if isGoogleRecaptchaHost(host, path: url.path) { return true }
344
+        if isCloudflareChallengeHost(host) { return true }
345
+        if isHCaptchaHost(host) { return true }
346
+        return false
347
+    }
348
+
349
+    static func isIndeedHost(_ host: String) -> Bool {
350
+        if host == "indeed.com" { return true }
351
+        if host.hasPrefix("indeed.") { return true }
352
+        return host.contains(".indeed.")
353
+    }
354
+
232
     static func allows(navigationAction: WKNavigationAction) -> Bool {
355
     static func allows(navigationAction: WKNavigationAction) -> Bool {
233
         guard let url = navigationAction.request.url else { return true }
356
         guard let url = navigationAction.request.url else { return true }
234
         let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? true
357
         let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? true
@@ -244,10 +367,9 @@ enum IndeedWebBrowsingPolicy {
244
         guard scheme == "http" || scheme == "https" else { return false }
367
         guard scheme == "http" || scheme == "https" else { return false }
245
         guard let host = url.host?.lowercased() else { return false }
368
         guard let host = url.host?.lowercased() else { return false }
246
 
369
 
247
-        if !isGooglePropertyHost(host) {
248
-            return true
249
-        }
250
-        return isAllowedGoogleSignInHost(host, path: url.path)
370
+        if shouldRemainInEmbeddedWebView(url: url) { return true }
371
+        if isRestrictedPlatformHost(host) { return false }
372
+        return true
251
     }
373
     }
252
 
374
 
253
     static func allowsSubframeNavigation(to url: URL) -> Bool {
375
     static func allowsSubframeNavigation(to url: URL) -> Bool {
@@ -256,14 +378,79 @@ enum IndeedWebBrowsingPolicy {
256
         guard scheme == "http" || scheme == "https" else { return false }
378
         guard scheme == "http" || scheme == "https" else { return false }
257
         guard let host = url.host?.lowercased() else { return false }
379
         guard let host = url.host?.lowercased() else { return false }
258
 
380
 
381
+        if isYouTubeRelatedHost(host) {
382
+            return false
383
+        }
259
         if !isGooglePropertyHost(host) {
384
         if !isGooglePropertyHost(host) {
260
             return true
385
             return true
261
         }
386
         }
262
         if isAllowedGoogleSignInHost(host, path: url.path) {
387
         if isAllowedGoogleSignInHost(host, path: url.path) {
263
             return true
388
             return true
264
         }
389
         }
265
-        let path = url.path.lowercased()
266
-        if host == "www.google.com", path.contains("/recaptcha") {
390
+        if isGooglePolicyOrLegalPage(host: host, path: url.path) {
391
+            return true
392
+        }
393
+        if isGoogleRecaptchaHost(host, path: url.path) {
394
+            return true
395
+        }
396
+        if isCloudflareChallengeHost(host) {
397
+            return true
398
+        }
399
+        return isHCaptchaHost(host)
400
+    }
401
+
402
+    /// Google, YouTube, and similar — stay in the sign-in flow; never open in the system browser.
403
+    private static func isRestrictedPlatformHost(_ host: String) -> Bool {
404
+        isGooglePropertyHost(host) || isYouTubeRelatedHost(host)
405
+    }
406
+
407
+    private static func isYouTubeRelatedHost(_ host: String) -> Bool {
408
+        if host == "youtu.be" { return true }
409
+        if host == "youtube.com" || host.hasSuffix(".youtube.com") { return true }
410
+        if host.contains("youtube") { return true }
411
+        return false
412
+    }
413
+
414
+    /// Privacy / Terms / Help links on the Google account chooser (not YouTube, Gmail, or Search).
415
+    private static func isGooglePolicyOrLegalPage(host: String, path: String) -> Bool {
416
+        if host == "policies.google.com" || host == "privacy.google.com" {
417
+            return true
418
+        }
419
+        if host == "support.google.com" {
420
+            let pathLower = path.lowercased()
421
+            return pathLower.contains("/accounts")
422
+        }
423
+        if host == "www.google.com" || host == "google.com" {
424
+            let pathLower = path.lowercased()
425
+            return pathLower.contains("/policies")
426
+                || pathLower.contains("/privacy")
427
+                || pathLower.contains("/terms")
428
+                || pathLower.contains("/intl/")
429
+        }
430
+        return false
431
+    }
432
+
433
+    /// Cloudflare Turnstile / bot check during Indeed apply (`challenges.cloudflare.com`, …).
434
+    private static func isCloudflareChallengeHost(_ host: String) -> Bool {
435
+        if host == "cloudflare.com" || host.hasSuffix(".cloudflare.com") { return true }
436
+        return false
437
+    }
438
+
439
+    /// hCaptcha widget hosts used on some apply flows.
440
+    private static func isHCaptchaHost(_ host: String) -> Bool {
441
+        host == "hcaptcha.com" || host.hasSuffix(".hcaptcha.com")
442
+    }
443
+
444
+    /// Google reCAPTCHA during Indeed Easy Apply (`recaptcha.net`, `google.com/recaptcha`, `gstatic.com`, …).
445
+    private static func isGoogleRecaptchaHost(_ host: String, path: String) -> Bool {
446
+        if host == "recaptcha.net" || host.hasSuffix(".recaptcha.net") {
447
+            return true
448
+        }
449
+        let pathLower = path.lowercased()
450
+        if host == "recaptcha.google.com" || host.hasPrefix("recaptcha.google.") {
451
+            return true
452
+        }
453
+        if host == "google.com" || host.hasSuffix(".google.com"), pathLower.contains("recaptcha") {
267
             return true
454
             return true
268
         }
455
         }
269
         if host == "gstatic.com" || host.hasSuffix(".gstatic.com") {
456
         if host == "gstatic.com" || host.hasSuffix(".gstatic.com") {
@@ -283,25 +470,72 @@ enum IndeedWebBrowsingPolicy {
283
         return false
470
         return false
284
     }
471
     }
285
 
472
 
286
-    /// True for Google OAuth / sign-in navigations where we inject `prompt=select_account`.
287
-    static func shouldForceGoogleAccountPicker(for url: URL) -> Bool {
473
+    /// Hosts used during “Sign in with Google” — not Help Center, Gmail, Drive, Search, or the apps launcher.
474
+    private static func isAllowedGoogleSignInHost(_ host: String, path: String) -> Bool {
475
+        if host == "accounts.google.com" || host.hasPrefix("accounts.google.") {
476
+            return true
477
+        }
478
+        if host == "signin.google.com" {
479
+            return true
480
+        }
481
+        if host == "oauth.googleusercontent.com" {
482
+            return true
483
+        }
484
+        if host == "googleusercontent.com" || host.hasSuffix(".googleusercontent.com") {
485
+            return true
486
+        }
487
+        if host == "apis.google.com" {
488
+            return true
489
+        }
490
+        if isGooglePolicyOrLegalPage(host: host, path: path) {
491
+            return true
492
+        }
493
+        if host == "myaccount.google.com" {
494
+            return isGoogleSignInRelatedPath(path)
495
+        }
496
+        if host == "www.google.com" || host == "google.com" {
497
+            return isGoogleSignInRelatedPath(path)
498
+        }
499
+        return false
500
+    }
501
+
502
+    private static func isGoogleSignInRelatedPath(_ path: String) -> Bool {
503
+        let pathLower = path.lowercased()
504
+        return pathLower.contains("/signin")
505
+            || pathLower.contains("/accounts")
506
+            || pathLower.contains("servicelogin")
507
+            || pathLower.contains("/oauth")
508
+            || pathLower.contains("/gsi")
509
+            || pathLower.contains("/device")
510
+            || pathLower.contains("/security")
511
+    }
512
+
513
+    /// First Google OAuth hop after leaving Indeed (`/o/oauth2/.../auth`, account chooser, …).
514
+    static func isGoogleSignInEntryURL(_ url: URL) -> Bool {
288
         guard let host = url.host?.lowercased(),
515
         guard let host = url.host?.lowercased(),
289
               host == "accounts.google.com" || host.hasPrefix("accounts.google.") else {
516
               host == "accounts.google.com" || host.hasPrefix("accounts.google.") else {
290
             return false
517
             return false
291
         }
518
         }
292
         let path = url.path.lowercased()
519
         let path = url.path.lowercased()
293
-        if path.contains("oauth")
294
-            || path.contains("accountchooser")
295
-            || path.contains("/gsi/")
296
-            || path.hasPrefix("/signin/") {
297
-            return true
298
-        }
520
+        if path.contains("/oauth2/") && path.contains("auth") { return true }
521
+        if path.contains("accountchooser") { return true }
522
+        return false
523
+    }
524
+
525
+    /// Whether to inject `prompt=select_account` once for this OAuth attempt.
526
+    static func shouldApplyGoogleAccountPicker(to url: URL) -> Bool {
527
+        guard isGoogleSignInEntryURL(url) else { return false }
528
+        let path = url.path.lowercased()
529
+        if path.contains("/signin/oauth/") || path.contains("/signin/v2/") { return false }
530
+
299
         let query = url.query?.lowercased() ?? ""
531
         let query = url.query?.lowercased() ?? ""
300
-        return query.contains("client_id=")
532
+        if query.contains("select_account") { return false }
533
+        if query.contains("authuser=") { return false }
534
+        return true
301
     }
535
     }
302
 
536
 
303
-    /// Rewrites Google sign-in URLs so the account chooser is always shown (not silent reuse of the last account).
304
-    static func urlForcingGoogleAccountPicker(_ url: URL) -> URL {
537
+    /// Adds `prompt=select_account` so Google shows the account chooser instead of silent sign-in.
538
+    static func urlAddingGoogleAccountPickerPrompt(_ url: URL) -> URL {
305
         guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }
539
         guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }
306
         var items = components.queryItems ?? []
540
         var items = components.queryItems ?? []
307
 
541
 
@@ -321,27 +555,4 @@ enum IndeedWebBrowsingPolicy {
321
         components.queryItems = items.isEmpty ? nil : items
555
         components.queryItems = items.isEmpty ? nil : items
322
         return components.url ?? url
556
         return components.url ?? url
323
     }
557
     }
324
-
325
-    /// Hosts used during “Sign in with Google” — not Help Center, Gmail, Drive, Search, or the apps launcher.
326
-    private static func isAllowedGoogleSignInHost(_ host: String, path: String) -> Bool {
327
-        if host == "accounts.google.com" || host.hasPrefix("accounts.google.") {
328
-            return true
329
-        }
330
-        if host == "signin.google.com" {
331
-            return true
332
-        }
333
-        if host == "oauth.googleusercontent.com" {
334
-            return true
335
-        }
336
-        if host == "googleusercontent.com" || host.hasSuffix(".googleusercontent.com") {
337
-            return true
338
-        }
339
-        if host == "policies.google.com" || host == "privacy.google.com" {
340
-            return true
341
-        }
342
-        if host == "apis.google.com" {
343
-            return true
344
-        }
345
-        return false
346
-    }
347
 }
558
 }