Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Fix #4835: Add the ability to remove site-specific web data
Browse files Browse the repository at this point in the history
  • Loading branch information
kylehickinson committed Jan 12, 2022
1 parent 8ae3f1c commit 7cb7b0f
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 0 deletions.
64 changes: 64 additions & 0 deletions BraveShared/BraveStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3396,3 +3396,67 @@ extension Strings {
comment: "Description for Brave Shields single stat widget on 'add widget' screen.")
}
}

// MARK: - ManageWebsiteData
extension Strings {
public static let manageWebsiteDataTitle = NSLocalizedString(
"websiteData.manageWebsiteDataTitle",
bundle: .braveShared,
value: "Manage Website Data",
comment: "A button or screen title describing that the user is there to manually manage website data that is persisted to their device. I.e. to manage data specific to a web page the user has visited"
)
public static let loadingWebsiteData = NSLocalizedString(
"websiteData.loadingWebsiteData",
bundle: .braveShared,
value: "Loading website data…",
comment: "A message displayed to users while the system fetches all website data to display."
)
public static let dataRecordCookies = NSLocalizedString(
"websiteData.dataRecordCookies",
bundle: .braveShared,
value: "Cookies",
comment: "The word used to describe small bits of state stored locally in a web browser (e.g. Browser cookies)"
)
public static let dataRecordCache = NSLocalizedString(
"websiteData.dataRecordCache",
bundle: .braveShared,
value: "Cache",
comment: "Temporary data that is stored on the users device to speed up future requests and interactions."
)
public static let dataRecordLocalStorage = NSLocalizedString(
"websiteData.dataRecordLocalStorage",
bundle: .braveShared,
value: "Local storage",
comment: "A kind of browser storage particularely for saving data on the users device for a specific webpage for the given session or longer time periods."
)
public static let dataRecordDatabases = NSLocalizedString(
"websiteData.dataRecordDatabases",
bundle: .braveShared,
value: "Databases",
comment: "Some data stored on disk that is a kind of database (such as WebSQL or IndexedDB.)"
)
public static let removeDataRecord = NSLocalizedString(
"websiteData.removeDataRecord",
bundle: .braveShared,
value: "Remove",
comment: "Shown when a user has attempted to delete a single webpage data record such as cookies, caches, or local storage that has been persisted on their device. Tapping it will delete that records and remove it from the list"
)
public static let removeSelectedDataRecord = NSLocalizedString(
"websiteData.removeSelectedDataRecord",
bundle: .braveShared,
value: "Remove %ld records",
comment: "Shown on a button when a user has selected multiple webpage data records (such as cookies, caches, or local storage) that has been persisted on their device. Tapping it will delete those records and remove them from the list"
)
public static let removeAllDataRecords = NSLocalizedString(
"websiteData.removeAllDataRecords",
bundle: .braveShared,
value: "Remove All",
comment: "Shown on a button to delete all displayed webpage records (such as cookies, caches, or local storage) that has been persisted on their device. Tapping it will delete those records and remove them from the list"
)
public static let noSavedWebsiteData = NSLocalizedString(
"websiteData.noSavedWebsiteData",
bundle: .braveShared,
value: "No Saved Website Data",
comment: "Shown when the user has no website data (such as cookies, caches, or local storage) persisted to their device."
)
}
4 changes: 4 additions & 0 deletions Client.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@
2748625D25AD309800BC40AA /* sqlcipher.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2748625C25AD308A00BC40AA /* sqlcipher.xcframework */; };
2748626925AD30A500BC40AA /* sqlcipher.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2748625C25AD308A00BC40AA /* sqlcipher.xcframework */; };
2748626A25AD30A500BC40AA /* sqlcipher.xcframework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 2748625C25AD308A00BC40AA /* sqlcipher.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
274AD90A278D455500FDF4D8 /* ManageWebsiteDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274AD909278D455500FDF4D8 /* ManageWebsiteDataView.swift */; };
275034E326EF9E6A00CF4C8A /* WalletSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275034E226EF9E6A00CF4C8A /* WalletSettingsView.swift */; };
2755AB7D23107BC600F0721F /* AdsReporting.js in Resources */ = {isa = PBXBuildFile; fileRef = 2755AB7C23107BC600F0721F /* AdsReporting.js */; };
2755AB8623107DBD00F0721F /* AdsMediaReporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2755AB8523107DBD00F0721F /* AdsMediaReporting.swift */; };
Expand Down Expand Up @@ -1938,6 +1939,7 @@
2746D28024A4F8DA00E38852 /* RewardsInternalsShareController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardsInternalsShareController.swift; sourceTree = "<group>"; };
2746D28224A4FB7400E38852 /* RewardsInternalsSharable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardsInternalsSharable.swift; sourceTree = "<group>"; };
2748625C25AD308A00BC40AA /* sqlcipher.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = sqlcipher.xcframework; sourceTree = "<group>"; };
274AD909278D455500FDF4D8 /* ManageWebsiteDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageWebsiteDataView.swift; sourceTree = "<group>"; };
275034E226EF9E6A00CF4C8A /* WalletSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletSettingsView.swift; sourceTree = "<group>"; };
2755AB7C23107BC600F0721F /* AdsReporting.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = AdsReporting.js; sourceTree = "<group>"; };
2755AB8523107DBD00F0721F /* AdsMediaReporting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsMediaReporting.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4782,6 +4784,7 @@
2F12A175260A3B17005A8C2B /* ShortcutSettingsViewController.swift */,
272B862A270BD96F005ED304 /* SandboxInspectorView.swift */,
2F1A8359274C35340089A8A9 /* RetentionPreferencesDebugMenuViewController.swift */,
274AD909278D455500FDF4D8 /* ManageWebsiteDataView.swift */,
);
path = Settings;
sourceTree = "<group>";
Expand Down Expand Up @@ -8400,6 +8403,7 @@
27EF6B8024BF48C7005E034F /* FeedSourceListViewController.swift in Sources */,
0A764F31230EE5CA003A1D9B /* OnboardingState.swift in Sources */,
D8D33A7D1FBD080300A20A28 /* SnapKitExtensions.swift in Sources */,
274AD90A278D455500FDF4D8 /* ManageWebsiteDataView.swift in Sources */,
27AC7CFA24C77EBC00441317 /* FeedActionAlertView.swift in Sources */,
27FD3F7825C8B0E700696156 /* BraveNewsAddSourceResultsViewController.swift in Sources */,
27E0652824CB6AE300134946 /* BraveNewsErrorView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Static
import Shared
import BraveShared
import BraveCore
import class SwiftUI.UIHostingController

private let log = Logger.browserLogger

Expand Down Expand Up @@ -38,6 +39,7 @@ class BraveShieldsAndPrivacySettingsController: TableViewController {
dataSource.sections = [
shieldsSection,
clearPrivateDataSection,
manageWebsiteDataSection,
otherSettingsSection
]
}
Expand Down Expand Up @@ -157,6 +159,20 @@ class BraveShieldsAndPrivacySettingsController: TableViewController {
)
}()

private lazy var manageWebsiteDataSection: Section = {
return Section(
rows: [
Row(text: Strings.manageWebsiteDataTitle, selection: { [weak self] in
// [unowned self] crashes here for some reason
let controller = UIHostingController(rootView: ManageWebsiteDataView())
// pushing SwiftUI with navigation/toolbars inside the PanModal is buggy…
// presenting over context is also buggy (eats swipe gestures)
self?.present(controller, animated: true)
}, accessory: .disclosureIndicator)
]
)
}()

private lazy var otherSettingsSection: Section = {
var section = Section(
header: .title(Strings.otherPrivacySettingsSection),
Expand Down
203 changes: 203 additions & 0 deletions Client/Frontend/Settings/ManageWebsiteDataView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright 2022 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 http://mozilla.org/MPL/2.0/.

import Foundation
import SwiftUI
import WebKit
import BraveUI
import struct Shared.Strings

struct ManageWebsiteDataView: View {
@State private var isLoading: Bool = false
@State private var dataRecords: [WKWebsiteDataRecord] = []
@State private var filter: String = ""
@State private var selectedRecordsIds: Set<String> = []
@State private var editMode: EditMode = .inactive
@Environment(\.presentationMode) @Binding private var presentationMode

private var filteredRecords: [WKWebsiteDataRecord] {
if filter.isEmpty {
return dataRecords
}
return dataRecords.filter {
$0.displayName.lowercased().contains(filter.lowercased())
}
}

private func removeRecords(_ records: [WKWebsiteDataRecord]) {
let recordIds = records.map(\.id)
WKWebsiteDataStore.default()
.removeData(
ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
for: records
) {
withAnimation {
if dataRecords.count == records.count {
dataRecords.removeAll()
} else {
dataRecords.removeAll(where: { recordIds.contains($0.displayName) })
}
}
}
}

private func removeRecordsWithIds(_ recordIds: [String]) {
let records = dataRecords.filter { recordIds.contains($0.id) }
removeRecords(records)
}

private var isEditMode: Bool {
!selectedRecordsIds.isEmpty && editMode == .active
}

private var removeButtonTitle: String {
if isEditMode {
if selectedRecordsIds.count == 1 {
return Strings.removeDataRecord
}
return String.localizedStringWithFormat(Strings.removeSelectedDataRecord, selectedRecordsIds.count)
}
return Strings.removeAllDataRecords
}

private func loadRecords() {
isLoading = true
WKWebsiteDataStore.default()
.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
completionHandler: { records in
withAnimation {
self.dataRecords = records
.filter { $0.displayName != "localhost" }
.sorted(by: { $0.displayName < $1.displayName })
self.isLoading = false
}
})
}

var body: some View {
NavigationView {
let visibleRecords = filteredRecords
List(selection: $selectedRecordsIds) {
Section {
if isLoading {
HStack {
ProgressView()
Text(Strings.loadingWebsiteData)
}
} else {
ForEach(visibleRecords) { record in
VStack(alignment: .leading, spacing: 2) {
let types = Set(
record.dataTypes.compactMap(localizedStringForDataRecordType)
).sorted()
Text(record.displayName)
.foregroundColor(Color(.bravePrimary))
if !types.isEmpty {
Text(ListFormatter.localizedString(byJoining: types))
.foregroundColor(Color(.braveLabel))
.font(.footnote)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.osAvailabilityModifiers { content in
if #available(iOS 15.0, *) {
// Better swipe gestures
content
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
removeRecords([record])
} label: {
Label(Strings.removeDataRecord, systemImage: "trash")
}
}
} else {
content
}
}
}
.onDelete { indexSet in
let recordsToDelete = indexSet.map { visibleRecords[$0] }
removeRecords(recordsToDelete)
}
}
}
.listRowBackground(Color(.braveBackground))
}
.listStyle(.plain)
.environment(\.editMode, $editMode)
.overlay(Group {
if !isLoading && visibleRecords.isEmpty {
Text(Strings.noSavedWebsiteData)
.font(.headline)
.foregroundColor(Color(.secondaryBraveLabel))
}
})
.toolbar {
ToolbarItemGroup(placement: .confirmationAction) {
Button(action: {
presentationMode.dismiss()
}) {
Text(Strings.done)
.foregroundColor(Color(.braveOrange))
}
}
ToolbarItemGroup(placement: .bottomBar) {
HStack {
Button(action: {
withAnimation {
editMode = editMode.isEditing ? .inactive : .active
}
}) {
Text(editMode.isEditing ? Strings.done : Strings.edit)
.foregroundColor(visibleRecords.isEmpty ? Color(.braveDisabled) : Color(.braveOrange))
}
.disabled(visibleRecords.isEmpty)
Spacer()
Button(action: {
if isEditMode {
removeRecordsWithIds(Array(selectedRecordsIds))
selectedRecordsIds = []
} else {
removeRecords(visibleRecords)
}
}) {
Text(removeButtonTitle)
.foregroundColor(visibleRecords.isEmpty ? Color(.braveDisabled) : .red)
.animation(nil, value: isEditMode)
}
.disabled(visibleRecords.isEmpty)
}
}
}
.filterable(text: $filter)
.navigationTitle(Strings.manageWebsiteDataTitle)
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(.stack)
.onAppear(perform: loadRecords)
}
}

private func localizedStringForDataRecordType(_ type: String) -> String? {
switch type {
case WKWebsiteDataTypeCookies:
return Strings.dataRecordCookies
case WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache, WKWebsiteDataTypeFetchCache,
WKWebsiteDataTypeOfflineWebApplicationCache, WKWebsiteDataTypeServiceWorkerRegistrations:
return Strings.dataRecordCache
case WKWebsiteDataTypeLocalStorage, WKWebsiteDataTypeSessionStorage:
return Strings.dataRecordLocalStorage
case WKWebsiteDataTypeWebSQLDatabases, WKWebsiteDataTypeIndexedDBDatabases:
return Strings.dataRecordDatabases
default:
return nil
}
}

extension WKWebsiteDataRecord: Identifiable {
public var id: String {
displayName
}
}

0 comments on commit 7cb7b0f

Please sign in to comment.