|
|
@@ -185,14 +185,163 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
|
|
185
|
185
|
updateNavigationButtons()
|
|
186
|
186
|
}
|
|
187
|
187
|
|
|
|
188
|
+ func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
|
|
189
|
+ guard IndeedWebBrowsingPolicy.allows(navigationAction: navigationAction) else {
|
|
|
190
|
+ decisionHandler(.cancel)
|
|
|
191
|
+ return
|
|
|
192
|
+ }
|
|
|
193
|
+
|
|
|
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
|
+ }
|
|
|
204
|
+ }
|
|
|
205
|
+
|
|
|
206
|
+ decisionHandler(.allow)
|
|
|
207
|
+ }
|
|
|
208
|
+
|
|
188
|
209
|
/// Target=_blank / `window.open` without a frame: load in this view so apply flows stay in-app.
|
|
189
|
210
|
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
190
|
|
- if navigationAction.targetFrame == nil {
|
|
191
|
|
- webView.load(navigationAction.request)
|
|
|
211
|
+ guard navigationAction.targetFrame == nil,
|
|
|
212
|
+ IndeedWebBrowsingPolicy.allows(navigationAction: navigationAction) else {
|
|
|
213
|
+ return nil
|
|
192
|
214
|
}
|
|
|
215
|
+ var request = navigationAction.request
|
|
|
216
|
+ if let url = request.url,
|
|
|
217
|
+ IndeedWebBrowsingPolicy.shouldForceGoogleAccountPicker(for: url) {
|
|
|
218
|
+ request = URLRequest(url: IndeedWebBrowsingPolicy.urlForcingGoogleAccountPicker(url))
|
|
|
219
|
+ }
|
|
|
220
|
+ webView.load(request)
|
|
193
|
221
|
return nil
|
|
194
|
222
|
}
|
|
195
|
223
|
|
|
196
|
224
|
/// Desktop Safari UA helps Indeed serve a full desktop apply experience.
|
|
197
|
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"
|
|
198
|
226
|
}
|
|
|
227
|
+
|
|
|
228
|
+// MARK: - Google sign-in only (no Gmail, Help hub, Search, etc.)
|
|
|
229
|
+
|
|
|
230
|
+/// Limits embedded browsing on Google-owned hosts to OAuth / sign-in, while leaving Indeed and third-party apply sites unrestricted.
|
|
|
231
|
+enum IndeedWebBrowsingPolicy {
|
|
|
232
|
+ static func allows(navigationAction: WKNavigationAction) -> Bool {
|
|
|
233
|
+ guard let url = navigationAction.request.url else { return true }
|
|
|
234
|
+ let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? true
|
|
|
235
|
+ if isMainFrame {
|
|
|
236
|
+ return allowsMainFrameNavigation(to: url)
|
|
|
237
|
+ }
|
|
|
238
|
+ return allowsSubframeNavigation(to: url)
|
|
|
239
|
+ }
|
|
|
240
|
+
|
|
|
241
|
+ static func allowsMainFrameNavigation(to url: URL) -> Bool {
|
|
|
242
|
+ guard let scheme = url.scheme?.lowercased() else { return false }
|
|
|
243
|
+ if scheme == "about" || scheme == "blob" || scheme == "data" { return true }
|
|
|
244
|
+ guard scheme == "http" || scheme == "https" else { return false }
|
|
|
245
|
+ guard let host = url.host?.lowercased() else { return false }
|
|
|
246
|
+
|
|
|
247
|
+ if !isGooglePropertyHost(host) {
|
|
|
248
|
+ return true
|
|
|
249
|
+ }
|
|
|
250
|
+ return isAllowedGoogleSignInHost(host, path: url.path)
|
|
|
251
|
+ }
|
|
|
252
|
+
|
|
|
253
|
+ static func allowsSubframeNavigation(to url: URL) -> Bool {
|
|
|
254
|
+ guard let scheme = url.scheme?.lowercased() else { return false }
|
|
|
255
|
+ if scheme == "about" || scheme == "blob" || scheme == "data" { return true }
|
|
|
256
|
+ guard scheme == "http" || scheme == "https" else { return false }
|
|
|
257
|
+ guard let host = url.host?.lowercased() else { return false }
|
|
|
258
|
+
|
|
|
259
|
+ if !isGooglePropertyHost(host) {
|
|
|
260
|
+ return true
|
|
|
261
|
+ }
|
|
|
262
|
+ if isAllowedGoogleSignInHost(host, path: url.path) {
|
|
|
263
|
+ return true
|
|
|
264
|
+ }
|
|
|
265
|
+ let path = url.path.lowercased()
|
|
|
266
|
+ if host == "www.google.com", path.contains("/recaptcha") {
|
|
|
267
|
+ return true
|
|
|
268
|
+ }
|
|
|
269
|
+ if host == "gstatic.com" || host.hasSuffix(".gstatic.com") {
|
|
|
270
|
+ return true
|
|
|
271
|
+ }
|
|
|
272
|
+ return false
|
|
|
273
|
+ }
|
|
|
274
|
+
|
|
|
275
|
+ private static func isGooglePropertyHost(_ host: String) -> Bool {
|
|
|
276
|
+ if host == "google.com" || host.hasSuffix(".google.com") { return true }
|
|
|
277
|
+ if host == "gmail.com" || host.hasSuffix(".gmail.com") { return true }
|
|
|
278
|
+ if host == "googleusercontent.com" || host.hasSuffix(".googleusercontent.com") { return true }
|
|
|
279
|
+ if host == "gstatic.com" || host.hasSuffix(".gstatic.com") { return true }
|
|
|
280
|
+ if host == "youtube.com" || host.hasSuffix(".youtube.com") { return true }
|
|
|
281
|
+ if host == "blogger.com" || host.hasSuffix(".blogger.com") { return true }
|
|
|
282
|
+ if host == "withgoogle.com" || host.hasSuffix(".withgoogle.com") { return true }
|
|
|
283
|
+ return false
|
|
|
284
|
+ }
|
|
|
285
|
+
|
|
|
286
|
+ /// True for Google OAuth / sign-in navigations where we inject `prompt=select_account`.
|
|
|
287
|
+ static func shouldForceGoogleAccountPicker(for url: URL) -> Bool {
|
|
|
288
|
+ guard let host = url.host?.lowercased(),
|
|
|
289
|
+ host == "accounts.google.com" || host.hasPrefix("accounts.google.") else {
|
|
|
290
|
+ return false
|
|
|
291
|
+ }
|
|
|
292
|
+ 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
|
+ }
|
|
|
299
|
+ let query = url.query?.lowercased() ?? ""
|
|
|
300
|
+ return query.contains("client_id=")
|
|
|
301
|
+ }
|
|
|
302
|
+
|
|
|
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 {
|
|
|
305
|
+ guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }
|
|
|
306
|
+ var items = components.queryItems ?? []
|
|
|
307
|
+
|
|
|
308
|
+ items.removeAll { $0.name.compare("login_hint", options: .caseInsensitive) == .orderedSame }
|
|
|
309
|
+ items.removeAll { $0.name.compare("hint", options: .caseInsensitive) == .orderedSame }
|
|
|
310
|
+
|
|
|
311
|
+ if let promptIndex = items.firstIndex(where: { $0.name.compare("prompt", options: .caseInsensitive) == .orderedSame }) {
|
|
|
312
|
+ let existing = items[promptIndex].value ?? ""
|
|
|
313
|
+ if !existing.lowercased().contains("select_account") {
|
|
|
314
|
+ let merged = existing.isEmpty ? "select_account" : "\(existing) select_account"
|
|
|
315
|
+ items[promptIndex] = URLQueryItem(name: "prompt", value: merged)
|
|
|
316
|
+ }
|
|
|
317
|
+ } else {
|
|
|
318
|
+ items.append(URLQueryItem(name: "prompt", value: "select_account"))
|
|
|
319
|
+ }
|
|
|
320
|
+
|
|
|
321
|
+ components.queryItems = items.isEmpty ? nil : items
|
|
|
322
|
+ return components.url ?? url
|
|
|
323
|
+ }
|
|
|
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
|
+}
|