bitwarden-estensione-browser/src/safari/safari/SafariExtensionViewControll...

403 lines
15 KiB
Swift
Raw Normal View History

2019-08-02 18:32:18 +02:00
import SafariServices
2019-08-02 22:08:04 +02:00
import WebKit
2019-08-02 18:32:18 +02:00
2019-08-02 22:08:04 +02:00
class SafariExtensionViewController: SFSafariExtensionViewController, WKScriptMessageHandler, WKNavigationDelegate {
var webView: WKWebView!
2019-08-20 03:04:03 +02:00
var initedWebView: Bool = false
2019-08-20 20:41:54 +02:00
var popoverOpenCount: Int = 0
2019-08-19 15:22:19 +02:00
2019-08-02 18:32:18 +02:00
static let shared: SafariExtensionViewController = {
let shared = SafariExtensionViewController()
2019-08-19 15:22:19 +02:00
shared.preferredContentSize = NSSize(width: 375, height: 600)
2019-08-02 18:32:18 +02:00
return shared
}()
2019-08-19 15:22:19 +02:00
2019-08-02 22:08:04 +02:00
func initWebView() {
2019-08-20 03:04:03 +02:00
if initedWebView {
return
}
2019-08-21 05:12:35 +02:00
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
2019-08-20 03:04:03 +02:00
initedWebView = true
2019-08-02 22:08:04 +02:00
let parentHeight = SafariExtensionViewController.shared.preferredContentSize.height
let parentWidth = SafariExtensionViewController.shared.preferredContentSize.width
let webViewConfig = WKWebViewConfiguration()
let bundleURL = Bundle.main.resourceURL!.absoluteURL
2019-08-21 05:38:51 +02:00
let html = bundleURL.appendingPathComponent("app/popup/index.html")
let url = URL(string: "\(html.absoluteString)?appVersion=\(version!)")
2019-08-15 20:48:02 +02:00
webViewConfig.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
2019-08-02 22:08:04 +02:00
webViewConfig.preferences.setValue(true, forKey: "developerExtrasEnabled")
2019-08-13 17:34:52 +02:00
webViewConfig.userContentController.add(self, name: "bitwardenApp")
2019-08-22 16:16:58 +02:00
webView = WKWebView(frame: CGRect(x: 0, y: 0, width: parentWidth, height: parentHeight),
2019-08-22 16:18:21 +02:00
configuration: webViewConfig)
2019-08-02 22:08:04 +02:00
webView.navigationDelegate = self
webView.allowsLinkPreview = false
2019-08-21 05:38:51 +02:00
webView.loadFileURL(url!, allowingReadAccessTo: bundleURL)
2019-08-19 15:22:19 +02:00
webView.alphaValue = 0.0
2019-08-23 15:55:45 +02:00
webView.uiDelegate = self
2019-08-19 15:22:19 +02:00
view.addSubview(webView)
2019-08-02 22:08:04 +02:00
}
2019-08-19 15:22:19 +02:00
func webView(_ webView: WKWebView, didFinish _: WKNavigation!) {
2019-10-17 15:04:10 +02:00
if #available(OSXApplicationExtension 10.12, *) {
NSAnimationContext.runAnimationGroup({ _ in
NSAnimationContext.current.duration = 0.35
webView.animator().alphaValue = 1.0
})
} else {
// Fallback on earlier versions
}
2019-08-02 22:08:04 +02:00
}
2019-08-19 15:22:19 +02:00
2019-08-02 22:08:04 +02:00
override func viewDidLoad() {
super.viewDidLoad()
2019-08-19 15:22:19 +02:00
let backgroundColor = NSColor(red: (39 / 255.0), green: (42 / 255.0), blue: (46 / 255.0), alpha: 1.0)
2019-08-02 22:08:04 +02:00
view.setValue(backgroundColor, forKey: "backgroundColor")
initWebView()
}
2019-08-19 15:22:19 +02:00
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name != "bitwardenApp" {
return
}
let messageBody = message.body as! String
let m: AppMessage? = jsonDeserialize(json: messageBody)
if m == nil {
return
}
let command = m!.command
2020-05-22 15:54:28 +02:00
NSLog("Command: \(command)")
2019-08-21 05:16:35 +02:00
if command == "storage_get" {
if m!.data != nil {
let obj = UserDefaults.standard.string(forKey: m!.data!)
m!.responseData = obj
replyMessage(message: m!)
}
} else if command == "storage_save" {
let data: StorageData? = jsonDeserialize(json: m!.data)
if data?.key != nil {
if data?.obj == nil {
UserDefaults.standard.removeObject(forKey: data!.key)
} else {
UserDefaults.standard.set(data?.obj, forKey: data!.key)
}
replyMessage(message: m!)
}
} else if command == "storage_remove" {
if m!.data != nil {
UserDefaults.standard.removeObject(forKey: m!.data!)
replyMessage(message: m!)
}
} else if command == "getLocaleStrings" {
let language = m!.data ?? "en"
let bundleURL = Bundle.main.resourceURL!.absoluteURL
let messagesUrl = bundleURL.appendingPathComponent("app/_locales/\(language)/messages.json")
do {
let json = try String(contentsOf: messagesUrl, encoding: .utf8)
webView.evaluateJavaScript("window.bitwardenLocaleStrings = \(json);", completionHandler: nil)
2020-05-21 22:21:23 +02:00
} catch {
2020-05-22 15:54:28 +02:00
NSLog("ERROR on getLocaleStrings, \(error)")
2020-05-21 22:21:23 +02:00
}
replyMessage(message: m!)
} else if command == "tabs_query" {
let options: TabQueryOptions? = jsonDeserialize(json: m!.data)
if options?.currentWindow ?? false {
SFSafariApplication.getActiveWindow { win in
if win != nil {
processWindowsForTabs(wins: [win!], options: options, complete: { tabs in
m!.responseData = jsonSerialize(obj: tabs)
self.replyMessage(message: m!)
})
} else {
SFSafariApplication.getAllWindows { wins in
processWindowsForTabs(wins: wins, options: options, complete: { tabs in
m!.responseData = jsonSerialize(obj: tabs)
self.replyMessage(message: m!)
})
}
}
}
2019-08-13 17:34:52 +02:00
} else {
SFSafariApplication.getAllWindows { wins in
2019-08-22 16:12:51 +02:00
processWindowsForTabs(wins: wins, options: options, complete: { tabs in
m!.responseData = jsonSerialize(obj: tabs)
self.replyMessage(message: m!)
})
}
}
} else if command == "tabs_message" {
let tabMsg: TabMessage? = jsonDeserialize(json: m!.data)
SFSafariApplication.getAllWindows { wins in
var theWin: SFSafariWindow?
var winIndex = 0
for win in wins {
if tabMsg?.tab.windowId == winIndex {
theWin = win
break
}
winIndex = winIndex + 1
}
var theTab: SFSafariTab?
theWin?.getAllTabs { tabs in
var tabIndex = 0
for tab in tabs {
if tabMsg?.tab.index == tabIndex {
theTab = tab
break
2019-08-16 16:44:28 +02:00
}
tabIndex = tabIndex + 1
2019-08-16 16:44:28 +02:00
}
theTab?.getActivePage { activePage in
activePage?.dispatchMessageToScript(withName: "bitwarden", userInfo: ["msg": tabMsg!.obj])
2019-08-16 21:07:07 +02:00
}
}
2019-08-13 17:34:52 +02:00
}
} else if command == "hidePopover" {
dismissPopover()
replyMessage(message: m!)
} else if command == "showPopover" {
if popoverOpenCount <= 0 {
SFSafariApplication.getActiveWindow { win in
win?.getToolbarItem(completionHandler: { item in
item?.showPopover()
})
}
}
2019-08-20 19:54:10 +02:00
} else if command == "isPopoverOpen" {
2019-08-21 05:48:17 +02:00
m!.responseData = popoverOpenCount > 0 ? "true" : "false"
2019-08-20 19:54:10 +02:00
replyMessage(message: m!)
2019-08-20 21:10:17 +02:00
} else if command == "createNewTab" {
2019-08-20 21:23:05 +02:00
if m!.data != nil {
SFSafariApplication.getActiveWindow { win in
win?.openTab(with: URL(string: m!.data!)!, makeActiveIfPossible: true, completionHandler: { _ in
// Tab opened
})
}
}
} else if command == "reloadExtension" {
webView?.reload()
replyMessage(message: m!)
} else if command == "copyToClipboard" {
let pasteboard = NSPasteboard.general
pasteboard.declareTypes([NSPasteboard.PasteboardType.string], owner: nil)
pasteboard.setString(m!.data ?? "", forType: NSPasteboard.PasteboardType.string)
replyMessage(message: m!)
} else if command == "readFromClipboard" {
let pasteboard = NSPasteboard.general
m!.responseData = pasteboard.pasteboardItems?.first?.string(forType: .string)
replyMessage(message: m!)
2019-08-22 03:10:38 +02:00
} else if command == "downloadFile" {
2019-08-23 15:55:45 +02:00
if m!.data != nil {
if let dlMsg: DownloadFileMessage = jsonDeserialize(json: m!.data) {
var data: Data?
if dlMsg.blobOptions?.type == "text/plain" {
data = dlMsg.blobData?.data(using: .utf8)
} else if dlMsg.blobData != nil {
2019-08-23 15:55:45 +02:00
data = Data(base64Encoded: dlMsg.blobData!)
2019-08-22 03:10:38 +02:00
}
2019-08-23 15:55:45 +02:00
if data != nil {
let panel = NSSavePanel()
panel.canCreateDirectories = true
panel.nameFieldStringValue = dlMsg.fileName
panel.begin { response in
if response == NSApplication.ModalResponse.OK {
if let url = panel.url {
do {
let fileManager = FileManager.default
if !fileManager.fileExists(atPath: url.absoluteString) {
2019-09-09 16:11:59 +02:00
fileManager.createFile(atPath: url.absoluteString, contents: Data(),
2019-09-09 16:13:36 +02:00
attributes: nil)
2019-08-23 15:55:45 +02:00
}
try data!.write(to: url)
} catch {
print(error)
2020-05-22 15:54:28 +02:00
NSLog("ERROR in downloadFile, \(error)")
2019-08-23 15:55:45 +02:00
}
}
}
}
}
}
2019-08-22 03:10:38 +02:00
}
2019-08-02 22:08:04 +02:00
}
}
2019-08-19 15:22:19 +02:00
2019-08-13 17:34:52 +02:00
func replyMessage(message: AppMessage) {
2019-08-19 15:22:19 +02:00
if webView == nil {
return
}
2019-08-15 20:48:02 +02:00
let json = (jsonSerialize(obj: message) ?? "null")
webView.evaluateJavaScript("window.bitwardenSafariAppMessageReceiver(\(json));", completionHandler: nil)
2019-08-13 17:34:52 +02:00
}
}
2019-08-23 05:11:05 +02:00
extension SafariExtensionViewController: WKUIDelegate {
2019-10-17 15:04:10 +02:00
@available(OSXApplicationExtension 10.12, *)
2019-09-09 16:11:59 +02:00
func webView(_: WKWebView, runOpenPanelWith _: WKOpenPanelParameters, initiatedByFrame _: WKFrameInfo,
2019-09-09 16:13:36 +02:00
completionHandler: @escaping ([URL]?) -> Void) {
2019-08-23 05:11:05 +02:00
let openPanel = NSOpenPanel()
openPanel.canChooseFiles = true
openPanel.begin { result in
if result == NSApplication.ModalResponse.OK && openPanel.url != nil {
completionHandler([openPanel.url!])
} else {
completionHandler(nil)
}
}
}
}
2019-08-16 17:46:24 +02:00
func processWindowsForTabs(wins: [SFSafariWindow], options: TabQueryOptions?, complete: @escaping ([Tab]) -> Void) {
2019-08-19 15:22:19 +02:00
if wins.count == 0 {
2019-08-16 16:44:28 +02:00
complete([])
return
}
var newTabs: [Tab] = []
let winGroup = DispatchGroup()
for win in wins {
winGroup.enter()
2019-08-19 15:22:19 +02:00
win.getActiveTab { activeTab in
win.getAllTabs { allTabs in
2019-08-16 16:44:28 +02:00
let tabGroup = DispatchGroup()
for tab in allTabs {
tabGroup.enter()
2019-08-19 15:22:19 +02:00
if options?.active ?? false {
if activeTab != nil && activeTab == tab {
let windowIndex = wins.firstIndex(of: win) ?? -100
let tabIndex = allTabs.firstIndex(of: tab) ?? -1
2019-08-22 16:16:58 +02:00
makeTabObject(tab: tab, activeTab: activeTab, windowIndex: windowIndex,
2019-08-22 16:18:21 +02:00
tabIndex: tabIndex, complete: { t in
newTabs.append(t)
tabGroup.leave()
2019-08-16 16:44:28 +02:00
})
2019-08-16 17:46:24 +02:00
} else {
tabGroup.leave()
2019-08-16 16:44:28 +02:00
}
} else {
let windowIndex = wins.firstIndex(of: win) ?? -100
let tabIndex = allTabs.firstIndex(of: tab) ?? -1
2019-08-22 16:16:58 +02:00
makeTabObject(tab: tab, activeTab: activeTab, windowIndex: windowIndex,
2019-08-22 16:18:21 +02:00
tabIndex: tabIndex, complete: { t in
newTabs.append(t)
tabGroup.leave()
2019-08-16 16:44:28 +02:00
})
}
}
2019-08-19 15:22:19 +02:00
tabGroup.notify(queue: .main) {
2019-08-16 17:46:24 +02:00
winGroup.leave()
}
2019-08-16 16:44:28 +02:00
}
}
}
2019-08-19 15:22:19 +02:00
winGroup.notify(queue: .main) {
2019-08-16 17:46:24 +02:00
complete(newTabs)
}
2019-08-16 16:44:28 +02:00
}
2019-08-22 16:16:58 +02:00
func makeTabObject(tab: SFSafariTab, activeTab: SFSafariTab?, windowIndex: Int, tabIndex: Int,
2019-08-22 16:18:21 +02:00
complete: @escaping (Tab) -> Void) {
2019-08-16 16:44:28 +02:00
let t = Tab()
t.active = activeTab != nil && tab == activeTab
t.windowId = windowIndex
t.index = tabIndex
t.id = "\(windowIndex)_\(tabIndex)"
2019-08-19 15:22:19 +02:00
tab.getActivePage { page in
if page == nil {
2019-08-16 16:44:28 +02:00
complete(t)
} else {
2019-08-19 15:22:19 +02:00
page!.getPropertiesWithCompletionHandler({ props in
2019-08-16 16:44:28 +02:00
t.title = props?.title
t.url = props?.url?.absoluteString
complete(t)
})
}
}
}
func jsonSerialize<T: Encodable>(obj: T?) -> String? {
2019-08-13 17:34:52 +02:00
let encoder = JSONEncoder()
do {
let data = try encoder.encode(obj)
return String(data: data, encoding: .utf8) ?? "null"
2019-08-13 17:34:52 +02:00
} catch _ {
return "null"
2019-08-13 17:34:52 +02:00
}
}
func jsonDeserialize<T: Decodable>(json: String?) -> T? {
2019-08-19 15:22:19 +02:00
if json == nil {
return nil
}
2019-08-13 17:34:52 +02:00
let decoder = JSONDecoder()
do {
let obj = try decoder.decode(T.self, from: json!.data(using: .utf8)!)
2019-08-13 17:34:52 +02:00
return obj
} catch _ {
return nil
}
}
2019-08-02 18:32:18 +02:00
2019-08-19 15:22:19 +02:00
class AppMessage: Decodable, Encodable {
init() {
id = ""
command = ""
data = nil
responseData = nil
2019-08-19 17:53:14 +02:00
responseError = nil
}
2019-08-19 15:22:19 +02:00
2019-08-13 17:34:52 +02:00
var id: String
var command: String
var data: String?
2019-08-15 20:48:02 +02:00
var responseData: String?
2019-08-19 17:53:14 +02:00
var responseError: Bool?
2019-08-20 19:06:38 +02:00
var senderTab: Tab?
}
2019-08-19 15:22:19 +02:00
class StorageData: Decodable, Encodable {
var key: String
var obj: String?
2019-08-02 18:32:18 +02:00
}
2019-08-16 16:44:28 +02:00
2019-08-19 15:22:19 +02:00
class TabQueryOptions: Decodable, Encodable {
2019-08-16 16:44:28 +02:00
var currentWindow: Bool?
var active: Bool?
}
2019-08-19 15:22:19 +02:00
class Tab: Decodable, Encodable {
2019-08-16 16:44:28 +02:00
init() {
id = ""
index = -1
windowId = -100
title = ""
active = false
url = ""
}
2019-08-19 15:22:19 +02:00
2019-08-16 16:44:28 +02:00
var id: String
var index: Int
var windowId: Int
var title: String?
var active: Bool
var url: String?
}
2019-08-16 21:07:07 +02:00
class TabMessage: Decodable, Encodable {
var tab: Tab
var obj: String
var options: TabMessageOptions?
}
class TabMessageOptions: Decodable, Encodable {
var frameId: Int?
}
2019-08-23 15:55:45 +02:00
class DownloadFileMessage: Decodable, Encodable {
var fileName: String
var blobData: String?
var blobOptions: DownloadFileMessageBlobOptions?
}
class DownloadFileMessageBlobOptions: Decodable, Encodable {
var type: String?
}