From 9c7ad15c8ce44879b4e8f45c0d2bf0c8d550b882 Mon Sep 17 00:00:00 2001 From: Kyle Hickinson Date: Wed, 7 May 2025 11:05:23 -0400 Subject: [PATCH] [iOS] Introduce ChromiumTabState & feature flag (#28433) --- .../profile/model/keyed_service_factories.mm | 2 + .../chrome_web_ui_ios_controller_factory.mm | 10 + .../navigation/crw_wk_navigation_handler.mm | 18 + .../ui/wk_web_view_configuration_provider.mm | 11 + .../App/iOS/Delegates/AppDelegate.swift | 2 +- .../App/iOS/Delegates/AppState.swift | 9 + .../BVC+TabDelegate.swift | 9 +- .../BVC+TabObserver.swift | 5 +- .../BVC+TabPolicyDecider.swift | 61 +- .../BVC+ToolbarDelegate.swift | 12 +- .../BrowserViewController.swift | 7 +- .../Browser/ChromeWebUIController.swift | 2 +- .../Browser/LinkPreviewViewController.swift | 9 +- .../Browser/Search/BraveSearchManager.swift | 4 +- .../Brave/Frontend/Browser/TabManager.swift | 27 +- .../Frontend/Browser/UserScriptManager.swift | 7 +- .../Brave/Frontend/ClientPreferences.swift | 2 - .../Settings/SettingsViewController.swift | 30 +- .../UserContent/UserScripts/__firefox__.js | 21 +- .../Sources/Brave/Migration/Migration.swift | 27 +- .../Sources/Data/models/SessionTab.swift | 14 + .../Sources/Playlist/AVKitExtensions.swift | 2 +- .../Playlist/PlaylistDownloadManager.swift | 4 +- .../Playlist/PlaylistMediaStreamer.swift | 2 +- .../Preferences/GlobalPreferences.swift | 8 + .../Shared/Extensions/URLExtensions.swift | 15 +- .../Sources/UserAgent/UserAgent.swift | 12 - .../UserAgent/UserAgentPreferences.swift | 19 - ios/brave-ios/Sources/Web/AnyTabState.swift | 4 + .../Web/Chromium/CWVWebViewExtensions.swift | 122 ++++ .../Web/Chromium/ChromiumDownload.swift | 56 ++ .../Web/Chromium/ChromiumTabState.swift | 612 ++++++++++++++++++ .../Chromium/TabCWVNavigationHandler.swift | 159 +++++ .../Web/Chromium/TabCWVUIHandler.swift | 168 +++++ ios/brave-ios/Sources/Web/TabObserver.swift | 16 +- .../Sources/Web/TabPolicyDecider.swift | 16 +- ios/brave-ios/Sources/Web/TabState.swift | 22 +- ios/brave-ios/Sources/Web/TabStateImpl.swift | 10 + .../Sources/Web/WebKit/TabWKUIHandler.swift | 8 +- .../Sources/Web/WebKit/WebKitTabState.swift | 8 +- .../Tests/ClientTests/TabManagerTests.swift | 3 +- .../MockScriptsViewController.swift | 8 +- .../Tests/UserAgentTests/UserAgentTests.swift | 30 +- .../default_host_content_settings.h | 3 + .../default_host_content_settings.mm | 12 + ios/browser/api/features/BUILD.gn | 1 + ios/browser/api/features/features.h | 1 + ios/browser/api/features/features.mm | 6 + ios/browser/api/web_view/BUILD.gn | 10 + ios/browser/api/web_view/brave_web_view.h | 30 + ios/browser/api/web_view/brave_web_view.mm | 21 +- .../crw_wk_navigation_handler_impl.mm | 23 + ios/browser/flags/about_flags.mm | 8 + ios/browser/flags/sources.gni | 1 + ios/browser/ui/web_view/BUILD.gn | 12 + ios/browser/ui/web_view/features.cc | 14 + ios/browser/ui/web_view/features.h | 17 + ios/browser/web/BUILD.gn | 1 + ios/browser/web/brave_web_client.h | 3 + ios/browser/web/brave_web_client.mm | 16 + ios/web_view/internal/cwv_web_view_extras.mm | 22 +- .../internal/cwv_x509_certificate_extras.mm | 18 +- ios/web_view/public/cwv_web_view_extras.h | 19 +- .../public/cwv_x509_certificate_extras.h | 6 +- ui/webui/resources/sources.gni | 10 +- 65 files changed, 1625 insertions(+), 222 deletions(-) create mode 100644 chromium_src/ios/chrome/browser/webui/ui_bundled/chrome_web_ui_ios_controller_factory.mm delete mode 100644 ios/brave-ios/Sources/UserAgent/UserAgentPreferences.swift create mode 100644 ios/brave-ios/Sources/Web/Chromium/CWVWebViewExtensions.swift create mode 100644 ios/brave-ios/Sources/Web/Chromium/ChromiumDownload.swift create mode 100644 ios/brave-ios/Sources/Web/Chromium/ChromiumTabState.swift create mode 100644 ios/brave-ios/Sources/Web/Chromium/TabCWVNavigationHandler.swift create mode 100644 ios/brave-ios/Sources/Web/Chromium/TabCWVUIHandler.swift create mode 100644 ios/browser/ui/web_view/BUILD.gn create mode 100644 ios/browser/ui/web_view/features.cc create mode 100644 ios/browser/ui/web_view/features.h diff --git a/chromium_src/ios/chrome/browser/profile/model/keyed_service_factories.mm b/chromium_src/ios/chrome/browser/profile/model/keyed_service_factories.mm index 977d419d747..77fd7ae5c29 100644 --- a/chromium_src/ios/chrome/browser/profile/model/keyed_service_factories.mm +++ b/chromium_src/ios/chrome/browser/profile/model/keyed_service_factories.mm @@ -16,6 +16,7 @@ #include "ios/chrome/browser/bookmarks/model/bookmark_undo_service_factory.h" #include "ios/chrome/browser/bookmarks/model/local_or_syncable_bookmark_sync_service_factory.h" #include "ios/chrome/browser/browsing_data/model/browsing_data_remover_factory.h" +#include "ios/chrome/browser/commerce/model/shopping_service_factory.h" #include "ios/chrome/browser/consent_auditor/model/consent_auditor_factory.h" #include "ios/chrome/browser/content_settings/model/host_content_settings_map_factory.h" #include "ios/chrome/browser/credential_provider/model/credential_provider_buildflags.h" @@ -86,6 +87,7 @@ void EnsureProfileKeyedServiceFactoriesBuilt() { autofill::AutofillLogRouterFactory::GetInstance(); autofill::PersonalDataManagerFactory::GetInstance(); + commerce::ShoppingServiceFactory::GetInstance(); data_sharing::DataSharingServiceFactory::GetInstance(); ios::AccountBookmarkSyncServiceFactory::GetInstance(); ios::AccountConsistencyServiceFactory::GetInstance(); diff --git a/chromium_src/ios/chrome/browser/webui/ui_bundled/chrome_web_ui_ios_controller_factory.mm b/chromium_src/ios/chrome/browser/webui/ui_bundled/chrome_web_ui_ios_controller_factory.mm new file mode 100644 index 00000000000..852923053e1 --- /dev/null +++ b/chromium_src/ios/chrome/browser/webui/ui_bundled/chrome_web_ui_ios_controller_factory.mm @@ -0,0 +1,10 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// 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 https://mozilla.org/MPL/2.0/. + +#import "ios/chrome/browser/webui/ui_bundled/translate_internals/translate_internals_ui.h" + +#define TranslateInternalsUI InternalDebugPagesDisabledUI +#include "src/ios/chrome/browser/webui/ui_bundled/chrome_web_ui_ios_controller_factory.mm" +#undef TranslateInternalsUI diff --git a/chromium_src/ios/web/navigation/crw_wk_navigation_handler.mm b/chromium_src/ios/web/navigation/crw_wk_navigation_handler.mm index 179941280c9..f4ce7b9d6d7 100644 --- a/chromium_src/ios/web/navigation/crw_wk_navigation_handler.mm +++ b/chromium_src/ios/web/navigation/crw_wk_navigation_handler.mm @@ -8,6 +8,8 @@ #import #import +#include "ios/web/common/user_agent.h" + namespace web { class WebState; } @@ -15,6 +17,9 @@ class WebState; namespace brave { bool ShouldBlockUniversalLinks(web::WebState* webState, NSURLRequest* request); bool ShouldBlockJavaScript(web::WebState* webState, NSURLRequest* request); +NSString* GetUserAgentForRequest(web::WebState* webState, + web::UserAgentType userAgentType, + NSURLRequest* request); } // namespace brave #include "src/ios/web/navigation/crw_wk_navigation_handler.mm" @@ -40,6 +45,19 @@ bool ShouldBlockJavaScript(web::WebState* webState, NSURLRequest* request); // Only ever update it to false preferences.allowsContentJavaScript = false; } + + const web::UserAgentType userAgentType = + [self userAgentForNavigationAction:action webView:webView]; + if (userAgentType != web::UserAgentType::NONE) { + NSString* userAgent = brave::GetUserAgentForRequest( + static_cast(self.webStateImpl), userAgentType, + action.request); + if (userAgent && + ![webView.customUserAgent isEqualToString:userAgent]) { + webView.customUserAgent = userAgent; + } + } + if (policy == WKNavigationActionPolicyAllow) { // Check if we want to explicitly block universal links bool forceBlockUniversalLinks = brave::ShouldBlockUniversalLinks( diff --git a/chromium_src/ios/web/web_state/ui/wk_web_view_configuration_provider.mm b/chromium_src/ios/web/web_state/ui/wk_web_view_configuration_provider.mm index d20880f66d7..4d4062801d3 100644 --- a/chromium_src/ios/web/web_state/ui/wk_web_view_configuration_provider.mm +++ b/chromium_src/ios/web/web_state/ui/wk_web_view_configuration_provider.mm @@ -17,6 +17,17 @@ namespace web { void BraveWKWebViewConfigurationProvider::ResetWithWebViewConfiguration( WKWebViewConfiguration* configuration) { + if (configuration != nil) { + // We need to ensure that each tab has isolated WKUserContentController & + // WKPreferences, because as of now we specifically adjust these values per + // web view created rather than when the configuration is created. + // + // This must happen prior to WKWebView's creation. + configuration.userContentController = + [[WKUserContentController alloc] init]; + configuration.preferences = [configuration.preferences copy]; + } + WKWebViewConfigurationProvider::ResetWithWebViewConfiguration(configuration); // Adjusts the underlying WKWebViewConfiguration for settings we don't want diff --git a/ios/brave-ios/App/iOS/Delegates/AppDelegate.swift b/ios/brave-ios/App/iOS/Delegates/AppDelegate.swift index 2d6fdb226a8..62861c1ddc5 100644 --- a/ios/brave-ios/App/iOS/Delegates/AppDelegate.swift +++ b/ios/brave-ios/App/iOS/Delegates/AppDelegate.swift @@ -391,7 +391,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } fileprivate func setUserAgent() { - let userAgent = UserAgent.userAgentForIdiom() + let userAgent = UserAgent.mobile // Set the favicon fetcher, and the image loader. // This only needs to be done once per runtime. Note that we use defaults here that are diff --git a/ios/brave-ios/App/iOS/Delegates/AppState.swift b/ios/brave-ios/App/iOS/Delegates/AppState.swift index 5407fcaf69f..e5782499c51 100644 --- a/ios/brave-ios/App/iOS/Delegates/AppState.swift +++ b/ios/brave-ios/App/iOS/Delegates/AppState.swift @@ -55,6 +55,15 @@ public class AppState { DataController.shared.initializeOnce() DataController.sharedInMemory.initializeOnce() Migration.migrateLostTabsActiveWindow() + + let useChromiumWebViews = FeatureList.kUseChromiumWebViews.enabled + if let value = Preferences.Chromium.lastWebViewsFlagState.value, + value != useChromiumWebViews + { + SessionTab.purgeSessionData() + } + + Preferences.Chromium.lastWebViewsFlagState.value = useChromiumWebViews } if !AppConstants.isOfficialBuild || Preferences.Debug.developerOptionsEnabled.value { diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabDelegate.swift index 52234d0cbc8..69bf4fc7a7d 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabDelegate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabDelegate.swift @@ -595,6 +595,7 @@ extension BrowserViewController { ) -> (any TabState)? { guard !request.isInternalUnprivileged, let navigationURL = request.url, + braveCore.defaultHostContentSettings.popupsAllowed, navigationURL.shouldRequestBeOpenedAsPopup() else { print("Denying popup from request: \(request)") @@ -647,16 +648,14 @@ extension BrowserViewController { if traitCollection.horizontalSizeClass == .compact && view.bounds.width < screenWidth / 2 { return mobile } - return UserAgent.shouldUseDesktopMode() ? desktop : mobile + return traitCollection.userInterfaceIdiom == .pad + && braveCore.defaultHostContentSettings.defaultPageMode == .desktop ? desktop : mobile case .desktop: return desktop case .mobile: return mobile } } public func tab(_ tab: some TabState, defaultUserAgentTypeForURL url: URL) -> UserAgentType { - if Preferences.UserAgent.alwaysRequestDesktopSite.value { - return .desktop - } - return .mobile + return braveCore.defaultHostContentSettings.defaultPageMode == .desktop ? .desktop : .mobile } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabObserver.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabObserver.swift index fcb199fa9c2..95a35b252d4 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabObserver.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabObserver.swift @@ -317,7 +317,10 @@ extension BrowserViewController: TabObserver { return } - ErrorPageHelper(certStore: profile.certStore).loadPage(error, forUrl: url, inTab: tab) + if !FeatureList.kUseChromiumWebViews.enabled { + // Only handle error pages ourselves for legacy web views + ErrorPageHelper(certStore: profile.certStore).loadPage(error, forUrl: url, inTab: tab) + } } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabPolicyDecider.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabPolicyDecider.swift index 4e6d2799c7c..53dc8eec33d 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabPolicyDecider.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabPolicyDecider.swift @@ -479,46 +479,47 @@ extension BrowserViewController: TabPolicyDecider { return .allow } + if requestURL.scheme?.contains("brave") == true || requestURL.scheme?.contains("chrome") == true + { + return .allow + } + // 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, - requestInfo: requestInfo - ) - let isSyntheticClick = !requestInfo.isUserInitiated + // Do not allow opening external URLs from child tabs + let shouldOpen = await handleExternalURL( + requestURL, + tab: tab, + requestInfo: requestInfo + ) + let isSyntheticClick = !requestInfo.isUserInitiated - // Do not show error message for JS navigated links or redirect - // as it's not the result of a user action. - if let tabData = tab.browserData, !shouldOpen, - requestInfo.navigationType == .linkActivated && !isSyntheticClick + // Do not show error message for JS navigated links or redirect + // as it's not the result of a user action. + if let tabData = tab.browserData, !shouldOpen, + requestInfo.navigationType == .linkActivated && !isSyntheticClick + { + if self.presentedViewController == nil && self.presentingViewController == nil + && !tabData.isExternalAppAlertPresented && !tabData.isExternalAppAlertSuppressed { - if self.presentedViewController == nil && self.presentingViewController == nil - && !tabData.isExternalAppAlertPresented && !tabData.isExternalAppAlertSuppressed - { - return await withCheckedContinuation { continuation in - // This alert does not need to be a BrowserAlertController because we return a policy - // without waiting for user action - 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) - } + return await withCheckedContinuation { continuation in + // This alert does not need to be a BrowserAlertController because we return a policy + // without waiting for user action + 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) } } } - - return shouldOpen ? .allow : .cancel } - return .cancel + return shouldOpen ? .allow : .cancel } } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift index cf7b2c8f35c..48a6c798575 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+ToolbarDelegate.swift @@ -286,18 +286,18 @@ extension BrowserViewController: TopToolbarDelegate { if let url = URL(string: text), url.scheme == "brave" || url.scheme == "chrome" { topToolbar.leaveOverlayMode() - return handleChromiumWebUIURL(url) + if FeatureList.kUseChromiumWebViews.enabled { + finishEditingAndSubmit(url, isUserDefinedURLNavigation: isUserDefinedURLNavigation) + return true + } else { + return handleChromiumWebUIURL(url) + } } guard let fixupURL = URIFixup.getURL(text) else { return false } - if fixupURL.scheme == "brave" || fixupURL.scheme == "chrome" { - topToolbar.leaveOverlayMode() - return handleChromiumWebUIURL(fixupURL) - } - // check text is decentralized DNS supported domain if let decentralizedDNSHelper = self.decentralizedDNSHelperFor(url: fixupURL) { topToolbar.leaveOverlayMode() diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift index 9bff8d12d16..8fcc671740e 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift @@ -317,8 +317,7 @@ public class BrowserViewController: UIViewController { windowId: windowId, prefs: profile.prefs, rewards: rewards, - tabGeneratorAPI: braveCore.tabGeneratorAPI, - historyAPI: braveCore.historyAPI, + braveCore: braveCore, privateBrowsingManager: privateBrowsingManager ) @@ -484,7 +483,6 @@ public class BrowserViewController: UIViewController { // Observe some user preferences Preferences.Privacy.privateBrowsingOnly.observe(from: self) Preferences.General.tabBarVisibility.observe(from: self) - Preferences.UserAgent.alwaysRequestDesktopSite.observe(from: self) Preferences.General.mediaAutoBackgrounding.observe(from: self) Preferences.General.youtubeHighQuality.observe(from: self) Preferences.General.defaultPageZoomLevel.observe(from: self) @@ -2915,9 +2913,6 @@ extension BrowserViewController: PreferencesObserver { setupTabs() updateTabsBarVisibility() updateApplicationShortcuts() - case Preferences.UserAgent.alwaysRequestDesktopSite.key: - tabManager.reset() - tabManager.reloadSelectedTab() case Preferences.Shields.blockScripts.key, Preferences.Shields.blockImages.key, Preferences.Shields.useRegionAdBlock.key: diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/ChromeWebUIController.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/ChromeWebUIController.swift index 1dd10422f41..40814acefca 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/ChromeWebUIController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/ChromeWebUIController.swift @@ -8,7 +8,7 @@ import Foundation import Strings /// Houses a CWVWebView to handle loading WebUI pages in a limited scope -class ChromeWebUIController: UIViewController, CWVUIDelegate { +class ChromeWebUIController: UIViewController, BraveWebViewUIDelegate { private let configuration: CWVWebViewConfiguration init(braveCore: BraveCoreMain, isPrivateBrowsing: Bool) { diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift index d64dfe49161..37d41957119 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/LinkPreviewViewController.swift @@ -26,15 +26,15 @@ class LinkPreviewViewController: UIViewController { required init?(coder aDecoder: NSCoder) { fatalError() } override func viewDidLoad() { - guard let parentTab = parentTab, - let browserController - else { + guard let browserController else { return } + let isPrivate = parentTab?.isPrivate ?? false let tab = TabStateFactory.create( with: .init( - initialConfiguration: parentTab.configuration, + initialConfiguration: isPrivate + ? TabManager.privateConfiguration : TabManager.defaultConfiguration, braveCore: browserController.braveCore ) ) @@ -44,6 +44,7 @@ class LinkPreviewViewController: UIViewController { tab.delegate = browserController tab.downloadDelegate = browserController tab.webViewProxy?.scrollView?.layer.masksToBounds = true + tab.isVisible = true self.currentTab = tab guard let currentTab = currentTab else { diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Search/BraveSearchManager.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Search/BraveSearchManager.swift index 6a8f5f4cebc..4c297bb72ac 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Search/BraveSearchManager.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Search/BraveSearchManager.swift @@ -119,7 +119,7 @@ class BraveSearchManager: NSObject { request.setValue($0.value, forHTTPHeaderField: $0.key) } - request.setValue(UserAgent.userAgentForIdiom(), forHTTPHeaderField: "User-Agent") + request.setValue(UserAgent.mobile, forHTTPHeaderField: "User-Agent") let session = URLSession(configuration: .ephemeral, delegate: authManager, delegateQueue: .main) @@ -200,7 +200,7 @@ class BraveSearchManager: NSObject { ) // Must be set, without it the fallback results may be not retrieved correctly. - request.addValue(UserAgent.userAgentForIdiom(), forHTTPHeaderField: "User-Agent") + request.addValue(UserAgent.mobile, forHTTPHeaderField: "User-Agent") request.addValue( "text/html;charset=UTF-8, text/plain;charset=UTF-8", diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManager.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManager.swift index 63a66b54487..46af6da8603 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManager.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/TabManager.swift @@ -90,6 +90,7 @@ class TabManager: NSObject { var privateTabSelectedIndex: Int = 0 var tempTabs: [any TabState]? private weak var rewards: BraveRewards? + private var braveCore: BraveCoreMain? private weak var tabGeneratorAPI: BraveTabGeneratorAPI? private var domainFrc = Domain.frc() private let syncedTabsQueue = DispatchQueue(label: "synced-tabs-queue") @@ -117,8 +118,7 @@ class TabManager: NSObject { windowId: UUID, prefs: Prefs, rewards: BraveRewards?, - tabGeneratorAPI: BraveTabGeneratorAPI?, - historyAPI: BraveHistoryAPI?, + braveCore: BraveCoreMain?, privateBrowsingManager: PrivateBrowsingManager ) { assert(Thread.isMainThread) @@ -126,13 +126,13 @@ class TabManager: NSObject { self.windowId = windowId self.prefs = prefs self.rewards = rewards - self.tabGeneratorAPI = tabGeneratorAPI - self.historyAPI = historyAPI + self.braveCore = braveCore + self.tabGeneratorAPI = braveCore?.tabGeneratorAPI + self.historyAPI = braveCore?.historyAPI self.privateBrowsingManager = privateBrowsingManager super.init() Preferences.Shields.blockImages.observe(from: self) - Preferences.General.blockPopups.observe(from: self) Preferences.General.nightModeEnabled.observe(from: self) domainFrc.delegate = self @@ -266,14 +266,13 @@ class TabManager: NSObject { } } - private static var defaultConfiguration = getNewConfiguration(isPrivate: false) - private static var privateConfiguration = getNewConfiguration(isPrivate: true) + private(set) static var defaultConfiguration = getNewConfiguration(isPrivate: false) + private(set) static var privateConfiguration = getNewConfiguration(isPrivate: true) private class func getNewConfiguration(isPrivate: Bool = false) -> WKWebViewConfiguration { let configuration: WKWebViewConfiguration = .init() configuration.processPool = WKProcessPool() - configuration.preferences.javaScriptCanOpenWindowsAutomatically = !Preferences.General - .blockPopups.value + configuration.preferences.javaScriptCanOpenWindowsAutomatically = true configuration.websiteDataStore = isPrivate ? sharedNonPersistentStore() : .default() // Dev note: Do NOT add `.link` to the list, it breaks interstitial pages @@ -455,7 +454,7 @@ class TabManager: NSObject { let popup = TabStateFactory.create( with: .init( initialConfiguration: parentTab.configuration, - braveCore: nil + braveCore: braveCore ) ) configureTab( @@ -549,7 +548,7 @@ class TabManager: NSObject { id: tabId, initialConfiguration: initialConfiguration, lastActiveTime: lastActiveTime, - braveCore: nil + braveCore: braveCore ) ) configureTab( @@ -1531,12 +1530,6 @@ extension TabManagerDelegate { extension TabManager: PreferencesObserver { func preferencesDidChange(for key: String) { switch key { - case Preferences.General.blockPopups.key: - let allowPopups = !Preferences.General.blockPopups.value - // Each tab may have its own configuration, so we should tell each of them in turn. - allTabs.forEach { - $0.configuration.preferences.javaScriptCanOpenWindowsAutomatically = allowPopups - } case Preferences.General.nightModeEnabled.key: DarkReaderScriptHandler.set( tabManager: self, diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/UserScriptManager.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/UserScriptManager.swift index 52ce6fe78d5..4a606565a57 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/UserScriptManager.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/UserScriptManager.swift @@ -279,7 +279,8 @@ class UserScriptManager { public func loadScripts( into userContentController: WKUserContentController, - scripts: Set + scripts: Set, + tab: any TabState ) { if Preferences.UserScript.blockAllScripts.value { return @@ -289,6 +290,7 @@ class UserScriptManager { userContentController.do { scriptController in scriptController.removeAllUserScripts() + tab.updateScripts() // Inject all base scripts self.baseScripts.forEach { @@ -351,7 +353,8 @@ class UserScriptManager { ContentBlockerManager.log.debug( "Loaded \(userScripts.count + customScripts.count) script(s): \n\(logComponents.joined(separator: "\n"))" ) - loadScripts(into: userContentController, scripts: userScripts) + + loadScripts(into: userContentController, scripts: userScripts, tab: tab) userContentController.do { scriptController in // TODO: Somehow refactor wallet and get rid of this diff --git a/ios/brave-ios/Sources/Brave/Frontend/ClientPreferences.swift b/ios/brave-ios/Sources/Brave/Frontend/ClientPreferences.swift index 47a1255fa3f..3a110636cbd 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/ClientPreferences.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/ClientPreferences.swift @@ -52,8 +52,6 @@ extension Preferences { public static let isFirstLaunch = Option(key: "general.first-launch", default: true) /// Whether or not to save logins in Brave static let saveLogins = Option(key: "general.save-logins", default: true) - /// Whether or not to block popups from websites automaticaly - static let blockPopups = Option(key: "general.block-popups", default: true) /// Controls how the tab bar should be shown (or not shown) static let tabBarVisibility = Option( key: "general.tab-bar-visiblity", diff --git a/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsViewController.swift index c51f2229b3d..e05b6ac5d62 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Settings/SettingsViewController.swift @@ -522,26 +522,40 @@ class SettingsViewController: TableViewController { ] ) + let defaultHostContentSettings = braveCore.defaultHostContentSettings if UIDevice.isIpad { + let defaultPageModeSwitch = SwitchAccessoryView( + initialValue: defaultHostContentSettings.defaultPageMode == .desktop, + valueChange: { value in + defaultHostContentSettings.defaultPageMode = value ? .desktop : .mobile + } + ) general.rows.append( - .boolRow( - title: Strings.alwaysRequestDesktopSite, - option: Preferences.UserAgent.alwaysRequestDesktopSite, - image: UIImage(braveSystemNamed: "leo.window.cursor") + Row( + text: Strings.alwaysRequestDesktopSite, + image: UIImage(braveSystemNamed: "leo.window.cursor"), + cellClass: MultilineSubtitleCell.self ) ) } + let blockPopupsSwitch = SwitchAccessoryView( + initialValue: !defaultHostContentSettings.popupsAllowed, + valueChange: { value in + defaultHostContentSettings.popupsAllowed = !value + } + ) general.rows.append(contentsOf: [ .boolRow( title: Strings.enablePullToRefresh, option: Preferences.General.enablePullToRefresh, image: UIImage(braveSystemNamed: "leo.browser.refresh") ), - .boolRow( - title: Strings.blockPopups, - option: Preferences.General.blockPopups, - image: UIImage(braveSystemNamed: "leo.shield.block") + Row( + text: Strings.blockPopups, + image: UIImage(braveSystemNamed: "leo.shield.block"), + accessory: .view(blockPopupsSwitch), + cellClass: MultilineSubtitleCell.self ), ]) diff --git a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/__firefox__.js b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/__firefox__.js index 670a8326555..a7f184714d4 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/__firefox__.js +++ b/ios/brave-ios/Sources/Brave/Frontend/UserContent/UserScripts/__firefox__.js @@ -413,22 +413,11 @@ if (!window.__firefox__) { return new Promise((resolve, reject) => { var oldWebkit = window.webkit; delete window['webkit']; - - // WebKit no longer restores the handler immediately! So we poll for when that happens and resolve the promise accordingly. - const timeout = 5000; - let startTime = Date.now(); - - // While loop blocks synchronously. SetTimeout or SetInterval can cause a race condition. - while(true) { - if (window.webkit.messageHandlers && window.webkit.messageHandlers[messageHandlerName]) { - let result = window.webkit.messageHandlers[messageHandlerName].postMessage(message); - window.webkit = oldWebkit; - result.then(resolve).catch(reject); - break; - } else if (Date.now() - startTime >= timeout) { - reject(new TypeError("undefined is not an object (evaluating 'webkit.messageHandlers')")); - break; - } + + if (window.webkit.messageHandlers && window.webkit.messageHandlers[messageHandlerName]) { + let result = window.webkit.messageHandlers[messageHandlerName].postMessage(message); + window.webkit = oldWebkit; + result.then(resolve).catch(reject); } }); }; diff --git a/ios/brave-ios/Sources/Brave/Migration/Migration.swift b/ios/brave-ios/Sources/Brave/Migration/Migration.swift index 9d730d5895a..9596c4af92c 100644 --- a/ios/brave-ios/Sources/Brave/Migration/Migration.swift +++ b/ios/brave-ios/Sources/Brave/Migration/Migration.swift @@ -42,6 +42,8 @@ public class Migration { migrateDeAmpPreferences() migrateDebouncePreferences() + migrateDefaultUserAgentPreferences() + migrateBlockPopupsPreferences() // Adding Observer to enable sync types NotificationCenter.default.addObserver( @@ -52,6 +54,18 @@ public class Migration { ) } + private func migrateDefaultUserAgentPreferences() { + Preferences.DeprecatedPreferences.alwaysRequestDesktopSite.migrate { value in + self.braveCore.defaultHostContentSettings.defaultPageMode = value ? .desktop : .mobile + } + } + + private func migrateBlockPopupsPreferences() { + Preferences.DeprecatedPreferences.blockPopups.migrate { value in + self.braveCore.defaultHostContentSettings.popupsAllowed = !value + } + } + private func migrateDeAmpPreferences() { guard let isDeAmpEnabled = Preferences.Shields.autoRedirectAMPPagesDeprecated.value else { return @@ -187,7 +201,7 @@ extension Migration { } extension Preferences { - private final class DeprecatedPreferences { + fileprivate final class DeprecatedPreferences { static let blockAdsAndTracking = Option( key: "shields.block-ads-and-tracking", default: true @@ -210,6 +224,16 @@ extension Preferences { key: "general.show-bookmark-toolbar-shortcut", default: UIDevice.isIpad ) + + /// Sets Desktop UA for iPad by default (iOS 13+ & iPad only). + /// Do not read it directly, prefer to use `UserAgent.shouldUseDesktopMode` instead. + static let alwaysRequestDesktopSite = Option( + key: "general.always-request-desktop-site", + default: UIDevice.current.userInterfaceIdiom == .pad + ) + + /// Whether or not to block popups from websites automaticaly + static let blockPopups = Option(key: "general.block-popups", default: true) } /// Migration preferences @@ -305,7 +329,6 @@ extension Preferences { // General migrate(key: "saveLogins", to: Preferences.General.saveLogins) - migrate(key: "blockPopups", to: Preferences.General.blockPopups) migrate(key: "kPrefKeyTabsBarShowPolicy", to: Preferences.General.tabBarVisibility) // Search diff --git a/ios/brave-ios/Sources/Data/models/SessionTab.swift b/ios/brave-ios/Sources/Data/models/SessionTab.swift index a17fb5b11ee..144479693e7 100644 --- a/ios/brave-ios/Sources/Data/models/SessionTab.swift +++ b/ios/brave-ios/Sources/Data/models/SessionTab.swift @@ -229,6 +229,20 @@ extension SessionTab { } } + public static func purgeSessionData() { + DataController.performOnMainContext { context in + for tab in Self.all() { + tab.interactionState = Data() + } + + do { + try context.save() + } catch { + Logger.module.error("Error: SessionTabs not saved!") + } + } + } + public static func createIfNeeded( windowId: UUID, tabId: UUID, diff --git a/ios/brave-ios/Sources/Playlist/AVKitExtensions.swift b/ios/brave-ios/Sources/Playlist/AVKitExtensions.swift index 69d0282e0b0..9611700568e 100644 --- a/ios/brave-ios/Sources/Playlist/AVKitExtensions.swift +++ b/ios/brave-ios/Sources/Playlist/AVKitExtensions.swift @@ -141,7 +141,7 @@ extension AVAsset { } public static var defaultOptions: [String: Any] { - let userAgent = UserAgent.userAgentForIdiom() + let userAgent = UserAgent.mobile var options: [String: Any] = [:] options[AVURLAssetHTTPUserAgentKey] = userAgent return options diff --git a/ios/brave-ios/Sources/Playlist/PlaylistDownloadManager.swift b/ios/brave-ios/Sources/Playlist/PlaylistDownloadManager.swift index 5e33f4738b9..e87db79a71c 100644 --- a/ios/brave-ios/Sources/Playlist/PlaylistDownloadManager.swift +++ b/ios/brave-ios/Sources/Playlist/PlaylistDownloadManager.swift @@ -715,7 +715,7 @@ private class PlaylistFileDownloadManager: NSObject, URLSessionDownloadDelegate // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range request.addValue("bytes=0-", forHTTPHeaderField: "Range") request.addValue(UUID().uuidString, forHTTPHeaderField: "X-Playback-Session-Id") - request.addValue(UserAgent.userAgentForIdiom(), forHTTPHeaderField: "User-Agent") + request.addValue(UserAgent.mobile, forHTTPHeaderField: "User-Agent") return request }() @@ -1033,7 +1033,7 @@ private class PlaylistDataDownloadManager: NSObject, URLSessionDataDelegate { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range request.addValue("bytes=0-", forHTTPHeaderField: "Range") request.addValue(UUID().uuidString, forHTTPHeaderField: "X-Playback-Session-Id") - request.addValue(UserAgent.userAgentForIdiom(), forHTTPHeaderField: "User-Agent") + request.addValue(UserAgent.mobile, forHTTPHeaderField: "User-Agent") return request }() diff --git a/ios/brave-ios/Sources/Playlist/PlaylistMediaStreamer.swift b/ios/brave-ios/Sources/Playlist/PlaylistMediaStreamer.swift index eefc5014000..7e4419d399e 100644 --- a/ios/brave-ios/Sources/Playlist/PlaylistMediaStreamer.swift +++ b/ios/brave-ios/Sources/Playlist/PlaylistMediaStreamer.swift @@ -167,7 +167,7 @@ public class PlaylistMediaStreamer { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range request.addValue("bytes=0-1", forHTTPHeaderField: "Range") request.addValue(UUID().uuidString, forHTTPHeaderField: "X-Playback-Session-Id") - request.addValue(UserAgent.userAgentForIdiom(), forHTTPHeaderField: "User-Agent") + request.addValue(UserAgent.mobile, forHTTPHeaderField: "User-Agent") return request }() diff --git a/ios/brave-ios/Sources/Preferences/GlobalPreferences.swift b/ios/brave-ios/Sources/Preferences/GlobalPreferences.swift index a34a35f8de9..e830e723e9a 100644 --- a/ios/brave-ios/Sources/Preferences/GlobalPreferences.swift +++ b/ios/brave-ios/Sources/Preferences/GlobalPreferences.swift @@ -251,6 +251,14 @@ extension Preferences { key: "chromium.last.bookmark.folder.node.id", default: nil ) + /// The last feature flag value for kUseChromiumWebViews + /// + /// Allows us to invalidate the session restore data for web views when changed by griffin or + /// the user via flags + public static let lastWebViewsFlagState = Option( + key: "chromium.last.webviewsflagstate", + default: nil + ) } public final class Debug { diff --git a/ios/brave-ios/Sources/Shared/Extensions/URLExtensions.swift b/ios/brave-ios/Sources/Shared/Extensions/URLExtensions.swift index 35c15493a26..896a54b8aba 100644 --- a/ios/brave-ios/Sources/Shared/Extensions/URLExtensions.swift +++ b/ios/brave-ios/Sources/Shared/Extensions/URLExtensions.swift @@ -483,19 +483,8 @@ public struct InternalURL { } public static func authorize(url: URL) -> URL? { - guard var components = URLComponents(string: url.absoluteString) else { return nil } - if components.queryItems == nil { - components.queryItems = [] - } - - if var item = components.queryItems?.first(where: { Param.uuidkey.matches($0.name) }) { - item.value = InternalURL.uuid - } else { - components.queryItems?.append( - URLQueryItem(name: Param.uuidkey.rawValue, value: InternalURL.uuid) - ) - } - return components.url + return (url as NSURL) + .replacingQueryParameter(key: Param.uuidkey.rawValue, value: InternalURL.uuid) } public var isErrorPage: Bool { diff --git a/ios/brave-ios/Sources/UserAgent/UserAgent.swift b/ios/brave-ios/Sources/UserAgent/UserAgent.swift index 4b3185d5bb2..3c1591ec90e 100644 --- a/ios/brave-ios/Sources/UserAgent/UserAgent.swift +++ b/ios/brave-ios/Sources/UserAgent/UserAgent.swift @@ -15,16 +15,4 @@ public struct UserAgent { public static let desktop = UserAgentBuilder().build(desktopMode: true) /// Desktop user agent for masking we are Brave public static let desktopMasked = UserAgentBuilder().build(desktopMode: true, maskBrave: true) - - public static func userAgentForIdiom( - _ idiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom - ) -> String { - return shouldUseDesktopMode(idiom: idiom) ? UserAgent.desktop : UserAgent.mobile - } - - public static func shouldUseDesktopMode( - idiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom - ) -> Bool { - return idiom == .pad ? Preferences.UserAgent.alwaysRequestDesktopSite.value : false - } } diff --git a/ios/brave-ios/Sources/UserAgent/UserAgentPreferences.swift b/ios/brave-ios/Sources/UserAgent/UserAgentPreferences.swift deleted file mode 100644 index e38194afa76..00000000000 --- a/ios/brave-ios/Sources/UserAgent/UserAgentPreferences.swift +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023 The Brave Authors. All rights reserved. -// 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 https://mozilla.org/MPL/2.0/. - -import Foundation -import Preferences -import UIKit - -extension Preferences { - public enum UserAgent { - /// Sets Desktop UA for iPad by default (iOS 13+ & iPad only). - /// Do not read it directly, prefer to use `UserAgent.shouldUseDesktopMode` instead. - public static let alwaysRequestDesktopSite = Option( - key: "general.always-request-desktop-site", - default: UIDevice.current.userInterfaceIdiom == .pad - ) - } -} diff --git a/ios/brave-ios/Sources/Web/AnyTabState.swift b/ios/brave-ios/Sources/Web/AnyTabState.swift index 2861a9c9e29..033a3d57435 100644 --- a/ios/brave-ios/Sources/Web/AnyTabState.swift +++ b/ios/brave-ios/Sources/Web/AnyTabState.swift @@ -209,4 +209,8 @@ public class AnyTabState: TabState { public func clearBackForwardList() { tab.clearBackForwardList() } + + public func updateScripts() { + tab.updateScripts() + } } diff --git a/ios/brave-ios/Sources/Web/Chromium/CWVWebViewExtensions.swift b/ios/brave-ios/Sources/Web/Chromium/CWVWebViewExtensions.swift new file mode 100644 index 00000000000..c4a64db33ef --- /dev/null +++ b/ios/brave-ios/Sources/Web/Chromium/CWVWebViewExtensions.swift @@ -0,0 +1,122 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// 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 https://mozilla.org/MPL/2.0/. + +import BraveCore +import Foundation + +// Adds Sequence conformance to the `CWVBackForwardListItemArray` which under the hood is not +// actually an `NSArray` and only conforms to the `NSFastEnumeration` protocol +extension CWVBackForwardListItemArray: @retroactive Sequence { + public typealias Element = CWVBackForwardListItem + + // A custom Iterator is used because `NSFastEnumerationIterator.Element` is `Any`, and we can't + // specialize it. + public struct Iterator: IteratorProtocol { + public typealias Element = CWVBackForwardListItem + + private var enumerator: NSFastEnumerationIterator + + fileprivate init(_ enumerable: CWVBackForwardListItemArray) { + self.enumerator = NSFastEnumerationIterator(enumerable) + } + + public mutating func next() -> Element? { + return enumerator.next() as? Element + } + } + + public func makeIterator() -> Iterator { + Iterator(self) + } +} + +// Helper types to properly discern between core types and qualifiers within a PageTransition +// mimics ui/base/page_transition_types.cc +extension CWVNavigationType { + // CWVNavigationType is marked with NS_OPTIONS_SET in the Obj-C side which turns this into a + // struct with static lets for each case on the Swift side, but since `CWVNavigationTypeLink` is + // equal to `0`, no symbol is generated for it because its implied that 0 should be an empty set. + // This adds it back since link is a core type which should be treated like an enum case + public static let link: CWVNavigationType = .init(rawValue: 0) + + /// The core navigation type, which only one value will exist from the list of CWVNavigationType + /// cases + public var coreType: CWVNavigationType { + .init(rawValue: self.rawValue & ~CWVNavigationType.qualifierMask.rawValue) + } + + /// Qualifiers that are stored within this type. + public var qualifiers: CWVNavigationType { + .init(rawValue: self.rawValue & CWVNavigationType.qualifierMask.rawValue) + } + + public static func == (lhs: CWVNavigationType, rhs: CWVNavigationType) -> Bool { + return lhs.coreType.rawValue == rhs.coreType.rawValue + } + + public func contains(_ member: CWVNavigationType) -> Bool { + assert( + member.rawValue > CWVNavigationType.lastCore.rawValue, + "\(member) is a core type, not a qualifier, replace with an equality check" + ) + return qualifiers.rawValue & member.rawValue != 0 + } + + public var isMainFrame: Bool { + self != .autoSubframe && self != .manualSubframe + } + + public var isRedirect: Bool { + rawValue & CWVNavigationType.isRedirectMask.rawValue != 0 + } + + public var isNewNavigation: Bool { + self != .reload && !contains(.forwardBack) + } + + public var isWebTriggerable: Bool { + switch coreType { + case .link, .autoSubframe, .manualSubframe, .formSubmit: + return true + default: + return false + } + } +} + +extension CWVNavigationType: @retroactive CustomDebugStringConvertible { + public var debugDescription: String { + switch coreType { + case .link: return "link" + case .typed: return "typed" + case .autoBookmark: return "auto_bookmark" + case .autoSubframe: return "auto_subframe" + case .manualSubframe: return "manual_subframe" + case .generated: return "generated" + case .autoToplevel: return "auto_toplevel" + case .formSubmit: return "form_submit" + case .reload: return "reload" + case .keyword: return "keyword" + case .keywordGenerated: return "keyword_generated" + default: return "" + } + } +} + +extension CWVUserAgentType: @retroactive CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .none: return "none" + case .automatic: return "automatic" + case .mobile: return "mobile" + case .desktop: return "desktop" + default: return "" + } + } +} + +public enum JavascriptError: Error { + case invalid +} diff --git a/ios/brave-ios/Sources/Web/Chromium/ChromiumDownload.swift b/ios/brave-ios/Sources/Web/Chromium/ChromiumDownload.swift new file mode 100644 index 00000000000..5b52e616d4e --- /dev/null +++ b/ios/brave-ios/Sources/Web/Chromium/ChromiumDownload.swift @@ -0,0 +1,56 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// 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 https://mozilla.org/MPL/2.0/. + +import BraveCore +import Foundation + +class ChromiumDownload: Download, CWVDownloadTaskDelegate { + var downloadTask: CWVDownloadTask + var didFinish: (ChromiumDownload, Error?) -> Void + + init( + downloadTask: CWVDownloadTask, + didFinish: @escaping (ChromiumDownload, Error?) -> Void + ) { + self.downloadTask = downloadTask + self.didFinish = didFinish + + super.init( + suggestedFilename: downloadTask.suggestedFileName, + originalURL: downloadTask.originalURL, + mimeType: downloadTask.mimeType + ) + + self.bytesDownloaded = downloadTask.receivedBytes + self.totalBytesExpected = downloadTask.totalBytes + + downloadTask.delegate = self + } + + override func startDownloadToLocalFileAtPath(_ path: String) { + super.startDownloadToLocalFileAtPath(path) + downloadTask.startDownloadToLocalFile(atPath: path) + } + + override func cancel() { + super.cancel() + downloadTask.cancel() + } + + override func resume() { + guard let destinationURL else { return } + downloadTask.startDownloadToLocalFile(atPath: destinationURL.path) + } + + func downloadTask(_ downloadTask: CWVDownloadTask, didFinishWithError error: (any Error)?) { + didFinish(self, error) + } + + func downloadTaskProgressDidChange(_ downloadTask: CWVDownloadTask) { + totalBytesExpected = downloadTask.totalBytes + bytesDownloaded = downloadTask.receivedBytes + delegate?.downloadDidUpgradeProgress(self) + } +} diff --git a/ios/brave-ios/Sources/Web/Chromium/ChromiumTabState.swift b/ios/brave-ios/Sources/Web/Chromium/ChromiumTabState.swift new file mode 100644 index 00000000000..db257dce6c3 --- /dev/null +++ b/ios/brave-ios/Sources/Web/Chromium/ChromiumTabState.swift @@ -0,0 +1,612 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// 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 https://mozilla.org/MPL/2.0/. + +import BraveCore +import Combine +import FaviconModels +import Foundation +import OrderedCollections +import Shared +import WebKit +import os + +class ChromiumTabState: TabState, TabStateImpl { + init( + id: UUID, + configuration: CWVWebViewConfiguration, + wkConfiguration: WKWebViewConfiguration? + ) { + self.id = id + self.cwvConfiguration = configuration + self.wkConfiguration = wkConfiguration + if let wkConfiguration { + assert( + configuration.isPersistent == wkConfiguration.websiteDataStore.isPersistent, + "Persistance of configurations must match" + ) + } + self.navigationHandler = .init(tab: self) + self.uiHandler = .init(tab: self) + } + + var webView: BraveWebView? + var cwvConfiguration: CWVWebViewConfiguration + var wkConfiguration: WKWebViewConfiguration? + private var containerView: CWVContainerView = .init() + private var navigationHandler: TabCWVNavigationHandler? + private var uiHandler: TabCWVUIHandler? + private var webViewObservations: [AnyCancellable] = [] + private var virtualURL: URL? + + private func webViewURLDidChange(_ newURL: URL?) { + if newURL != nil { + virtualURL = nil + } + observers.forEach { + $0.tabDidUpdateURL(self) + } + } + + private func webViewLastCommittedURLChanged(_ previousValue: URL?) { + previousCommittedURL = previousValue + } + + private func webViewIsLoadingDidChange(_ isLoading: Bool) { + observers.forEach { + if isLoading { + $0.tabDidStartLoading(self) + } else { + $0.tabDidStopLoading(self) + } + } + } + + private func webViewProgressDidChange() { + observers.forEach { + $0.tabDidChangeLoadProgress(self) + } + } + + private func webviewBackForwardStateDidChange() { + observers.forEach { + $0.tabDidChangeBackForwardState(self) + } + } + + private func webViewTitleDidChange() { + if let webView = webView?.internalWebView, let url = webView.url, + webView.configuration.preferences.isFraudulentWebsiteWarningEnabled, + webView.responds(to: Selector(("_safeBrowsingWarning"))), + webView.value(forKey: "_safeBrowsingWarning") != nil + { + self.virtualURL = url // We can update the URL whenever showing an interstitial warning + observers.forEach { + $0.tabDidUpdateURL(self) + } + } + + observers.forEach { + $0.tabDidChangeTitle(self) + } + } + + private func webViewSecurityStateDidChange() { + observers.forEach { + $0.tabDidChangeVisibleSecurityState(self) + } + } + + private func webViewSampledPageTopColorDidChange() { + observers.forEach { + $0.tabDidChangeSampledPageTopColor(self) + } + } + + private func attachWebObservers() { + guard let webView else { return } + + let keyValueObservations = [ + webView.observe( + \.visibleURL, + options: [.new], + changeHandler: { [weak self] _, change in + guard let newValue = change.newValue else { return } + self?.webViewURLDidChange(newValue) + } + ), + webView.observe( + \.lastCommittedURL, + options: [.old], + changeHandler: { [weak self] _, change in + guard let oldValue = change.oldValue else { return } + self?.webViewLastCommittedURLChanged(oldValue) + } + ), + webView.observe( + \.isLoading, + options: [.new], + changeHandler: { [weak self] _, change in + guard let newValue = change.newValue else { return } + self?.webViewIsLoadingDidChange(newValue) + } + ), + webView.observe( + \.estimatedProgress, + changeHandler: { [weak self] _, _ in + self?.webViewProgressDidChange() + } + ), + webView.observe( + \.canGoBack, + changeHandler: { [weak self] _, _ in + self?.webviewBackForwardStateDidChange() + } + ), + webView.observe( + \.canGoForward, + changeHandler: { [weak self] _, _ in + self?.webviewBackForwardStateDidChange() + } + ), + webView.observe( + \.title, + changeHandler: { [weak self] _, _ in + self?.webViewTitleDidChange() + } + ), + webView.observe( + \.visibleSSLStatus, + changeHandler: { [weak self] _, _ in + self?.webViewSecurityStateDidChange() + } + ), + ] + webViewObservations.append( + contentsOf: keyValueObservations.map { observation in .init { observation.invalidate() } } + ) + if let webView = webView.internalWebView { + let sampledPageTopColorObservation = + StringKeyPathObserver( + object: webView, + keyPath: "_sampl\("edPageTopC")olor", + changeHandler: { [weak self] _ in + self?.webViewSampledPageTopColorDidChange() + } + ) + webViewObservations.append( + .init { sampledPageTopColorObservation.invalidate() } + ) + } + } + + private func detachWebObservers() { + webViewObservations.removeAll() + } + + // MARK: - Tab + + var id: UUID + var isPrivate: Bool { + !cwvConfiguration.isPersistent + } + + var data: TabDataValues { + get { _data.withLock { $0 } } + set { _data.withLock { $0 = newValue } } + } + private var _data: OSAllocatedUnfairLock = .init(uncheckedState: .init()) + + var view: UIView { + containerView + } + var opener: (any TabState)? + var isVisible: Bool = false { + didSet { + containerView.webView = isVisible ? webView : nil + for observer in observers { + if isVisible { + observer.tabWasShown(self) + } else { + observer.tabWasHidden(self) + } + } + } + } + var lastActiveTime: Date? { + webView?.lastActiveTime + } + + var webViewProxy: (any WebViewProxy)? { + webView + } + var isWebViewCreated: Bool { + webView != nil + } + func createWebView() { + if webView != nil { + return + } + CWVWebView.webInspectorEnabled = true + var createdWKWebView: WKWebView? + let webView = BraveWebView( + frame: .init(width: 1, height: 1), + configuration: cwvConfiguration, + wkConfiguration: wkConfiguration, + createdWKWebView: &createdWKWebView + ) + webView.navigationDelegate = navigationHandler + webView.uiDelegate = uiHandler + + self.webView = webView + + if isVisible { + containerView.webView = webView + } + + attachWebObservers() + + if createdWKWebView != nil { + // CWVWebView only creates the underlying WKWebView if you pass in a WKWebViewConfiguration. + // When a new web view is created via window.open we must wait until WebState creates the + // underlying web view using the configuration passed by WebKit + // + // See: `TabCWVUIHandler.webView(_:createWebViewWith:for)` and + // `TabCWVUIHandler.webViewDidCreateNewWebView` + didCreateWebView() + } + } + + func deleteWebView() { + observers.forEach { + $0.tabWillDeleteWebView(self) + } + detachWebObservers() + webView?.removeFromSuperview() + webView = nil + } + + weak var delegate: TabDelegate? + weak var downloadDelegate: TabDownloadDelegate? + + var visibleSecureContentState: SecureContentState { + guard let lastCommittedURL = lastCommittedURL else { return .unknown } + + let isAppSpecificURL = + lastCommittedURL.scheme == "brave" || lastCommittedURL.scheme == "chrome" + || InternalURL.isValid(url: lastCommittedURL) + if isAppSpecificURL { + if let internalURL = InternalURL(lastCommittedURL), internalURL.isAboutHomeURL { + // New Tab Page is a special case, should be treated as `unknown` instead of `localhost` + return .unknown + } + return .localhost + } + + guard let visibleSSLStatus = webView?.visibleSSLStatus else { return .unknown } + switch visibleSSLStatus.securityStyle { + case .authenticated: + if !visibleSSLStatus.hasOnlySecureContent { + return .mixedContent + } + return .secure + case .authenticationBroken: + return .invalidCertificate + case .unauthenticated: + return .missingSSL + case .unknown: + return .unknown + @unknown default: + return .unknown + } + } + var serverTrust: SecTrust? { + return webView?.visibleSSLStatus?.certificate?.createServerTrust() + } + var favicon: Favicon? + var url: URL? { + visibleURL + } + var visibleURL: URL? { + virtualURL ?? webView?.visibleURL + } + var lastCommittedURL: URL? { + webView?.lastCommittedURL + } + var previousCommittedURL: URL? + var contentsMimeType: String? { + webView?.contentsMIMEType + } + var title: String? { + webView?.title + } + var isLoading: Bool { + webView?.isLoading ?? false + } + var estimatedProgress: Double { + webView?.estimatedProgress ?? 0 + } + var sessionData: Data? { + guard let webView else { return nil } + let archiver = NSKeyedArchiver(requiringSecureCoding: false) + webView.encodeRestorableState(with: archiver) + return archiver.encodedData + } + + func restore(using sessionData: Data) { + guard let webView, CWVWebView.isRestoreDataValid(sessionData) else { return } + do { + isRestoring = true + let coder = try NSKeyedUnarchiver(forReadingFrom: sessionData) + coder.requiresSecureCoding = false + webView.decodeRestorableState(with: coder) + } catch { + Logger.module.error("Failed to restore web view with session data: \(error)") + } + } + + var canGoBack: Bool { + webView?.canGoBack ?? false + } + var canGoForward: Bool { + webView?.canGoForward ?? false + } + var backForwardList: (any BackForwardListProxy)? { + if isRestoring { + // When restoring there's a chance the back forward list isn't completely ready yet and + // accessing it will crash + return nil + } + return webView?.backForwardList.flatMap { ChromiumBackForwardList($0) } + } + + var redirectChain: [URL] = [] + + var currentInitialURL: URL? { + webView?.originalRequestURLForLastCommitedNavigation + } + + var isRestoring: Bool = false + + var currentUserAgentType: UserAgentType { + if let webView { + return .init(webView.currentItemUserAgentType()) + } + if let delegate, let url { + let type = delegate.tab(self, defaultUserAgentTypeForURL: url) + if type != .automatic, type != .none { + return type + } + } + return .mobile + } + + func loadRequest(_ request: URLRequest) { + webView?.load(request) + } + + func setVirtualURL(_ url: URL?) { + virtualURL = url + } + + func reload() { + webView?.reload() + } + + func reloadWithUserAgentType(_ userAgentType: UserAgentType) { + webView?.reload(withUserAgentType: .init(userAgentType)) + } + + func stopLoading() { + webView?.stopLoading() + } + + func goBack() { + webView?.goBack() + } + + func goForward() { + webView?.goForward() + } + + func goToBackForwardListItem(_ item: any BackForwardListItemProxy) { + guard let item = (item as? ChromiumBackForwardList.Item)?.item else { return } + webView?.go(to: item) + } + + var canTakeSnapshot: Bool { + webView?.canTakeSnapshot() ?? false + } + + func takeSnapshot(rect: CGRect, handler: @escaping (UIImage?) -> Void) { + guard let webView else { + handler(nil) + return + } + webView.takeSnapshot(with: rect, completionHandler: handler) + } + + @MainActor func createFullPagePDF() async throws -> Data? { + return await webView?.createFullPagePDF() + } + + func presentFindInteraction(with text: String) { + webView?.findInPageController.findString(inPage: text) + } + + func dismissFindInteraction() { + webView?.findInPageController.stopFindInPage() + } + + func evaluateJavaScriptUnsafe(_ javascript: String) { + webView?.evaluateJavaScript(javascript) + } + + func loadHTMLString(_ html: String, baseURL: URL?) { + webView?.internalWebView?.loadHTMLString(html, baseURL: baseURL) + } + + @MainActor func evaluateJavaScript( + functionName: String, + args: [Any], + frame: WKFrameInfo?, + contentWorld: WKContentWorld, + escapeArgs: Bool, + asFunction: Bool + ) async throws -> Any? { + try await webView?.internalWebView?.evaluateJavaScript( + functionName: functionName, + args: args, + frame: frame, + contentWorld: contentWorld, + escapeArgs: escapeArgs, + asFunction: asFunction + ) + } + + @MainActor func callAsyncJavaScript( + _ functionBody: String, + arguments: [String: Any], + in frame: WKFrameInfo?, + contentWorld: WKContentWorld + ) async throws -> Any? { + try await webView?.internalWebView?.callAsyncJavaScript( + functionBody, + arguments: arguments, + in: frame, + contentWorld: contentWorld + ) + } + + var configuration: WKWebViewConfiguration { + if let configuration = webView?.internalWebView?.configuration { + return configuration + } + return wkConfiguration ?? .init() + } + + var dataForDisplayedPDF: Data? { + return webView?.internalWebView?.dataForDisplayedPDF + } + + var sampledPageTopColor: UIColor? { + return webView?.internalWebView?.sampledPageTopColor + } + + var viewPrintFormatter: UIViewPrintFormatter? { + // We can technically get the print formatter from `WebState::GetView()` as that returns + // the underlying CRWContainerView which exposes the underlying `WKWebView`'s viewPrintFormatter + // but for now this is enough. + return webView?.internalWebView?.viewPrintFormatter() + } + + var viewScale: CGFloat { + get { + webView?.internalWebView?.viewScale ?? 1 + } + set { + webView?.internalWebView?.viewScale = newValue + } + } + + func clearBackForwardList() { + webView?.internalWebView?.backForwardList.clear() + } + + func updateScripts() { + webView?.updateScripts() + } + + // MARK: - TabImpl + + var observers: OrderedSet = [] + var policyDeciders: OrderedSet = [] +} + +extension CWVWebView: WebViewProxy {} + +extension CWVUserAgentType { + init(_ userAgentType: UserAgentType) { + switch userAgentType { + case .none: self = .none + case .automatic: self = .automatic + case .desktop: self = .desktop + case .mobile: self = .mobile + } + } +} + +extension UserAgentType { + init(_ userAgentType: CWVUserAgentType) { + switch userAgentType { + case .none: self = .none + case .automatic: self = .automatic + case .desktop: self = .desktop + case .mobile: self = .mobile + default: self = .none + } + } +} + +private struct ChromiumBackForwardList: BackForwardListProxy { + struct Item: BackForwardListItemProxy { + var item: CWVBackForwardListItem + + var url: URL { + item.url + } + var title: String? { + item.title + } + } + + init(_ backForwardList: CWVBackForwardList) { + self.backForwardList = backForwardList + } + var backForwardList: CWVBackForwardList + + var backList: [any BackForwardListItemProxy] { + backForwardList.backList.map(Item.init) + } + var forwardList: [any BackForwardListItemProxy] { + backForwardList.forwardList.map(Item.init) + } + var currentItem: (any BackForwardListItemProxy)? { + backForwardList.currentItem.flatMap(Item.init) + } + var backItem: (any BackForwardListItemProxy)? { + backForwardList.backItem.flatMap(Item.init) + } + var forwardItem: (any BackForwardListItemProxy)? { + backForwardList.forwardItem.flatMap(Item.init) + } +} + +/// A simple container view used to maintain the visibility and layout of the containing CWVWebView +/// +/// CWVWebView calls `WasShown`/`WasHidden` based on being in the view hierarchy +class CWVContainerView: UIView { + var webView: CWVWebView? { + willSet { + webView?.removeFromSuperview() + } + didSet { + if let webView { + addSubview(webView) + setNeedsLayout() + } + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + var webViewFrame = bounds + // CWVWebView must not have a zero size even if the parent has none + if webViewFrame.width == 0 || webViewFrame.height == 0 { + webViewFrame.size = .init(width: 1, height: 1) + } + webView?.frame = webViewFrame + } +} diff --git a/ios/brave-ios/Sources/Web/Chromium/TabCWVNavigationHandler.swift b/ios/brave-ios/Sources/Web/Chromium/TabCWVNavigationHandler.swift new file mode 100644 index 00000000000..67f0b0a54d7 --- /dev/null +++ b/ios/brave-ios/Sources/Web/Chromium/TabCWVNavigationHandler.swift @@ -0,0 +1,159 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// 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 https://mozilla.org/MPL/2.0/. + +import BraveCore + +class TabCWVNavigationHandler: NSObject, BraveWebViewNavigationDelegate { + private weak var tab: ChromiumTabState? + + init(tab: ChromiumTabState) { + self.tab = tab + } + + public func webView( + _ webView: CWVWebView, + shouldBlockJavaScriptFor request: URLRequest + ) -> Bool { + guard let tab, let delegate = tab.delegate else { return false } + return delegate.tab(tab, shouldBlockJavaScriptForRequest: request) + } + + public func webView( + _ webView: CWVWebView, + shouldBlockUniversalLinksFor request: URLRequest + ) -> Bool { + guard let tab, let delegate = tab.delegate else { return false } + return delegate.tab(tab, shouldBlockUniversalLinksForRequest: request) + } + + func webView( + _ webView: CWVWebView, + userAgentForUserAgentType userAgentType: CWVUserAgentType, + request: URLRequest + ) -> String? { + guard let tab, let delegate = tab.delegate else { return nil } + return delegate.tab(tab, userAgentForType: .init(userAgentType), request: request) + } + + public func webView( + _ webView: CWVWebView, + didRequestHTTPAuthFor protectionSpace: URLProtectionSpace, + proposedCredential: URLCredential, + completionHandler handler: @escaping (String?, String?) -> Void + ) { + Task { @MainActor in + guard let tab, let delegate = tab.delegate else { + handler(nil, nil) + return + } + let resolvedCredential = await delegate.tab( + tab, + didRequestHTTPAuthFor: protectionSpace, + proposedCredential: proposedCredential, + previousFailureCount: 0 + ) + handler(resolvedCredential?.user, resolvedCredential?.password) + } + } + + public func webViewDidStartNavigation(_ webView: CWVWebView) { + guard let tab else { return } + + // Reset redirect chain + tab.redirectChain = [] + if let url = webView.visibleURL { + tab.redirectChain.append(url) + } + + tab.didStartNavigation() + } + + public func webView( + _ webView: CWVWebView, + decidePolicyFor navigationAction: CWVNavigationAction, + decisionHandler: @escaping (CWVNavigationActionPolicy) -> Void + ) { + guard let tab else { return } + let navigationType: WebNavigationType = { + if navigationAction.navigationType.contains(.forwardBack) { + return .backForward + } + switch navigationAction.navigationType.coreType { + case .link: + return .linkActivated + case .reload: + return .reload + case .formSubmit: + return .formSubmitted + default: + return .other + } + }() + + Task { @MainActor in + let policy = await tab.shouldAllowRequest( + navigationAction.request, + requestInfo: .init( + navigationType: navigationType, + isMainFrame: navigationAction.navigationType.isMainFrame, + isNewWindow: navigationAction.navigationType == .newWindow, + isUserInitiated: navigationAction.isUserInitiated + ) + ) + decisionHandler(policy == .allow ? .allow : .cancel) + } + } + + public func webViewDidCommitNavigation(_ webView: CWVWebView) { + guard let tab else { return } + tab.isRestoring = false + tab.didCommitNavigation() + } + + public func webViewDidRedirectNavigation(_ webView: CWVWebView) { + guard let tab else { return } + if let url = webView.visibleURL { + tab.redirectChain.append(url) + } + tab.didRedirectNavigation() + } + + public func webView( + _ webView: CWVWebView, + decidePolicyFor navigationResponse: CWVNavigationResponse, + decisionHandler: @escaping (CWVNavigationResponsePolicy) -> Void + ) { + guard let tab else { return } + Task { @MainActor in + let policy = await tab.shouldAllowResponse( + navigationResponse.response, + responseInfo: .init(isForMainFrame: navigationResponse.isForMainFrame) + ) + decisionHandler(policy == .allow ? .allow : .cancel) + } + } + + public func webViewDidFinishNavigation(_ webView: CWVWebView) { + guard let tab else { return } + tab.didFinishNavigation() + } + + public func webView(_ webView: CWVWebView, didFailNavigationWithError error: any Error) { + guard let tab else { return } + tab.didFailNavigation(with: error) + } + + public func webView(_ webView: CWVWebView, didRequestDownloadWith task: CWVDownloadTask) { + guard let tab else { return } + let pendingDownload = ChromiumDownload( + downloadTask: task, + didFinish: { [weak tab] download, error in + guard let tab else { return } + tab.downloadDelegate?.tab(tab, didFinishDownload: download, error: error) + } + ) + tab.downloadDelegate?.tab(tab, didCreateDownload: pendingDownload) + } +} diff --git a/ios/brave-ios/Sources/Web/Chromium/TabCWVUIHandler.swift b/ios/brave-ios/Sources/Web/Chromium/TabCWVUIHandler.swift new file mode 100644 index 00000000000..5aee97a9bea --- /dev/null +++ b/ios/brave-ios/Sources/Web/Chromium/TabCWVUIHandler.swift @@ -0,0 +1,168 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// 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 https://mozilla.org/MPL/2.0/. + +import BraveCore + +class TabCWVUIHandler: NSObject, BraveWebViewUIDelegate { + private weak var tab: ChromiumTabState? + + init(tab: ChromiumTabState) { + self.tab = tab + } + + func webView( + _ webView: CWVWebView, + createWebViewWith configuration: CWVWebViewConfiguration, + for action: CWVNavigationAction + ) -> CWVWebView? { + guard let tab, + let childTab = tab.delegate?.tab( + tab, + createNewTabWithRequest: action.request + ) as? ChromiumTabState + else { return nil } + childTab.cwvConfiguration = configuration + // Child CWVWebView's must be created without a WKWebViewConfiguration to ensure it uses the + // correct configuration passed by WebKit's delegate method + childTab.wkConfiguration = nil + childTab.createWebView() + return childTab.webView + } + + func webViewDidCreateNewWebView(_ webView: CWVWebView) { + guard let tab else { return } + tab.didCreateWebView() + } + + func webViewDidClose(_ webView: CWVWebView) { + guard let tab else { return } + tab.delegate?.tabWebViewDidClose(tab) + } + + func webView( + _ webView: CWVWebView, + contextMenuConfigurationFor element: CWVHTMLElement, + completionHandler: @escaping (UIContextMenuConfiguration?) -> Void + ) { + guard let tab else { + completionHandler(nil) + return + } + Task { @MainActor in + let configuration = await tab.delegate?.tab( + tab, + contextMenuConfigurationForLinkURL: element.hyperlink + ) + completionHandler(configuration) + } + } + + func webView( + _ webView: CWVWebView, + requestMediaCapturePermissionFor type: CWVMediaCaptureType, + decisionHandler: @escaping (CWVPermissionDecision) -> Void + ) { + guard let tab, let captureType = WebMediaCaptureType(type), let delegate = tab.delegate + else { + decisionHandler(.prompt) + return + } + Task { @MainActor in + let permission = await delegate.tab(tab, requestMediaCapturePermissionsFor: captureType) + decisionHandler(.init(permission)) + } + } + + func webView( + _ webView: CWVWebView, + runJavaScriptAlertPanelWithMessage message: String, + pageURL url: URL, + completionHandler: @escaping () -> Void + ) { + guard let tab else { + completionHandler() + return + } + Task { + await tab.delegate?.tab(tab, runJavaScriptAlertPanelWithMessage: message, pageURL: url) + completionHandler() + } + } + + func webView( + _ webView: CWVWebView, + runJavaScriptConfirmPanelWithMessage message: String, + pageURL url: URL, + completionHandler: @escaping (Bool) -> Void + ) { + guard let tab, let delegate = tab.delegate else { + completionHandler(false) + return + } + Task { @MainActor in + let result = await delegate.tab( + tab, + runJavaScriptConfirmPanelWithMessage: message, + pageURL: url + ) + completionHandler(result) + } + } + + func webView( + _ webView: CWVWebView, + runJavaScriptTextInputPanelWithPrompt prompt: String, + defaultText: String, + pageURL url: URL, + completionHandler: @escaping (String?) -> Void + ) { + guard let tab, let delegate = tab.delegate else { + completionHandler(nil) + return + } + Task { + let result = await delegate.tab( + tab, + runJavaScriptConfirmPanelWithPrompt: prompt, + defaultText: defaultText, + pageURL: url + ) + completionHandler(result) + } + } + + func webView(_ webView: CWVWebView, buildEditMenuWith builder: any UIMenuBuilder) { + guard let tab, let delegate = tab.delegate else { return } + delegate.tab(tab, buildEditMenuWithBuilder: builder) + } +} + +extension WebMediaCaptureType { + init?(_ mediaCaptureType: CWVMediaCaptureType) { + switch mediaCaptureType { + case .microphone: + self = .microphone + case .camera: + self = .camera + case .cameraAndMicrophone: + self = .cameraAndMicrophone + default: + return nil + } + } +} + +extension CWVPermissionDecision { + init(_ decision: WebPermissionDecision) { + switch decision { + case .prompt: + self = .prompt + case .grant: + self = .grant + case .deny: + self = .deny + } + } +} diff --git a/ios/brave-ios/Sources/Web/TabObserver.swift b/ios/brave-ios/Sources/Web/TabObserver.swift index 6edf90d46bb..293882d6dbe 100644 --- a/ios/brave-ios/Sources/Web/TabObserver.swift +++ b/ios/brave-ios/Sources/Web/TabObserver.swift @@ -59,8 +59,11 @@ extension TabObserver { public func tabWillBeDestroyed(_ tab: some TabState) {} } -class AnyTabObserver: TabObserver, Hashable { +class AnyTabObserver: TabObserver, Hashable, CustomDebugStringConvertible { let id: ObjectIdentifier + #if DEBUG + let objectName: String + #endif private let _tabDidCreateWebView: (any TabState) -> Void private let _tabWillDeleteWebView: (any TabState) -> Void @@ -83,6 +86,14 @@ class AnyTabObserver: TabObserver, Hashable { private let _tabDidChangeSampledPageTopColor: (any TabState) -> Void private let _tabWillBeDestroyed: (any TabState) -> Void + var debugDescription: String { + #if DEBUG + return "AnyTabObserver: \(objectName)" + #else + return "AnyTabObserver: \(id)" + #endif + } + func hash(into hasher: inout Hasher) { hasher.combine(id) } @@ -93,6 +104,9 @@ class AnyTabObserver: TabObserver, Hashable { init(_ observer: some TabObserver) { id = ObjectIdentifier(observer) + #if DEBUG + objectName = String(describing: observer) + #endif _tabDidCreateWebView = { [weak observer] in observer?.tabDidCreateWebView($0) } _tabWillDeleteWebView = { [weak observer] in observer?.tabWillDeleteWebView($0) } _tabWasShown = { [weak observer] in observer?.tabWasShown($0) } diff --git a/ios/brave-ios/Sources/Web/TabPolicyDecider.swift b/ios/brave-ios/Sources/Web/TabPolicyDecider.swift index 7031bb26427..eaccd53905c 100644 --- a/ios/brave-ios/Sources/Web/TabPolicyDecider.swift +++ b/ios/brave-ios/Sources/Web/TabPolicyDecider.swift @@ -40,8 +40,11 @@ extension TabPolicyDecider { } } -class AnyTabPolicyDecider: TabPolicyDecider, Hashable { +class AnyTabPolicyDecider: TabPolicyDecider, Hashable, CustomDebugStringConvertible { var id: ObjectIdentifier + #if DEBUG + let objectName: String + #endif private let _shouldAllowRequest: (any TabState, URLRequest, WebRequestInfo) async -> WebPolicyDecision @@ -50,6 +53,9 @@ class AnyTabPolicyDecider: TabPolicyDecider, Hashable { init(_ policyDecider: some TabPolicyDecider) { id = ObjectIdentifier(policyDecider) + #if DEBUG + objectName = String(describing: policyDecider) + #endif _shouldAllowRequest = { [weak policyDecider] in await policyDecider?.tab($0, shouldAllowRequest: $1, requestInfo: $2) ?? .allow } @@ -58,6 +64,14 @@ class AnyTabPolicyDecider: TabPolicyDecider, Hashable { } } + var debugDescription: String { + #if DEBUG + return "AnyTabPolicyDecider: \(objectName)" + #else + return "AnyTabPolicyDecider: \(id)" + #endif + } + func tab( _ tab: some TabState, shouldAllowRequest request: URLRequest, diff --git a/ios/brave-ios/Sources/Web/TabState.swift b/ios/brave-ios/Sources/Web/TabState.swift index 0b3041767c7..9061724259f 100644 --- a/ios/brave-ios/Sources/Web/TabState.swift +++ b/ios/brave-ios/Sources/Web/TabState.swift @@ -48,11 +48,22 @@ public class TabStateFactory { public static func create(with params: CreateTabParams) -> any TabState { let wkConfiguration = params.initialConfiguration ?? .init() wkConfiguration.enablePageTopColorSampling() - let tabState = WebKitTabState(id: params.id, configuration: wkConfiguration) - if let lastActiveTime = params.lastActiveTime { - tabState.lastActiveTime = lastActiveTime + if let braveCore = params.braveCore, FeatureList.kUseChromiumWebViews.enabled { + let cwvConfiuration = + wkConfiguration.websiteDataStore.isPersistent + ? braveCore.defaultWebViewConfiguration + : braveCore.nonPersistentWebViewConfiguration + return ChromiumTabState( + id: params.id, + configuration: cwvConfiuration, + wkConfiguration: wkConfiguration + ) } - return tabState + let webKitTabState = WebKitTabState(id: params.id, configuration: wkConfiguration) + if let lastActiveTime = params.lastActiveTime { + webKitTabState.lastActiveTime = lastActiveTime + } + return webKitTabState } } @@ -242,6 +253,9 @@ public protocol TabState: AnyObject { var viewScale: CGFloat { get set } /// Clears the back forward list of the WKWebView func clearBackForwardList() + + // MARK: - Chromium specific + func updateScripts() } extension TabState { diff --git a/ios/brave-ios/Sources/Web/TabStateImpl.swift b/ios/brave-ios/Sources/Web/TabStateImpl.swift index 969ec075869..ee8ae7d37fc 100644 --- a/ios/brave-ios/Sources/Web/TabStateImpl.swift +++ b/ios/brave-ios/Sources/Web/TabStateImpl.swift @@ -20,6 +20,8 @@ protocol TabStateImpl: TabState { responseInfo: WebResponseInfo ) async -> WebPolicyDecision + func didCreateWebView() + func didStartNavigation() func didCommitNavigation() @@ -108,6 +110,14 @@ extension TabStateImpl { } } + func didCreateWebView() { + // Make sure to remove any message handlers on newly created web views + configuration.userContentController.removeAllScriptMessageHandlers() + observers.forEach { + $0.tabDidCreateWebView(self) + } + } + func didStartNavigation() { observers.forEach { $0.tabDidStartNavigation(self) diff --git a/ios/brave-ios/Sources/Web/WebKit/TabWKUIHandler.swift b/ios/brave-ios/Sources/Web/WebKit/TabWKUIHandler.swift index b4d4600faaa..cc069bc4982 100644 --- a/ios/brave-ios/Sources/Web/WebKit/TabWKUIHandler.swift +++ b/ios/brave-ios/Sources/Web/WebKit/TabWKUIHandler.swift @@ -55,7 +55,7 @@ class TabWKUIHandler: NSObject, WKUIDelegate { } let requestMediaPermissions: () -> Void = { - Task { + Task { @MainActor in let permission = await delegate.tab(tab, requestMediaCapturePermissionsFor: captureType) decisionHandler(.init(permission)) } @@ -80,7 +80,7 @@ class TabWKUIHandler: NSObject, WKUIDelegate { completionHandler() return } - Task { + Task { @MainActor in await delegate.tab(tab, runJavaScriptAlertPanelWithMessage: message, pageURL: url) completionHandler() } @@ -96,7 +96,7 @@ class TabWKUIHandler: NSObject, WKUIDelegate { completionHandler(false) return } - Task { + Task { @MainActor in let result = await delegate.tab( tab, runJavaScriptConfirmPanelWithMessage: message, @@ -117,7 +117,7 @@ class TabWKUIHandler: NSObject, WKUIDelegate { completionHandler(nil) return } - Task { + Task { @MainActor in let result = await delegate.tab( tab, runJavaScriptConfirmPanelWithPrompt: prompt, diff --git a/ios/brave-ios/Sources/Web/WebKit/WebKitTabState.swift b/ios/brave-ios/Sources/Web/WebKit/WebKitTabState.swift index 0a7fbdf17d2..4b353713c10 100644 --- a/ios/brave-ios/Sources/Web/WebKit/WebKitTabState.swift +++ b/ios/brave-ios/Sources/Web/WebKit/WebKitTabState.swift @@ -342,9 +342,7 @@ class WebKitTabState: TabState, TabStateImpl { attachWebObservers() - observers.forEach { - $0.tabDidCreateWebView(self) - } + didCreateWebView() } func deleteWebView() { @@ -586,6 +584,10 @@ class WebKitTabState: TabState, TabStateImpl { webView?.backForwardList.clear() } + func updateScripts() { + // Nothing to do + } + // MARK: - TabStateImpl weak var delegate: TabDelegate? diff --git a/ios/brave-ios/Tests/ClientTests/TabManagerTests.swift b/ios/brave-ios/Tests/ClientTests/TabManagerTests.swift index 4f3cc765916..3122330db60 100644 --- a/ios/brave-ios/Tests/ClientTests/TabManagerTests.swift +++ b/ios/brave-ios/Tests/ClientTests/TabManagerTests.swift @@ -137,8 +137,7 @@ open class MockTabManagerDelegate: TabManagerDelegate { windowId: testWindowId, prefs: profile.prefs, rewards: nil, - tabGeneratorAPI: nil, - historyAPI: nil, + braveCore: nil, privateBrowsingManager: privateBrowsingManager ) privateBrowsingManager.isPrivateBrowsing = false diff --git a/ios/brave-ios/Tests/ClientTests/User Scripts/MockScriptsViewController.swift b/ios/brave-ios/Tests/ClientTests/User Scripts/MockScriptsViewController.swift index 8aefb462629..d566120ed7a 100644 --- a/ios/brave-ios/Tests/ClientTests/User Scripts/MockScriptsViewController.swift +++ b/ios/brave-ios/Tests/ClientTests/User Scripts/MockScriptsViewController.swift @@ -5,6 +5,7 @@ import CryptoKit import UIKit +import Web import WebKit import XCTest @@ -31,9 +32,14 @@ class MockScriptsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + let tab = TabStateFactory.create(with: .init()) // Will load some base scripts into this webview - userScriptManager.loadScripts(into: webView.configuration.userContentController, scripts: []) + userScriptManager.loadScripts( + into: webView.configuration.userContentController, + scripts: [], + tab: tab + ) self.view.addSubview(webView) webView.snp.makeConstraints { make in diff --git a/ios/brave-ios/Tests/UserAgentTests/UserAgentTests.swift b/ios/brave-ios/Tests/UserAgentTests/UserAgentTests.swift index 3e48397e3cd..3b554e029f8 100644 --- a/ios/brave-ios/Tests/UserAgentTests/UserAgentTests.swift +++ b/ios/brave-ios/Tests/UserAgentTests/UserAgentTests.swift @@ -12,11 +12,6 @@ import XCTest class UserAgentTests: XCTestCase { - override func setUp() { - super.setUp() - Preferences.UserAgent.alwaysRequestDesktopSite.reset() - } - let desktopUARegex: (String) -> Bool = { ua in let range = ua.range( of: @@ -49,7 +44,7 @@ class UserAgentTests: XCTestCase { let expectation = self.expectation(description: "Found Firefox user agent") let webView = WKWebView(frame: .zero) - webView.customUserAgent = UserAgent.userAgentForIdiom() + webView.customUserAgent = UserAgent.mobile webView.evaluateJavaScript("navigator.userAgent") { result, error in let userAgent = result as! String @@ -96,27 +91,4 @@ class UserAgentTests: XCTestCase { waitForExpectations(timeout: 60, handler: nil) } - - func testDesktopUserAgentOnPad() { - Preferences.UserAgent.alwaysRequestDesktopSite.value = true - - XCTAssertTrue(desktopUARegex(UserAgent.desktop), "User agent computes correctly.") - - let userAgent = UserAgent.userAgentForIdiom(.pad) - - if self.mobileUARegex(userAgent) || !self.desktopUARegex(userAgent) { - XCTFail("User agent did not match expected pattern! \(userAgent)") - } - } - - func testMobileUserAgentOnPad() { - Preferences.UserAgent.alwaysRequestDesktopSite.value = false - - XCTAssertTrue(mobileUARegex(UserAgent.mobile), "User agent computes correctly.") - let userAgent = UserAgent.userAgentForIdiom(.pad) - - if !self.mobileUARegex(userAgent) || self.desktopUARegex(userAgent) { - XCTFail("User agent did not match expected pattern! \(userAgent)") - } - } } diff --git a/ios/browser/api/content_settings/default_host_content_settings.h b/ios/browser/api/content_settings/default_host_content_settings.h index 4fb47c3f03a..8a47bb84ef2 100644 --- a/ios/browser/api/content_settings/default_host_content_settings.h +++ b/ios/browser/api/content_settings/default_host_content_settings.h @@ -23,6 +23,9 @@ OBJC_EXPORT /// The default page mode in which pages should be loaded. @property(nonatomic) DefaultPageMode defaultPageMode; +/// Whether or not popups are allowed by default +@property(nonatomic) BOOL popupsAllowed; + @end NS_ASSUME_NONNULL_END diff --git a/ios/browser/api/content_settings/default_host_content_settings.mm b/ios/browser/api/content_settings/default_host_content_settings.mm index 3e9f775b625..fd5f4503f74 100644 --- a/ios/browser/api/content_settings/default_host_content_settings.mm +++ b/ios/browser/api/content_settings/default_host_content_settings.mm @@ -33,4 +33,16 @@ : CONTENT_SETTING_BLOCK); } +- (BOOL)popupsAllowed { + auto setting = _settingsMap->GetDefaultContentSetting( + ContentSettingsType::POPUPS, nullptr); + return setting == CONTENT_SETTING_ALLOW; +} + +- (void)setPopupsAllowed:(BOOL)popupsAllowed { + _settingsMap->SetDefaultContentSetting( + ContentSettingsType::POPUPS, + popupsAllowed ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK); +} + @end diff --git a/ios/browser/api/features/BUILD.gn b/ios/browser/api/features/BUILD.gn index e6e0a8bd065..2ebcb405ff1 100644 --- a/ios/browser/api/features/BUILD.gn +++ b/ios/browser/api/features/BUILD.gn @@ -30,6 +30,7 @@ source_set("features") { "//brave/ios/browser/api/translate:features", "//brave/ios/browser/playlist", "//brave/ios/browser/ui/browser_menu:features", + "//brave/ios/browser/ui/web_view:features", "//build:blink_buildflags", "//net", ] diff --git a/ios/browser/api/features/features.h b/ios/browser/api/features/features.h index be51f8822ba..3c1d6b73a80 100644 --- a/ios/browser/api/features/features.h +++ b/ios/browser/api/features/features.h @@ -81,6 +81,7 @@ OBJC_EXPORT @property(class, nonatomic, readonly) Feature* kBraveTranslateEnabled; @property(class, nonatomic, readonly) Feature* kBraveAppleTranslateEnabled; @property(class, nonatomic, readonly) Feature* kUseBraveUserAgent; +@property(class, nonatomic, readonly) Feature* kUseChromiumWebViews; @end NS_ASSUME_NONNULL_END diff --git a/ios/browser/api/features/features.mm b/ios/browser/api/features/features.mm index 28470915062..7214eb07d9b 100644 --- a/ios/browser/api/features/features.mm +++ b/ios/browser/api/features/features.mm @@ -27,6 +27,7 @@ #include "brave/ios/browser/api/translate/features.h" #include "brave/ios/browser/playlist/features.h" #include "brave/ios/browser/ui/browser_menu/features.h" +#include "brave/ios/browser/ui/web_view/features.h" #import "build/blink_buildflags.h" #include "build/build_config.h" #include "net/base/features.h" @@ -345,4 +346,9 @@ initWithFeature:&brave_user_agent::features::kUseBraveUserAgent]; } ++ (Feature*)kUseChromiumWebViews { + return + [[Feature alloc] initWithFeature:&brave::features::kUseChromiumWebViews]; +} + @end diff --git a/ios/browser/api/web_view/BUILD.gn b/ios/browser/api/web_view/BUILD.gn index ac6aacc173e..c8d8ecb6e28 100644 --- a/ios/browser/api/web_view/BUILD.gn +++ b/ios/browser/api/web_view/BUILD.gn @@ -3,6 +3,14 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at https://mozilla.org/MPL/2.0/. +config("config") { + # For our own public headers to be able to import the official public headers + include_dirs = [ + "//ios/web_view/public", + "//brave/ios/web_view/public", + ] +} + source_set("web_view") { sources = [ "brave_web_view.h", @@ -19,6 +27,8 @@ source_set("web_view") { ] frameworks = [ "Foundation.framework", + "UIKit.framework", "WebKit.framework", ] + public_configs = [ ":config" ] } diff --git a/ios/browser/api/web_view/brave_web_view.h b/ios/browser/api/web_view/brave_web_view.h index 7c4b9f9ece5..35744fff941 100644 --- a/ios/browser/api/web_view/brave_web_view.h +++ b/ios/browser/api/web_view/brave_web_view.h @@ -7,11 +7,14 @@ #define BRAVE_IOS_BROWSER_API_WEB_VIEW_BRAVE_WEB_VIEW_H_ #import +#import #import #import "cwv_export.h" // NOLINT #import "cwv_navigation_delegate.h" // NOLINT +#import "cwv_ui_delegate.h" // NOLINT #import "cwv_web_view.h" // NOLINT +#import "cwv_web_view_extras.h" // NOLINT NS_ASSUME_NONNULL_BEGIN @@ -26,6 +29,10 @@ CWV_EXPORT /// Decides whether or not JavaScript should be blocked on the resulting page - (BOOL)webView:(CWVWebView*)webView shouldBlockJavaScriptForRequest:(NSURLRequest*)request; +/// Asks the delegate for a custom user agent to set for a given request +- (nullable NSString*)webView:(CWVWebView*)webView + userAgentForUserAgentType:(CWVUserAgentType)userAgentType + request:(NSURLRequest*)request; /// Notifies the delegate that basic authentication is required to access the /// requested resource - (void)webView:(CWVWebView*)webView @@ -34,6 +41,26 @@ CWV_EXPORT completionHandler: (void (^)(NSString* _Nullable username, NSString* _Nullable password))handler; +/// Notifies the delegate that a server redirect occured. At the point when this +/// is called, the url will already be updated. +- (void)webViewDidRedirectNavigation:(CWVWebView*)webView; +@end + +CWV_EXPORT +@protocol BraveWebViewUIDelegate +@optional +/// Notifies the delegate that the underlying web view has been created +/// +/// This will be called if you create a `BraveWebView` without providing it a +/// `WKWebViewConfiguration` since `CWVWebView` will rely on `WebState` to +/// handle creating the web view if the config is not provided up front. This +/// is a typical flow for when handling window.open since the underlying +/// web view must be created with the configuration provided by Apple. +- (void)webViewDidCreateNewWebView:(CWVWebView*)webView; +/// Build the edit menu that will be displayed when long pressing static content +/// on the page. +- (void)webView:(CWVWebView*)webView + buildEditMenuWithBuilder:(id)builder; @end /// A CWVWebView with Chrome tab helpers attached and the ability to handle @@ -45,6 +72,9 @@ CWV_EXPORT @property(nonatomic, weak, nullable) id navigationDelegate; +// This web view's UI delegate. +@property(nonatomic, weak, nullable) id UIDelegate; + @end NS_ASSUME_NONNULL_END diff --git a/ios/browser/api/web_view/brave_web_view.mm b/ios/browser/api/web_view/brave_web_view.mm index 78a5d27a458..a6cc44f3dd4 100644 --- a/ios/browser/api/web_view/brave_web_view.mm +++ b/ios/browser/api/web_view/brave_web_view.mm @@ -12,12 +12,13 @@ @interface CWVWebView () - (void)attachSecurityInterstitialHelpersToWebStateIfNecessary; +- (void)updateCurrentURLs; @end @implementation BraveWebView // These are shadowed CWVWebView properties -@dynamic navigationDelegate; +@dynamic navigationDelegate, UIDelegate; - (void)attachSecurityInterstitialHelpersToWebStateIfNecessary { [super attachSecurityInterstitialHelpersToWebStateIfNecessary]; @@ -54,4 +55,22 @@ } } +- (void)webStateDidCreateWebView:(web::WebState*)webState { + SEL selector = @selector(webViewDidCreateNewWebView:); + if ([self.UIDelegate respondsToSelector:selector]) { + [self.UIDelegate webViewDidCreateNewWebView:self]; + } +} + +#pragma mark - CRWWebStateObserver + +- (void)webState:(web::WebState*)webState + didRedirectNavigation:(web::NavigationContext*)navigationContext { + [self updateCurrentURLs]; + if ([self.navigationDelegate + respondsToSelector:@selector(webViewDidRedirectNavigation:)]) { + [self.navigationDelegate webViewDidRedirectNavigation:self]; + } +} + @end diff --git a/ios/browser/api/web_view/crw_wk_navigation_handler_impl.mm b/ios/browser/api/web_view/crw_wk_navigation_handler_impl.mm index d3f7983aa48..1fe62e558dd 100644 --- a/ios/browser/api/web_view/crw_wk_navigation_handler_impl.mm +++ b/ios/browser/api/web_view/crw_wk_navigation_handler_impl.mm @@ -6,7 +6,9 @@ #import #import "brave/ios/browser/api/web_view/brave_web_view.h" +#include "brave/ios/web_view/public/cwv_web_view_extras.h" #include "ios/web/common/url_scheme_util.h" +#include "ios/web/common/user_agent.h" #include "ios/web/public/web_state.h" #import "ios/web_view/internal/cwv_web_view_internal.h" @@ -49,4 +51,25 @@ bool ShouldBlockUniversalLinks(web::WebState* webState, NSURLRequest* request) { return false; } +NSString* GetUserAgentForRequest(web::WebState* webState, + web::UserAgentType userAgentType, + NSURLRequest* request) { + BraveWebView* webView = + static_cast([BraveWebView webViewForWebState:webState]); + if (!webView) { + return nil; + } + + id navigationDelegate = + webView.navigationDelegate; + if ([navigationDelegate respondsToSelector:@selector + (webView:userAgentForUserAgentType:request:)]) { + return [navigationDelegate + webView:webView + userAgentForUserAgentType:static_cast(userAgentType) + request:request]; + } + return nil; +} + } // namespace brave diff --git a/ios/browser/flags/about_flags.mm b/ios/browser/flags/about_flags.mm index 956789d7913..9641863d0c4 100644 --- a/ios/browser/flags/about_flags.mm +++ b/ios/browser/flags/about_flags.mm @@ -19,6 +19,7 @@ #include "brave/ios/browser/api/translate/features.h" #include "brave/ios/browser/playlist/features.h" #include "brave/ios/browser/ui/browser_menu/features.h" +#include "brave/ios/browser/ui/web_view/features.h" #include "build/build_config.h" #include "components/webui/flags/feature_entry_macros.h" #include "components/webui/flags/flags_state.h" @@ -198,6 +199,13 @@ flags_ui::kOsIos, \ FEATURE_VALUE_TYPE(brave_user_agent::features::kUseBraveUserAgent), \ }, \ + { \ + "brave-use-chromium-web-embedder", \ + "Use Chromium Web Embedder", \ + "Replace WKWebView usages with Chromium web views", \ + flags_ui::kOsIos, \ + FEATURE_VALUE_TYPE(brave::features::kUseChromiumWebViews), \ + }, \ { \ "brave-ntp-branded-wallpaper-demo", \ "New Tab Page Demo Branded Wallpaper", \ diff --git a/ios/browser/flags/sources.gni b/ios/browser/flags/sources.gni index 387e189a10d..b893da44bfd 100644 --- a/ios/browser/flags/sources.gni +++ b/ios/browser/flags/sources.gni @@ -18,6 +18,7 @@ brave_flags_deps = [ "//brave/components/skus/common", "//brave/ios/browser/playlist", "//brave/ios/browser/ui/browser_menu:features", + "//brave/ios/browser/ui/web_view:features", "//components/webui/flags", "//net", ] diff --git a/ios/browser/ui/web_view/BUILD.gn b/ios/browser/ui/web_view/BUILD.gn new file mode 100644 index 00000000000..4b63b5d26a9 --- /dev/null +++ b/ios/browser/ui/web_view/BUILD.gn @@ -0,0 +1,12 @@ +# Copyright (c) 2025 The Brave Authors. All rights reserved. +# 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 https://mozilla.org/MPL/2.0/. + +source_set("features") { + sources = [ + "features.cc", + "features.h", + ] + deps = [ "//base" ] +} diff --git a/ios/browser/ui/web_view/features.cc b/ios/browser/ui/web_view/features.cc new file mode 100644 index 00000000000..8ac00459802 --- /dev/null +++ b/ios/browser/ui/web_view/features.cc @@ -0,0 +1,14 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// 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 https://mozilla.org/MPL/2.0/. + +#include "brave/ios/browser/ui/web_view/features.h" + +namespace brave::features { + +BASE_FEATURE(kUseChromiumWebViews, + "UseChromiumWebViews", + base::FEATURE_DISABLED_BY_DEFAULT); + +} diff --git a/ios/browser/ui/web_view/features.h b/ios/browser/ui/web_view/features.h new file mode 100644 index 00000000000..3fbb504e48d --- /dev/null +++ b/ios/browser/ui/web_view/features.h @@ -0,0 +1,17 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// 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 https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_IOS_BROWSER_UI_WEB_VIEW_FEATURES_H_ +#define BRAVE_IOS_BROWSER_UI_WEB_VIEW_FEATURES_H_ + +#include "base/feature_list.h" + +namespace brave::features { + +BASE_DECLARE_FEATURE(kUseChromiumWebViews); + +} + +#endif // BRAVE_IOS_BROWSER_UI_WEB_VIEW_FEATURES_H_ diff --git a/ios/browser/web/BUILD.gn b/ios/browser/web/BUILD.gn index 0049455733a..9c27e8f3842 100644 --- a/ios/browser/web/BUILD.gn +++ b/ios/browser/web/BUILD.gn @@ -17,6 +17,7 @@ source_set("web") { "//brave/components/brave_user_agent/browser", "//brave/components/brave_wallet/browser", "//brave/components/constants", + "//brave/ios/browser/api/web_view", "//brave/ios/browser/application_context", "//components/component_updater/installer_policies", "//components/translate/ios/browser", diff --git a/ios/browser/web/brave_web_client.h b/ios/browser/web/brave_web_client.h index 5f9b262e52b..4061608a1f9 100644 --- a/ios/browser/web/brave_web_client.h +++ b/ios/browser/web/brave_web_client.h @@ -37,6 +37,9 @@ class BraveWebClient : public ChromeWebClient { void PostBrowserURLRewriterCreation( web::BrowserURLRewriter* rewriter) override; + void BuildEditMenu(web::WebState* web_state, + id) const override; + private: std::string legacy_user_agent_; }; diff --git a/ios/browser/web/brave_web_client.mm b/ios/browser/web/brave_web_client.mm index 1efa0ed5c07..e94c651ff33 100644 --- a/ios/browser/web/brave_web_client.mm +++ b/ios/browser/web/brave_web_client.mm @@ -10,6 +10,7 @@ #include "base/ios/ns_error_util.h" #include "base/strings/sys_string_conversions.h" #include "brave/components/constants/url_constants.h" +#include "brave/ios/browser/api/web_view/brave_web_view.h" #include "brave/ios/browser/web/brave_web_main_parts.h" #import "components/translate/ios/browser/translate_java_script_feature.h" #include "ios/chrome/browser/shared/model/url/chrome_url_constants.h" @@ -97,3 +98,18 @@ bool BraveWebClient::EnableWebInspector( void BraveWebClient::SetLegacyUserAgent(const std::string& user_agent) { legacy_user_agent_ = user_agent; } + +void BraveWebClient::BuildEditMenu(web::WebState* web_state, + id builder) const { + BraveWebView* webView = + static_cast([BraveWebView webViewForWebState:web_state]); + if (!webView) { + return; + } + id uiDelegate = webView.UIDelegate; + + if ([uiDelegate respondsToSelector:@selector(webView: + buildEditMenuWithBuilder:)]) { + return [uiDelegate webView:webView buildEditMenuWithBuilder:builder]; + } +} diff --git a/ios/web_view/internal/cwv_web_view_extras.mm b/ios/web_view/internal/cwv_web_view_extras.mm index d1db5e0d988..a875a9589ce 100644 --- a/ios/web_view/internal/cwv_web_view_extras.mm +++ b/ios/web_view/internal/cwv_web_view_extras.mm @@ -20,6 +20,7 @@ #include "ios/web_view/internal/cwv_web_view_internal.h" #include "ios/web_view/internal/web_view_browser_state.h" #include "ios/web_view/public/cwv_navigation_delegate.h" +#include "net/base/apple/url_conversions.h" const CWVUserAgentType CWVUserAgentTypeNone = static_cast(web::UserAgentType::NONE); @@ -121,11 +122,22 @@ const CWVUserAgentType CWVUserAgentTypeDesktop = return web_controller.webView; } -- (WKWebViewConfiguration*)WKConfiguration { - web::WKWebViewConfigurationProvider& config_provider = - web::WKWebViewConfigurationProvider::FromBrowserState( - self.webState->GetBrowserState()); - return config_provider.GetWebViewConfiguration(); +- (NSURL*)originalRequestURLForLastCommitedNavigation { + // Since CWVBackForwardItemListItem doesn't provide the original URL request + web::NavigationItem* item = + self.webState->GetNavigationManager()->GetLastCommittedItem(); + if (!item) { + return nil; + } + return net::NSURLWithGURL(item->GetOriginalRequestURL()); +} + +- (NSString*)contentsMIMEType { + return base::SysUTF8ToNSString(self.webState->GetContentsMimeType()); +} + +- (NSDate*)lastActiveTime { + return self.webState->GetLastActiveTime().ToNSDate(); } @end diff --git a/ios/web_view/internal/cwv_x509_certificate_extras.mm b/ios/web_view/internal/cwv_x509_certificate_extras.mm index 3b7d3985da3..15c916c30c5 100644 --- a/ios/web_view/internal/cwv_x509_certificate_extras.mm +++ b/ios/web_view/internal/cwv_x509_certificate_extras.mm @@ -3,15 +3,25 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. +#include "brave/ios/web_view/public/cwv_x509_certificate_extras.h" + #include "ios/web_view/internal/cwv_x509_certificate_internal.h" #include "net/cert/x509_util_apple.h" @implementation CWVX509Certificate (Extras) -- (SecCertificateRef)certificateRef { - return net::x509_util::CreateSecCertificateFromX509Certificate( - self.internalCertificate.get()) - .release(); // Swift bridging should handle the lifetime +- (SecTrustRef)createServerTrust { + auto certificates = + net::x509_util::CreateSecCertificateArrayForX509Certificate( + self.internalCertificate.get()); + base::apple::ScopedCFTypeRef policy( + SecPolicyCreateSSL(true, nullptr)); + SecTrustRef trustRef; + if (SecTrustCreateWithCertificates(certificates.get(), policy.get(), + &trustRef) != errSecSuccess) { + return nil; + } + return trustRef; } @end diff --git a/ios/web_view/public/cwv_web_view_extras.h b/ios/web_view/public/cwv_web_view_extras.h index 63b0c4d67fe..f3ca3a6379b 100644 --- a/ios/web_view/public/cwv_web_view_extras.h +++ b/ios/web_view/public/cwv_web_view_extras.h @@ -32,6 +32,19 @@ CWV_EXPORT /// Reloads the page with a specific user agent type - (void)reloadWithUserAgentType:(CWVUserAgentType)userAgentType; +/// Return the last committed navigation's original URL request +/// +/// This is the same as WebKit's back/forward list current item `initialURL` +/// property. +@property(readonly, nullable) + NSURL* originalRequestURLForLastCommitedNavigation; + +/// The MIME type for the contents currently loaded in the web view +@property(readonly) NSString* contentsMIMEType; + +/// The last time that the web view was active +@property(readonly) NSDate* lastActiveTime; + #pragma mark - /// Creates a PDF of the current page @@ -75,12 +88,6 @@ CWV_EXPORT /// specific paths. @property(readonly, nullable) WKWebView* internalWebView; -/// The underlying WKWebViewConfiguration for this CWVWebView -/// -/// This is only available for `use_blink=false` builds and be used for WebKit -/// specific paths. -@property(readonly) WKWebViewConfiguration* WKConfiguration; - @end NS_ASSUME_NONNULL_END diff --git a/ios/web_view/public/cwv_x509_certificate_extras.h b/ios/web_view/public/cwv_x509_certificate_extras.h index 5e66653d728..bca6d3d0efa 100644 --- a/ios/web_view/public/cwv_x509_certificate_extras.h +++ b/ios/web_view/public/cwv_x509_certificate_extras.h @@ -6,14 +6,16 @@ #ifndef BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_X509_CERTIFICATE_EXTRAS_H_ #define BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_X509_CERTIFICATE_EXTRAS_H_ +#import + #include "cwv_x509_certificate.h" // NOLINT /// Adds additional functionality to CWVX509Certificate that is not be supported /// out of the box but can be implemented using the underlying /// net::X509Certificate @interface CWVX509Certificate (Extras) -/// The underlying security reference -@property(readonly, nullable) SecCertificateRef certificateRef; +/// A security trust built using the underlying certificate & intermediates +- (nullable SecTrustRef)createServerTrust CF_RETURNS_RETAINED; @end #endif // BRAVE_IOS_WEB_VIEW_PUBLIC_CWV_X509_CERTIFICATE_EXTRAS_H_ diff --git a/ui/webui/resources/sources.gni b/ui/webui/resources/sources.gni index 3c61bcdfe1a..99c13cc26fd 100644 --- a/ui/webui/resources/sources.gni +++ b/ui/webui/resources/sources.gni @@ -9,6 +9,8 @@ brave_resources_extra_grdps_path = "$root_gen_dir/brave/ui/webui/resources" brave_resources_extra_grdps = [ + "$brave_resources_extra_grdps_path/brave_fonts_resources.grdp", + "$brave_resources_extra_grdps_path/brave_icons_resources.grdp", "$brave_resources_extra_grdps_path/brave_static_resources.grdp", "$root_gen_dir/brave/web-ui-leo/leo.grdp", "$root_gen_dir/brave/web-ui-opaque_ke/opaque_ke.grdp", @@ -16,14 +18,6 @@ brave_resources_extra_grdps = [ brave_resources_extra_grdps_deps = [ "//brave/ui/webui/resources:grdp" ] -# ios does not need font or icon resources -if (!is_ios) { - brave_resources_extra_grdps += [ - "$brave_resources_extra_grdps_path/brave_fonts_resources.grdp", - "$brave_resources_extra_grdps_path/brave_icons_resources.grdp", - ] -} - # At the moment, all non-static resources are only required for polymer WebUI. # This could change and, when it does, the `include_polymer` conditional can be removed # here and the one in //brave/ui/webui/resources/BUILD.gn be relied on to only add