WebxdcViewController.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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. case sendToChat = "sendToChat"
  10. }
  11. let INTERNALSCHEMA = "webxdc"
  12. var messageId: Int
  13. var webxdcUpdateObserver: NSObjectProtocol?
  14. var webxdcName: String = ""
  15. var sourceCodeUrl: String?
  16. private var allowInternet: Bool = false
  17. private var shortcutManager: ShortcutManager?
  18. private lazy var moreButton: UIBarButtonItem = {
  19. let image: UIImage?
  20. if #available(iOS 13.0, *) {
  21. image = UIImage(systemName: "ellipsis.circle")
  22. } else {
  23. image = UIImage(named: "ic_more")
  24. }
  25. return UIBarButtonItem(image: image,
  26. style: .plain,
  27. target: self,
  28. action: #selector(moreButtonPressed))
  29. }()
  30. // Block just everything, except of webxdc urls
  31. let blockRules = """
  32. [
  33. {
  34. "trigger": {
  35. "url-filter": ".*"
  36. },
  37. "action": {
  38. "type": "block"
  39. }
  40. },
  41. {
  42. "trigger": {
  43. "url-filter": "webxdc://*"
  44. },
  45. "action": {
  46. "type": "ignore-previous-rules"
  47. }
  48. }
  49. ]
  50. """
  51. lazy var webxdcbridge: String = {
  52. let addr = dcContext.addr?
  53. .addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
  54. let displayname = (dcContext.displayname ?? dcContext.addr)?
  55. .addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
  56. let script = """
  57. window.webxdc = (() => {
  58. var log = (s)=>webkit.messageHandlers.log.postMessage(s);
  59. var update_listener = () => {};
  60. let should_run_again = false;
  61. let running = false;
  62. let lastSerial = 0;
  63. window.__webxdcUpdate = async () => {
  64. if (running) {
  65. should_run_again = true
  66. return
  67. }
  68. should_run_again = false
  69. running = true;
  70. try {
  71. const updates = await fetch("webxdc-update.json?"+lastSerial).then((response) => response.json())
  72. updates.forEach((update) => {
  73. update_listener(update);
  74. if (lastSerial < update["max_serial"]){
  75. lastSerial = update["max_serial"]
  76. }
  77. });
  78. } catch (e) {
  79. log("json error: "+ e.message)
  80. } finally {
  81. running = false;
  82. if (should_run_again) {
  83. await window.__webxdcUpdate()
  84. }
  85. }
  86. }
  87. return {
  88. selfAddr: decodeURI("\((addr ?? "unknown"))"),
  89. selfName: decodeURI("\((displayname ?? "unknown"))"),
  90. setUpdateListener: (cb, serial) => {
  91. update_listener = cb
  92. return window.__webxdcUpdate()
  93. },
  94. getAllUpdates: () => {
  95. console.error("deprecated 2022-02-20 all updates are returned through the callback set by setUpdateListener");
  96. return Promise.resolve([]);
  97. },
  98. sendUpdate: (payload, descr) => {
  99. // only one parameter is allowed, we we create a new parameter object here
  100. var parameter = {
  101. payload: payload,
  102. descr: descr
  103. };
  104. webkit.messageHandlers.sendStatusUpdateHandler.postMessage(parameter);
  105. },
  106. sendToChat: async (message) => {
  107. const data = {};
  108. /** @type {(file: Blob) => Promise<string>} */
  109. const blobToBase64 = (file) => {
  110. const dataStart = ";base64,";
  111. return new Promise((resolve, reject) => {
  112. const reader = new FileReader();
  113. reader.readAsDataURL(file);
  114. reader.onload = () => {
  115. let data = reader.result;
  116. resolve(data.slice(data.indexOf(dataStart) + dataStart.length));
  117. };
  118. reader.onerror = () => reject(reader.error);
  119. });
  120. };
  121. if (!message.file && !message.text) {
  122. return Promise.reject("sendToChat() error: file or text missing");
  123. }
  124. if (message.text) {
  125. data.text = message.text;
  126. }
  127. if (message.file) {
  128. if (!message.file.name) {
  129. return Promise.reject("sendToChat() error: file name missing");
  130. }
  131. if (Object.keys(message.file).filter((key) => ["blob", "base64", "plainText"].includes(key)).length > 1) {
  132. return Promise.reject("sendToChat() error: only one of blob, base64 or plainText allowed");
  133. }
  134. if (message.file.blob instanceof Blob) {
  135. data.base64 = await blobToBase64(message.file.blob);
  136. } else if (typeof message.file.base64 === "string") {
  137. data.base64 = message.file.base64;
  138. } else if (typeof message.file.plainText === "string") {
  139. data.base64 = await blobToBase64(new Blob([message.file.plainText]));
  140. } else {
  141. return Promise.reject("sendToChat() error: none of blob, base64 or plainText set correctly");
  142. }
  143. data.name = message.file.name;
  144. }
  145. webkit.messageHandlers.sendToChat.postMessage(data);
  146. },
  147. importFiles: (filters) => {
  148. var element = document.createElement("input");
  149. element.type = "file";
  150. element.accept = [
  151. ...(filters.extensions || []),
  152. ...(filters.mimeTypes || []),
  153. ].join(",");
  154. element.multiple = filters.multiple || false;
  155. const promise = new Promise((resolve, _reject) => {
  156. element.onchange = (_ev) => {
  157. console.log("element.files", element.files);
  158. const files = Array.from(element.files || []);
  159. document.body.removeChild(element);
  160. resolve(files);
  161. };
  162. });
  163. element.style.display = "none";
  164. document.body.appendChild(element);
  165. element.click();
  166. console.log(element);
  167. return promise;
  168. },
  169. };
  170. })();
  171. """
  172. return script
  173. }()
  174. override var configuration: WKWebViewConfiguration {
  175. let config = WKWebViewConfiguration()
  176. let preferences = WKPreferences()
  177. let contentController = WKUserContentController()
  178. contentController.add(self, name: WebxdcHandler.sendStatusUpdate.rawValue)
  179. contentController.add(self, name: WebxdcHandler.setUpdateListener.rawValue)
  180. contentController.add(self, name: WebxdcHandler.log.rawValue)
  181. contentController.add(self, name: WebxdcHandler.sendToChat.rawValue)
  182. let scriptSource = """
  183. window.RTCPeerConnection = ()=>{};
  184. RTCPeerConnection = ()=>{};
  185. try {
  186. window.webkitRTCPeerConnection = ()=>{};
  187. webkitRTCPeerConnection = ()=>{};
  188. } catch (e){}
  189. """
  190. let script = WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
  191. contentController.addUserScript(script)
  192. config.userContentController = contentController
  193. config.setURLSchemeHandler(self, forURLScheme: INTERNALSCHEMA)
  194. config.mediaTypesRequiringUserActionForPlayback = []
  195. config.allowsInlineMediaPlayback = true
  196. if #available(iOS 13.0, *) {
  197. preferences.isFraudulentWebsiteWarningEnabled = true
  198. }
  199. if #available(iOS 14.0, *) {
  200. config.defaultWebpagePreferences.allowsContentJavaScript = true
  201. } else {
  202. preferences.javaScriptEnabled = true
  203. }
  204. preferences.javaScriptCanOpenWindowsAutomatically = false
  205. config.preferences = preferences
  206. return config
  207. }
  208. init(dcContext: DcContext, messageId: Int) {
  209. self.messageId = messageId
  210. self.shortcutManager = ShortcutManager(dcContext: dcContext, messageId: messageId)
  211. super.init(dcContext: dcContext)
  212. }
  213. required init?(coder: NSCoder) {
  214. fatalError("init(coder:) has not been implemented")
  215. }
  216. override func viewDidLoad() {
  217. super.viewDidLoad()
  218. let msg = dcContext.getMessage(id: messageId)
  219. let dict = msg.getWebxdcInfoDict()
  220. let document = dict["document"] as? String ?? ""
  221. webxdcName = dict["name"] as? String ?? "ErrName" // name should not be empty
  222. let chatName = dcContext.getChat(chatId: msg.chatId).name
  223. self.allowInternet = dict["internet_access"] as? Bool ?? false
  224. self.title = document.isEmpty ? "\(webxdcName) – \(chatName)" : "\(document) – \(chatName)"
  225. navigationItem.rightBarButtonItem = moreButton
  226. if let sourceCode = dict["source_code_url"] as? String,
  227. !sourceCode.isEmpty {
  228. sourceCodeUrl = sourceCode
  229. }
  230. }
  231. override func willMove(toParent parent: UIViewController?) {
  232. super.willMove(toParent: parent)
  233. let willBeRemoved = parent == nil
  234. navigationController?.interactivePopGestureRecognizer?.isEnabled = willBeRemoved
  235. if willBeRemoved {
  236. let nc = NotificationCenter.default
  237. if let webxdcUpdateObserver = webxdcUpdateObserver {
  238. nc.removeObserver(webxdcUpdateObserver)
  239. }
  240. shortcutManager = nil
  241. } else {
  242. addObserver()
  243. }
  244. }
  245. private func addObserver() {
  246. let nc = NotificationCenter.default
  247. webxdcUpdateObserver = nc.addObserver(
  248. forName: dcNotificationWebxdcUpdate,
  249. object: nil,
  250. queue: OperationQueue.main
  251. ) { [weak self] notification in
  252. guard let self = self else { return }
  253. guard let ui = notification.userInfo,
  254. let messageId = ui["message_id"] as? Int else {
  255. logger.error("failed to handle dcNotificationWebxdcUpdate")
  256. return
  257. }
  258. if messageId == self.messageId {
  259. self.updateWebxdc()
  260. }
  261. }
  262. }
  263. override func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
  264. if let url = navigationAction.request.url {
  265. if url.scheme == "mailto" {
  266. openChatFor(url: url)
  267. decisionHandler(.cancel)
  268. return
  269. } else if url.scheme != INTERNALSCHEMA {
  270. logger.debug("cancel loading: \(url)")
  271. decisionHandler(.cancel)
  272. return
  273. }
  274. }
  275. logger.debug("loading: \(String(describing: navigationAction.request.url))")
  276. decisionHandler(.allow)
  277. }
  278. override func viewWillAppear(_ animated: Bool) {
  279. super.viewWillAppear(animated)
  280. if allowInternet {
  281. loadHtml()
  282. } else {
  283. loadRestrictedHtml()
  284. }
  285. }
  286. override func viewDidDisappear(_ animated: Bool) {
  287. super.viewDidDisappear(animated)
  288. if #available(iOS 15.0, *) {
  289. webView.setAllMediaPlaybackSuspended(true)
  290. }
  291. }
  292. private func loadRestrictedHtml() {
  293. WKContentRuleListStore.default().compileContentRuleList(
  294. forIdentifier: "WebxdcContentBlockingRules",
  295. encodedContentRuleList: blockRules) { (contentRuleList, error) in
  296. guard let contentRuleList = contentRuleList, error == nil else {
  297. return
  298. }
  299. let configuration = self.webView.configuration
  300. configuration.userContentController.add(contentRuleList)
  301. self.loadHtml()
  302. }
  303. }
  304. private func loadHtml() {
  305. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  306. guard let self = self else { return }
  307. let url = URL(string: "\(self.INTERNALSCHEMA)://acc\(self.dcContext.id)-msg\(self.messageId).localhost/index.html")
  308. let urlRequest = URLRequest(url: url!)
  309. DispatchQueue.main.async {
  310. self.webView.load(urlRequest)
  311. }
  312. }
  313. }
  314. private func updateWebxdc() {
  315. webView.evaluateJavaScript("window.__webxdcUpdate()", completionHandler: nil)
  316. }
  317. @objc private func moreButtonPressed() {
  318. let alert = UIAlertController(title: webxdcName + " – " + String.localized("webxdc_app"),
  319. message: nil,
  320. preferredStyle: .safeActionSheet)
  321. let addToHomescreenAction = UIAlertAction(title: String.localized("add_to_home_screen"), style: .default, handler: addToHomeScreen(_:))
  322. alert.addAction(addToHomescreenAction)
  323. if sourceCodeUrl != nil {
  324. let sourceCodeAction = UIAlertAction(title: String.localized("source_code"), style: .default, handler: openUrl(_:))
  325. alert.addAction(sourceCodeAction)
  326. }
  327. let cancelAction = UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil)
  328. alert.addAction(cancelAction)
  329. self.present(alert, animated: true, completion: nil)
  330. }
  331. private func addToHomeScreen(_ action: UIAlertAction) {
  332. shortcutManager?.showShortcutLandingPage()
  333. }
  334. private func openUrl(_ action: UIAlertAction) {
  335. if let sourceCodeUrl = sourceCodeUrl,
  336. let url = URL(string: sourceCodeUrl) {
  337. UIApplication.shared.open(url)
  338. }
  339. }
  340. }
  341. extension WebxdcViewController: WKScriptMessageHandler {
  342. func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
  343. let handler = WebxdcHandler(rawValue: message.name)
  344. switch handler {
  345. case .log:
  346. guard let msg = message.body as? String else {
  347. logger.error("could not convert param \(message.body) to string")
  348. return
  349. }
  350. logger.debug("webxdc log msg: "+msg)
  351. case .sendStatusUpdate:
  352. guard let dict = message.body as? [String: AnyObject],
  353. let payloadDict = dict["payload"] as? [String: AnyObject],
  354. let payloadJson = try? JSONSerialization.data(withJSONObject: payloadDict, options: []),
  355. let payloadString = String(data: payloadJson, encoding: .utf8),
  356. let description = dict["descr"] as? String else {
  357. logger.error("Failed to parse status update parameters \(message.body)")
  358. return
  359. }
  360. _ = dcContext.sendWebxdcStatusUpdate(msgId: messageId, payload: payloadString, description: description)
  361. case .sendToChat:
  362. let alert = UIAlertController(title: String.localized("chat_share_with_title"), message: nil, preferredStyle: .safeActionSheet)
  363. alert.addAction(UIAlertAction(title: String.localized("select_chat"), style: .default, handler: { _ in
  364. if let dict = message.body as? [String: AnyObject] {
  365. let base64 = dict["base64"] as? String
  366. let data = base64 != nil ? Data(base64Encoded: base64 ?? "") : nil
  367. RelayHelper.shared.setForwardMessage(text: dict["text"] as? String, fileData: data, fileName: dict["name"] as? String)
  368. if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
  369. let rootController = appDelegate.appCoordinator.tabBarController.selectedViewController as? UINavigationController {
  370. appDelegate.appCoordinator.showTab(index: appDelegate.appCoordinator.chatsTab)
  371. rootController.popToRootViewController(animated: false)
  372. }
  373. }
  374. }))
  375. alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
  376. self.present(alert, animated: true, completion: nil)
  377. default:
  378. logger.debug("another method was called")
  379. }
  380. }
  381. }
  382. extension WebxdcViewController: WKURLSchemeHandler {
  383. func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
  384. if let url = urlSchemeTask.request.url, let scheme = url.scheme, scheme == INTERNALSCHEMA {
  385. let data: Data
  386. let mimeType: String
  387. let statusCode: Int
  388. if url.path == "/webxdc-update.json" || url.path == "webxdc-update.json" {
  389. let lastKnownSerial = Int(url.query ?? "0") ?? 0
  390. data = Data(
  391. dcContext.getWebxdcStatusUpdates(msgId: messageId, lastKnownSerial: lastKnownSerial).utf8)
  392. mimeType = "application/json; charset=utf-8"
  393. statusCode = 200
  394. } else {
  395. let file = url.path
  396. let dcMsg = dcContext.getMessage(id: messageId)
  397. if url.lastPathComponent == "webxdc.js" {
  398. data = Data(webxdcbridge.utf8)
  399. } else {
  400. data = dcMsg.getWebxdcBlob(filename: file)
  401. }
  402. mimeType = DcUtils.getMimeTypeForPath(path: file)
  403. statusCode = (data.isEmpty ? 404 : 200)
  404. }
  405. var headerFields = [
  406. "Content-Type": mimeType,
  407. "Content-Length": "\(data.count)",
  408. ]
  409. if !self.allowInternet {
  410. headerFields["Content-Security-Policy"] = """
  411. default-src 'self';
  412. style-src 'self' 'unsafe-inline' blob: ;
  413. font-src 'self' data: blob: ;
  414. script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: ;
  415. connect-src 'self' data: blob: ;
  416. img-src 'self' data: blob: ;
  417. webrtc 'block' ;
  418. """
  419. }
  420. guard let response = HTTPURLResponse(
  421. url: url,
  422. statusCode: statusCode,
  423. httpVersion: "HTTP/1.1",
  424. headerFields: headerFields
  425. ) else {
  426. return
  427. }
  428. urlSchemeTask.didReceive(response)
  429. urlSchemeTask.didReceive(data)
  430. urlSchemeTask.didFinish()
  431. } else {
  432. logger.debug("not loading \(String(describing: urlSchemeTask.request.url))")
  433. }
  434. }
  435. func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
  436. }
  437. }