|
|
@@ -8,15 +8,25 @@ import WebKit
|
|
8
|
8
|
|
|
9
|
9
|
/// Indeed job listing and apply flow in a `WKWebView`, embedded in the dashboard main panel or hosted in a window.
|
|
10
|
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
|
14
|
/// When set, a leading **Home** control calls this so the host can hide the embedded browser (same-window UX).
|
|
12
|
15
|
var onDismissEmbedded: (() -> Void)?
|
|
13
|
16
|
private let webView: WKWebView = {
|
|
14
|
17
|
let configuration = WKWebViewConfiguration()
|
|
|
18
|
+ configuration.processPool = IndeedJobBrowserViewController.sharedProcessPool
|
|
|
19
|
+ configuration.websiteDataStore = .default()
|
|
15
|
20
|
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
|
21
|
+ if #available(macOS 11.0, *) {
|
|
|
22
|
+ configuration.defaultWebpagePreferences.allowsContentJavaScript = true
|
|
|
23
|
+ }
|
|
16
|
24
|
return WKWebView(frame: .zero, configuration: configuration)
|
|
17
|
25
|
}()
|
|
18
|
26
|
|
|
19
|
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
|
31
|
private let backButton = NSButton()
|
|
22
|
32
|
private let forwardButton = NSButton()
|
|
|
@@ -178,6 +188,9 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
|
|
178
|
188
|
}
|
|
179
|
189
|
|
|
180
|
190
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
191
|
+ if let host = webView.url?.host?.lowercased(), IndeedWebBrowsingPolicy.isIndeedHost(host) {
|
|
|
192
|
+ pendingGoogleAccountPicker = false
|
|
|
193
|
+ }
|
|
181
|
194
|
updateNavigationButtons()
|
|
182
|
195
|
}
|
|
183
|
196
|
|
|
|
@@ -191,44 +204,154 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
|
|
191
|
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
|
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
|
225
|
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
211
|
226
|
guard navigationAction.targetFrame == nil,
|
|
212
|
227
|
IndeedWebBrowsingPolicy.allows(navigationAction: navigationAction) else {
|
|
213
|
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
|
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
|
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
|
355
|
static func allows(navigationAction: WKNavigationAction) -> Bool {
|
|
233
|
356
|
guard let url = navigationAction.request.url else { return true }
|
|
234
|
357
|
let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? true
|
|
|
@@ -244,10 +367,9 @@ enum IndeedWebBrowsingPolicy {
|
|
244
|
367
|
guard scheme == "http" || scheme == "https" else { return false }
|
|
245
|
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
|
375
|
static func allowsSubframeNavigation(to url: URL) -> Bool {
|
|
|
@@ -256,14 +378,79 @@ enum IndeedWebBrowsingPolicy {
|
|
256
|
378
|
guard scheme == "http" || scheme == "https" else { return false }
|
|
257
|
379
|
guard let host = url.host?.lowercased() else { return false }
|
|
258
|
380
|
|
|
|
381
|
+ if isYouTubeRelatedHost(host) {
|
|
|
382
|
+ return false
|
|
|
383
|
+ }
|
|
259
|
384
|
if !isGooglePropertyHost(host) {
|
|
260
|
385
|
return true
|
|
261
|
386
|
}
|
|
262
|
387
|
if isAllowedGoogleSignInHost(host, path: url.path) {
|
|
263
|
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
|
454
|
return true
|
|
268
|
455
|
}
|
|
269
|
456
|
if host == "gstatic.com" || host.hasSuffix(".gstatic.com") {
|
|
|
@@ -283,25 +470,72 @@ enum IndeedWebBrowsingPolicy {
|
|
283
|
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
|
515
|
guard let host = url.host?.lowercased(),
|
|
289
|
516
|
host == "accounts.google.com" || host.hasPrefix("accounts.google.") else {
|
|
290
|
517
|
return false
|
|
291
|
518
|
}
|
|
292
|
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
|
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
|
539
|
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }
|
|
306
|
540
|
var items = components.queryItems ?? []
|
|
307
|
541
|
|
|
|
@@ -321,27 +555,4 @@ enum IndeedWebBrowsingPolicy {
|
|
321
|
555
|
components.queryItems = items.isEmpty ? nil : items
|
|
322
|
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
|
}
|