This repository has been archived by the owner on May 10, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 441
/
BVC+WKNavigationDelegate.swift
1493 lines (1271 loc) · 63.1 KB
/
BVC+WKNavigationDelegate.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import WebKit
import Shared
import Data
import BraveShields
import Preferences
import BraveCore
import BraveUI
import BraveWallet
import os.log
import Favicon
import Growth
import SafariServices
import LocalAuthentication
import BraveShared
import UniformTypeIdentifiers
import CertificateUtilities
extension WKNavigationAction {
/// Allow local requests only if the request is privileged.
/// If the request is internal or unprivileged, we should deny it.
var isInternalUnprivileged: Bool {
guard let url = request.url else {
return true
}
if let url = InternalURL(url) {
return !url.isAuthorized
} else {
return false
}
}
}
extension WKNavigationType: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .linkActivated: return "linkActivated"
case .formResubmitted: return "formResubmitted"
case .backForward: return "backForward"
case .formSubmitted: return "formSubmitted"
case .other: return "other"
case .reload: return "reload"
@unknown default:
return "Unknown(\(rawValue))"
}
}
}
extension UTType {
static let textCalendar = UTType(mimeType: "text/calendar")! // Not the same as `calendarEvent`
static let mobileConfiguration = UTType(mimeType: "application/x-apple-aspen-config")!
}
// MARK: WKNavigationDelegate
extension BrowserViewController: WKNavigationDelegate {
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
if tabManager.selectedTab?.webView !== webView {
return
}
toolbarVisibilityViewModel.toolbarState = .expanded
// check if web view is loading a different origin than the one currently loaded
if let selectedTab = tabManager.selectedTab,
selectedTab.url?.origin != webView.url?.origin {
if let url = webView.url {
if !InternalURL.isValid(url: url) {
// reset secure content state to unknown until page can be evaluated
selectedTab.sslPinningError = nil
selectedTab.sslPinningTrust = nil
selectedTab.secureContentState = .unknown
logSecureContentState(tab: selectedTab, details: "DidStartProvisionalNavigation - Reset secure content state to unknown until page can be evaluated")
updateToolbarSecureContentState(.unknown)
}
}
// new site has a different origin, hide wallet icon.
tabManager.selectedTab?.isWalletIconVisible = false
// new site, reset connected addresses
tabManager.selectedTab?.clearSolanaConnectedAccounts()
// close wallet panel if it's open
if let popoverController = self.presentedViewController as? PopoverController,
popoverController.contentController is WalletPanelHostingController {
self.dismiss(animated: true)
}
}
if #unavailable(iOS 16.0) {
updateFindInPageVisibility(visible: false)
}
displayPageZoom(visible: false)
// If we are going to navigate to a new page, hide the reader mode button. Unless we
// are going to a about:reader page. Then we keep it on screen: it will change status
// (orange color) as soon as the page has loaded.
if let url = webView.url {
if !url.isReaderModeURL {
topToolbar.updateReaderModeState(ReaderModeState.unavailable)
hideReaderModeBar(animated: false)
}
}
}
// Recognize an Apple Maps URL. This will trigger the native app. But only if a search query is present.
// Otherwise it could just be a visit to a regular page on maps.apple.com.
// Exchaging https/https scheme with maps in order to open URLS properly on Apple Maps
fileprivate func isAppleMapsURL(_ url: URL) -> (enabled: Bool, url: URL)? {
if url.scheme == "http" || url.scheme == "https" {
if url.host == "maps.apple.com" && url.query != nil {
guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return nil
}
urlComponents.scheme = "maps"
if let url = urlComponents.url {
return (true, url)
}
return nil
}
}
return (false, url)
}
// Recognize a iTunes Store URL. These all trigger the native apps. Note that appstore.com and phobos.apple.com
// used to be in this list. I have removed them because they now redirect to itunes.apple.com. If we special case
// them then iOS will actually first open Safari, which then redirects to the app store. This works but it will
// leave a 'Back to Safari' button in the status bar, which we do not want.
fileprivate func isStoreURL(_ url: URL) -> Bool {
let isStoreScheme = ["itms-apps", "itms-appss", "itmss"].contains(url.scheme)
if isStoreScheme {
return true
}
let isHttpScheme = ["http", "https"].contains(url.scheme)
let isAppStoreHost = ["itunes.apple.com", "apps.apple.com", "appsto.re"].contains(url.host)
return isHttpScheme && isAppStoreHost
}
// This is the place where we decide what to do with a new navigation action. There are a number of special schemes
// and http(s) urls that need to be handled in a different way. All the logic for that is inside this delegate
// method.
fileprivate func isUpholdOAuthAuthorization(_ url: URL) -> Bool {
return url.scheme == "rewards" && url.host == "uphold"
}
@MainActor
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences) async -> (WKNavigationActionPolicy, WKWebpagePreferences) {
guard var requestURL = navigationAction.request.url else {
return (.cancel, preferences)
}
if InternalURL.isValid(url: requestURL) {
if navigationAction.navigationType != .backForward, navigationAction.isInternalUnprivileged,
(navigationAction.sourceFrame != nil || navigationAction.targetFrame?.isMainFrame == false || navigationAction.request.cachePolicy == .useProtocolCachePolicy) {
Logger.module.warning("Denying unprivileged request: \(navigationAction.request)")
return (.cancel, preferences)
}
return (.allow, preferences)
}
if requestURL.scheme == "about" {
return (.allow, preferences)
}
if requestURL.isBookmarklet {
return (.cancel, preferences)
}
// Universal links do not work if the request originates from the app, manual handling is required.
if let mainDocURL = navigationAction.request.mainDocumentURL,
let universalLink = UniversalLinkManager.universalLinkType(for: mainDocURL, checkPath: true) {
switch universalLink {
case .buyVPN:
presentCorrespondingVPNViewController()
return (.cancel, preferences)
}
}
// First special case are some schemes that are about Calling. We prompt the user to confirm this action. This
// gives us the exact same behaviour as Safari.
let tab = tab(for: webView)
if ["sms", "tel", "facetime", "facetime-audio"].contains(requestURL.scheme) {
let shouldOpen = await handleExternalURL(requestURL, tab: tab, navigationAction: navigationAction)
return (shouldOpen ? .allow : .cancel, preferences)
}
// Second special case are a set of URLs that look like regular http links, but should be handed over to iOS
// instead of being loaded in the webview.
// In addition we are exchaging actual scheme with "maps" scheme
// So the Apple maps URLs will open properly
if let mapsURL = isAppleMapsURL(requestURL), mapsURL.enabled {
let shouldOpen = await handleExternalURL(mapsURL.url, tab: tab, navigationAction: navigationAction)
return (shouldOpen ? .allow : .cancel, preferences)
}
if isStoreURL(requestURL) {
let shouldOpen = await handleExternalURL(requestURL, tab: tab, navigationAction: navigationAction)
return (shouldOpen ? .allow : .cancel, preferences)
}
// Handles custom mailto URL schemes.
if requestURL.scheme == "mailto" {
let shouldOpen = await handleExternalURL(requestURL, tab: tab, navigationAction: navigationAction)
return (shouldOpen ? .allow : .cancel, preferences)
}
// handles IPFS URL schemes
if requestURL.isIPFSScheme {
if navigationAction.targetFrame?.isMainFrame == true {
handleIPFSSchemeURL(requestURL)
}
return (.cancel, preferences)
}
// handles Decentralized DNS
if let decentralizedDNSHelper = self.decentralizedDNSHelperFor(url: requestURL),
navigationAction.targetFrame?.isMainFrame == true {
topToolbar.locationView.loading = true
let result = await decentralizedDNSHelper.lookup(domain: requestURL.schemelessAbsoluteDisplayString)
topToolbar.locationView.loading = tabManager.selectedTab?.loading ?? false
guard !Task.isCancelled else { // user pressed stop, or typed new url
return (.cancel, preferences)
}
switch result {
case let .loadInterstitial(service):
showWeb3ServiceInterstitialPage(service: service, originalURL: requestURL)
return (.cancel, preferences)
case let .load(resolvedURL):
if resolvedURL.isIPFSScheme {
handleIPFSSchemeURL(resolvedURL)
return (.cancel, preferences)
} else { // non-ipfs, treat as normal url / link tapped
requestURL = resolvedURL
}
case .none:
break
}
}
let isPrivateBrowsing = privateBrowsingManager.isPrivateBrowsing
tab?.currentRequestURL = requestURL
// Website redirection logic
if requestURL.isWebPage(includeDataURIs: false),
navigationAction.targetFrame?.isMainFrame == true,
let redirectURL = WebsiteRedirects.redirect(for: requestURL) {
tab?.loadRequest(URLRequest(url: redirectURL))
return (.cancel, preferences)
}
// before loading any ad-block scripts
// await the preparation of the ad-block services
await LaunchHelper.shared.prepareAdBlockServices(
adBlockService: self.braveCore.adblockService
)
if let mainDocumentURL = navigationAction.request.mainDocumentURL {
if mainDocumentURL != tab?.currentPageData?.mainFrameURL {
// Clear the current page data if the page changes.
// Do this before anything else so that we have a clean slate.
tab?.currentPageData = PageData(mainFrameURL: mainDocumentURL)
}
let domainForMainFrame = Domain.getOrCreate(forUrl: mainDocumentURL, persistent: !isPrivateBrowsing)
if let tab = tab, let modifiedRequest = getInternalRedirect(
from: navigationAction, in: tab, domainForMainFrame: domainForMainFrame) {
tab.isInternalRedirect = true
tab.loadRequest(modifiedRequest)
if let url = modifiedRequest.url {
ContentBlockerManager.log.debug("Redirected user to `\(url.absoluteString, privacy: .private)`")
}
return (.cancel, preferences)
} else {
tab?.isInternalRedirect = false
}
// Set some additional user scripts
if navigationAction.targetFrame?.isMainFrame == true {
tab?.setScripts(scripts: [
// Add de-amp script
// The user script manager will take care to not reload scripts if this value doesn't change
.deAmp: Preferences.Shields.autoRedirectAMPPages.value,
// Add request blocking script
// This script will block certian `xhr` and `window.fetch()` requests
.requestBlocking: requestURL.isWebPage(includeDataURIs: false) &&
domainForMainFrame.isShieldExpected(.AdblockAndTp, considerAllShieldsOption: true),
// The tracker protection script
// This script will track what is blocked and increase stats
.trackerProtectionStats: requestURL.isWebPage(includeDataURIs: false) &&
domainForMainFrame.isShieldExpected(.AdblockAndTp, considerAllShieldsOption: true)
])
}
// Check if custom user scripts must be added to or removed from the web view.
if let targetFrame = navigationAction.targetFrame {
tab?.currentPageData?.addSubframeURL(forRequestURL: requestURL, isForMainFrame: targetFrame.isMainFrame)
let scriptTypes = await tab?.currentPageData?.makeUserScriptTypes(domain: domainForMainFrame) ?? []
tab?.setCustomUserScript(scripts: scriptTypes)
}
}
// Brave Search logic.
if navigationAction.targetFrame?.isMainFrame == true,
BraveSearchManager.isValidURL(requestURL) {
// Add Brave Search headers if Rewards is enabled
if !isPrivateBrowsing && rewards.isEnabled && navigationAction.request.allHTTPHeaderFields?["X-Brave-Ads-Enabled"] == nil {
var modifiedRequest = URLRequest(url: requestURL)
modifiedRequest.setValue("1", forHTTPHeaderField: "X-Brave-Ads-Enabled")
tab?.loadRequest(modifiedRequest)
return (.cancel, preferences)
}
// We fetch cookies to determine if backup search was enabled on the website.
let profile = self.profile
let cookies = await webView.configuration.websiteDataStore.httpCookieStore.allCookies()
tab?.braveSearchManager = BraveSearchManager(profile: profile, url: requestURL, cookies: cookies)
if let braveSearchManager = tab?.braveSearchManager {
braveSearchManager.fallbackQueryResultsPending = true
braveSearchManager.shouldUseFallback { backupQuery in
guard let query = backupQuery else {
braveSearchManager.fallbackQueryResultsPending = false
return
}
if query.found {
braveSearchManager.fallbackQueryResultsPending = false
} else {
braveSearchManager.backupSearch(with: query) { completion in
braveSearchManager.fallbackQueryResultsPending = false
tab?.injectResults()
}
}
}
}
} else {
tab?.braveSearchManager = nil
}
// This is the normal case, opening a http or https url, which we handle by loading them in this WKWebView. We
// always allow this. Additionally, data URIs are also handled just like normal web pages.
if ["http", "https", "data", "blob", "file"].contains(requestURL.scheme) {
if navigationAction.targetFrame?.isMainFrame == true {
tab?.updateUserAgent(webView, newURL: requestURL)
if let etldP1 = requestURL.baseDomain, tab?.proceedAnywaysDomainList.contains(etldP1) == false {
let domain = Domain.getOrCreate(forUrl: requestURL, persistent: !isPrivateBrowsing)
let shouldBlock = await AdBlockStats.shared.shouldBlock(
requestURL: requestURL, sourceURL: requestURL, resourceType: .document,
isAggressiveMode: domain.blockAdsAndTrackingLevel.isAggressive
)
if shouldBlock, let escapingURL = requestURL.absoluteString.escape() {
var components = URLComponents(string: InternalURL.baseUrl)
components?.path = "/\(InternalURL.Path.blocked.rawValue)"
components?.queryItems = [URLQueryItem(name: "url", value: escapingURL)]
if let url = components?.url {
let request = PrivilegedRequest(url: url) as URLRequest
tab?.loadRequest(request)
return (.cancel, preferences)
}
}
}
}
pendingRequests[requestURL.absoluteString] = navigationAction.request
// Adblock logic,
// Only use main document URL, not the request URL
// If an iFrame is loaded, shields depending on the main frame, not the iFrame request
// Weird behavior here with `targetFram` and `sourceFrame`, on refreshing page `sourceFrame` is not nil (it is non-optional)
// however, it is still an uninitialized object, making it an unreliable source to compare `isMainFrame` against.
// Rather than using `sourceFrame.isMainFrame` or even comparing `sourceFrame == targetFrame`, a simple URL check is used.
// No adblocking logic is be used on session restore urls. It uses javascript to retrieve the
// request then the page is reloaded with a proper url and adblocking rules are applied.
if let mainDocumentURL = navigationAction.request.mainDocumentURL,
mainDocumentURL.schemelessAbsoluteString == requestURL.schemelessAbsoluteString,
!(InternalURL(requestURL)?.isSessionRestore ?? false),
navigationAction.sourceFrame.isMainFrame || navigationAction.targetFrame?.isMainFrame == true {
// Identify specific block lists that need to be applied to the requesting domain
let domainForShields = Domain.getOrCreate(forUrl: mainDocumentURL, persistent: !isPrivateBrowsing)
// Load rule lists
let ruleLists = await ContentBlockerManager.shared.ruleLists(for: domainForShields)
tab?.contentBlocker.set(ruleLists: ruleLists)
}
let documentTargetURL: URL? = navigationAction.request.mainDocumentURL ??
navigationAction.targetFrame?.request.mainDocumentURL ??
requestURL // Should be the same as the sourceFrame URL
if let documentTargetURL = documentTargetURL {
let domainForShields = Domain.getOrCreate(forUrl: documentTargetURL, persistent: !isPrivateBrowsing)
let isScriptsEnabled = !domainForShields.isShieldExpected(.NoScript, considerAllShieldsOption: true)
// Due to a bug in iOS WKWebpagePreferences.allowsContentJavaScript does NOT work!
// https://github.com/brave/brave-ios/issues/8585
//
// However, the deprecated API WKWebViewConfiguration.preferences.javaScriptEnabled DOES work!
// Even though `configuration` is @NSCopying, somehow this actually updates the preferences LIVE!!
// This follows the same behaviour as Safari
//
// - Brandon T.
//
preferences.allowsContentJavaScript = isScriptsEnabled
webView.configuration.preferences.javaScriptEnabled = isScriptsEnabled
}
// Cookie Blocking code below
if let tab = tab {
tab.setScript(script: .cookieBlocking, enabled: Preferences.Privacy.blockAllCookies.value)
}
// Reset the block alert bool on new host.
if let newHost: String = requestURL.host, let oldHost: String = webView.url?.host, newHost != oldHost {
self.tabManager.selectedTab?.alertShownCount = 0
self.tabManager.selectedTab?.blockAllAlerts = false
}
if navigationAction.shouldPerformDownload {
self.shouldDownloadNavigationResponse = true
}
return (.allow, preferences)
}
// Standard schemes are handled in previous if-case.
// This check handles custom app schemes to open external apps.
// Our own 'brave' scheme does not require the switch-app prompt.
if requestURL.scheme?.contains("brave") == false {
// Do not allow opening external URLs from child tabs
let shouldOpen = await handleExternalURL(requestURL, tab: tab, navigationAction: navigationAction)
let isSyntheticClick = navigationAction.responds(to: Selector(("_syntheticClickType"))) &&
navigationAction.value(forKey: "syntheticClickType") as? Int == 0
// Do not show error message for JS navigated links or redirect
// as it's not the result of a user action.
if !shouldOpen, navigationAction.navigationType == .linkActivated && !isSyntheticClick {
if self.presentedViewController == nil &&
self.presentingViewController == nil &&
tab?.isExternalAppAlertPresented == false &&
tab?.isExternalAppAlertSuppressed == false {
return await withCheckedContinuation { continuation in
let alert = UIAlertController(title: Strings.unableToOpenURLErrorTitle, message: Strings.unableToOpenURLError, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: Strings.OKString, style: .default, handler: nil))
self.present(alert, animated: true) {
continuation.resume(returning: (shouldOpen ? .allow : .cancel, preferences))
}
}
}
}
return (shouldOpen ? .allow : .cancel, preferences)
}
return (.cancel, preferences)
}
/// Handles a link by opening it in an SFSafariViewController and presenting it on the BVC.
///
/// This is unfortunately neccessary to handle certain downloads natively such as ics/calendar invites and
/// mobileconfiguration files.
///
/// The user unfortunately has to dismiss it manually after they have handled the file.
/// Chrome iOS does the same
private func handleLinkWithSafariViewController(_ url: URL, tab: Tab?) {
let vc = SFSafariViewController(url: url, configuration: .init())
vc.modalPresentationStyle = .formSheet
self.present(vc, animated: true)
// If the website opened this URL in a separate tab, remove the empty tab
if let tab = tab, tab.url == nil || tab.url?.absoluteString == "about:blank" {
tabManager.removeTab(tab)
}
}
@MainActor
public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
let isPrivateBrowsing = privateBrowsingManager.isPrivateBrowsing
let response = navigationResponse.response
let responseURL = response.url
let tab = tab(for: webView)
// Check if we upgraded to https and if so we need to update the url of frame evaluations
if let responseURL = responseURL,
let domain = tab?.currentPageData?.domain(persistent: !isPrivateBrowsing),
tab?.currentPageData?.upgradeFrameURL(forResponseURL: responseURL, isForMainFrame: navigationResponse.isForMainFrame) == true {
let scriptTypes = await tab?.currentPageData?.makeUserScriptTypes(domain: domain) ?? []
tab?.setCustomUserScript(scripts: scriptTypes)
}
if let tab = tab,
let responseURL = responseURL,
InternalURL(responseURL)?.isSessionRestore == true {
tab.shouldClassifyLoadsForAds = false
}
var request: URLRequest?
if let url = responseURL {
request = pendingRequests.removeValue(forKey: url.absoluteString)
}
// We can only show this content in the web view if this web view is not pending
// download via the context menu.
let canShowInWebView = navigationResponse.canShowMIMEType && (webView != pendingDownloadWebView)
let forceDownload = webView == pendingDownloadWebView
let mimeTypesThatRequireSFSafariViewControllerHandling: [UTType] = [
.textCalendar,
.mobileConfiguration
]
// SFSafariViewController only supports http/https links
if navigationResponse.isForMainFrame, let url = responseURL, url.isWebPage(includeDataURIs: false),
let mimeType = response.mimeType.flatMap({ UTType(mimeType: $0) }),
mimeTypesThatRequireSFSafariViewControllerHandling.contains(mimeType) {
let isAboutHome = InternalURL(url)?.isAboutHomeURL == true
let isNonActiveTab = isAboutHome ? false : url.host != tabManager.selectedTab?.url?.host
// Check website is trying to open Safari Controller in non-active tab
if !isNonActiveTab {
handleLinkWithSafariViewController(url, tab: tab)
}
return .cancel
}
// Check if this response should be handed off to Passbook.
if shouldDownloadNavigationResponse {
shouldDownloadNavigationResponse = false
if response.mimeType == MIMEType.passbook {
return .download
}
}
if let passbookHelper = OpenPassBookHelper(request: request, response: response, canShowInWebView: canShowInWebView, forceDownload: forceDownload, browserViewController: self) {
// Open our helper and cancel this response from the webview.
passbookHelper.open()
return .cancel
}
// Check if this response should be downloaded.
let cookieStore = webView.configuration.websiteDataStore.httpCookieStore
if let downloadHelper = DownloadHelper(request: request, response: response, cookieStore: cookieStore, canShowInWebView: canShowInWebView, forceDownload: forceDownload) {
// Clear the pending download web view so that subsequent navigations from the same
// web view don't invoke another download.
pendingDownloadWebView = nil
let downloadAlertAction: (HTTPDownload) -> Void = { [weak self] download in
self?.downloadQueue.enqueue(download)
}
// Open our helper and cancel this response from the webview.
if let downloadAlert = downloadHelper.downloadAlert(from: view, okAction: downloadAlertAction) {
present(downloadAlert, animated: true, completion: nil)
}
return .cancel
}
// If the content type is not HTML, create a temporary document so it can be downloaded and
// shared to external applications later. Otherwise, clear the old temporary document.
if let tab = tab, navigationResponse.isForMainFrame {
if response.mimeType?.isKindOfHTML == false, let request = request {
tab.temporaryDocument = TemporaryDocument(preflightResponse: response, request: request, tab: tab)
} else {
tab.temporaryDocument = nil
}
tab.mimeType = response.mimeType
}
if canShowInWebView {
return .allow
}
// If none of our helpers are responsible for handling this response,
// just let the webview handle it as normal.
return .allow
}
public func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
Logger.module.error("ERROR: Should Never download NavigationAction since we never return .download from decidePolicyForAction.")
}
public func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
download.delegate = self
}
nonisolated public func webView(_ webView: WKWebView, respondTo challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
// If this is a certificate challenge, see if the certificate has previously been
// accepted by the user.
let host = challenge.protectionSpace.host
let origin = "\(host):\(challenge.protectionSpace.port)"
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let trust = challenge.protectionSpace.serverTrust,
let cert = (SecTrustCopyCertificateChain(trust) as? [SecCertificate])?.first, profile.certStore.containsCertificate(cert, forOrigin: origin) {
return (.useCredential, URLCredential(trust: trust))
}
// Certificate Pinning
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
if let serverTrust = challenge.protectionSpace.serverTrust {
let host = challenge.protectionSpace.host
let port = challenge.protectionSpace.port
let result = await BraveCertificateUtils.verifyTrust(serverTrust, host: host, port: port)
let certificateChain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate] ?? []
// Cert is valid and should be pinned
if result == 0 {
return (.useCredential, URLCredential(trust: serverTrust))
}
// Cert is valid and should not be pinned
// Let the system handle it and we'll show an error if the system cannot validate it
if result == Int32.min {
// Cert is POTENTIALLY invalid and cannot be pinned
await MainActor.run {
// Handle the potential error later in `didFailProvisionalNavigation`
self.tab(for: webView)?.sslPinningTrust = serverTrust
}
// Let WebKit handle the request and validate the cert
// This is the same as calling `BraveCertificateUtils.evaluateTrust`
return (.performDefaultHandling, nil)
}
// Cert is invalid and cannot be pinned
Logger.module.error("CERTIFICATE_INVALID")
let errorCode = CFNetworkErrors.braveCertificatePinningFailed.rawValue
let underlyingError = NSError(domain: kCFErrorDomainCFNetwork as String,
code: Int(errorCode),
userInfo: ["_kCFStreamErrorCodeKey": Int(errorCode)])
let error = await NSError(domain: kCFErrorDomainCFNetwork as String,
code: Int(errorCode),
userInfo: [NSURLErrorFailingURLErrorKey: webView.url as Any,
"NSErrorPeerCertificateChainKey": certificateChain,
NSUnderlyingErrorKey: underlyingError])
await MainActor.run {
// Handle the error later in `didFailProvisionalNavigation`
self.tab(for: webView)?.sslPinningError = error
}
return (.cancelAuthenticationChallenge, nil)
}
}
// URLAuthenticationChallenge isn't Sendable atm
let protectionSpace = challenge.protectionSpace
let credential = challenge.proposedCredential
let previousFailureCount = challenge.previousFailureCount
return await Task { @MainActor in
guard protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic ||
protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest ||
protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM,
let tab = tab(for: webView)
else {
return (.performDefaultHandling, nil)
}
// The challenge may come from a background tab, so ensure it's the one visible.
tabManager.selectTab(tab)
do {
let credentials = try await Authenticator.handleAuthRequest(
self,
credential: credential,
protectionSpace: protectionSpace,
previousFailureCount: previousFailureCount
)
if BasicAuthCredentialsManager.validDomains.contains(host) {
BasicAuthCredentialsManager.setCredential(origin: origin, credential: credentials.credentials)
}
return (.useCredential, credentials.credentials)
} catch {
return (.rejectProtectionSpace, nil)
}
}.value
}
public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
guard let tab = tab(for: webView) else { return }
// Set the committed url which will also set tab.url
tab.committedURL = webView.url
// Server Trust and URL is also updated in didCommit
// However, WebKit does NOT trigger the `serverTrust` observer when the URL changes, but the trust has not.
// WebKit also does NOT trigger the `serverTrust` observer when the page is actually insecure (non-https).
// So manually trigger it with the current trust.
logSecureContentState(tab: tab, details: "ObserveValue trigger in didCommit")
observeValue(forKeyPath: KVOConstants.serverTrust.keyPath,
of: webView,
change: [.newKey: webView.serverTrust as Any, .kindKey: 1],
context: nil)
// Need to evaluate Night mode script injection after url is set inside the Tab
tab.nightMode = Preferences.General.nightModeEnabled.value
tab.clearSolanaConnectedAccounts()
// Providers need re-initialized when changing origin to align with desktop in
// `BraveContentBrowserClient::RegisterBrowserInterfaceBindersForFrame`
// https://github.com/brave/brave-core/blob/1.52.x/browser/brave_content_browser_client.cc#L608
if let provider = braveCore.braveWalletAPI.ethereumProvider(with: tab, isPrivateBrowsing: tab.isPrivate) {
// The Ethereum provider will fetch allowed accounts from it's delegate (the tab)
// on initialization. Fetching allowed accounts requires the origin; so we need to
// initialize after `commitedURL` / `url` are updated above
tab.walletEthProvider = provider
tab.walletEthProvider?.`init`(tab)
}
if let provider = braveCore.braveWalletAPI.solanaProvider(with: tab, isPrivateBrowsing: tab.isPrivate) {
tab.walletSolProvider = provider
tab.walletSolProvider?.`init`(tab)
}
rewards.reportTabNavigation(tabId: tab.rewardsId)
if tabManager.selectedTab === tab {
updateUIForReaderHomeStateForTab(tab)
}
updateForwardStatusIfNeeded(webView: webView)
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if let tab = tabManager[webView] {
// Deciding whether to inject app's IAP receipt for Brave SKUs or not
if let url = tab.url,
let braveSkusHelper = BraveSkusWebHelper(for: url),
let receiptData = braveSkusHelper.receiptData,
!tab.isPrivate {
tab.injectLocalStorageItem(key: receiptData.key, value: receiptData.value)
}
// Second attempt to inject results to the BraveSearch.
// This will be called if we got fallback results faster than
// the page navigation.
if let braveSearchManager = tab.braveSearchManager {
// Fallback results are ready before navigation finished,
// they must be injected here.
if !braveSearchManager.fallbackQueryResultsPending {
tab.injectResults()
}
} else {
// If not applicable, null results must be injected regardless.
// The website waits on us until this is called with either results or null.
tab.injectResults()
}
navigateInTab(tab: tab, to: navigation)
if let url = tab.url, tab.shouldClassifyLoadsForAds {
rewards.reportTabUpdated(
tab: tab,
url: url,
isSelected: tabManager.selectedTab == tab,
isPrivate: privateBrowsingManager.isPrivateBrowsing
)
}
Task {
await tab.updateEthereumProperties()
await tab.updateSolanaProperties()
}
tab.reportPageLoad(to: rewards, redirectionURLs: tab.redirectURLs)
tab.redirectURLs = []
if webView.url?.isLocal == false {
// Reset should classify
tab.shouldClassifyLoadsForAds = true
// Set rewards inter site url as new page load url.
rewardsXHRLoadURL = webView.url
}
if tab.walletEthProvider != nil {
tab.emitEthereumEvent(.connect)
}
}
// Added this method to determine long press menu actions better
// Since these actions are depending on tabmanager opened WebsiteCount
updateToolbarUsingTabManager(tabManager)
recordFinishedPageLoadP3A()
}
public func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
guard let tab = tab(for: webView), let url = webView.url, rewards.isEnabled else { return }
tab.redirectURLs.append(url)
}
/// Invoked when an error occurs while starting to load data for the main frame.
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
guard let tab = tab(for: webView) else { return }
// WebKit does not update certs on cancellation of a frame load
// So manually trigger the notification with the current cert
// Also, when Chromium cert validation passes, BUT Apple cert validation fails, the request is cancelled automatically by WebKit
// In such a case, the webView.serverTrust is `nil`. The only time we have a valid trust is when we received the challenge
// so we need to update the URL-Bar to show that serverTrust when WebKit's is nil.
logSecureContentState(tab: tab, details: "ObserveValue trigger in didFailProvisionalNavigation")
observeValue(forKeyPath: KVOConstants.serverTrust.keyPath,
of: webView,
change: [.newKey: webView.serverTrust ?? tab.sslPinningTrust as Any, .kindKey: 1],
context: nil)
// Ignore the "Frame load interrupted" error that is triggered when we cancel a request
// to open an external application and hand it over to UIApplication.openURL(). The result
// will be that we switch to the external app, for example the app store, while keeping the
// original web page in the tab instead of replacing it with an error page.
var error = error as NSError
if error.domain == "WebKitErrorDomain" && error.code == 102 {
if tab === tabManager.selectedTab {
updateToolbarCurrentURL(tab.url?.displayURL)
updateWebViewPageZoom(tab: tab)
}
return
}
if checkIfWebContentProcessHasCrashed(webView, error: error) {
return
}
if let sslPinningError = tab.sslPinningError {
error = sslPinningError as NSError
}
if error.code == Int(CFNetworkErrors.cfurlErrorCancelled.rawValue) {
if tab === tabManager.selectedTab {
updateToolbarCurrentURL(tab.url?.displayURL)
updateWebViewPageZoom(tab: tab)
}
return
}
if let url = error.userInfo[NSURLErrorFailingURLErrorKey] as? URL {
// The certificate came from the WebKit SSL Handshake validation and the cert is untrusted
if webView.serverTrust == nil, let serverTrust = tab.sslPinningTrust, error.userInfo["NSErrorPeerCertificateChainKey"] == nil {
// Build a cert chain error to display in the cert viewer in such cases, as we aren't given one by WebKit
var userInfo = error.userInfo
userInfo["NSErrorPeerCertificateChainKey"] = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate] ?? []
userInfo["NSErrorPeerUntrustedByApple"] = true
error = NSError(domain: error.domain, code: error.code, userInfo: userInfo)
}
ErrorPageHelper(certStore: profile.certStore).loadPage(error, forUrl: url, inWebView: webView)
// Submitting same errornous URL using toolbar will cause progress bar get stuck
// Reseting the progress bar in case there is an error is necessary
topToolbar.hideProgressBar()
// If the local web server isn't working for some reason (Brave cellular data is
// disabled in settings, for example), we'll fail to load the session restore URL.
// We rely on loading that page to get the restore callback to reset the restoring
// flag, so if we fail to load that page, reset it here.
if InternalURL(url)?.aboutComponent == "sessionrestore" {
tab.restoring = false
}
}
}
}
// MARK: WKNavigationDelegateHelper
extension BrowserViewController {
fileprivate func recordFinishedPageLoadP3A() {
var storage = P3ATimedStorage<Int>.pagesLoadedStorage
storage.add(value: 1, to: Date())
UmaHistogramRecordValueToBucket(
"Brave.Core.PagesLoaded",
buckets: [
0,
.r(1...10),
.r(11...50),
.r(51...100),
.r(101...500),
.r(501...1000),
.r(1001...),
],
value: storage.combinedValue
)
}
private func tab(for webView: WKWebView) -> Tab? {
tabManager[webView] ?? (webView as? TabWebView)?.tab
}
private func handleExternalURL(
_ url: URL,
tab: Tab?,
navigationAction: WKNavigationAction) async -> Bool {
// Do not open external links for child tabs automatically
// The user must tap on the link to open it.
if tab?.parent != nil && navigationAction.navigationType != .linkActivated {
return false
}
// Check if the current url of the caller has changed
if let domain = tab?.url?.baseDomain,
domain != tab?.externalAppURLDomain {
tab?.externalAppAlertCounter = 0
tab?.isExternalAppAlertSuppressed = false
}
tab?.externalAppURLDomain = tab?.url?.baseDomain
// Do not try to present over existing warning
if tab?.isExternalAppAlertPresented == true || tab?.isExternalAppAlertSuppressed == true {
return false
}
// External dialog should not be shown for non-active tabs #6687 - #7835
let isVisibleTab = tab?.isTabVisible() == true
// Check user trying to open on NTP like external link browsing
var isAboutHome = false
if let url = tab?.url {
isAboutHome = InternalURL(url)?.isAboutHomeURL == true
}
// Finally check non-active tab
let isNonActiveTab = isAboutHome ? false : tab?.url?.host != topToolbar.currentURL?.host
if !isVisibleTab || isNonActiveTab {
return false
}
var alertTitle = Strings.openExternalAppURLGenericTitle
if let displayHost = tab?.url?.withoutWWW.host {
alertTitle = String(format: Strings.openExternalAppURLTitle, displayHost)
}
// Handling condition when Tab is empty when handling an external URL we should remove the tab once the user decides
let removeTabIfEmpty = { [weak self] in
if let tab = tab, tab.url == nil {
self?.tabManager.removeTab(tab)
}
}
// Show the external sceheme invoke alert
@MainActor
func showExternalSchemeAlert(isSuppressActive: Bool, openedURLCompletionHandler: @escaping (Bool) -> Void) {
// Check if active controller is bvc otherwise do not show show external sceheme alerts
guard shouldShowExternalSchemeAlert() else {
openedURLCompletionHandler(false)
return
}
view.endEditing(true)
tab?.isExternalAppAlertPresented = true
let popup = AlertPopupView(
imageView: nil,
title: alertTitle,
message: String(format: Strings.openExternalAppURLMessage, url.relativeString),
titleWeight: .semibold,
titleSize: 21
)
if isSuppressActive {
popup.addButton(title: Strings.suppressAlertsActionTitle, type: .destructive) { [weak tab] () -> PopupViewDismissType in
openedURLCompletionHandler(false)
tab?.isExternalAppAlertSuppressed = true
return .flyDown
}
} else {
popup.addButton(title: Strings.openExternalAppURLDontAllow) { [weak tab] () -> PopupViewDismissType in
openedURLCompletionHandler(false)
removeTabIfEmpty()