[iOS] Introduce ChromiumTabState & feature flag (#28433)
This commit is contained in:
@@ -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();
|
||||
|
||||
+10
@@ -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
|
||||
@@ -8,6 +8,8 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
#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<web::WebState*>(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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+4
-5
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+31
-30
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -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()
|
||||
|
||||
+1
-6
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -279,7 +279,8 @@ class UserScriptManager {
|
||||
|
||||
public func loadScripts(
|
||||
into userContentController: WKUserContentController,
|
||||
scripts: Set<ScriptType>
|
||||
scripts: Set<ScriptType>,
|
||||
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
|
||||
|
||||
@@ -52,8 +52,6 @@ extension Preferences {
|
||||
public static let isFirstLaunch = Option<Bool>(key: "general.first-launch", default: true)
|
||||
/// Whether or not to save logins in Brave
|
||||
static let saveLogins = Option<Bool>(key: "general.save-logins", default: true)
|
||||
/// Whether or not to block popups from websites automaticaly
|
||||
static let blockPopups = Option<Bool>(key: "general.block-popups", default: true)
|
||||
/// Controls how the tab bar should be shown (or not shown)
|
||||
static let tabBarVisibility = Option<Int>(
|
||||
key: "general.tab-bar-visiblity",
|
||||
|
||||
@@ -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
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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<Bool>(
|
||||
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<Bool>(
|
||||
key: "general.always-request-desktop-site",
|
||||
default: UIDevice.current.userInterfaceIdiom == .pad
|
||||
)
|
||||
|
||||
/// Whether or not to block popups from websites automaticaly
|
||||
static let blockPopups = Option<Bool>(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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
|
||||
@@ -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<Bool?>(
|
||||
key: "chromium.last.webviewsflagstate",
|
||||
default: nil
|
||||
)
|
||||
}
|
||||
|
||||
public final class Debug {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Bool>(
|
||||
key: "general.always-request-desktop-site",
|
||||
default: UIDevice.current.userInterfaceIdiom == .pad
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -209,4 +209,8 @@ public class AnyTabState: TabState {
|
||||
public func clearBackForwardList() {
|
||||
tab.clearBackForwardList()
|
||||
}
|
||||
|
||||
public func updateScripts() {
|
||||
tab.updateScripts()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "<unknown>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 "<unknown>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum JavascriptError: Error {
|
||||
case invalid
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<WKWebView, UIColor>(
|
||||
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<TabDataValues> = .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<AnyTabObserver> = []
|
||||
var policyDeciders: OrderedSet<AnyTabPolicyDecider> = []
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" ]
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
#define BRAVE_IOS_BROWSER_API_WEB_VIEW_BRAVE_WEB_VIEW_H_
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
#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 <CWVUIDelegate>
|
||||
@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<UIMenuBuilder>)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<BraveWebViewNavigationDelegate>
|
||||
navigationDelegate;
|
||||
|
||||
// This web view's UI delegate.
|
||||
@property(nonatomic, weak, nullable) id<BraveWebViewUIDelegate> UIDelegate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#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*>([BraveWebView webViewForWebState:webState]);
|
||||
if (!webView) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
id<BraveWebViewNavigationDelegate> navigationDelegate =
|
||||
webView.navigationDelegate;
|
||||
if ([navigationDelegate respondsToSelector:@selector
|
||||
(webView:userAgentForUserAgentType:request:)]) {
|
||||
return [navigationDelegate
|
||||
webView:webView
|
||||
userAgentForUserAgentType:static_cast<CWVUserAgentType>(userAgentType)
|
||||
request:request];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
} // namespace brave
|
||||
|
||||
@@ -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", \
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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" ]
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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_
|
||||
@@ -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",
|
||||
|
||||
@@ -37,6 +37,9 @@ class BraveWebClient : public ChromeWebClient {
|
||||
void PostBrowserURLRewriterCreation(
|
||||
web::BrowserURLRewriter* rewriter) override;
|
||||
|
||||
void BuildEditMenu(web::WebState* web_state,
|
||||
id<UIMenuBuilder>) const override;
|
||||
|
||||
private:
|
||||
std::string legacy_user_agent_;
|
||||
};
|
||||
|
||||
@@ -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<UIMenuBuilder> builder) const {
|
||||
BraveWebView* webView =
|
||||
static_cast<BraveWebView*>([BraveWebView webViewForWebState:web_state]);
|
||||
if (!webView) {
|
||||
return;
|
||||
}
|
||||
id<BraveWebViewUIDelegate> uiDelegate = webView.UIDelegate;
|
||||
|
||||
if ([uiDelegate respondsToSelector:@selector(webView:
|
||||
buildEditMenuWithBuilder:)]) {
|
||||
return [uiDelegate webView:webView buildEditMenuWithBuilder:builder];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CWVUserAgentType>(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
|
||||
|
||||
@@ -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<SecPolicyRef> policy(
|
||||
SecPolicyCreateSSL(true, nullptr));
|
||||
SecTrustRef trustRef;
|
||||
if (SecTrustCreateWithCertificates(certificates.get(), policy.get(),
|
||||
&trustRef) != errSecSuccess) {
|
||||
return nil;
|
||||
}
|
||||
return trustRef;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <Security/Security.h>
|
||||
|
||||
#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_
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user