WebxdcViewController.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import UIKit
  2. import WebKit
  3. import DcCore
  4. class WebxdcViewController: WebViewViewController {
  5. enum WebxdcHandler: String {
  6. case log = "log"
  7. case setUpdateListener = "setUpdateListener"
  8. case sendStatusUpdate = "sendStatusUpdateHandler"
  9. }
  10. let INTERNALSCHEMA = "webxdc"
  11. var messageId: Int
  12. var dcContext: DcContext
  13. var webxdcUpdateObserver: NSObjectProtocol?
  14. // Block just everything, except of webxdc urls
  15. let blockRules = """
  16. [
  17. {
  18. "trigger": {
  19. "url-filter": ".*"
  20. },
  21. "action": {
  22. "type": "block"
  23. }
  24. },
  25. {
  26. "trigger": {
  27. "url-filter": "webxdc://*"
  28. },
  29. "action": {
  30. "type": "ignore-previous-rules"
  31. }
  32. }
  33. ]
  34. """
  35. lazy var webxdcbridge: String = {
  36. let script = """
  37. window.webxdc = (() => {
  38. var log = (s)=>webkit.messageHandlers.log.postMessage(s);
  39. var update_listener = () => {};
  40. window.__webxdcUpdate = (updateString) => {
  41. try {
  42. var updates = JSON.parse(updateString);
  43. updates.forEach((update) => {
  44. update_listener(update);
  45. });
  46. } catch (e) {
  47. log("json error: "+ e.message)
  48. }
  49. }
  50. return {
  51. selfAddr: atob("\((dcContext.addr ?? "unknown").toBase64())"),
  52. selfName: atob("\((dcContext.displayname ?? dcContext.addr ?? "unknown").toBase64())"),
  53. setUpdateListener: (cb, serial) => {
  54. update_listener = cb
  55. webkit.messageHandlers.setUpdateListener.postMessage(typeof serial === "undefined" ? 0 : parseInt(serial));
  56. },
  57. getAllUpdates: () => {
  58. console.error("deprecated 2022-02-20 all updates are returned through the callback set by setUpdateListener");
  59. return Promise.resolve([]);
  60. },
  61. sendUpdate: (payload, descr) => {
  62. // only one parameter is allowed, we we create a new parameter object here
  63. var parameter = {
  64. payload: payload,
  65. descr: descr
  66. };
  67. webkit.messageHandlers.sendStatusUpdateHandler.postMessage(parameter);
  68. },
  69. };
  70. })();
  71. """
  72. return script
  73. }()
  74. override var configuration: WKWebViewConfiguration {
  75. let config = WKWebViewConfiguration()
  76. let preferences = WKPreferences()
  77. let contentController = WKUserContentController()
  78. contentController.add(self, name: WebxdcHandler.sendStatusUpdate.rawValue)
  79. contentController.add(self, name: WebxdcHandler.setUpdateListener.rawValue)
  80. contentController.add(self, name: WebxdcHandler.log.rawValue)
  81. config.userContentController = contentController
  82. config.setURLSchemeHandler(self, forURLScheme: INTERNALSCHEMA)
  83. config.mediaTypesRequiringUserActionForPlayback = []
  84. config.allowsInlineMediaPlayback = true
  85. if #available(iOS 13.0, *) {
  86. preferences.isFraudulentWebsiteWarningEnabled = true
  87. }
  88. if #available(iOS 14.0, *) {
  89. config.defaultWebpagePreferences.allowsContentJavaScript = true
  90. } else {
  91. preferences.javaScriptEnabled = true
  92. }
  93. preferences.javaScriptCanOpenWindowsAutomatically = false
  94. config.preferences = preferences
  95. return config
  96. }
  97. init(dcContext: DcContext, messageId: Int) {
  98. self.dcContext = dcContext
  99. self.messageId = messageId
  100. super.init()
  101. }
  102. required init?(coder: NSCoder) {
  103. fatalError("init(coder:) has not been implemented")
  104. }
  105. override func viewDidLoad() {
  106. super.viewDidLoad()
  107. self.title = dcContext.getMessage(id: messageId).getWebxdcInfoDict()["name"] as? String
  108. }
  109. override func willMove(toParent parent: UIViewController?) {
  110. super.willMove(toParent: parent)
  111. if parent == nil {
  112. // remove observer
  113. let nc = NotificationCenter.default
  114. if let webxdcUpdateObserver = webxdcUpdateObserver {
  115. nc.removeObserver(webxdcUpdateObserver)
  116. }
  117. } else {
  118. addObserver()
  119. }
  120. }
  121. private func addObserver() {
  122. let nc = NotificationCenter.default
  123. webxdcUpdateObserver = nc.addObserver(
  124. forName: dcNotificationWebxdcUpdate,
  125. object: nil,
  126. queue: OperationQueue.main
  127. ) { [weak self] notification in
  128. guard let self = self else { return }
  129. guard let ui = notification.userInfo,
  130. let messageId = ui["message_id"] as? Int else {
  131. logger.error("failed to handle dcNotificationWebxdcUpdate")
  132. return
  133. }
  134. if messageId == self.messageId {
  135. self.updateWebxdc()
  136. }
  137. }
  138. }
  139. override func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
  140. // TODO: what about tel:// and mailto://
  141. if let url = navigationAction.request.url,
  142. url.scheme != INTERNALSCHEMA {
  143. logger.debug("cancel loading: \(url)")
  144. decisionHandler(.cancel)
  145. return
  146. }
  147. logger.debug("loading: \(String(describing: navigationAction.request.url))")
  148. decisionHandler(.allow)
  149. }
  150. override func viewWillAppear(_ animated: Bool) {
  151. super.viewWillAppear(animated)
  152. loadRestrictedHtml()
  153. }
  154. override func viewDidDisappear(_ animated: Bool) {
  155. super.viewDidDisappear(animated)
  156. if #available(iOS 15.0, *) {
  157. webView.setAllMediaPlaybackSuspended(true)
  158. }
  159. }
  160. private func loadRestrictedHtml() {
  161. // TODO: compile only once
  162. WKContentRuleListStore.default().compileContentRuleList(
  163. forIdentifier: "WebxdcContentBlockingRules",
  164. encodedContentRuleList: blockRules) { (contentRuleList, error) in
  165. guard let contentRuleList = contentRuleList, error == nil else {
  166. return
  167. }
  168. let configuration = self.webView.configuration
  169. configuration.userContentController.add(contentRuleList)
  170. self.loadHtml()
  171. }
  172. }
  173. private func loadHtml() {
  174. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  175. guard let self = self else { return }
  176. let url = URL(string: "\(self.INTERNALSCHEMA)://acc\(self.dcContext.id)-msg\(self.messageId).localhost/index.html")
  177. let urlRequest = URLRequest(url: url!)
  178. DispatchQueue.main.async {
  179. self.webView.load(urlRequest)
  180. }
  181. }
  182. }
  183. var lastSerial: Int?
  184. private func updateWebxdc() {
  185. if let lastSerial = lastSerial {
  186. let statusUpdates = dcContext.getWebxdcStatusUpdates(msgId: messageId, lastKnownSerial: lastSerial)
  187. if let data: Data = statusUpdates.data(using: .utf8),
  188. let array = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [Any],
  189. let first = array.first as? [String: Any],
  190. let maxSerial = first["max_serial"] as? Int {
  191. self.lastSerial = maxSerial
  192. }
  193. webView.evaluateJavaScript("window.__webxdcUpdate(atob(\"\(statusUpdates.toBase64())\"))", completionHandler: nil)
  194. }
  195. }
  196. }
  197. extension WebxdcViewController: WKScriptMessageHandler {
  198. func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
  199. let handler = WebxdcHandler(rawValue: message.name)
  200. switch handler {
  201. case .setUpdateListener:
  202. guard let lastKnownSerial = message.body as? Int else {
  203. logger.error("could not convert param \(message.body) to int")
  204. return
  205. }
  206. lastSerial = lastKnownSerial
  207. updateWebxdc()
  208. case .log:
  209. guard let msg = message.body as? String else {
  210. logger.error("could not convert param \(message.body) to string")
  211. return
  212. }
  213. logger.debug("webxdc log msg: "+msg)
  214. case .sendStatusUpdate:
  215. guard let dict = message.body as? [String: AnyObject],
  216. let payloadDict = dict["payload"] as? [String: AnyObject],
  217. let payloadJson = try? JSONSerialization.data(withJSONObject: payloadDict, options: []),
  218. let payloadString = String(data: payloadJson, encoding: .utf8),
  219. let description = dict["descr"] as? String else {
  220. logger.error("Failed to parse status update parameters \(message.body)")
  221. return
  222. }
  223. _ = dcContext.sendWebxdcStatusUpdate(msgId: messageId, payload: payloadString, description: description)
  224. default:
  225. logger.debug("another method was called")
  226. }
  227. }
  228. }
  229. extension WebxdcViewController: WKURLSchemeHandler {
  230. func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
  231. if let url = urlSchemeTask.request.url, let scheme = url.scheme, scheme == INTERNALSCHEMA {
  232. let file = url.path
  233. let dcMsg = dcContext.getMessage(id: messageId)
  234. var data: Data
  235. if url.lastPathComponent == "webxdc.js" {
  236. data = Data(webxdcbridge.utf8)
  237. } else {
  238. data = dcMsg.getWebxdcBlob(filename: file)
  239. }
  240. let mimeType = DcUtils.getMimeTypeForPath(path: file)
  241. let response = URLResponse(url: url, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
  242. urlSchemeTask.didReceive(response)
  243. urlSchemeTask.didReceive(data)
  244. urlSchemeTask.didFinish()
  245. } else {
  246. logger.debug("not loading \(String(describing: urlSchemeTask.request.url))")
  247. }
  248. }
  249. func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
  250. }
  251. }