mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 07:48:38 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
|
|
@ -0,0 +1,310 @@
|
|||
/* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue