diff --git a/Zotero/Controllers/IdentifierLookupController.swift b/Zotero/Controllers/IdentifierLookupController.swift index 02ab53d40..79b896454 100644 --- a/Zotero/Controllers/IdentifierLookupController.swift +++ b/Zotero/Controllers/IdentifierLookupController.swift @@ -23,6 +23,11 @@ protocol IdentifierLookupPresenter: AnyObject { final class IdentifierLookupController { // MARK: Types + struct LookupSettings: Hashable { + let libraryIdentifier: LibraryIdentifier + let collectionKeys: Set + } + struct Update { enum Kind { case lookupError(error: Swift.Error) @@ -148,7 +153,7 @@ final class IdentifierLookupController { } } } - private var lookupWebViewHandlersByLookupSettings: [LookupWebViewHandler.LookupSettings: LookupWebViewHandler] = [:] + private var lookupWebViewHandlersByLookupSettings: [LookupSettings: LookupWebViewHandler] = [:] // MARK: Object Lifecycle init( @@ -186,7 +191,7 @@ final class IdentifierLookupController { completion(lookupData) } guard let self else { return } - let lookupSettings = LookupWebViewHandler.LookupSettings(libraryIdentifier: libraryId, collectionKeys: collectionKeys) + let lookupSettings = LookupSettings(libraryIdentifier: libraryId, collectionKeys: collectionKeys) if lookupWebViewHandlersByLookupSettings[lookupSettings] != nil { lookupData = Array(self.lookupData.values) return @@ -194,7 +199,7 @@ final class IdentifierLookupController { var lookupWebViewHandler: LookupWebViewHandler? inMainThread(sync: true) { if let webView = self.webViewProvider?.addWebView() { - lookupWebViewHandler = LookupWebViewHandler(lookupSettings: lookupSettings, webView: webView, translatorsController: self.translatorsController) + lookupWebViewHandler = LookupWebViewHandler(webView: webView, translatorsController: self.translatorsController) } } guard let lookupWebViewHandler else { @@ -202,13 +207,13 @@ final class IdentifierLookupController { return } lookupWebViewHandlersByLookupSettings[lookupSettings] = lookupWebViewHandler - setupObserver(for: lookupWebViewHandler) + setupObserver(for: lookupWebViewHandler, libraryId: libraryId, collectionKeys: collectionKeys) lookupData = Array(self.lookupData.values) } } func lookUp(libraryId: LibraryIdentifier, collectionKeys: Set, identifier: String) { - let lookupSettings = LookupWebViewHandler.LookupSettings(libraryIdentifier: libraryId, collectionKeys: collectionKeys) + let lookupSettings = LookupSettings(libraryIdentifier: libraryId, collectionKeys: collectionKeys) guard let lookupWebViewHandler = lookupWebViewHandlersByLookupSettings[lookupSettings] else { DDLogError("IdentifierLookupController: can't find lookup web view handler for settings - \(lookupSettings)") return @@ -300,7 +305,7 @@ final class IdentifierLookupController { } } - private func setupObserver(for lookupWebViewHandler: LookupWebViewHandler) { + private func setupObserver(for lookupWebViewHandler: LookupWebViewHandler, libraryId: LibraryIdentifier, collectionKeys: Set) { lookupWebViewHandler.observable .subscribe { result in process(result: result) @@ -373,8 +378,6 @@ final class IdentifierLookupController { return } - let libraryId = lookupWebViewHandler.lookupSettings.libraryIdentifier - let collectionKeys = lookupWebViewHandler.lookupSettings.collectionKeys guard let itemData = data["data"] as? [[String: Any]], let item = itemData.first, let (response, attachments) = parse(item, libraryId: libraryId, collectionKeys: collectionKeys, schemaController: schemaController, dateParser: dateParser) diff --git a/Zotero/Controllers/Web View Handling/LookupWebViewHandler.swift b/Zotero/Controllers/Web View Handling/LookupWebViewHandler.swift index 85f8e1643..18f399be3 100644 --- a/Zotero/Controllers/Web View Handling/LookupWebViewHandler.swift +++ b/Zotero/Controllers/Web View Handling/LookupWebViewHandler.swift @@ -14,11 +14,6 @@ import RxCocoa import RxSwift final class LookupWebViewHandler { - struct LookupSettings: Hashable { - let libraryIdentifier: LibraryIdentifier - let collectionKeys: Set - } - /// Handlers for communication with JS in `webView` enum JSHandlers: String, CaseIterable { /// Handler used for reporting new items. @@ -51,7 +46,6 @@ final class LookupWebViewHandler { case failed(Swift.Error) } - let lookupSettings: LookupSettings let webViewHandler: WebViewHandler private let translatorsController: TranslatorsAndStylesController private let disposeBag: DisposeBag @@ -59,134 +53,132 @@ final class LookupWebViewHandler { private var isLoading: BehaviorRelay - init(lookupSettings: LookupSettings, webView: WKWebView, translatorsController: TranslatorsAndStylesController) { - self.lookupSettings = lookupSettings + init(webView: WKWebView, translatorsController: TranslatorsAndStylesController) { self.translatorsController = translatorsController - self.webViewHandler = WebViewHandler(webView: webView, javascriptHandlers: JSHandlers.allCases.map({ $0.rawValue })) - self.observable = PublishSubject() - self.disposeBag = DisposeBag() - self.isLoading = BehaviorRelay(value: .inProgress) + webViewHandler = WebViewHandler(webView: webView, javascriptHandlers: JSHandlers.allCases.map({ $0.rawValue })) + observable = PublishSubject() + disposeBag = DisposeBag() + isLoading = BehaviorRelay(value: .inProgress) - self.webViewHandler.receivedMessageHandler = { [weak self] name, body in + webViewHandler.receivedMessageHandler = { [weak self] name, body in self?.receiveMessage(name: name, body: body) } - self.initialize() + initialize() .subscribe(on: MainScheduler.instance) .observe(on: MainScheduler.instance) - .subscribe(with: self, onSuccess: { `self`, _ in + .subscribe(onSuccess: { [weak self] _ in DDLogInfo("LookupWebViewHandler: initialization succeeded") - self.isLoading.accept(.initialized) - }, onFailure: { `self`, error in + self?.isLoading.accept(.initialized) + }, onFailure: { [weak self] error in DDLogInfo("LookupWebViewHandler: initialization failed - \(error)") - self.isLoading.accept(.failed(error)) + self?.isLoading.accept(.failed(error)) }) - .disposed(by: self.disposeBag) - } - - convenience init(libraryIdentifier: LibraryIdentifier, collectionKeys: Set, webView: WKWebView, translatorsController: TranslatorsAndStylesController) { - let lookupSettings = LookupSettings(libraryIdentifier: libraryIdentifier, collectionKeys: collectionKeys) - self.init(lookupSettings: lookupSettings, webView: webView, translatorsController: translatorsController) + .disposed(by: disposeBag) + + func initialize() -> Single { + DDLogInfo("LookupWebViewHandler: initialize web view") + return loadIndex() + .flatMap { _ -> Single<(String, String)> in + DDLogInfo("LookupWebViewHandler: load bundled files") + return loadBundledFiles() + } + .flatMap { encodedSchema, encodedDateFormats -> Single in + DDLogInfo("LookupWebViewHandler: init schema and date formats") + return self.webViewHandler.call(javascript: "initSchemaAndDateFormats(\(encodedSchema), \(encodedDateFormats));") + } + .flatMap { _ -> Single<[RawTranslator]> in + DDLogInfo("LookupWebViewHandler: load translators") + return translatorsController.translators() + } + .flatMap { translators -> Single in + DDLogInfo("LookupWebViewHandler: encode translators") + let encodedTranslators = WebViewEncoder.encodeAsJSONForJavascript(translators) + return self.webViewHandler.call(javascript: "initTranslators(\(encodedTranslators));") + } + + func loadIndex() -> Single<()> { + guard let indexUrl = Bundle.main.url(forResource: "lookup", withExtension: "html", subdirectory: "translation") else { + return .error(Error.cantFindFile) + } + return webViewHandler.load(fileUrl: indexUrl) + } + + func loadBundledFiles() -> Single<(String, String)> { + return .create { subscriber in + guard let schemaUrl = Bundle.main.url(forResource: "schema", withExtension: "json", subdirectory: "Bundled"), let schemaData = try? Data(contentsOf: schemaUrl) else { + DDLogError("WebViewHandler: can't load schema json") + subscriber(.failure(Error.cantFindFile)) + return Disposables.create() + } + + guard let dateFormatsUrl = Bundle.main.url(forResource: "dateFormats", withExtension: "json", subdirectory: "translation/translate/modules/utilities/resource"), + let dateFormatData = try? Data(contentsOf: dateFormatsUrl) + else { + DDLogError("WebViewHandler: can't load dateFormats json") + subscriber(.failure(Error.cantFindFile)) + return Disposables.create() + } + + let encodedSchema = WebViewEncoder.encodeForJavascript(schemaData) + let encodedFormats = WebViewEncoder.encodeForJavascript(dateFormatData) + + DDLogInfo("WebViewHandler: loaded bundled files") + + subscriber(.success((encodedSchema, encodedFormats))) + + return Disposables.create() + } + } + } } func lookUp(identifier: String) { - switch self.isLoading.value { + switch isLoading.value { case .failed(let error): - self.observable.on(.next(.failure(error))) + observable.on(.next(.failure(error))) case .initialized: - self._lookUp(identifier: identifier) + performLookUp(for: identifier) case .inProgress: - self.isLoading.filter { result in + isLoading.filter { result in switch result { - case .inProgress: return false - case .initialized, .failed: return true + case .inProgress: + return false + + case .initialized, .failed: + return true } } .first() - .subscribe(with: self, onSuccess: { `self`, result in - guard let result = result else { return } + .subscribe(onSuccess: { [weak self] result in + guard let self, let result else { return } switch result { case .failed(let error): - self.observable.on(.next(.failure(error))) + observable.on(.next(.failure(error))) case .initialized: - self._lookUp(identifier: identifier) + performLookUp(for: identifier) - case .inProgress: break + case .inProgress: + break } }) - .disposed(by: self.disposeBag) - } - } - - private func _lookUp(identifier: String) { - DDLogInfo("LookupWebViewHandler: call translate js") - let encodedIdentifiers = WebViewEncoder.encodeForJavascript(identifier.data(using: .utf8)) - return self.webViewHandler.call(javascript: "lookup(\(encodedIdentifiers));") - .subscribe(on: MainScheduler.instance) - .observe(on: MainScheduler.instance) - .subscribe(onFailure: { [weak self] error in - DDLogError("WebViewHandler: translation failed - \(error)") - self?.observable.on(.next(.failure(error))) - }) - .disposed(by: self.disposeBag) - } - - private func initialize() -> Single { - DDLogInfo("LookupWebViewHandler: initialize web view") - return self.loadIndex() - .flatMap { _ -> Single<(String, String)> in - DDLogInfo("LookupWebViewHandler: load bundled files") - return self.loadBundledFiles() - } - .flatMap { encodedSchema, encodedDateFormats -> Single in - DDLogInfo("LookupWebViewHandler: init schema and date formats") - return self.webViewHandler.call(javascript: "initSchemaAndDateFormats(\(encodedSchema), \(encodedDateFormats));") - } - .flatMap { _ -> Single<[RawTranslator]> in - DDLogInfo("LookupWebViewHandler: load translators") - return self.translatorsController.translators() - } - .flatMap { translators -> Single in - DDLogInfo("LookupWebViewHandler: encode translators") - let encodedTranslators = WebViewEncoder.encodeAsJSONForJavascript(translators) - return self.webViewHandler.call(javascript: "initTranslators(\(encodedTranslators));") - } - } - - private func loadIndex() -> Single<()> { - guard let indexUrl = Bundle.main.url(forResource: "lookup", withExtension: "html", subdirectory: "translation") else { - return Single.error(Error.cantFindFile) + .disposed(by: disposeBag) } - return self.webViewHandler.load(fileUrl: indexUrl) - } - - private func loadBundledFiles() -> Single<(String, String)> { - return Single.create { subscriber in - guard let schemaUrl = Bundle.main.url(forResource: "schema", withExtension: "json", subdirectory: "Bundled"), - let schemaData = try? Data(contentsOf: schemaUrl) else { - DDLogError("WebViewHandler: can't load schema json") - subscriber(.failure(Error.cantFindFile)) - return Disposables.create() - } - - guard let dateFormatsUrl = Bundle.main.url(forResource: "dateFormats", withExtension: "json", subdirectory: "translation/translate/modules/utilities/resource"), - let dateFormatData = try? Data(contentsOf: dateFormatsUrl) else { - DDLogError("WebViewHandler: can't load dateFormats json") - subscriber(.failure(Error.cantFindFile)) - return Disposables.create() - } - - let encodedSchema = WebViewEncoder.encodeForJavascript(schemaData) - let encodedFormats = WebViewEncoder.encodeForJavascript(dateFormatData) - - DDLogInfo("WebViewHandler: loaded bundled files") - - subscriber(.success((encodedSchema, encodedFormats))) - return Disposables.create() + func performLookUp(for identifier: String) { + DDLogInfo("LookupWebViewHandler: call translate js") + let encodedIdentifiers = WebViewEncoder.encodeForJavascript(identifier.data(using: .utf8)) + return webViewHandler.call(javascript: "lookup(\(encodedIdentifiers));") + .subscribe(on: MainScheduler.instance) + .observe(on: MainScheduler.instance) + .subscribe(onFailure: { [weak self] error in + DDLogError("LookupWebViewHandler: translation failed - \(error)") + self?.observable.on(.next(.failure(error))) + }) + .disposed(by: disposeBag) } } @@ -200,22 +192,22 @@ final class LookupWebViewHandler { guard let errorNumber = body as? Int else { return } switch errorNumber { case 0: - self.observable.on(.next(.failure(Error.invalidIdentifiers))) + observable.on(.next(.failure(Error.invalidIdentifiers))) case 1: - self.observable.on(.next(.failure(Error.noSuccessfulTranslators))) + observable.on(.next(.failure(Error.noSuccessfulTranslators))) default: - self.observable.on(.next(.failure(Error.lookupFailed))) + observable.on(.next(.failure(Error.lookupFailed))) } case .items: guard let rawData = body as? [String: Any] else { return } - self.observable.on(.next(.success(.item(rawData)))) + observable.on(.next(.success(.item(rawData)))) case .identifiers: guard let rawData = body as? [[String: String]] else { return } - self.observable.on(.next(.success(.identifiers(rawData)))) + observable.on(.next(.success(.identifiers(rawData)))) case .log: DDLogInfo("JSLOG: \(body)") @@ -223,20 +215,20 @@ final class LookupWebViewHandler { case .request: guard let body = body as? [String: Any], let messageId = body["messageId"] as? Int else { - DDLogError("TranslationWebViewHandler: request missing body - \(body)") + DDLogError("LookupWebViewHandler: request missing body - \(body)") return } if let options = body["payload"] as? [String: Any] { do { - try self.webViewHandler.sendRequest(with: options, for: messageId) + try webViewHandler.sendRequest(with: options, for: messageId) } catch let error { - DDLogError("TranslationWebViewHandler: send request error \(error)") - self.webViewHandler.sendMessaging(error: "Could not create request", for: messageId) + DDLogError("LookupWebViewHandler: send request error \(error)") + webViewHandler.sendMessaging(error: "Could not create request", for: messageId) } } else { - DDLogError("TranslationWebViewHandler: request missing payload - \(body)") - self.webViewHandler.sendMessaging(error: "HTTP request missing payload", for: messageId) + DDLogError("LookupWebViewHandler: request missing payload - \(body)") + webViewHandler.sendMessaging(error: "HTTP request missing payload", for: messageId) } } } diff --git a/Zotero/Controllers/Web View Handling/WebViewHandler.swift b/Zotero/Controllers/Web View Handling/WebViewHandler.swift index 7b26c1acb..72f43252a 100644 --- a/Zotero/Controllers/Web View Handling/WebViewHandler.swift +++ b/Zotero/Controllers/Web View Handling/WebViewHandler.swift @@ -30,7 +30,7 @@ final class WebViewHandler: NSObject { // MARK: - Lifecycle - init(webView: WKWebView, javascriptHandlers: [String]?, userAgent: String? = nil) { + init(webView: WKWebView, javascriptHandlers: [String]?) { let storage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: AppGroup.identifier) storage.cookieAcceptPolicy = .always @@ -39,7 +39,7 @@ final class WebViewHandler: NSObject { configuration.httpShouldSetCookies = true configuration.httpCookieAcceptPolicy = .always - self.session = URLSession(configuration: configuration) + session = URLSession(configuration: configuration) self.webView = webView super.init() @@ -48,10 +48,8 @@ final class WebViewHandler: NSObject { let userAgent = webView.value(forKey: "userAgent") ?? "" webView.customUserAgent = "\(userAgent) Zotero_iOS/\(DeviceInfoProvider.versionString ?? "")-\(DeviceInfoProvider.buildString ?? "")" - if let handlers = javascriptHandlers { - handlers.forEach { handler in - webView.configuration.userContentController.add(self, name: handler) - } + javascriptHandlers?.forEach { handler in + webView.configuration.userContentController.add(self, name: handler) } } @@ -64,31 +62,31 @@ final class WebViewHandler: NSObject { } func load(fileUrl: URL) -> Single<()> { - guard let webView = self.webView else { + guard let webView else { DDLogError("WebViewHandler: web view is nil") - return Single.error(Error.webViewMissing) + return .error(Error.webViewMissing) } webView.loadFileURL(fileUrl, allowingReadAccessTo: fileUrl.deletingLastPathComponent()) - return self.createWebLoadedSingle() + return createWebLoadedSingle() } func load(webUrl: URL) -> Single<()> { - guard let webView = self.webView else { + guard let webView else { DDLogError("WebViewHandler: web view is nil") - return Single.error(Error.webViewMissing) + return .error(Error.webViewMissing) } let request = URLRequest(url: webUrl) // Share extension started crashing when `load()` was called immediately, a little delay fixed the crash (##616) DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { webView.load(request) } - return self.createWebLoadedSingle() + return createWebLoadedSingle() } func call(javascript: String) -> Single { - guard let webView = self.webView else { + guard let webView else { DDLogError("WebViewHandler: web view is nil") - return Single.error(Error.webViewMissing) + return .error(Error.webViewMissing) } return webView.call(javascript: javascript) } @@ -111,16 +109,16 @@ final class WebViewHandler: NSObject { payload = ["error": ["status": statusCode, "responseText": responseText] as [String: Any]] } - self.sendMessaging(response: payload, for: messageId) + sendMessaging(response: payload, for: messageId) } func sendMessaging(error: String, for messageId: Int) { - self.sendMessaging(response: ["error": ["message": error]], for: messageId) + sendMessaging(response: ["error": ["message": error]], for: messageId) } /// Create single which is fired when webview loads a resource or fails. private func createWebLoadedSingle() -> Single<()> { - return Single.create { [weak self] subscriber -> Disposable in + return .create { [weak self] subscriber -> Disposable in self?.webDidLoad = subscriber return Disposables.create { self?.webDidLoad = nil @@ -140,7 +138,7 @@ final class WebViewHandler: NSObject { DDLogInfo("\(options)") let data = "Incorrect URL request from javascript".data(using: .utf8) - self.sendHttpResponse(data: data, statusCode: -1, url: nil, successCodes: [200], headers: [:], for: messageId) + sendHttpResponse(data: data, statusCode: -1, url: nil, successCodes: [200], headers: [:], for: messageId) return } @@ -158,7 +156,7 @@ final class WebViewHandler: NSObject { DDLogInfo("WebViewHandler: send request to \(url.absoluteString)") - self.session.set(cookies: self.cookies, domain: url.host ?? "") + session.set(cookies: cookies, domain: url.host ?? "") var request = URLRequest(url: url) request.httpMethod = method @@ -174,14 +172,14 @@ final class WebViewHandler: NSObject { request.httpBody = body?.data(using: .utf8) request.timeoutInterval = timeout - let task = self.session.dataTask(with: request) { [weak self] data, response, error in - guard let self = self else { return } + let task = session.dataTask(with: request) { [weak self] data, response, error in + guard let self else { return } if let response = response as? HTTPURLResponse { - self.sendHttpResponse(data: data, statusCode: response.statusCode, url: response.url, successCodes: successCodes, headers: response.allHeaderFields, for: messageId) - } else if let error = error { - self.sendHttpResponse(data: error.localizedDescription.data(using: .utf8), statusCode: -1, url: nil, successCodes: successCodes, headers: [:], for: messageId) + sendHttpResponse(data: data, statusCode: response.statusCode, url: response.url, successCodes: successCodes, headers: response.allHeaderFields, for: messageId) + } else if let error { + sendHttpResponse(data: error.localizedDescription.data(using: .utf8), statusCode: -1, url: nil, successCodes: successCodes, headers: [:], for: messageId) } else { - self.sendHttpResponse(data: "unknown error".data(using: .utf8), statusCode: -1, url: nil, successCodes: successCodes, headers: [:], for: messageId) + sendHttpResponse(data: "unknown error".data(using: .utf8), statusCode: -1, url: nil, successCodes: successCodes, headers: [:], for: messageId) } } task.resume() @@ -198,7 +196,7 @@ extension WebViewHandler: WKNavigationDelegate { func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Swift.Error) { DDLogError("WebViewHandler: did fail - \(error)") - self.webDidLoad?(.failure(error)) + webDidLoad?(.failure(error)) } }