diff --git a/src/popup/App.svelte b/src/popup/App.svelte index 52cb2cb8..92fbc72a 100644 --- a/src/popup/App.svelte +++ b/src/popup/App.svelte @@ -545,13 +545,14 @@
Popup inactive on extension page
{:else if firstGuide}
- Welcome, first please:  +

Welcome, first use please: 

+

to complete the initialization

{:else if initError}
@@ -716,13 +717,11 @@ } .none { - align-items: center; - color: var(--text-color-disabled); - display: flex; font-weight: 600; - justify-content: center; - inset: 0; - position: absolute; + color: var(--text-color-disabled); + text-align: center; + padding: 3rem 0; + line-height: 2.5; } .items.disabled { diff --git a/xcode/Mac-App/AppDelegate.swift b/xcode/Mac-App/AppDelegate.swift index 23e8a127..a834109c 100644 --- a/xcode/Mac-App/AppDelegate.swift +++ b/xcode/Mac-App/AppDelegate.swift @@ -7,6 +7,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var windowForego = false private var windowLoaded = false + @IBOutlet weak var enbaleNativeLogger: NSMenuItem! + func application(_ application: NSApplication, open urls: [URL]) { // if open panel is already open, stop processing the URL scheme if NSApplication.shared.keyWindow?.accessibilityIdentifier() == "open-panel" { return } @@ -26,6 +28,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationDidFinishLaunching(_ aNotification: Notification) { + // Initialize menu items + enbaleNativeLogger.state = Preferences.enableLogger ? .on : .off if windowForego { return } let storyboard = NSStoryboard(name: "View", bundle: Bundle.main) let windowController = storyboard.instantiateInitialController() as! NSWindowController @@ -47,4 +51,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { return true } + @IBAction func enableLogger(_ sender: NSMenuItem) { + if sender.state == .on { + Preferences.enableLogger = false + sender.state = .off + } else { + Preferences.enableLogger = true + sender.state = .on + } + } + + @IBAction func applicationHelp(_ sender: NSMenuItem) { + if let url = URL(string: "https://github.com/quoid/userscripts") { + NSWorkspace.shared.open(url) + } + } + } diff --git a/xcode/Mac-App/Base.lproj/Main.storyboard b/xcode/Mac-App/Base.lproj/Main.storyboard index ed7ecf33..859f7dde 100644 --- a/xcode/Mac-App/Base.lproj/Main.storyboard +++ b/xcode/Mac-App/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -30,13 +30,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + diff --git a/xcode/Mac-App/Functions.swift b/xcode/Mac-App/Functions.swift index 61d753b7..94831a17 100644 --- a/xcode/Mac-App/Functions.swift +++ b/xcode/Mac-App/Functions.swift @@ -2,37 +2,10 @@ import AppKit import SafariServices import os -let extensionIdentifier = Bundle.main.infoDictionary?["US_EXT_IDENTIFIER"] as! String -fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) +private let logger = USLogger(#fileID) func getSaveLocationURL() -> URL { - var url: URL - // default url - if #available(macOS 13.0, *) { - url = getDocumentsDirectory().appending(path: "scripts") - } else { - url = getDocumentsDirectory().appendingPathComponent("scripts") - } - // if not in safari extension environment, replace with extension path - if let bundleIdentifier = Bundle.main.bundleIdentifier, bundleIdentifier != extensionIdentifier { - let s = url.absoluteString.replacingOccurrences(of: bundleIdentifier, with: extensionIdentifier) - // avoid being encode again, decode first - if let decodePath = s.removingPercentEncoding { - url = URL(fileURLWithPath: decodePath, isDirectory: true) - } - } - // bookmark url - if let data = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName) { - if let bookmarkURL = readBookmark(data: data, isSecure: false) { - if directoryExists(path: bookmarkURL.path) { - url = bookmarkURL - } else { - UserDefaults(suiteName: SharedDefaults.suiteName)?.removeObject(forKey: SharedDefaults.keyName) - NSLog("removed shared bookmark because it's directory is non-existent, permanently deleted or in trash") - } - } - } - return url + return Preferences.scriptsDirectoryUrl } func setSaveLocationURL(url: URL) -> Bool { @@ -42,17 +15,14 @@ func setSaveLocationURL(url: URL) -> Bool { alert.runModal() return false } - guard saveBookmark(url: url, isShared: true, keyName: SharedDefaults.keyName, isSecure: false) else { - logger.error("\(#function, privacy: .public) - couldn't save new location from host app") - return false - } - return true + Preferences.scriptsDirectoryUrl = url + return Preferences.scriptsDirectoryUrl == url } func sendExtensionMessage(name: String, userInfo: [String : Any]? = nil, completion: ((Error?) -> Void)? = nil) { SFSafariApplication.dispatchMessage( withName: name, - toExtensionWithIdentifier: extensionIdentifier, + toExtensionWithIdentifier: extIdentifier, userInfo: userInfo ) { error in // always be called if error != nil { @@ -83,6 +53,8 @@ func schemeChangeSaveLocation() { let response = panel.runModal() // check if clicked open button and there is a valid result guard response == .OK, let url: URL = panel.urls.first else { return } + // revoke implicitly starts security-scoped access + defer { url.stopAccessingSecurityScopedResource() } // check if path has indeed changed if url.absoluteString == saveLocationURL.absoluteString { return } // try set new save location path to bookmark diff --git a/xcode/Mac-App/ViewController.swift b/xcode/Mac-App/ViewController.swift index 3e2ffeaf..8b9a779a 100644 --- a/xcode/Mac-App/ViewController.swift +++ b/xcode/Mac-App/ViewController.swift @@ -2,7 +2,7 @@ import Cocoa import SafariServices.SFSafariApplication import os -fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) +private let logger = USLogger(#fileID) class ViewController: NSViewController { @@ -14,7 +14,6 @@ class ViewController: NSViewController { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" - let extensionID = extensionIdentifier override func viewDidLoad() { super.viewDidLoad() @@ -35,13 +34,13 @@ class ViewController: NSViewController { } @objc func setExtensionState() { - SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionID) { (state, error) in + SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extIdentifier) { (state, error) in guard let state = state else { self.enabledText.stringValue = "Safari Extension State Unknown" if let error = error { - logger.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") } else { - logger.error("\(#function, privacy: .public) - couldn't get safari extension state in containing app") + logger?.error("\(#function, privacy: .public) - couldn't get safari extension state in containing app") } return } @@ -59,6 +58,8 @@ class ViewController: NSViewController { panel.beginSheetModal(for: window, completionHandler: { response in // check if clicked open button and there is a valid result guard response == .OK, let url: URL = panel.urls.first else { return } + // revoke implicitly starts security-scoped access + defer { url.stopAccessingSecurityScopedResource() } // check if path has indeed changed if url.absoluteString == saveLocationURL.absoluteString { return } // try set new save location path to bookmark @@ -78,6 +79,6 @@ class ViewController: NSViewController { } @IBAction func openSafariExtensionPreferences(_ sender: AnyObject?) { - SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionID) + SFSafariApplication.showPreferencesForExtension(withIdentifier: extIdentifier) } } diff --git a/xcode/Safari-Extension/Functions.swift b/xcode/Safari-Extension/Functions.swift index bd15f4ca..6f0c9053 100644 --- a/xcode/Safari-Extension/Functions.swift +++ b/xcode/Safari-Extension/Functions.swift @@ -2,7 +2,7 @@ import Foundation import SafariServices import os -fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) +private let logger = USLogger(#fileID) // helpers func getRequireLocation() -> URL { @@ -53,76 +53,12 @@ func normalizeWeight(_ weight: String) -> String { } func getSaveLocation() -> URL? { - #if os(iOS) - if - let sharedBookmarkData = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName), - let bookmarkUrl = readBookmark(data: sharedBookmarkData, isSecure: true) - { - return bookmarkUrl - } else { - return nil - } - #elseif os(macOS) - let standardDefaults = UserDefaults.standard - let userSaveLocationKey = "userSaveLocation" - var defaultSaveLocation:URL - - // get the default save location, if key doesn't exist write it to user defaults - if let saveLocationValue = standardDefaults.url(forKey: "saveLocation") { - defaultSaveLocation = saveLocationValue - } else { - logger.info("\(#function, privacy: .public) - default save location not set, writing to user defaults") - let url = getDocumentsDirectory().appendingPathComponent("scripts") - UserDefaults.standard.set(url, forKey: "saveLocation") - defaultSaveLocation = url - } - - // check if shared bookmark data exists - // check if can get shared bookmark url - // won't be able to if directory trashed - guard - let sharedBookmarkData = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName), - let sharedBookmark = readBookmark(data: sharedBookmarkData, isSecure: false), - directoryExists(path: sharedBookmark.path) - else { - // can't get shared bookmark, use default location and remove shared bookmark key from shared user defaults - UserDefaults(suiteName: SharedDefaults.suiteName)?.removeObject(forKey: SharedDefaults.keyName) - logger.info("\(#function, privacy: .public) - removed sharedbookmark because it was either permanently deleted or in trash") - return defaultSaveLocation - } - - // at this point, it's known sharedbookmark exists - // check local bookmark exists, can read url from bookmark and if bookmark url == shared bookmark url - // if local bookmark exists, no need to check if directory exists for it - // can't think of an instance where shared bookmark directory exists (checked above), yet local bookmark directory does not - if - let userSaveLocationData = standardDefaults.data(forKey: userSaveLocationKey), - let userSaveLocation = readBookmark(data: userSaveLocationData, isSecure: true), - sharedBookmark == userSaveLocation - { - return userSaveLocation - } - - // at this point one of the following conditions met - // - local bookmark data doesn't exist - // - for some reason can't get url from local bookmark data - // - local bookmark url != shared bookmark url (user updated save location) - // when any of those conditions are met, create new local bookmark from shared bookmark - if saveBookmark(url: sharedBookmark, isShared: false, keyName: userSaveLocationKey, isSecure: true) { - // read the newly saved bookmark and return it - guard - let localBookmarkData = standardDefaults.data(forKey: userSaveLocationKey), - let localBookmarkUrl = readBookmark(data: localBookmarkData, isSecure: true) - else { - logger.error("\(#function, privacy: .public) - failed reading local bookmark") - return nil - } - return localBookmarkUrl - } else { - logger.error("\(#function, privacy: .public) - could not save local version of shared bookmark") - return nil - } - #endif +#if os(iOS) + if Preferences.scriptsDirectoryUrl == getDefaultScriptsDirectoryUrl() { return nil } +#endif + let url = Preferences.scriptsDirectoryUrl + logger?.debug("\(#function, privacy: .public) - \(url, privacy: .public)") + return url } func openSaveLocation() -> Bool { @@ -146,7 +82,7 @@ func validateUrl(_ urlString: String) -> Bool { if let decodedUrl = urlChecked.removingPercentEncoding { urlChecked = decodedUrl } else { - logger.error("\(#function, privacy: .public) - failed at (1), couldn't decode url, \(urlString, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (1), couldn't decode url, \(urlString, privacy: .public)") return false } } @@ -154,7 +90,7 @@ func validateUrl(_ urlString: String) -> Bool { if let encodedUrl = urlChecked.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { urlChecked = encodedUrl } else { - logger.error("\(#function, privacy: .public) - failed at (2), couldn't percent encode url, \(urlString, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (2), couldn't percent encode url, \(urlString, privacy: .public)") return false } guard @@ -162,7 +98,7 @@ func validateUrl(_ urlString: String) -> Bool { let ptcl = parts["protocol"], let path = parts["pathname"] else { - logger.error("\(#function, privacy: .public) - failed at (3) for \(urlString, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (3) for \(urlString, privacy: .public)") return false } if @@ -312,7 +248,7 @@ func updateManifest(with data: Manifest) -> Bool { try fileContent.write(to: url, atomically: false, encoding: .utf8) return true } catch { - logger.error("\(#function, privacy: .public) - failed to update manifest: \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed to update manifest: \(error.localizedDescription, privacy: .public)") return false } } @@ -345,7 +281,7 @@ func getManifest() -> Manifest { } func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) -> Bool { - logger.info("\(#function, privacy: .public) - started") + logger?.info("\(#function, privacy: .public) - started") // only get all files if files were not provided var files = [[String: Any]]() if optionalFilesArray.isEmpty { @@ -386,7 +322,7 @@ func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) -> Bool { if let index = manifest.declarativeNetRequest.firstIndex(of: filename) { manifest.declarativeNetRequest.remove(at: index) } else { - logger.error("\(#function, privacy: .public) - failed to remove \(filename, privacy: .public) from dNR array") + logger?.error("\(#function, privacy: .public) - failed to remove \(filename, privacy: .public) from dNR array") } } @@ -397,11 +333,11 @@ func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) -> Bool { manifest.include = updatePatternDict(filename, included, manifest.include) if !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - failed to update manifest matches") + logger?.error("\(#function, privacy: .public) - failed to update manifest matches") return false } } - logger.info("\(#function, privacy: .public) - complete") + logger?.info("\(#function, privacy: .public) - completed") return true } @@ -416,7 +352,7 @@ func updatePatternDict(_ filename: String, _ filePatterns: [String], _ manifestK for key in keys { // key is an array of filenames guard let filenames = returnDictionary[key] else { - logger.error("\(#function, privacy: .public) - failed to get values for manifest key, \(key, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed to get values for manifest key, \(key, privacy: .public)") continue } for name in filenames { @@ -463,19 +399,19 @@ func updatePatternDict(_ filename: String, _ filePatterns: [String], _ manifestK } func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool { - logger.info("\(#function, privacy: .public) - started") + logger?.info("\(#function, privacy: .public) - started") // only get all files if files were not provided var files = [[String: Any]]() if optionalFilesArray.isEmpty { guard let getFiles = getAllFiles() else { - logger.info("\(#function, privacy: .public) - count not get files") + logger?.info("\(#function, privacy: .public) - count not get files") return false } files = getFiles } else { files = optionalFilesArray } - logger.info("\(#function, privacy: .public) - will loop through \(files.count, privacy: .public)") + logger?.info("\(#function, privacy: .public) - will loop through \(files.count, privacy: .public)") var manifest = getManifest() for file in files { // can be force unwrapped because getAllFiles didn't return nil @@ -483,10 +419,10 @@ func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool let metadata = file["metadata"] as! [String: [String]] let type = file["type"] as! String let required = metadata["require"] ?? [] - logger.info("\(#function, privacy: .public) - begin \(filename, privacy: .public)") + logger?.info("\(#function, privacy: .public) - begin \(filename, privacy: .public)") // get required resources for file, if fail, skip updating manifest if !getRequiredCode(filename, required, type) { - logger.error("\(#function, privacy: .public) - couldn't fetch remote content for \(filename, privacy: .public)") + logger?.error("\(#function, privacy: .public) - couldn't fetch remote content for \(filename, privacy: .public)") continue } @@ -504,21 +440,21 @@ func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool if !r.isEmpty && r != manifest.require[filename] { manifest.require[filename] = r if !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - couldn't update manifest when getting required resources") + logger?.error("\(#function, privacy: .public) - couldn't update manifest when getting required resources") } } - logger.info("\(#function, privacy: .public) - end \(filename, privacy: .public)") + logger?.info("\(#function, privacy: .public) - end \(filename, privacy: .public)") } - logger.info("\(#function, privacy: .public) - complete") + logger?.info("\(#function, privacy: .public) - completed") return true } func updateManifestDeclarativeNetRequests(_ optionalFilesArray: [[String: Any]] = []) -> Bool { - logger.info("\(#function, privacy: .public) - started") + logger?.info("\(#function, privacy: .public) - started") var files = [[String: Any]]() if optionalFilesArray.isEmpty { guard let getFiles = getAllFiles() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return false } files = getFiles @@ -576,16 +512,16 @@ func updateManifestDeclarativeNetRequests(_ optionalFilesArray: [[String: Any]] } } if update, !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - failed at (2)") + logger?.error("\(#function, privacy: .public) - failed at (2)") return false } } - logger.info("\(#function, privacy: .public) - complete") + logger?.info("\(#function, privacy: .public) - completed") return true } func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { - logger.info("\(#function, privacy: .public) - started") + logger?.info("\(#function, privacy: .public) - started") // purge all manifest keys of any stale entries var update = false, manifest = getManifest(), allSaveLocationFilenames = [String]() // only get all files if files were not provided @@ -611,14 +547,14 @@ func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { if let index = manifest.match[pattern]?.firstIndex(of: filename) { manifest.match[pattern]?.remove(at: index) update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from match pattern - \(pattern, privacy: .public)") + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from match pattern - \(pattern, privacy: .public)") } } } if let length = manifest.match[pattern]?.count { if length < 1, let ind = manifest.match.index(forKey: pattern) { manifest.match.remove(at: ind) - logger.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) match pattern, removed from manifest") + logger?.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) match pattern, removed from manifest") } } } @@ -628,14 +564,14 @@ func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { if let index = manifest.excludeMatch[pattern]?.firstIndex(of: filename) { manifest.excludeMatch[pattern]?.remove(at: index) update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude-match pattern - \(pattern, privacy: .public)") + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude-match pattern - \(pattern, privacy: .public)") } } } if let length = manifest.excludeMatch[pattern]?.count { if length < 1, let ind = manifest.excludeMatch.index(forKey: pattern) { manifest.excludeMatch.remove(at: ind) - logger.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude-match pattern, removed from manifest") + logger?.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude-match pattern, removed from manifest") } } } @@ -645,14 +581,14 @@ func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { if let index = manifest.exclude[pattern]?.firstIndex(of: filename) { manifest.exclude[pattern]?.remove(at: index) update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude pattern - \(pattern, privacy: .public)") + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude pattern - \(pattern, privacy: .public)") } } } if let length = manifest.exclude[pattern]?.count { if length < 1, let ind = manifest.exclude.index(forKey: pattern) { manifest.exclude.remove(at: ind) - logger.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude pattern, removed from manifest") + logger?.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude pattern, removed from manifest") } } } @@ -662,14 +598,14 @@ func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { if let index = manifest.include[pattern]?.firstIndex(of: filename) { manifest.include[pattern]?.remove(at: index) update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude pattern - \(pattern, privacy: .public)") + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude pattern - \(pattern, privacy: .public)") } } } if let length = manifest.include[pattern]?.count { if length < 1, let ind = manifest.include.index(forKey: pattern) { manifest.include.remove(at: ind) - logger.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude pattern, removed from manifest") + logger?.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude pattern, removed from manifest") } } } @@ -680,10 +616,10 @@ func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { manifest.require.remove(at: index) // remove associated resources if !getRequiredCode(filename, [], (filename as NSString).pathExtension) { - logger.error("\(#function, privacy: .public) - failed to remove required resources when purging \(filename, privacy: .public) from manifest required records") + logger?.error("\(#function, privacy: .public) - failed to remove required resources when purging \(filename, privacy: .public) from manifest required records") } update = true - logger.info("\(#function, privacy: .public) - No more required resources for \(filename, privacy: .public), removed from manifest along with resource folder") + logger?.info("\(#function, privacy: .public) - No more required resources for \(filename, privacy: .public), removed from manifest along with resource folder") } } } @@ -693,7 +629,7 @@ func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { if let index = manifest.disabled.firstIndex(of: filename) { manifest.disabled.remove(at: index) update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from disabled") + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from disabled") } } } @@ -703,7 +639,7 @@ func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { if let index = manifest.declarativeNetRequest.firstIndex(of: filename) { manifest.declarativeNetRequest.remove(at: index) update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from dNR") + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from dNR") } } } @@ -712,14 +648,14 @@ func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { if !defaultSettings.keys.contains(setting.key) { manifest.settings.removeValue(forKey: setting.key) update = true - logger.info("\(#function, privacy: .public) - Removed obsolete setting - \(setting.key, privacy: .public)") + logger?.info("\(#function, privacy: .public) - Removed obsolete setting - \(setting.key, privacy: .public)") } } if update, !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - failed to purge manifest") + logger?.error("\(#function, privacy: .public) - failed to purge manifest") return false } - logger.info("\(#function, privacy: .public) - complete") + logger?.info("\(#function, privacy: .public) - completed") return true } @@ -737,7 +673,7 @@ func checkSettings() -> Bool { } } if update, !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - failed to update manifest settings") + logger?.error("\(#function, privacy: .public) - failed to update manifest settings") return false } return true @@ -747,7 +683,7 @@ func updateSettings(_ settings: [String: String]) -> Bool { var manifest = getManifest() manifest.settings = settings if updateManifest(with: manifest) != true { - logger.error("\(#function, privacy: .public) - failed to update settings") + logger?.error("\(#function, privacy: .public) - failed to update settings") return false } return true @@ -755,12 +691,13 @@ func updateSettings(_ settings: [String: String]) -> Bool { // files func getAllFiles(includeCode: Bool = false) -> [[String: Any]]? { + logger?.info("\(#function, privacy: .public) - started") // returns all files of proper type with filenames, metadata & more var files = [[String: Any]]() let fm = FileManager.default let manifest = getManifest() guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } // security scope @@ -770,7 +707,7 @@ func getAllFiles(includeCode: Bool = false) -> [[String: Any]]? { } // get all file urls within save location guard let urls = try? fm.contentsOfDirectory(at: saveLocation, includingPropertiesForKeys: []) else { - logger.error("\(#function, privacy: .public) - failed at (2)") + logger?.error("\(#function, privacy: .public) - failed at (2)") return nil } for url in urls { @@ -788,7 +725,7 @@ func getAllFiles(includeCode: Bool = false) -> [[String: Any]]? { let metadata = parsed["metadata"] as? [String: [String]], let type = filename.split(separator: ".").last else { - logger.info("\(#function, privacy: .public) - ignoring \(filename, privacy: .public), file missing or metadata missing from file contents") + logger?.info("\(#function, privacy: .public) - ignoring \(filename, privacy: .public), file missing or metadata missing from file contents") continue } fileData["canUpdate"] = false @@ -819,7 +756,7 @@ func getAllFiles(includeCode: Bool = false) -> [[String: Any]]? { } files.append(fileData) } - logger.info("\(#function, privacy: .public) - completed") + logger?.info("\(#function, privacy: .public) - completed") return files } @@ -833,7 +770,7 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri do { try FileManager.default.removeItem(at: directory) } catch { - logger.error("\(#function, privacy: .public) - failed to remove directory: \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed to remove directory: \(error.localizedDescription, privacy: .public)") } } return true @@ -845,7 +782,7 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri // get the path of the url string guard let resourceUrlPath = URLComponents(string: resourceUrlString)?.path else { // if path can not be obtained, skip and log - logger.info("\(#function, privacy: .public) - failed to get path on \(filename, privacy: .public) for \(resourceUrlString, privacy: .public)") + logger?.info("\(#function, privacy: .public) - failed to get path on \(filename, privacy: .public) for \(resourceUrlString, privacy: .public)") continue } // skip urls pointing to files of different types @@ -861,13 +798,13 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri // check if file specific folder exists at requires directory if !FileManager.default.fileExists(atPath: directory.path) { guard ((try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false)) != nil) else { - logger.info("\(#function, privacy: .public) - failed to create required code directory for \(filename, privacy: .public)") + logger?.info("\(#function, privacy: .public) - failed to create required code directory for \(filename, privacy: .public)") return false } } // finally write file to directory guard ((try? contents.write(to: fileURL, atomically: false, encoding: .utf8)) != nil) else { - logger.info("\(#function, privacy: .public) - failed to write content to file for \(filename, privacy: .public) from \(resourceUrlString, privacy: .public)") + logger?.info("\(#function, privacy: .public) - failed to write content to file for \(filename, privacy: .public) from \(resourceUrlString, privacy: .public)") return false } } @@ -885,13 +822,13 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri for abandonFileUrl in abandonedUrls { do { try FileManager.default.removeItem(at: abandonFileUrl) - logger.info("\(#function, privacy: .public) - cleanup abandoned resource: \(unsanitize(abandonFileUrl.lastPathComponent), privacy: .public)") + logger?.info("\(#function, privacy: .public) - cleanup abandoned resource: \(unsanitize(abandonFileUrl.lastPathComponent), privacy: .public)") } catch { - logger.error("\(#function, privacy: .public) - failed to remove abandoned resource: \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed to remove abandoned resource: \(error.localizedDescription, privacy: .public)") } } } catch { - logger.error("\(#function, privacy: .public) - failed to cleanup resources: \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed to cleanup resources: \(error.localizedDescription, privacy: .public)") } return true } @@ -901,7 +838,7 @@ func checkForRemoteUpdates(_ optionalFilesArray: [[String: Any]] = []) -> [[Stri var files = [[String: Any]]() if optionalFilesArray.isEmpty { guard let getFiles = getAllFiles() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } files = getFiles @@ -917,7 +854,7 @@ func checkForRemoteUpdates(_ optionalFilesArray: [[String: Any]] = []) -> [[Stri let metadata = file["metadata"] as! [String: [String]] let type = file["type"] as! String let name = metadata["name"]![0] - logger.info("\(#function, privacy: .public) - Checking for remote updates for \(filename, privacy: .public)") + logger?.info("\(#function, privacy: .public) - Checking for remote updates for \(filename, privacy: .public)") if canUpdate { let currentVersion = metadata["version"]![0] let updateUrl = metadata["updateURL"]![0] @@ -929,7 +866,7 @@ func checkForRemoteUpdates(_ optionalFilesArray: [[String: Any]] = []) -> [[Stri let remoteMetadata = remoteFileContentsParsed["metadata"] as? [String: [String]], let remoteVersion = remoteMetadata["version"]?[0] else { - logger.error("\(#function, privacy: .public) - failed to parse remote file contents") + logger?.error("\(#function, privacy: .public) - failed to parse remote file contents") return nil } let remoteVersionNewer = isVersionNewer(currentVersion, remoteVersion) @@ -938,24 +875,24 @@ func checkForRemoteUpdates(_ optionalFilesArray: [[String: Any]] = []) -> [[Stri } } } - logger.info("\(#function, privacy: .public) - Finished checking for remote updates for \(files.count, privacy: .public) files") + logger?.info("\(#function, privacy: .public) - Finished checking for remote updates for \(files.count, privacy: .public) files") return hasUpdates } func getRemoteFileContents(_ url: String) -> String? { - logger.info("\(#function, privacy: .public) - started for \(url, privacy: .public)") + logger?.info("\(#function, privacy: .public) - started for \(url, privacy: .public)") // if url is http change to https var urlChecked = url if urlChecked.hasPrefix("http:") { urlChecked = urlChecked.replacingOccurrences(of: "http:", with: "https:") - logger.info("\(#function, privacy: .public) - \(url, privacy: .public) is using insecure http, attempt to fetch remote content with https") + logger?.info("\(#function, privacy: .public) - \(url, privacy: .public) is using insecure http, attempt to fetch remote content with https") } // if the url is already encoded, decode it if isEncoded(urlChecked) { if let decodedUrl = urlChecked.removingPercentEncoding { urlChecked = decodedUrl } else { - logger.error("\(#function, privacy: .public) - failed at (1), couldn't decode url, \(url, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (1), couldn't decode url, \(url, privacy: .public)") return nil } } @@ -963,12 +900,12 @@ func getRemoteFileContents(_ url: String) -> String? { if let encodedUrl = urlChecked.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { urlChecked = encodedUrl } else { - logger.error("\(#function, privacy: .public) - failed at (2), couldn't percent encode url, \(url, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (2), couldn't percent encode url, \(url, privacy: .public)") return nil } // convert url string to url guard let solidURL = URL(string: urlChecked) else { - logger.error("\(#function, privacy: .public) - failed at (3), couldn't convert string to url, \(url, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (3), couldn't convert string to url, \(url, privacy: .public)") return nil } var contents = "" @@ -991,10 +928,10 @@ func getRemoteFileContents(_ url: String) -> String? { // if made it to this point and contents still an empty string, something went wrong with the request if contents.isEmpty { - logger.error("\(#function, privacy: .public) - failed at (4), contents empty, \(url, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (4), contents empty, \(url, privacy: .public)") return nil } - logger.info("\(#function, privacy: .public) - complete for \(url, privacy: .public)") + logger?.info("\(#function, privacy: .public) - completed for \(url, privacy: .public)") return contents } @@ -1004,7 +941,7 @@ func updateAllFiles(_ optionalFilesArray: [[String: Any]] = []) -> Bool { let filesWithUpdates = checkForRemoteUpdates(optionalFilesArray), let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed to update files (1)") + logger?.error("\(#function, privacy: .public) - failed to update files (1)") return false } // security scope @@ -1022,7 +959,7 @@ func updateAllFiles(_ optionalFilesArray: [[String: Any]] = []) -> Bool { let metadata = parsed["metadata"] as? [String: [String]], let updateUrl = metadata["updateURL"]?[0] else { - logger.error("\(#function, privacy: .public) - failed to update files (2)") + logger?.error("\(#function, privacy: .public) - failed to update files (2)") continue } let downloadUrl = metadata["downloadURL"] != nil ? metadata["downloadURL"]![0] : updateUrl @@ -1030,10 +967,10 @@ func updateAllFiles(_ optionalFilesArray: [[String: Any]] = []) -> Bool { let remoteFileContents = getRemoteFileContents(downloadUrl), ((try? remoteFileContents.write(to: fileUrl, atomically: false, encoding: .utf8)) != nil) else { - logger.error("\(#function, privacy: .public) - failed to update files (3)") + logger?.error("\(#function, privacy: .public) - failed to update files (3)") continue } - logger.info("\(#function, privacy: .public) - updated \(filename, privacy: .public) with contents fetched from \(downloadUrl, privacy: .public)") + logger?.info("\(#function, privacy: .public) - updated \(filename, privacy: .public) with contents fetched from \(downloadUrl, privacy: .public)") } return true } @@ -1041,7 +978,7 @@ func updateAllFiles(_ optionalFilesArray: [[String: Any]] = []) -> Bool { func toggleFile(_ filename: String,_ action: String) -> Bool { // if file doesn't exist return false guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return false } let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() @@ -1050,7 +987,7 @@ func toggleFile(_ filename: String,_ action: String) -> Bool { } let path = saveLocation.appendingPathComponent(filename).path if !FileManager.default.fileExists(atPath: path) { - logger.error("\(#function, privacy: .public) - failed at (2)") + logger?.error("\(#function, privacy: .public) - failed at (2)") return false } var manifest = getManifest() @@ -1063,13 +1000,13 @@ func toggleFile(_ filename: String,_ action: String) -> Bool { // remove filename from disabled array if enabling if (action == "enable") { guard let index = manifest.disabled.firstIndex(of: filename) else { - logger.error("\(#function, privacy: .public) - failed at (3)") + logger?.error("\(#function, privacy: .public) - failed at (3)") return false } manifest.disabled.remove(at: index) } if !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - failed at (4)") + logger?.error("\(#function, privacy: .public) - failed at (4)") return false } return true @@ -1085,7 +1022,7 @@ func checkDefaultDirectories() -> Bool { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false) } catch { // could not create the save location directory, show error - logger.error("\(#function, privacy: .public) - failed at (1) - \(url, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (1) - \(url, privacy: .public) - \(error.localizedDescription, privacy: .public)") return false } } @@ -1100,7 +1037,7 @@ func getUrlProps(_ url: String) -> [String: String]? { let ptcl = parts.scheme, let host = parts.host else { - logger.error("\(#function, privacy: .public) - failed to parse url") + logger?.error("\(#function, privacy: .public) - failed to parse url") return nil } var search = "" @@ -1133,7 +1070,7 @@ func match(_ url: String, _ matchPattern: String) -> Bool { let host = parts["host"], var path = parts["pathname"] else { - logger.error("\(#function, privacy: .public) - invalid url \(url, privacy: .public)") + logger?.error("\(#function, privacy: .public) - invalid url \(url, privacy: .public)") return false } @@ -1155,7 +1092,7 @@ func match(_ url: String, _ matchPattern: String) -> Bool { let partsPatternReg = try! NSRegularExpression(pattern: partsPattern, options: .caseInsensitive) let range = NSMakeRange(0, matchPattern.utf16.count) guard let parts = partsPatternReg.firstMatch(in: matchPattern, options: [], range: range) else { - logger.error("\(#function, privacy: .public) - malformed regex match pattern") + logger?.error("\(#function, privacy: .public) - malformed regex match pattern") return false } // ensure url protocol matches pattern protocol @@ -1169,13 +1106,13 @@ func match(_ url: String, _ matchPattern: String) -> Bool { hostPattern = hostPattern.replacingOccurrences(of: "^*$", with: ".*") hostPattern = hostPattern.replacingOccurrences(of: "*\\.", with: "(.*\\.)?") guard let hostRegEx = try? NSRegularExpression(pattern: hostPattern, options: .caseInsensitive) else { - logger.error("\(#function, privacy: .public) - invalid host regex") + logger?.error("\(#function, privacy: .public) - invalid host regex") return false } // construct path regex from matchPattern let matchPatternPath = matchPattern[Range(parts.range(at: 3), in: matchPattern)!] guard let pathRegEx = stringToRegex(String(matchPatternPath)) else { - logger.error("\(#function, privacy: .public) - invalid path regex") + logger?.error("\(#function, privacy: .public) - invalid path regex") return false } guard @@ -1193,13 +1130,13 @@ func include(_ url: String,_ pattern: String) -> Bool { if pattern.hasPrefix("/") && pattern.hasSuffix("/") { let p = String(pattern.dropFirst().dropLast()) guard let exp = try? NSRegularExpression(pattern: p, options: .caseInsensitive) else { - logger.error("\(#function, privacy: .public) - invalid regex") + logger?.error("\(#function, privacy: .public) - invalid regex") return false } regex = exp } else { guard let exp = stringToRegex(pattern) else { - logger.error("\(#function, privacy: .public) - coudn't convert string to regex") + logger?.error("\(#function, privacy: .public) - coudn't convert string to regex") return false } regex = exp @@ -1211,8 +1148,8 @@ func include(_ url: String,_ pattern: String) -> Bool { } func getMatchedFiles(_ url: String, _ optionalManifest: Manifest?, _ checkBlocklist: Bool) -> [String] { - logger.info("\(#function, privacy: .public) - Getting matched files for \(url, privacy: .private(mask: .hash))") - // logger.debug("\(#function, privacy: .public) - Getting matched files for \(url, privacy: .public)") + logger?.info("\(#function, privacy: .public) - Getting matched files for \(url, privacy: .private(mask: .hash))") + // logger?.debug("\(#function, privacy: .public) - Getting matched files for \(url, privacy: .public)") let manifest = optionalManifest ?? getManifest() // filenames that should not load for the passed url @@ -1245,7 +1182,7 @@ func getMatchedFiles(_ url: String, _ optionalManifest: Manifest?, _ checkBlockl for pattern in excludeMatchPatterns { if match(url, pattern) { guard let filenames = manifest.excludeMatch[pattern] else { - logger.error("\(#function, privacy: .public) - failed at (2) for \(pattern, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (2) for \(pattern, privacy: .public)") continue } excludedFilenames = excludedFilenames.union(filenames) @@ -1254,7 +1191,7 @@ func getMatchedFiles(_ url: String, _ optionalManifest: Manifest?, _ checkBlockl for exp in excludeExpressions { if include(url, exp) { guard let filenames = manifest.exclude[exp] else { - logger.error("\(#function, privacy: .public) - failed at (3) for \(exp, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (3) for \(exp, privacy: .public)") continue } excludedFilenames = excludedFilenames.union(filenames) @@ -1263,7 +1200,7 @@ func getMatchedFiles(_ url: String, _ optionalManifest: Manifest?, _ checkBlockl for pattern in matchPatterns { if match(url, pattern) { guard let filenames = manifest.match[pattern] else { - logger.error("\(#function, privacy: .public) - failed at (4) for \(pattern, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (4) for \(pattern, privacy: .public)") continue } matchedFilenames = matchedFilenames.union(filenames) @@ -1272,15 +1209,15 @@ func getMatchedFiles(_ url: String, _ optionalManifest: Manifest?, _ checkBlockl for exp in includeExpressions { if include(url, exp) { guard let filenames = manifest.include[exp] else { - logger.error("\(#function, privacy: .public) - failed at (5) for \(exp, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (5) for \(exp, privacy: .public)") continue } matchedFilenames = matchedFilenames.union(filenames) } } matchedFilenames = matchedFilenames.subtracting(excludedFilenames) - logger.info("\(#function, privacy: .public) - Got \(matchedFilenames.count) matched files for \(url, privacy: .private(mask: .hash))") - // logger.debug("\(#function, privacy: .public) - Got \(matchedFilenames.count) matched files for \(url, privacy: .public)") + logger?.info("\(#function, privacy: .public) - Got \(matchedFilenames.count) matched files for \(url, privacy: .private(mask: .hash))") + // logger?.debug("\(#function, privacy: .public) - Got \(matchedFilenames.count) matched files for \(url, privacy: .public)") return Array(matchedFilenames) } @@ -1291,7 +1228,7 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { var menuFiles = [Any]() guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } @@ -1302,7 +1239,7 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { let type = filename.split(separator: ".").last else { // if guard fails, log error continue to next file - logger.error("\(#function, privacy: .public) - failed at (2) for \(filename, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (2) for \(filename, privacy: .public)") continue } // can force unwrap b/c getFileContentsParsed ensures metadata exists @@ -1424,7 +1361,7 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { if let requiredContent = try? String(contentsOf: requiredFileURL, encoding: .utf8) { code = "\(requiredContent)\n\(code)" } else { - logger.error("\(#function, privacy: .public) - failed at (3) for \(require, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (3) for \(require, privacy: .public)") } } } @@ -1469,7 +1406,7 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { func getFileContentsParsed(_ url: URL) -> [String: Any]? { guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } // security scope @@ -1493,7 +1430,7 @@ func getInjectionFilenames(_ url: String) -> [String]? { let manifest = getManifest() let matched = getMatchedFiles(url, manifest, true) guard let active = manifest.settings["active"] else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } // if injection is disabled return empty array @@ -1510,7 +1447,7 @@ func getRequestScripts() -> [[String: String]]? { // check the manifest to see if injection is enabled let manifest = getManifest() guard let active = manifest.settings["active"] else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } // if not enabled, do not apply any net requests, ie. return empty array @@ -1518,7 +1455,7 @@ func getRequestScripts() -> [[String: String]]? { return requestScripts } guard let files = getAllFiles(includeCode: true) else { - logger.error("\(#function, privacy: .public) - failed at (2)") + logger?.error("\(#function, privacy: .public) - failed at (2)") return nil } for file in files { @@ -1544,7 +1481,7 @@ func getContextMenuScripts() -> [String: Any]? { // check the manifest to see if injection is enabled let manifest = getManifest() guard let active = manifest.settings["active"] else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } // if not enabled return empty array @@ -1553,17 +1490,17 @@ func getContextMenuScripts() -> [String: Any]? { } // get all files at save location guard let files = getAllFiles() else { - logger.error("\(#function, privacy: .public) - failed at (2)") + logger?.error("\(#function, privacy: .public) - failed at (2)") return nil } // loop through files and find @run-at context-menu script filenames for file in files { guard let filename = file["filename"] as? String else { - logger.error("\(#function, privacy: .public) - failed at (3), couldn't get filename") + logger?.error("\(#function, privacy: .public) - failed at (3), couldn't get filename") continue } guard let fileMetadata = file["metadata"] as? [String: [String]] else { - logger.error("\(#function, privacy: .public) - failed at (4), couldn't get metadata for \(filename, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (4), couldn't get metadata for \(filename, privacy: .public)") continue } let runAt = fileMetadata["run-at"]?[0] ?? "document-end" @@ -1574,7 +1511,7 @@ func getContextMenuScripts() -> [String: Any]? { } // get and return script objects for all context-menu scripts guard let scripts = getCode(menuFilenames, true) else { - logger.error("\(#function, privacy: .public) - failed at (5)") + logger?.error("\(#function, privacy: .public) - failed at (5)") return nil } return scripts @@ -1593,7 +1530,7 @@ func getPopupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]] guard let files = getAllFiles() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } // filter out the files that are present in both files and matched @@ -1656,7 +1593,7 @@ func getPopupBadgeCount(_ url: String, _ subframeUrls: [String]) -> Int? { guard var matches = getPopupMatches(url, subframeUrls) else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } for pattern in manifest.blacklist { @@ -1670,7 +1607,7 @@ func getPopupBadgeCount(_ url: String, _ subframeUrls: [String]) -> Int? { func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [String]) -> [[String: Any]]? { guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } // security scope @@ -1685,7 +1622,7 @@ func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [Strin let metadata = parsed["metadata"] as? [String: [String]], let updateUrl = metadata["updateURL"]?[0] else { - logger.error("\(#function, privacy: .public) - failed at (2)") + logger?.error("\(#function, privacy: .public) - failed at (2)") return nil } let downloadUrl = metadata["downloadURL"] != nil ? metadata["downloadURL"]![0] : updateUrl @@ -1693,7 +1630,7 @@ func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [Strin let remoteFileContents = getRemoteFileContents(downloadUrl), ((try? remoteFileContents.write(to: fileUrl, atomically: false, encoding: .utf8)) != nil) else { - logger.error("\(#function, privacy: .public) - failed at (3)") + logger?.error("\(#function, privacy: .public) - failed at (3)") return nil } guard @@ -1703,7 +1640,7 @@ func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [Strin purgeManifest(files), let matches = getPopupMatches(url, subframeUrls) else { - logger.error("\(#function, privacy: .public) - failed at (4)") + logger?.error("\(#function, privacy: .public) - failed at (4)") return nil } return matches @@ -1712,7 +1649,7 @@ func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [Strin // page func getInitData() -> [String: Any]? { guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return nil } return [ @@ -1733,7 +1670,7 @@ func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { var response = [String: Any]() let newContent = content guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return ["error": "failed to get save location when attempting to save"] } guard @@ -1798,7 +1735,7 @@ func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { do { try newContent.write(to: newFileUrl, atomically: false, encoding: .utf8) } catch { - logger.error("\(#function, privacy: .public) - failed at (2)") + logger?.error("\(#function, privacy: .public) - failed at (2)") return ["error": "failed to write file to disk"] } @@ -1808,7 +1745,7 @@ func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { guard let dateMod = try? FileManager.default.attributesOfItem(atPath: newFileUrl.path)[.modificationDate] as? Date else { - logger.error("\(#function, privacy: .public) - failed at (3)") + logger?.error("\(#function, privacy: .public) - failed at (3)") return ["error": "failed to read modified date in save function"] } @@ -1831,7 +1768,7 @@ func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { updateManifestDeclarativeNetRequests(allFiles), purgeManifest(allFiles) else { - logger.error("\(#function, privacy: .public) - failed at (4)") + logger?.error("\(#function, privacy: .public) - failed at (4)") return ["error": "file save but manifest couldn't be updated"] } @@ -1864,7 +1801,7 @@ func trashFile(_ item: [String: Any]) -> Bool { let saveLocation = getSaveLocation(), let filename = item["filename"] as? String else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return false } // security scope @@ -1878,13 +1815,13 @@ func trashFile(_ item: [String: Any]) -> Bool { do { try FileManager.default.trashItem(at: url, resultingItemURL: nil) } catch { - logger.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") return false } } // update manifest guard updateManifestMatches(), updateManifestRequired(), purgeManifest() else { - logger.error("\(#function, privacy: .public) - failed at (2)") + logger?.error("\(#function, privacy: .public) - failed at (2)") return false } return true; @@ -1945,11 +1882,11 @@ func getFileRemoteUpdate(_ content: String) -> [String: String] { // background func nativeChecks() -> [String: String] { - logger.info("\(#function, privacy: .public) - started") + logger?.info("\(#function, privacy: .public) - started") #if os(iOS) // check the save location is set guard (getSaveLocation() != nil) else { - logger.error("\(#function, privacy: .public) - save location unset (iOS)") + logger?.error("\(#function, privacy: .public) - save location unset (iOS)") return [ "error": "Native checks error (0)", "saveLocation": "unset", @@ -1959,41 +1896,41 @@ func nativeChecks() -> [String: String] { #endif // check the default directories guard checkDefaultDirectories() else { - logger.error("\(#function, privacy: .public) - checkDefaultDirectories failed") + logger?.error("\(#function, privacy: .public) - checkDefaultDirectories failed") return ["error": "Native checks error (1)"] } // check the settings guard checkSettings() else { - logger.error("\(#function, privacy: .public) - checkSettings failed") + logger?.error("\(#function, privacy: .public) - checkSettings failed") return ["error": "Native checks error (2)"] } // get all files to pass as arguments to function below guard let allFiles = getAllFiles() else { - logger.error("\(#function, privacy: .public) - getAllFiles failed") + logger?.error("\(#function, privacy: .public) - getAllFiles failed") return ["error": "Native checks error (3)"] } // purge the manifest of old records guard purgeManifest(allFiles) else { - logger.error("\(#function, privacy: .public) - purgeManifest failed") + logger?.error("\(#function, privacy: .public) - purgeManifest failed") return ["error": "Native checks error (4)"] } // update matches in manifest guard updateManifestMatches(allFiles) else { - logger.error("\(#function, privacy: .public) - updateManifestMatches failed") + logger?.error("\(#function, privacy: .public) - updateManifestMatches failed") return ["error": "Native checks error (5)"] } // update the required resources guard updateManifestRequired(allFiles) else { - logger.error("\(#function, privacy: .public) - updateManifestRequired failed") + logger?.error("\(#function, privacy: .public) - updateManifestRequired failed") return ["error": "Native checks error (6)"] } // update declarativeNetRequest guard updateManifestDeclarativeNetRequests(allFiles) else { - logger.error("\(#function, privacy: .public) - updateManifestDeclarativeNetRequests failed") + logger?.error("\(#function, privacy: .public) - updateManifestDeclarativeNetRequests failed") return ["error": "Native checks error (7)"] } // pass some info in response - logger.info("\(#function, privacy: .public) - complete") + logger?.info("\(#function, privacy: .public) - completed") return ["success": "Native checks complete"] } @@ -2002,7 +1939,7 @@ func installCheck(_ content: String) -> [String: Any] { // this func checks a userscript's metadata to determine if it's already installed guard let files = getAllFiles() else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return ["error": "installCheck failed at (1)"] } @@ -2054,7 +1991,7 @@ func installUserscript(_ content: String) -> [String: Any] { let metadata = parsed["metadata"] as? [String: [String]], let n = metadata["name"]?[0] else { - logger.error("\(#function, privacy: .public) - failed at (1)") + logger?.error("\(#function, privacy: .public) - failed at (1)") return ["error": "installUserscript failed at (1)"] } let name = sanitize(n) diff --git a/xcode/Safari-Extension/Info.plist b/xcode/Safari-Extension/Info.plist index eda50006..2cc72fe3 100644 --- a/xcode/Safari-Extension/Info.plist +++ b/xcode/Safari-Extension/Info.plist @@ -33,6 +33,8 @@ $(MAC_SHARED_GID) US_SHARED_GID-iphoneos $(IOS_SHARED_GID) + US_EXT_IDENTIFIER + $(EXT_IDENTIFIER) US_URL_SCHEME $(APP_URL_SCHEME) diff --git a/xcode/Safari-Extension/Mac.entitlements b/xcode/Safari-Extension/Mac.entitlements index f123ee4d..aa2f466a 100644 --- a/xcode/Safari-Extension/Mac.entitlements +++ b/xcode/Safari-Extension/Mac.entitlements @@ -8,8 +8,6 @@ $(MAC_SHARED_GID) - com.apple.security.files.bookmarks.app-scope - com.apple.security.files.user-selected.read-write com.apple.security.network.client diff --git a/xcode/Safari-Extension/SafariWebExtensionHandler.swift b/xcode/Safari-Extension/SafariWebExtensionHandler.swift index d6bb4afc..c8e93769 100644 --- a/xcode/Safari-Extension/SafariWebExtensionHandler.swift +++ b/xcode/Safari-Extension/SafariWebExtensionHandler.swift @@ -3,15 +3,15 @@ import os class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { func beginRequest(with context: NSExtensionContext) { - let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) + let logger = USLogger(#fileID) let item = context.inputItems[0] as? NSExtensionItem let message = item?.userInfo?[SFExtensionMessageKey] as? [String: Any] // if message received without name, ignore guard let name = message?["name"] as? String else { - logger.error("\(#function, privacy: .public) - could not get message name from web extension") + logger?.error("\(#function, privacy: .public) - could not get message name from web extension") return } - logger.info("\(#function, privacy: .public) - Got message with name: \(name, privacy: .public)") + logger?.info("\(#function, privacy: .public) - Got message with name: \(name, privacy: .public)") // got a valid message, construct response based on message received let response = NSExtensionItem() // send standard error when there's an issue parsing inbound message diff --git a/xcode/Shared/Preferences.swift b/xcode/Shared/Preferences.swift new file mode 100644 index 00000000..2ca9c634 --- /dev/null +++ b/xcode/Shared/Preferences.swift @@ -0,0 +1,306 @@ +import Foundation +import os + +private let logger = USLogger(#fileID) +private let suiteName = Bundle.main.infoDictionary!["US_SHARED_GID"] as! String +private let SDefaults = UserDefaults(suiteName: suiteName) // Shared UserDefaults + +let bundleIdentifier = Bundle.main.bundleIdentifier! +let extIdentifier = Bundle.main.infoDictionary?["US_EXT_IDENTIFIER"] as! String + +func getDefaultScriptsDirectoryUrl() -> URL { + var url: URL + if #available(macOS 13.0, iOS 16.0, *) { + url = getDocumentsDirectory().appending(path: "scripts") + } else { + url = getDocumentsDirectory().appendingPathComponent("scripts") + } + // if not in safari extension environment, replace with extension path + if bundleIdentifier != extIdentifier { + var path: String + if #available(macOS 13.0, iOS 16.0, *) { + path = url.path(percentEncoded: false) + } else { + path = url.path.removingPercentEncoding ?? url.path + } + let truePath = path.replacingOccurrences(of: bundleIdentifier, with: extIdentifier) + url = URL(fileURLWithPath: truePath, isDirectory: true) + } + return url +} + +// https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties#Property-Wrappers +// https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/ +@propertyWrapper +private struct SharedUserDefaults { + private let key: String + private let defaultValue: T + + init(wrappedValue: T, _ key: String) { + self.key = key + self.defaultValue = wrappedValue + SDefaults?.register(defaults: [key: wrappedValue]) + } + + var wrappedValue: T { + get { SDefaults?.object(forKey: key) as? T ?? defaultValue } + set { SDefaults?.set(newValue, forKey: key) } + } +} + +#if os(macOS) +/* https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox + # Design ideas and processes + Since the security-scoped URL bookmark is only available in the current app, + pass to extensions can only use URL bookmark with an implicit security scope. + app set URL write to: + - `_SetterId` (setter app bundleIdentifier) + - `_Transfer` (with an implicit security scope) + - `_SecurityScoped_` (with an explicit security scope) + app get URL read from: `_SecurityScoped_` + ext get URL read flow: + - if `_Transfer` exist, remove to `_SecurityScoped_` + - if `_Transfer` empty, read from `_SecurityScoped_` + */ +@propertyWrapper +private struct SecurityScopedBookmark { + private let key: String + private let keySetter: String + private let keyTransfer: String + private let keySecurityScoped: String + private let defaultValue: URL + + init(wrappedValue: URL, _ key: String) { + self.key = key + self.keySetter = key + "/SetterId" + self.keyTransfer = key + "/Transfer" + self.keySecurityScoped = key + "/SecurityScoped/" + bundleIdentifier + self.defaultValue = wrappedValue + } + + private func createBookmark(_ url: URL, _ withSecurityScope: Bool) -> Data? { + do { + return try url.bookmarkData( + options: withSecurityScope ? .withSecurityScope : [], + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + } catch { + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + } + return nil + } + + private func resolvBookmark(_ data: Data, _ withSecurityScope: Bool, _ isStale: inout Bool) -> URL? { + do { + return try URL( + resolvingBookmarkData: data, + options: withSecurityScope ? .withSecurityScope : [], + relativeTo: nil, + bookmarkDataIsStale: &isStale + ) + } catch { + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + } + return nil + } + + private func updateBookmark() { + guard let setterIdentifier = SDefaults?.string(forKey: keySetter) else { + logger?.debug("\(#function, privacy: .public) - setterId not exist: \(key, privacy: .public)") + return + } + guard bundleIdentifier != setterIdentifier else { return } // update only in non-setter environment + guard let data = SDefaults?.data(forKey: keyTransfer) else { // no need to update due empty data + logger?.debug("\(#function, privacy: .public) - no update: \(key, privacy: .public)") + return + } + var isStale = false // no need to renew since will remove anyway + guard let url = resolvBookmark(data, false, &isStale) else { // get URL bookmark with an implicit security scope + removeBookmark() + return + } + defer { url.stopAccessingSecurityScopedResource() } // revoke implicitly starts security-scoped access + if let data = createBookmark(url, true) { // set URL bookmark with an explicit security scope + logger?.info("\(#function, privacy: .public) - update bookmark: \(key, privacy: .public) \(url, privacy: .public)") + SDefaults?.removeObject(forKey: keyTransfer) + SDefaults?.set(data, forKey: keySecurityScoped) + } + } + + private func removeBookmark() { + logger?.info("\(#function, privacy: .public) - remove invalid bookmark: \(key, privacy: .public)") + SDefaults?.removeObject(forKey: keyTransfer) + SDefaults?.removeObject(forKey: keySecurityScoped) + if let setterIdentifier = SDefaults?.string(forKey: keySetter) { + SDefaults?.removeObject(forKey: key + "/SecurityScoped/" + setterIdentifier) + } + } + + // NOTE: This function will be deleted after several public versions of v4.5.0 + private func legacyBookmarkImporter() { + guard key == "ScriptsDirectoryUrlBookmarkData" else { return } + guard bundleIdentifier == extIdentifier else { return } + func legacyCleanup() { + logger?.debug("\(#function, privacy: .public) - cleanup legacy bookmark data") + SDefaults?.removeObject(forKey: "hostSelectedSaveLocation") // shared + UserDefaults.standard.removeObject(forKey: "saveLocation") // ext + UserDefaults.standard.removeObject(forKey: "userSaveLocation") // ext + } + guard let data = UserDefaults.standard.data(forKey: "userSaveLocation") else { // get from old key + logger?.debug("\(#function, privacy: .public) - legacy bookmark not exist") + return + } + guard SDefaults?.string(forKey: keySetter) == nil else { // already a new key + return legacyCleanup() + } + logger?.debug("\(#function, privacy: .public) - Import legacy bookmark data") + var isStale = false // no need to renew since will remove anyway + guard let url = resolvBookmark(data, true, &isStale) else { // get URL bookmark with an explicit security scope + return legacyCleanup() + } + guard url.startAccessingSecurityScopedResource() else { + return legacyCleanup() + } + defer { url.stopAccessingSecurityScopedResource() } + guard let transferData = createBookmark(url, false) else { + return legacyCleanup() + } + SDefaults?.set(bundleIdentifier, forKey: keySetter) + SDefaults?.set(transferData, forKey: keyTransfer) // set URL bookmark with an implicit security scope + SDefaults?.set(data, forKey: keySecurityScoped) // set URL bookmark with an explicit security scope + legacyCleanup() + } + + var wrappedValue: URL { + get { + legacyBookmarkImporter() + updateBookmark() + logger?.info("\(#function, privacy: .public) - try get bookmark: \(key, privacy: .public)") + guard let data = SDefaults?.data(forKey: keySecurityScoped) else { + logger?.debug("\(#function, privacy: .public) - bookmark not exist: \(key, privacy: .public)") + return defaultValue + } + var isStale = false + guard let url = resolvBookmark(data, true, &isStale) else { // get URL bookmark with an explicit security scope + removeBookmark() // remove data that cannot be resolved + return defaultValue + } + if isStale, url.startAccessingSecurityScopedResource() { // renew URL bookmark + defer { url.stopAccessingSecurityScopedResource() } + if let data = createBookmark(url, true) { // set URL bookmark with an explicit security scope + logger?.info("\(#function, privacy: .public) - renew bookmark: \(key, privacy: .public) \(url, privacy: .public)") + SDefaults?.set(data, forKey: keySecurityScoped) + } + } + return url + } + set(url) { + let k = key // key cannot be log directly with error: Escaping autoclosure captures mutating 'self' parameter + logger?.info("\(#function, privacy: .public) - try set bookmark: \(k, privacy: .public) \(url, privacy: .public)") + guard let tdata = createBookmark(url, false), let sdata = createBookmark(url, true) else { + logger?.info("\(#function, privacy: .public) - failed create bookmark: \(k, privacy: .public) \(url, privacy: .public)") + return + } + SDefaults?.set(bundleIdentifier, forKey: keySetter) + SDefaults?.set(tdata, forKey: keyTransfer) // set URL bookmark with an implicit security scope + SDefaults?.set(sdata, forKey: keySecurityScoped) // set URL bookmark with an explicit security scope + } + } +} +#elseif os(iOS) +/* https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories#3331285 + iOS and macOS have completely different processes, iOS has no implicit security scope + The security-scoped URL is not bind to the app and can be shared directly between the app and ext + Separating the wrappers for the two platforms is intentional for ease of viewing code logic + */ +@propertyWrapper +private struct SecurityScopedBookmark { + private let key: String + private let defaultValue: URL + + init(wrappedValue: URL, _ key: String) { + self.key = key + self.defaultValue = wrappedValue + } + + private func createBookmark(_ url: URL) -> Data? { + do { + return try url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil, relativeTo: nil) + } catch { + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + } + return nil + } + + private func resolvBookmark(_ data: Data, _ isStale: inout Bool) -> URL? { + do { + return try URL(resolvingBookmarkData: data, bookmarkDataIsStale: &isStale) + } catch { + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + } + return nil + } + + // NOTE: This function will be deleted after several public versions of v4.5.0 + private func legacyBookmarkImporter() { + guard let data = SDefaults?.data(forKey: "iosReadLocation") else { // get from old key + logger?.debug("\(#function, privacy: .public) - legacy bookmark not exist") + return + } + logger?.debug("\(#function, privacy: .public) - Import legacy bookmark data") + SDefaults?.set(data, forKey: key) + SDefaults?.removeObject(forKey: "iosReadLocation") + } + + var wrappedValue: URL { + get { + legacyBookmarkImporter() + logger?.info("\(#function, privacy: .public) - try get bookmark: \(key, privacy: .public)") + guard let data = SDefaults?.data(forKey: key) else { + logger?.debug("\(#function, privacy: .public) - bookmark not exist: \(key, privacy: .public)") + return defaultValue + } + var isStale = false + guard let url = resolvBookmark(data, &isStale) else { // get security-scoped URL + SDefaults?.removeObject(forKey: key) // remove data that cannot be resolved + return defaultValue + } + if isStale, url.startAccessingSecurityScopedResource() { // renew URL bookmark + defer { url.stopAccessingSecurityScopedResource() } + if let data = createBookmark(url) { // set security-scoped URL + logger?.info("\(#function, privacy: .public) - renew bookmark: \(key, privacy: .public) \(url, privacy: .public)") + SDefaults?.set(data, forKey: key) + } + } + return url + } + set(url) { + let k = key // key cannot be log directly with error: Escaping autoclosure captures mutating 'self' parameter + logger?.info("\(#function, privacy: .public) - try set bookmark: \(k, privacy: .public) \(url, privacy: .public)") + let didStartAccessing = url.startAccessingSecurityScopedResource() + defer { + if didStartAccessing { url.stopAccessingSecurityScopedResource() } + } + guard let data = createBookmark(url) else { + logger?.info("\(#function, privacy: .public) - failed create bookmark: \(k, privacy: .public) \(url, privacy: .public)") + return + } + SDefaults?.set(data, forKey: key) // set security-scoped URL + } + } +} +#endif + +// Define shared preferences, default values ​​determine data type +struct Preferences { +/* Example preference, can be get or set with: Preferences.propertyName + @SharedUserDefaults("SharedUserDefaultsKeyName") + static var propertyName = "DefaultValue" // String +*/ + @SecurityScopedBookmark("ScriptsDirectoryUrlBookmarkData") + static var scriptsDirectoryUrl = getDefaultScriptsDirectoryUrl() + + @SharedUserDefaults("EnableLogger") + static var enableLogger = false +} diff --git a/xcode/Shared/Shared.swift b/xcode/Shared/Shared.swift index 7f2c4fb8..d07a5667 100644 --- a/xcode/Shared/Shared.swift +++ b/xcode/Shared/Shared.swift @@ -2,16 +2,16 @@ import Foundation import SafariServices import os -fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) - -struct SharedDefaults { - // https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups - static let suiteName = Bundle.main.infoDictionary?["US_SHARED_GID"] as? String - #if os(iOS) - static let keyName = "iosReadLocation" - #elseif os(macOS) - static let keyName = "hostSelectedSaveLocation" - #endif +func USLogger(_ category: String) -> Logger? { + let subsystem = Bundle.main.bundleIdentifier! +#if DEBUG // Always enable logger in DEBUG builds + return Logger(subsystem: subsystem, category: category) +#else + if Preferences.enableLogger { + return Logger(subsystem: subsystem, category: category) + } + return nil +#endif } func getDocumentsDirectory() -> URL { @@ -42,60 +42,3 @@ func getPlatform() -> String { #endif return platform } - -func saveBookmark(url: URL, isShared: Bool, keyName: String, isSecure: Bool) -> Bool { - #if os(iOS) - let options:URL.BookmarkCreationOptions = [] - #elseif os(macOS) - let options:URL.BookmarkCreationOptions = isSecure ? [.withSecurityScope] : [] - #endif - do { - let bookmark = try url.bookmarkData( - options: options, - includingResourceValuesForKeys: nil, - relativeTo: nil - ) - #if os(iOS) - UserDefaults(suiteName: SharedDefaults.suiteName)?.set(bookmark, forKey: keyName) - #elseif os(macOS) - if isShared { - UserDefaults(suiteName: SharedDefaults.suiteName)?.set(bookmark, forKey: keyName) - } else { - UserDefaults.standard.set(bookmark, forKey: keyName) - } - #endif - return true - } catch let error { - logger.error("\(#function, privacy: .public) - \(error, privacy: .public)") - return false - } -} - -func readBookmark(data: Data, isSecure: Bool) -> URL? { - #if os(iOS) - let options:URL.BookmarkResolutionOptions = [] - #elseif os(macOS) - let options:URL.BookmarkResolutionOptions = isSecure ? [.withSecurityScope] : [] - #endif - do { - var bookmarkIsStale = false - let url = try URL( - resolvingBookmarkData: data, - options: options, - relativeTo: nil, - bookmarkDataIsStale: &bookmarkIsStale - ) - if bookmarkIsStale { - NSLog("Stale bookmark, renewing it \(url)") - if saveBookmark(url: url, isShared: true, keyName: SharedDefaults.keyName, isSecure: false) { - NSLog("Successfully renewed stale bookmark - \(url)") - } else { - NSLog("Could not renew stale bookmark - \(url)") - } - } - return url - } catch let error { - logger.error("\(#function, privacy: .public) - \(error, privacy: .public)") - return nil - } -} diff --git a/xcode/Userscripts.xcodeproj/project.pbxproj b/xcode/Userscripts.xcodeproj/project.pbxproj index 8286d541..1f7f2371 100644 --- a/xcode/Userscripts.xcodeproj/project.pbxproj +++ b/xcode/Userscripts.xcodeproj/project.pbxproj @@ -7,9 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 030C64F62AC62CC900548FBD /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 030C64F52AC62CC900548FBD /* Settings.bundle */; }; 0336619F294DF7C900CFE179 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0336619E294DF7C900CFE179 /* Functions.swift */; }; 033661A529510B7900CFE179 /* View.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033661A329510B7900CFE179 /* View.storyboard */; }; 0376905B29808EEA00F3474D /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = 0376905A29808EEA00F3474D /* background.js */; }; + 03C24F972ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; + 03C24F982ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; + 03C24F992ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; + 03C24F9A2ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; + 03C24F9B2ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; 03E20D06291E170000B4F692 /* dist in Resources */ = {isa = PBXBuildFile; fileRef = 03E20D05291E170000B4F692 /* dist */; }; 03E20D07291E170000B4F692 /* dist in Resources */ = {isa = PBXBuildFile; fileRef = 03E20D05291E170000B4F692 /* dist */; }; 4A143AAC279DE6FF0029BFD0 /* UserscriptsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */; }; @@ -99,9 +105,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 030C64F52AC62CC900548FBD /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 0336619E294DF7C900CFE179 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 033661A429510B7900CFE179 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/View.storyboard; sourceTree = ""; }; 0376905A29808EEA00F3474D /* background.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = background.js; sourceTree = ""; }; + 03C24F962ABD2CBB00F130F9 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 03E20D05291E170000B4F692 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; 4A143AA9279DE6FF0029BFD0 /* Mac-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Mac-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserscriptsTests.swift; sourceTree = ""; }; @@ -196,6 +204,7 @@ children = ( 4A4CF6F2270A38BF00111584 /* Info.plist */, 4A4CF718270A38C700111584 /* App.entitlements */, + 030C64F52AC62CC900548FBD /* Settings.bundle */, 4A4CF6DF270A38BD00111584 /* Resources */, 4A4CF6F1270A38BF00111584 /* Assets.xcassets */, 4A4CF6DD270A38BD00111584 /* AppDelegate.swift */, @@ -259,12 +268,12 @@ 4A57BA06227235CE008A9763 /* Safari-Extension */ = { isa = PBXGroup; children = ( - 4AF1DD0D268265DB00086E88 /* Resources */, - 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */, - 4AED6491268CDB58001794BF /* Functions.swift */, 4A57BA0E227235CE008A9763 /* Info.plist */, 4A57BA1D227235E6008A9763 /* Mac.entitlements */, 7878ED2D299BA40800E36A24 /* iOS.entitlements */, + 4AF1DD0D268265DB00086E88 /* Resources */, + 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */, + 4AED6491268CDB58001794BF /* Functions.swift */, ); path = "Safari-Extension"; sourceTree = ""; @@ -300,6 +309,7 @@ isa = PBXGroup; children = ( 78B25582299D4C27000B2E9B /* Shared.swift */, + 03C24F962ABD2CBB00F130F9 /* Preferences.swift */, ); path = Shared; sourceTree = ""; @@ -333,6 +343,7 @@ 4A4CF6D8270A38BD00111584 /* Frameworks */, 4A4CF6D9270A38BD00111584 /* Resources */, 4A4CF713270A38BF00111584 /* Embed Foundation Extensions */, + 030C64F72AC713A600548FBD /* Update Settings.bundle */, ); buildRules = ( ); @@ -468,6 +479,7 @@ 4A4CF6F0270A38BD00111584 /* Main.storyboard in Resources */, 4A4CF6ED270A38BD00111584 /* LaunchScreen.storyboard in Resources */, 4A4CF6E2270A38BD00111584 /* Main.html in Resources */, + 030C64F62AC62CC900548FBD /* Settings.bundle in Resources */, 4A4CF710270A38BF00111584 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -510,6 +522,30 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 030C64F72AC713A600548FBD /* Update Settings.bundle */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/$(TARGET_NAME)/Settings.bundle/Root.plist", + "$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)", + ); + name = "Update Settings.bundle"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)/Settings.bundle/Root.plist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Dynamically introduce US_SHARED_GID in xcconfig for Settings.bundle\nUS_SHARED_GID=$(/usr/libexec/PlistBuddy -c \"Print US_SHARED_GID\" \"$TARGET_BUILD_DIR/$INFOPLIST_PATH\")\nSETTINGS_BUNDLE=$TARGET_BUILD_DIR/$EXECUTABLE_FOLDER_PATH/Settings.bundle/Root.plist\n/usr/libexec/PlistBuddy -c \"Set ApplicationGroupContainerIdentifier $US_SHARED_GID\" \"$SETTINGS_BUNDLE\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 4A143AA5279DE6FF0029BFD0 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -517,6 +553,7 @@ files = ( 7878ED24299BA27B00E36A24 /* SafariWebExtensionHandler.swift in Sources */, 4A143AB2279DEA170029BFD0 /* Functions.swift in Sources */, + 03C24F9B2ABD2CBB00F130F9 /* Preferences.swift in Sources */, 78B25587299D4C27000B2E9B /* Shared.swift in Sources */, 4A143AAC279DE6FF0029BFD0 /* UserscriptsTests.swift in Sources */, ); @@ -528,6 +565,7 @@ files = ( 4A4CF6EA270A38BD00111584 /* ViewController.swift in Sources */, 4A4CF6DE270A38BD00111584 /* AppDelegate.swift in Sources */, + 03C24F992ABD2CBB00F130F9 /* Preferences.swift in Sources */, 78B25585299D4C27000B2E9B /* Shared.swift in Sources */, 4A4CF6E8270A38BD00111584 /* SceneDelegate.swift in Sources */, ); @@ -539,6 +577,7 @@ files = ( 78B25586299D4C27000B2E9B /* Shared.swift in Sources */, 4A301B2A270A474400C7E9E1 /* SafariWebExtensionHandler.swift in Sources */, + 03C24F9A2ABD2CBB00F130F9 /* Preferences.swift in Sources */, 4A301B32270A49F200C7E9E1 /* Functions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -549,6 +588,7 @@ files = ( 4A57B9F7227235CD008A9763 /* ViewController.swift in Sources */, 4A57B9F2227235CD008A9763 /* AppDelegate.swift in Sources */, + 03C24F972ABD2CBB00F130F9 /* Preferences.swift in Sources */, 78B25583299D4C27000B2E9B /* Shared.swift in Sources */, 0336619F294DF7C900CFE179 /* Functions.swift in Sources */, ); @@ -560,6 +600,7 @@ files = ( 78B25584299D4C27000B2E9B /* Shared.swift in Sources */, 4A57BA08227235CE008A9763 /* SafariWebExtensionHandler.swift in Sources */, + 03C24F982ABD2CBB00F130F9 /* Preferences.swift in Sources */, 4AED6492268CDB58001794BF /* Functions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/xcode/iOS-App/AppDelegate.swift b/xcode/iOS-App/AppDelegate.swift index e1fe4a8a..8bb92da4 100644 --- a/xcode/iOS-App/AppDelegate.swift +++ b/xcode/iOS-App/AppDelegate.swift @@ -7,6 +7,7 @@ // import UIKit +import os @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -15,6 +16,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. + let logger = USLogger(#fileID) + // set the scripts directory to the app document on first use + if Preferences.scriptsDirectoryUrl == getDefaultScriptsDirectoryUrl() { + logger?.info("\(#function, privacy: .public) - Initialize default directory") + Preferences.scriptsDirectoryUrl = getDocumentsDirectory() + } return true } diff --git a/xcode/iOS-App/Base.lproj/Main.html b/xcode/iOS-App/Base.lproj/Main.html index e718ab5a..bfd9ae28 100644 --- a/xcode/iOS-App/Base.lproj/Main.html +++ b/xcode/iOS-App/Base.lproj/Main.html @@ -1 +1 @@ -
Userscripts App Icon

You can turn on the Userscripts iOS Safari extension in Settings. Read the docs.

CURRENT DIRECTORY:
init
+
Userscripts App Icon

You can turn on the Userscripts iOS Safari extension in Settings. Read the docs.

CURRENT DIRECTORY:
init
diff --git a/xcode/iOS-App/Info.plist b/xcode/iOS-App/Info.plist index a5539a85..b6d4f637 100644 --- a/xcode/iOS-App/Info.plist +++ b/xcode/iOS-App/Info.plist @@ -38,7 +38,13 @@ UIStatusBarStyleLightContent UIViewControllerBasedStatusBarAppearance + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + US_SHARED_GID $(IOS_SHARED_GID) + US_EXT_IDENTIFIER + $(EXT_IDENTIFIER) diff --git a/xcode/iOS-App/Settings.bundle/Root.plist b/xcode/iOS-App/Settings.bundle/Root.plist new file mode 100644 index 00000000..b9ad077e --- /dev/null +++ b/xcode/iOS-App/Settings.bundle/Root.plist @@ -0,0 +1,31 @@ + + + + + ApplicationGroupContainerIdentifier + // US_SHARED_GID will be introduced in the custom build script + StringsTable + Root + PreferenceSpecifiers + + + Type + PSGroupSpecifier + Title + GroupDebug + FooterText + FooterText + + + Type + PSToggleSwitchSpecifier + Title + EnableLogger + Key + EnableLogger + DefaultValue + + + + + diff --git a/xcode/iOS-App/Settings.bundle/en.lproj/Root.strings b/xcode/iOS-App/Settings.bundle/en.lproj/Root.strings new file mode 100644 index 00000000..dc826ef0 Binary files /dev/null and b/xcode/iOS-App/Settings.bundle/en.lproj/Root.strings differ diff --git a/xcode/iOS-App/ViewController.swift b/xcode/iOS-App/ViewController.swift index f1a3347c..d7734b76 100644 --- a/xcode/iOS-App/ViewController.swift +++ b/xcode/iOS-App/ViewController.swift @@ -11,7 +11,7 @@ import WebKit import UniformTypeIdentifiers import os -fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) +private let logger = USLogger(#fileID) class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler, UIDocumentPickerDelegate { @@ -33,17 +33,7 @@ class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHan func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" - var readLocation:String - if let sharedBookmarkData = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName) { - if let bookmarkUrl = readBookmark(data: sharedBookmarkData, isSecure: true) { - readLocation = bookmarkUrl.absoluteString - } else { - readLocation = "Failed to get read directory" - } - } else { - readLocation = "Select a directory to load userscripts" - } - webView.evaluateJavaScript("printDirectory('\(readLocation)')") + webView.evaluateJavaScript("printDirectory('\(Preferences.scriptsDirectoryUrl.absoluteString)')") webView.evaluateJavaScript("printVersion('v\(appVersion)', '(\(buildNumber))')") } @@ -64,32 +54,21 @@ class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHan func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let name = message.body as? String else { - logger.error("\(#function, privacy: .public) - Userscripts iOS received a message without a name") + logger?.error("\(#function, privacy: .public) - Userscripts iOS received a message without a name") return } if name == "SET_READ_LOCATION" { - logger.info("\(#function, privacy: .public) - Userscripts iOS has requested to set the readLocation") + // https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories + logger?.info("\(#function, privacy: .public) - Userscripts iOS has requested to set the readLocation") let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder]) documentPicker.delegate = self + documentPicker.directoryURL = Preferences.scriptsDirectoryUrl present(documentPicker, animated: true, completion: nil) } } func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { - // https://developer.apple.com/videos/play/wwdc2018/216 - do { - let shouldStopAccessing = url.startAccessingSecurityScopedResource() - defer { - if shouldStopAccessing {url.stopAccessingSecurityScopedResource()} - } - if saveBookmark(url: url, isShared: true, keyName: SharedDefaults.keyName, isSecure: false) { - webView.evaluateJavaScript("printDirectory('\(url.absoluteString)')") - } else { - throw NSError(domain: "Failed to saved bookmark", code: 0, userInfo: [:]) - } - } catch let error { - logger.error("\(#function, privacy: .public) - \(error, privacy: .public)") - return - } + Preferences.scriptsDirectoryUrl = url + webView.evaluateJavaScript("printDirectory('\(Preferences.scriptsDirectoryUrl.absoluteString)')") } } diff --git a/xcode/xcconfig/Userscripts-Debug.xcconfig b/xcode/xcconfig/Userscripts-Debug.xcconfig index f7b4cc7f..a0060f16 100644 --- a/xcode/xcconfig/Userscripts-Debug.xcconfig +++ b/xcode/xcconfig/Userscripts-Debug.xcconfig @@ -9,7 +9,7 @@ MAC_APP_VERSION = 1.0.0 // Sign to Run Locally CODE_SIGN_IDENTITY = - // Organization Identifier -ORG_IDENTIFIER = debug +ORG_IDENTIFIER = dev.debug // Application Identifier APP_IDENTIFIER = $(ORG_IDENTIFIER).userscripts // Safari Extension Identifier diff --git a/xcode/xcconfig/Userscripts-Release.xcconfig b/xcode/xcconfig/Userscripts-Release.xcconfig index 516d42eb..3869c42d 100644 --- a/xcode/xcconfig/Userscripts-Release.xcconfig +++ b/xcode/xcconfig/Userscripts-Release.xcconfig @@ -11,12 +11,13 @@ DEVELOPMENT_TEAM = J74Q8V8V8N // Organization Identifier ORG_IDENTIFIER = com.userscripts // Application Identifier -APP_IDENTIFIER = com.userscripts.macos +APP_IDENTIFIER = $(ORG_IDENTIFIER).macos // Safari Extension Identifier EXT_IDENTIFIER = $(APP_IDENTIFIER).Userscripts-Extension // App Groups and Group Containers -IOS_SHARED_GID = group.com.userscripts.ios -MAC_SHARED_GID = $(DEVELOPMENT_TEAM).com.userscripts.macos +// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups +IOS_SHARED_GID = group.$(ORG_IDENTIFIER).ios +MAC_SHARED_GID = $(DEVELOPMENT_TEAM).$(ORG_IDENTIFIER).macos // App URL Scheme APP_URL_SCHEME = userscriptsurlscheme