From 740f91117ca9860e24a91616c8555e8c35702ccd Mon Sep 17 00:00:00 2001 From: Brandon T Date: Fri, 9 Jun 2023 15:05:42 -0400 Subject: [PATCH] Add persistent private browsing --- .../Browser/BrowserViewController.swift | 25 +++++---- .../Brave/Frontend/Browser/TabManager.swift | 51 +++++++++++-------- .../Tabs/TabTray/TabTrayController.swift | 17 +++++-- .../Brave/Frontend/ClientPreferences.swift | 2 + .../OtherPrivacySettingsSectionView.swift | 21 ++++++++ Sources/BraveStrings/BraveStrings.swift | 1 + Sources/Data/models/SessionTab.swift | 9 +++- 7 files changed, 87 insertions(+), 39 deletions(-) diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController.swift b/Sources/Brave/Frontend/Browser/BrowserViewController.swift index 62698b9d494..7a1813f9b81 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController.swift @@ -1130,10 +1130,9 @@ public class BrowserViewController: UIViewController { } else { userActivity?.targetContentIdentifier = windowId.uuidString userActivity?.addUserInfoEntries(from: ["WindowID": windowId.uuidString, - "isPrivate": isPrivateBrowsing]) + "isPrivate": isPrivateBrowsing]) } - view.window?.windowScene?.userActivity = userActivity view.window?.windowScene?.session.userInfo = ["WindowID": windowId.uuidString, "isPrivate": isPrivateBrowsing] @@ -2235,21 +2234,21 @@ public class BrowserViewController: UIViewController { webView.evaluateSafeJavaScript(functionName: "\(ReaderModeNamespace).checkReadability", contentWorld: ReaderModeScriptHandler.scriptSandbox) // Only add history of a url which is not a localhost url - if !tab.isPrivate, !url.isReaderModeURL { + if !url.isReaderModeURL { // The visitType is checked If it is "typed" or not to determine the History object we are adding // should be synced or not. This limitation exists on browser side so we are aligning with this - if let visitType = typedNavigation.first(where: { - $0.key.typedDisplayString == url.typedDisplayString - })?.value, visitType == .typed { - braveCore.historyAPI.add(url: url, title: tab.title, dateAdded: Date()) - } else { - braveCore.historyAPI.add(url: url, title: tab.title, dateAdded: Date(), isURLTyped: false) - } - - // Saving Tab. Private Mode - not supported yet. if !tab.isPrivate { - tabManager.saveTab(tab) + if let visitType = typedNavigation.first(where: { + $0.key.typedDisplayString == url.typedDisplayString + })?.value, visitType == .typed { + braveCore.historyAPI.add(url: url, title: tab.title, dateAdded: Date()) + } else { + braveCore.historyAPI.add(url: url, title: tab.title, dateAdded: Date(), isURLTyped: false) + } } + + // Saving Tab. + tabManager.saveTab(tab) } } diff --git a/Sources/Brave/Frontend/Browser/TabManager.swift b/Sources/Brave/Frontend/Browser/TabManager.swift index 61fcf89157b..5a2eace6075 100644 --- a/Sources/Brave/Frontend/Browser/TabManager.swift +++ b/Sources/Brave/Frontend/Browser/TabManager.swift @@ -306,7 +306,7 @@ class TabManager: NSObject { privateBrowsingManager.isPrivateBrowsing = true } // Make sure to wipe the private tabs if the user has the pref turned on - if !TabType.of(tab).isPrivate { + if !TabType.of(tab).isPrivate && (Preferences.Privacy.privateBrowsingOnly.value || !Preferences.Privacy.persistentPrivateBrowsing.value) { removeAllPrivateTabs() } @@ -381,7 +381,9 @@ class TabManager: NSObject { // we only want to remove all private tabs when leaving PBM and not when entering. func willSwitchTabMode(leavingPBM: Bool) { if leavingPBM { - removeAllPrivateTabs() + if Preferences.Privacy.privateBrowsingOnly.value || !Preferences.Privacy.persistentPrivateBrowsing.value { + removeAllPrivateTabs() + } } } @@ -498,7 +500,7 @@ class TabManager: NSObject { } private func saveTabOrder() { - if privateBrowsingManager.isPrivateBrowsing { return } + if Preferences.Privacy.privateBrowsingOnly.value || (privateBrowsingManager.isPrivateBrowsing && !Preferences.Privacy.persistentPrivateBrowsing.value) { return } let allTabIds = allTabs.compactMap { $0.id } SessionTab.saveTabOrder(tabIds: allTabIds) } @@ -507,11 +509,14 @@ class TabManager: NSObject { assert(Thread.isMainThread) let isPrivate = tab.type == .private - if !isPrivate { + let isPersistentTab = !isPrivate || (isPrivate && !Preferences.Privacy.privateBrowsingOnly.value && Preferences.Privacy.persistentPrivateBrowsing.value) + + if isPersistentTab { SessionTab.createIfNeeded(windowId: windowId, tabId: tab.id, title: Strings.newTab, - tabURL: request?.url ?? TabManager.ntpInteralURL) + tabURL: request?.url ?? TabManager.ntpInteralURL, + isPrivate: isPrivate) } delegates.forEach { $0.get()?.tabManager(self, willAddTab: tab) } @@ -543,7 +548,7 @@ class TabManager: NSObject { } // Ignore on restore. - if flushToDisk && !zombie && !isPrivate { + if flushToDisk && !zombie && isPersistentTab { saveTab(tab, saveOrder: true) } @@ -558,11 +563,13 @@ class TabManager: NSObject { tab.webStateDebounceTimer?.invalidate() if state == .complete || state == .loaded || state == .pushstate || state == .popstate || state == .replacestate { - // Saving Tab Private Mode - not supported yet. - if !tab.isPrivate { - self.preserveScreenshots() - self.saveTab(tab) + + if Preferences.Privacy.privateBrowsingOnly.value || (tab.isPrivate && !Preferences.Privacy.persistentPrivateBrowsing.value) { + return } + + self.preserveScreenshots() + self.saveTab(tab) } } } @@ -583,8 +590,10 @@ class TabManager: NSObject { } func saveAllTabs() { - if privateBrowsingManager.isPrivateBrowsing { return } - SessionTab.updateAll(tabs: tabs(withType: .regular).compactMap({ + if Preferences.Privacy.privateBrowsingOnly.value || (privateBrowsingManager.isPrivateBrowsing && !Preferences.Privacy.persistentPrivateBrowsing.value) { return } + + let tabs = Preferences.Privacy.persistentPrivateBrowsing.value ? allTabs : tabs(withType: .regular) + SessionTab.updateAll(tabs: tabs.compactMap({ if let sessionData = $0.webView?.sessionData { return ($0.id, sessionData, $0.title, $0.url ?? TabManager.ntpInteralURL) } @@ -593,7 +602,7 @@ class TabManager: NSObject { } func saveTab(_ tab: Tab, saveOrder: Bool = false) { - if privateBrowsingManager.isPrivateBrowsing { return } + if Preferences.Privacy.privateBrowsingOnly.value || (tab.isPrivate && !Preferences.Privacy.persistentPrivateBrowsing.value) { return } SessionTab.update(tabId: tab.id, interactionState: tab.webView?.sessionData ?? Data(), title: tab.title, url: tab.url ?? TabManager.ntpInteralURL) if saveOrder { saveTabOrder() @@ -922,20 +931,19 @@ class TabManager: NSObject { flushToDisk: false, zombie: true, id: savedTab.tabId, - isPrivate: false) - - let isPrivateBrowsing = privateBrowsingManager.isPrivateBrowsing + isPrivate: savedTab.isPrivate) tab.lastTitle = savedTab.title tab.favicon = FaviconFetcher.getIconFromCache(for: tabURL) ?? Favicon.default tab.setScreenshot(savedTab.screenshot) Task { @MainActor in - tab.favicon = try await FaviconFetcher.loadIcon(url: tabURL, kind: .smallIcon, persistent: !isPrivateBrowsing) + tab.favicon = try await FaviconFetcher.loadIcon(url: tabURL, kind: .smallIcon, persistent: tab.isPrivate) tab.setScreenshot(savedTab.screenshot) } - if savedTab.isSelected { + // Do not select the private tab since we always restore to regular mode! + if savedTab.isSelected && !savedTab.isPrivate { tabToSelect = tab } } @@ -1151,8 +1159,11 @@ extension TabManager: WKNavigationDelegate { return } - // Saving Tab Private Mode - not supported yet. - if let tab = tabForWebView(webView), !tab.isPrivate { + if let tab = tabForWebView(webView) { + if Preferences.Privacy.privateBrowsingOnly.value || (tab.isPrivate && !Preferences.Privacy.persistentPrivateBrowsing.value) { + return + } + preserveScreenshots() saveTab(tab) } diff --git a/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift b/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift index f01327e00b0..d30e764e4a7 100644 --- a/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift +++ b/Sources/Brave/Frontend/Browser/Tabs/TabTray/TabTrayController.swift @@ -603,10 +603,19 @@ class TabTrayController: LoadingViewController { tabTypeSelector.selectedSegmentIndex = 0 tabTypeSelector.sendActions(for: UIControl.Event.valueChanged) - tabTrayView.showPrivateModeInfo() - // New private tab is created immediately to reflect changes on NTP. - // If user drags the modal down or dismisses it, a new private tab will be ready. - tabManager.addTabAndSelect(isPrivate: true) + if !Preferences.Privacy.persistentPrivateBrowsing.value { + tabTrayView.showPrivateModeInfo() + // New private tab is created immediately to reflect changes on NTP. + // If user drags the modal down or dismisses it, a new private tab will be ready. + tabManager.addTabAndSelect(isPrivate: true) + } else { + if tabManager.tabsForCurrentMode.isEmpty { + tabManager.addTabAndSelect(isPrivate: true) + } + + tabTrayView.hidePrivateModeInfo() + tabTrayView.collectionView.reloadData() + } } else { tabTrayView.hidePrivateModeInfo() diff --git a/Sources/Brave/Frontend/ClientPreferences.swift b/Sources/Brave/Frontend/ClientPreferences.swift index f645536a6da..f800201e2ce 100644 --- a/Sources/Brave/Frontend/ClientPreferences.swift +++ b/Sources/Brave/Frontend/ClientPreferences.swift @@ -123,6 +123,8 @@ extension Preferences { static let lockWithPasscode = Option(key: "privacy.lock-with-passcode", default: false) /// Forces all private tabs public static let privateBrowsingOnly = Option(key: "privacy.private-only", default: false) + /// Whether or not private browsing tabs can be session restored (persistent private browsing) + public static let persistentPrivateBrowsing = Option(key: "privacy.private-browsing-persistence", default: false) /// Blocks all cookies and access to local storage static let blockAllCookies = Option(key: "privacy.block-all-cookies", default: false) /// The toggles states for clear private data screen diff --git a/Sources/Brave/Frontend/Settings/Shields/OtherPrivacySettingsSectionView.swift b/Sources/Brave/Frontend/Settings/Shields/OtherPrivacySettingsSectionView.swift index 3dfc390891d..721e8dd139c 100644 --- a/Sources/Brave/Frontend/Settings/Shields/OtherPrivacySettingsSectionView.swift +++ b/Sources/Brave/Frontend/Settings/Shields/OtherPrivacySettingsSectionView.swift @@ -7,6 +7,7 @@ import SwiftUI import Preferences import Strings import BraveShared +import Data struct OtherPrivacySettingsSectionView: View { @State private var showPrivateBrowsingConfirmation = false @@ -34,6 +35,7 @@ struct OtherPrivacySettingsSectionView: View { Task { @MainActor in try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100) + Preferences.Privacy.persistentPrivateBrowsing.value = false await settings.clearPrivateData([CookiesAndCacheClearable()]) // First remove all tabs so that only a blank tab exists. @@ -52,6 +54,25 @@ struct OtherPrivacySettingsSectionView: View { ) }) + if !Preferences.Privacy.privateBrowsingOnly.value { + OptionToggleView(title: Strings.persistentPrivateBrowsing, + subtitle: nil, + option: Preferences.Privacy.persistentPrivateBrowsing) { newValue in + Task { @MainActor in + if newValue { + settings.tabManager.saveAllTabs() + } else { + let tabs = settings.tabManager.allTabs.filter({ $0.isPrivate }) + if settings.tabManager.privateBrowsingManager.isPrivateBrowsing { + SessionTab.deleteAll(tabIds: tabs.map({ $0.id })) + } else { + settings.tabManager.removeTabs(tabs) + } + } + } + } + } + ShieldToggleView( title: Strings.blockMobileAnnoyances, subtitle: nil, diff --git a/Sources/BraveStrings/BraveStrings.swift b/Sources/BraveStrings/BraveStrings.swift index 16ce78ad868..a35418dd985 100644 --- a/Sources/BraveStrings/BraveStrings.swift +++ b/Sources/BraveStrings/BraveStrings.swift @@ -1017,6 +1017,7 @@ extension Strings { public static let mediaAutoBackgrounding = NSLocalizedString("MediaAutoBackgrounding", tableName: "BraveShared", bundle: .module, value: "Enable Background Audio", comment: "Setting to allow media to play in the background") public static let showTabsBar = NSLocalizedString("ShowTabsBar", tableName: "BraveShared", bundle: .module, value: "Tabs Bar", comment: "Setting to show/hide the tabs bar") public static let privateBrowsingOnly = NSLocalizedString("PrivateBrowsingOnly", tableName: "BraveShared", bundle: .module, value: "Private Browsing Only", comment: "Setting to keep app in private mode") + public static let persistentPrivateBrowsing = NSLocalizedString("PersistentPrivateBrowsing", tableName: "BraveShared", bundle: .module, value: "Persistent Private Browsing", comment: "Setting to allow the app to restore private browsing tabs") public static let shieldsDefaults = NSLocalizedString("ShieldsDefaults", tableName: "BraveShared", bundle: .module, value: "Brave Shields Global Defaults", comment: "Section title for adbblock, tracking protection, HTTPS-E, and cookies") public static let shieldsDefaultsFooter = NSLocalizedString("ShieldsDefaultsFooter", tableName: "BraveShared", bundle: .module, value: "These are the default Shields settings for new sites. Changing these won't affect your existing per-site settings.", comment: "Section footer for global shields defaults") public static let HTTPSEverywhere = NSLocalizedString("HTTPSEverywhere", tableName: "BraveShared", bundle: .module, value: "Upgrade Connections to HTTPS", comment: "") diff --git a/Sources/Data/models/SessionTab.swift b/Sources/Data/models/SessionTab.swift index 0a0fee5cf03..c087e433ba7 100644 --- a/Sources/Data/models/SessionTab.swift +++ b/Sources/Data/models/SessionTab.swift @@ -127,6 +127,11 @@ extension SessionTab { deleteAll(context: .new(inMemory: false)) } + public static func deleteAll(tabIds: [UUID]) { + let predicate = NSPredicate(format: "\(#keyPath(SessionTab.tabId)) IN %@", tabIds) + deleteAll(predicate: predicate, context: .new(inMemory: false)) + } + public static func deleteAll(olderThan timeInterval: TimeInterval) { let lastUpdatedKeyPath = #keyPath(SessionTab.lastUpdated) let date = Date().advanced(by: -timeInterval) as NSDate @@ -207,7 +212,7 @@ extension SessionTab { } } - public static func createIfNeeded(windowId: UUID, tabId: UUID, title: String, tabURL: URL) { + public static func createIfNeeded(windowId: UUID, tabId: UUID, title: String, tabURL: URL, isPrivate: Bool) { DataController.perform { context in guard !SessionTab.exists(tabId: tabId, in: context), let window = SessionWindow.from(windowId: windowId, in: context) else { return } @@ -217,7 +222,7 @@ extension SessionTab { sessionTabGroup: nil, index: Int32(window.sessionTabs?.count ?? 0), interactionState: Data(), - isPrivate: false, + isPrivate: isPrivate, isSelected: false, lastUpdated: .now, screenshotData: Data(),