mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-20 19:33:08 +09:00
310 lines
11 KiB
Swift
310 lines
11 KiB
Swift
/* 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 WebKit
|
|
import Shared
|
|
import Deferred
|
|
|
|
fileprivate let NotificationContentBlockerReloadNeeded = "NotificationContentBlockerReloadNeeded"
|
|
|
|
@available(iOS 11.0, *)
|
|
class ContentBlockerHelper {
|
|
static let PrefKeyEnabledState = "prefkey.trackingprotection.enabled"
|
|
static let PrefKeyStrength = "prefkey.trackingprotection.strength"
|
|
fileprivate let blocklistBasic = ["disconnect-advertising", "disconnect-analytics", "disconnect-social"]
|
|
fileprivate let blocklistStrict = ["disconnect-content"]
|
|
fileprivate let ruleStore: WKContentRuleListStore
|
|
fileprivate weak var tab: Tab?
|
|
fileprivate weak var profile: Profile?
|
|
|
|
static var blockImagesRule: WKContentRuleList?
|
|
|
|
// Raw values are stored to prefs, be careful changing them.
|
|
enum EnabledState: String {
|
|
case on
|
|
case onInPrivateBrowsing
|
|
case off
|
|
|
|
var settingTitle: String {
|
|
switch self {
|
|
case .on:
|
|
return Strings.TrackingProtectionOptionAlwaysOn
|
|
case .onInPrivateBrowsing:
|
|
return Strings.TrackingProtectionOptionOnInPrivateBrowsing
|
|
case .off:
|
|
return Strings.TrackingProtectionOptionAlwaysOff
|
|
}
|
|
}
|
|
|
|
static let allOptions: [EnabledState] = [.on, .onInPrivateBrowsing, .off]
|
|
}
|
|
|
|
// Raw values are stored to prefs, be careful changing them.
|
|
enum BlockingStrength: String {
|
|
case basic
|
|
case strict
|
|
|
|
var settingTitle: String {
|
|
switch self {
|
|
case .basic:
|
|
return Strings.TrackingProtectionOptionBlockListTypeBasic
|
|
case .strict:
|
|
return Strings.TrackingProtectionOptionBlockListTypeStrict
|
|
}
|
|
}
|
|
|
|
var subtitle: String {
|
|
switch self {
|
|
case .basic:
|
|
return Strings.TrackingProtectionOptionBlockListTypeBasicDescription
|
|
case .strict:
|
|
return Strings.TrackingProtectionOptionBlockListTypeStrictDescription
|
|
}
|
|
}
|
|
|
|
static let allOptions: [BlockingStrength] = [.basic, .strict]
|
|
}
|
|
|
|
static func prefsChanged() {
|
|
NotificationCenter.default.post(name: Notification.Name(rawValue: NotificationContentBlockerReloadNeeded), object: nil)
|
|
}
|
|
|
|
class func name() -> String {
|
|
return "ContentBlockerHelper"
|
|
}
|
|
|
|
private static var heavyInitHasRunOnce = false
|
|
|
|
init(tab: Tab, profile: Profile) {
|
|
self.ruleStore = WKContentRuleListStore.default()
|
|
self.tab = tab
|
|
self.profile = profile
|
|
|
|
if ContentBlockerHelper.heavyInitHasRunOnce {
|
|
return
|
|
}
|
|
ContentBlockerHelper.heavyInitHasRunOnce = true
|
|
|
|
removeOldListsByDateFromStore() {
|
|
self.removeOldListsByNameFromStore() {
|
|
self.compileListsNotInStore(completion: {})
|
|
}
|
|
}
|
|
|
|
let blockImages = "[{'trigger':{'url-filter':'.*','resource-type':['image']},'action':{'type':'block'}}]".replacingOccurrences(of: "'", with: "\"")
|
|
ruleStore.compileContentRuleList(forIdentifier: "images", encodedContentRuleList: blockImages) {
|
|
rule, error in
|
|
assert(rule != nil && error == nil)
|
|
ContentBlockerHelper.blockImagesRule = rule
|
|
}
|
|
}
|
|
|
|
func setupForWebView() {
|
|
NotificationCenter.default.addObserver(self, selector: #selector(ContentBlockerHelper.reloadTab), name: NSNotification.Name(rawValue: NotificationContentBlockerReloadNeeded), object: nil)
|
|
addActiveRulesToTab()
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
@objc func reloadTab() {
|
|
addActiveRulesToTab()
|
|
}
|
|
|
|
fileprivate var blockingStrengthPref: BlockingStrength {
|
|
let pref = profile?.prefs.stringForKey(ContentBlockerHelper.PrefKeyStrength) ?? ""
|
|
return BlockingStrength(rawValue: pref) ?? .basic
|
|
}
|
|
|
|
fileprivate var enabledStatePref: EnabledState {
|
|
let pref = profile?.prefs.stringForKey(ContentBlockerHelper.PrefKeyEnabledState) ?? ""
|
|
return EnabledState(rawValue: pref) ?? .onInPrivateBrowsing
|
|
}
|
|
|
|
fileprivate func addActiveRulesToTab() {
|
|
guard let tab = tab else { return }
|
|
removeTrackingProtectionFromTab()
|
|
|
|
switch enabledStatePref {
|
|
case .off:
|
|
return
|
|
case .on:
|
|
break
|
|
case .onInPrivateBrowsing:
|
|
if !tab.isPrivate {
|
|
return
|
|
}
|
|
}
|
|
|
|
let rules = blocklistBasic + (blockingStrengthPref == .strict ? blocklistStrict : [])
|
|
for name in rules {
|
|
ruleStore.lookUpContentRuleList(forIdentifier: name) { rule, error in
|
|
guard let rule = rule else {
|
|
let msg = "lookUpContentRuleList for \(name): \(error?.localizedDescription ?? "empty rules")"
|
|
Sentry.shared.send(message: "Content blocker error", tag: .general, description: msg)
|
|
return
|
|
}
|
|
self.addToTab(contentRuleList: rule)
|
|
}
|
|
}
|
|
}
|
|
|
|
func removeTrackingProtectionFromTab() {
|
|
guard let tab = tab else { return }
|
|
tab.webView?.configuration.userContentController.removeAllContentRuleLists()
|
|
|
|
if let rule = ContentBlockerHelper.blockImagesRule, tab.noImageMode {
|
|
addToTab(contentRuleList: rule)
|
|
}
|
|
}
|
|
|
|
fileprivate func addToTab(contentRuleList: WKContentRuleList) {
|
|
tab?.webView?.configuration.userContentController.add(contentRuleList)
|
|
}
|
|
|
|
func noImageMode(enabled: Bool) {
|
|
guard let rule = ContentBlockerHelper.blockImagesRule else { return }
|
|
|
|
if enabled {
|
|
addToTab(contentRuleList: rule)
|
|
} else {
|
|
tab?.webView?.configuration.userContentController.remove(rule)
|
|
}
|
|
|
|
// Async required here to ensure remove() call is processed.
|
|
DispatchQueue.main.async() {
|
|
self.tab?.webView?.evaluateJavaScript("window.__firefox__.NoImageMode.setEnabled(\(enabled))", completionHandler: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Private initialization code
|
|
// The rule store can compile JSON rule files into a private format which is cached on disk.
|
|
// On app boot, we need to check if the ruleStore's data is out-of-date, or if the names of the rule files
|
|
// no longer match. Finally, any JSON rule files that aren't in the ruleStore need to be compiled and stored in the
|
|
// ruleStore.
|
|
@available(iOS 11, *)
|
|
extension ContentBlockerHelper {
|
|
fileprivate func loadJsonFromBundle(forResource file: String, completion: @escaping (_ jsonString: String) -> Void) {
|
|
DispatchQueue.global().async {
|
|
guard let path = Bundle.main.path(forResource: file, ofType: "json"),
|
|
let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String else {
|
|
return
|
|
}
|
|
DispatchQueue.main.async {
|
|
completion(source)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func lastModifiedSince1970(forFileAtPath path: String) -> Timestamp? {
|
|
do {
|
|
let url = URL(fileURLWithPath: path)
|
|
let attr = try FileManager.default.attributesOfItem(atPath: url.path)
|
|
guard let date = attr[FileAttributeKey.modificationDate] as? Date else { return nil }
|
|
return UInt64(1000.0 * date.timeIntervalSince1970)
|
|
} catch {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
fileprivate func dateOfMostRecentBlockerFile() -> Timestamp {
|
|
let blocklists = blocklistBasic + blocklistStrict
|
|
return blocklists.reduce(Timestamp(0)) { result, filename in
|
|
guard let path = Bundle.main.path(forResource: filename, ofType: "json") else { return result }
|
|
let date = lastModifiedSince1970(forFileAtPath: path) ?? 0
|
|
return date > result ? date : result
|
|
}
|
|
}
|
|
|
|
fileprivate func removeAllRulesInStore(completion: @escaping () -> Void) {
|
|
ruleStore.getAvailableContentRuleListIdentifiers { available in
|
|
guard let available = available else {
|
|
completion()
|
|
return
|
|
}
|
|
let deferreds: [Deferred<Void>] = available.map { filename in
|
|
let result = Deferred<Void>()
|
|
self.ruleStore.removeContentRuleList(forIdentifier: filename) { _ in
|
|
result.fill()
|
|
}
|
|
return result
|
|
}
|
|
all(deferreds).uponQueue(.main) { _ in
|
|
completion()
|
|
}
|
|
}
|
|
}
|
|
|
|
// If any blocker files are newer than the date saved in prefs,
|
|
// remove all the content blockers and reload them.
|
|
fileprivate func removeOldListsByDateFromStore(completion: @escaping () -> Void) {
|
|
let fileDate = self.dateOfMostRecentBlockerFile()
|
|
let prefsNewestDate = profile?.prefs.longForKey("blocker-file-date") ?? 0
|
|
if prefsNewestDate < 1 || fileDate <= prefsNewestDate {
|
|
completion()
|
|
return
|
|
}
|
|
|
|
profile?.prefs.setTimestamp(fileDate, forKey: "blocker-file-date")
|
|
self.removeAllRulesInStore() {
|
|
completion()
|
|
}
|
|
}
|
|
|
|
fileprivate func removeOldListsByNameFromStore(completion: @escaping () -> Void) {
|
|
var noMatchingIdentifierFoundForRule = false
|
|
|
|
ruleStore.getAvailableContentRuleListIdentifiers { available in
|
|
guard let available = available else {
|
|
completion()
|
|
return
|
|
}
|
|
|
|
let blocklists = self.blocklistBasic + self.blocklistStrict
|
|
for contentRuleIdentifier in available {
|
|
if !blocklists.contains(where: { $0 == contentRuleIdentifier }) {
|
|
noMatchingIdentifierFoundForRule = true
|
|
break
|
|
}
|
|
}
|
|
|
|
let fileDate = self.dateOfMostRecentBlockerFile()
|
|
let prefsNewestDate = self.profile?.prefs.timestampForKey("blocker-file-date") ?? 0
|
|
if prefsNewestDate > 0 && fileDate <= prefsNewestDate && !noMatchingIdentifierFoundForRule {
|
|
completion()
|
|
return
|
|
}
|
|
self.profile?.prefs.setTimestamp(fileDate, forKey: "blocker-file-date")
|
|
|
|
self.removeAllRulesInStore {
|
|
completion()
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func compileListsNotInStore(completion: @escaping () -> Void) {
|
|
let blocklists = blocklistBasic + blocklistStrict
|
|
let deferreds: [Deferred<Void>] = blocklists.map { filename in
|
|
let result = Deferred<Void>()
|
|
ruleStore.lookUpContentRuleList(forIdentifier: filename) { contentRuleList, error in
|
|
if contentRuleList != nil {
|
|
result.fill()
|
|
return
|
|
}
|
|
self.loadJsonFromBundle(forResource: filename) { jsonString in
|
|
self.ruleStore.compileContentRuleList(forIdentifier: filename, encodedContentRuleList: jsonString) { _, _ in
|
|
result.fill()
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
all(deferreds).uponQueue(.main) { _ in
|
|
completion()
|
|
}
|
|
}
|
|
}
|