[iOS] Introduce ChromiumTabState & feature flag (#28433)

This commit is contained in:
Kyle Hickinson
2025-05-07 11:05:23 -04:00
committed by GitHub
parent 0aece75668
commit 9c7ad15c8c
65 changed files with 1625 additions and 222 deletions
@@ -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();
@@ -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 {
@@ -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
}
}
@@ -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)
}
}
}
@@ -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
}
}
@@ -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()
@@ -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
}
}
}
+15 -1
View File
@@ -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,
+18 -4
View File
@@ -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
+1
View File
@@ -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",
]
+1
View File
@@ -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
+6
View File
@@ -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
+10
View File
@@ -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" ]
}
+30
View File
@@ -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
+20 -1
View File
@@ -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
+8
View File
@@ -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", \
+1
View File
@@ -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",
]
+12
View File
@@ -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" ]
}
+14
View File
@@ -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);
}
+17
View File
@@ -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_
+1
View File
@@ -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",
+3
View File
@@ -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_;
};
+16
View File
@@ -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];
}
}
+17 -5
View File
@@ -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
+13 -6
View File
@@ -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_
+2 -8
View File
@@ -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