3099 lines
154 KiB
Swift
Executable File
3099 lines
154 KiB
Swift
Executable File
//
|
|
// InAppWebView.swift
|
|
// flutter_inappwebview
|
|
//
|
|
// Created by Lorenzo on 21/10/18.
|
|
//
|
|
|
|
import Flutter
|
|
import Foundation
|
|
import WebKit
|
|
|
|
public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate,
|
|
WKNavigationDelegate, WKScriptMessageHandler, UIGestureRecognizerDelegate,
|
|
WKDownloadDelegate,
|
|
PullToRefreshDelegate,
|
|
Disposable {
|
|
static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_"
|
|
|
|
var id: Any? // viewId
|
|
var windowId: Int64?
|
|
var windowCreated = false
|
|
var inAppBrowserDelegate: InAppBrowserDelegate?
|
|
var channelDelegate: WebViewChannelDelegate?
|
|
var settings: InAppWebViewSettings?
|
|
var pullToRefreshControl: PullToRefreshControl?
|
|
var findInteractionController: FindInteractionController?
|
|
var webMessageChannels: [String:WebMessageChannel] = [:]
|
|
var webMessageListeners: [WebMessageListener] = []
|
|
var currentOriginalUrl: URL?
|
|
var inFullscreen = false
|
|
|
|
static var sslCertificatesMap: [String: SslCertificate] = [:] // [URL host name : SslCertificate]
|
|
static var credentialsProposed: [URLCredential] = []
|
|
|
|
var lastScrollX: CGFloat = 0
|
|
var lastScrollY: CGFloat = 0
|
|
|
|
// Used to manage pauseTimers() and resumeTimers()
|
|
var isPausedTimers = false
|
|
var isPausedTimersCompletionHandler: (() -> Void)?
|
|
|
|
var contextMenu: [String: Any]?
|
|
var initialUserScripts: [UserScript] = []
|
|
|
|
// https://github.com/mozilla-mobile/firefox-ios/blob/50531a7e9e4d459fb11d4fcb7d4322e08103501f/Client/Frontend/Browser/ContextMenuHelper.swift
|
|
fileprivate var nativeHighlightLongPressRecognizer: UILongPressGestureRecognizer?
|
|
fileprivate var nativeLoupeGesture: UILongPressGestureRecognizer?
|
|
var longPressRecognizer: UILongPressGestureRecognizer!
|
|
var recognizerForDisablingContextMenuOnLinks: UILongPressGestureRecognizer!
|
|
var lastLongPressTouchPoint: CGPoint?
|
|
|
|
var panGestureRecognizer: UIPanGestureRecognizer!
|
|
|
|
var lastTouchPoint: CGPoint?
|
|
var lastTouchPointTimestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
|
|
|
var contextMenuIsShowing = false
|
|
// flag used for the workaround to trigger onCreateContextMenu event as the same on Android
|
|
var onCreateContextMenuEventTriggeredWhenMenuDisabled = false
|
|
|
|
var customIMPs: [IMP] = []
|
|
|
|
static var windowWebViews: [Int64:WebViewTransport] = [:]
|
|
static var windowAutoincrementId: Int64 = 0;
|
|
|
|
var callAsyncJavaScriptBelowIOS14Results: [String:((Any?) -> Void)] = [:]
|
|
|
|
var oldZoomScale = Float(1.0)
|
|
|
|
init(id: Any?, registrar: FlutterPluginRegistrar?, frame: CGRect, configuration: WKWebViewConfiguration,
|
|
contextMenu: [String: Any]?, userScripts: [UserScript] = []) {
|
|
super.init(frame: frame, configuration: configuration)
|
|
self.id = id
|
|
if let id = id, let registrar = registrar {
|
|
let channel = FlutterMethodChannel(name: InAppWebView.METHOD_CHANNEL_NAME_PREFIX + String(describing: id),
|
|
binaryMessenger: registrar.messenger())
|
|
self.channelDelegate = WebViewChannelDelegate(webView: self, channel: channel)
|
|
}
|
|
self.contextMenu = contextMenu
|
|
self.initialUserScripts = userScripts
|
|
uiDelegate = self
|
|
navigationDelegate = self
|
|
scrollView.delegate = self
|
|
longPressRecognizer = UILongPressGestureRecognizer()
|
|
longPressRecognizer.delegate = self
|
|
longPressRecognizer.addTarget(self, action: #selector(longPressGestureDetected))
|
|
recognizerForDisablingContextMenuOnLinks = UILongPressGestureRecognizer()
|
|
recognizerForDisablingContextMenuOnLinks.delegate = self
|
|
recognizerForDisablingContextMenuOnLinks.addTarget(self, action: #selector(longPressGestureDetected))
|
|
recognizerForDisablingContextMenuOnLinks?.minimumPressDuration = 0.45
|
|
panGestureRecognizer = UIPanGestureRecognizer()
|
|
panGestureRecognizer.delegate = self
|
|
panGestureRecognizer.addTarget(self, action: #selector(endDraggingDetected))
|
|
}
|
|
|
|
override public var frame: CGRect {
|
|
get {
|
|
return super.frame
|
|
}
|
|
set {
|
|
super.frame = newValue
|
|
|
|
self.scrollView.contentInset = UIEdgeInsets.zero;
|
|
if #available(iOS 11, *) {
|
|
// Above iOS 11, adjust contentInset to compensate the adjustedContentInset so the sum will
|
|
// always be 0.
|
|
if (scrollView.adjustedContentInset != UIEdgeInsets.zero) {
|
|
let insetToAdjust = self.scrollView.adjustedContentInset;
|
|
scrollView.contentInset = UIEdgeInsets(top: -insetToAdjust.top, left: -insetToAdjust.left,
|
|
bottom: -insetToAdjust.bottom, right: -insetToAdjust.right);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
super.init(coder: aDecoder)!
|
|
}
|
|
|
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
return true
|
|
}
|
|
|
|
// BVC KVO events for all changes on the webview will call this.
|
|
// It is called frequently during a page load (particularly on progress changes and URL changes).
|
|
// As of iOS 12, WKContentView gesture setup is async, but it has been called by the time
|
|
// the webview is ready to load an URL. After this has happened, we can override the gesture.
|
|
func replaceGestureHandlerIfNeeded() {
|
|
DispatchQueue.main.async {
|
|
if self.gestureRecognizerWithDescriptionFragment("InAppWebView") == nil {
|
|
self.replaceWebViewLongPress()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func replaceWebViewLongPress() {
|
|
// WebKit installs gesture handlers async. If `replaceWebViewLongPress` is called after a wkwebview in most cases a small delay is sufficient
|
|
// See also https://bugs.webkit.org/show_bug.cgi?id=193366
|
|
nativeHighlightLongPressRecognizer = gestureRecognizerWithDescriptionFragment("action=_highlightLongPressRecognized:")
|
|
nativeLoupeGesture = gestureRecognizerWithDescriptionFragment("action=loupeGesture:")
|
|
|
|
if let nativeLongPressRecognizer = gestureRecognizerWithDescriptionFragment("action=_longPressRecognized:") {
|
|
nativeLongPressRecognizer.removeTarget(nil, action: nil)
|
|
nativeLongPressRecognizer.addTarget(self, action: #selector(self.longPressGestureDetected))
|
|
}
|
|
}
|
|
|
|
private func gestureRecognizerWithDescriptionFragment(_ descriptionFragment: String) -> UILongPressGestureRecognizer? {
|
|
let result = self.scrollView.subviews.compactMap({ $0.gestureRecognizers }).joined().first(where: {
|
|
return (($0 as? UILongPressGestureRecognizer) != nil) && $0.description.contains(descriptionFragment)
|
|
})
|
|
return result as? UILongPressGestureRecognizer
|
|
}
|
|
|
|
@objc func longPressGestureDetected(_ sender: UIGestureRecognizer) {
|
|
if sender.state == .cancelled {
|
|
return
|
|
}
|
|
|
|
guard sender.state == .began else {
|
|
return
|
|
}
|
|
|
|
if sender == recognizerForDisablingContextMenuOnLinks,
|
|
let settings = settings, !settings.disableLongPressContextMenuOnLinks {
|
|
return
|
|
}
|
|
|
|
if sender == longPressRecognizer {
|
|
// To prevent the tapped link from proceeding with navigation, "cancel" the native WKWebView
|
|
// `_highlightLongPressRecognizer`. This preserves the original behavior as seen here:
|
|
// https://github.com/WebKit/webkit/blob/d591647baf54b4b300ca5501c21a68455429e182/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm#L1600-L1614
|
|
if let nativeHighlightLongPressRecognizer = nativeHighlightLongPressRecognizer,
|
|
nativeHighlightLongPressRecognizer.isEnabled {
|
|
nativeHighlightLongPressRecognizer.isEnabled = false
|
|
nativeHighlightLongPressRecognizer.isEnabled = true
|
|
}
|
|
}
|
|
|
|
//Finding actual touch location in webView
|
|
var touchLocation = sender.location(in: self)
|
|
touchLocation.x -= scrollView.contentInset.left
|
|
touchLocation.y -= scrollView.contentInset.top
|
|
touchLocation.x /= scrollView.zoomScale
|
|
touchLocation.y /= scrollView.zoomScale
|
|
|
|
lastLongPressTouchPoint = touchLocation
|
|
|
|
evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(touchLocation.x),\(touchLocation.y))", completionHandler: {(value, error) in
|
|
if error != nil {
|
|
print("Long press gesture recognizer error: \(error?.localizedDescription ?? "")")
|
|
} else if let value = value as? [String: Any?] {
|
|
let hitTestResult = HitTestResult.fromMap(map: value)!
|
|
self.nativeLoupeGesture = self.gestureRecognizerWithDescriptionFragment("action=loupeGesture:")
|
|
|
|
if sender == self.recognizerForDisablingContextMenuOnLinks,
|
|
hitTestResult.type.rawValue > HitTestResultType.unknownType.rawValue,
|
|
hitTestResult.type.rawValue < HitTestResultType.editTextType.rawValue {
|
|
self.nativeLoupeGesture?.isEnabled = false
|
|
self.nativeLoupeGesture?.isEnabled = true
|
|
} else {
|
|
self.channelDelegate?.onLongPressHitTestResult(hitTestResult: hitTestResult)
|
|
}
|
|
} else if sender == self.longPressRecognizer {
|
|
self.channelDelegate?.onLongPressHitTestResult(hitTestResult: HitTestResult(type: .unknownType, extra: nil))
|
|
}
|
|
})
|
|
}
|
|
|
|
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
lastTouchPoint = point
|
|
lastTouchPointTimestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
|
SharedLastTouchPointTimestamp[self] = lastTouchPointTimestamp
|
|
|
|
// re-build context menu items for the current webview
|
|
UIMenuController.shared.menuItems = []
|
|
if let menu = self.contextMenu {
|
|
if let menuItems = menu["menuItems"] as? [[String : Any]] {
|
|
for menuItem in menuItems {
|
|
let id = menuItem["id"]!
|
|
let title = menuItem["title"] as! String
|
|
let targetMethodName = "onContextMenuActionItemClicked-" + String(self.hash) + "-" +
|
|
(id is Int64 ? String(id as! Int64) : id as! String)
|
|
if !self.responds(to: Selector(targetMethodName)) {
|
|
let customAction: () -> Void = {
|
|
self.channelDelegate?.onContextMenuActionItemClicked(id: id, title: title)
|
|
}
|
|
let castedCustomAction: AnyObject = unsafeBitCast(customAction as @convention(block) () -> Void, to: AnyObject.self)
|
|
let swizzledImplementation = imp_implementationWithBlock(castedCustomAction)
|
|
class_addMethod(InAppWebView.self, Selector(targetMethodName), swizzledImplementation, nil)
|
|
self.customIMPs.append(swizzledImplementation)
|
|
}
|
|
let item = UIMenuItem(title: title, action: Selector(targetMethodName))
|
|
UIMenuController.shared.menuItems!.append(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
|
if let _ = sender as? UIMenuController {
|
|
if self.settings?.disableContextMenu == true {
|
|
if !onCreateContextMenuEventTriggeredWhenMenuDisabled {
|
|
// workaround to trigger onCreateContextMenu event as the same on Android
|
|
self.onCreateContextMenu()
|
|
onCreateContextMenuEventTriggeredWhenMenuDisabled = true
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
self.onCreateContextMenuEventTriggeredWhenMenuDisabled = false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
if let menu = contextMenu {
|
|
let contextMenuSettings = ContextMenuSettings()
|
|
if let contextMenuSettingsMap = menu["settings"] as? [String: Any?] {
|
|
let _ = contextMenuSettings.parse(settings: contextMenuSettingsMap)
|
|
if !action.description.starts(with: "onContextMenuActionItemClicked-") && contextMenuSettings.hideDefaultSystemContextMenuItems {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
if contextMenuIsShowing, !action.description.starts(with: "onContextMenuActionItemClicked-") {
|
|
let id = action.description.compactMap({ $0.asciiValue?.description }).joined()
|
|
self.channelDelegate?.onContextMenuActionItemClicked(id: id, title: action.description)
|
|
}
|
|
}
|
|
|
|
return super.canPerformAction(action, withSender: sender)
|
|
}
|
|
|
|
// For some reasons, using the scrollViewDidEndDragging event, in some rare cases, could block
|
|
// the scroll gesture
|
|
@objc func endDraggingDetected() {
|
|
// detect end dragging
|
|
if panGestureRecognizer.state == .ended {
|
|
// fix for pull-to-refresh jittering when the touch drag event is held
|
|
if let pullToRefreshControl = pullToRefreshControl,
|
|
pullToRefreshControl.shouldCallOnRefresh {
|
|
pullToRefreshControl.onRefresh()
|
|
}
|
|
}
|
|
}
|
|
|
|
public func prepare() {
|
|
scrollView.addGestureRecognizer(self.longPressRecognizer)
|
|
scrollView.addGestureRecognizer(self.recognizerForDisablingContextMenuOnLinks)
|
|
scrollView.addGestureRecognizer(self.panGestureRecognizer)
|
|
scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset), options: [.new, .old], context: nil)
|
|
scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.zoomScale), options: [.new, .old], context: nil)
|
|
|
|
addObserver(self,
|
|
forKeyPath: #keyPath(WKWebView.estimatedProgress),
|
|
options: .new,
|
|
context: nil)
|
|
|
|
addObserver(self,
|
|
forKeyPath: #keyPath(WKWebView.url),
|
|
options: [.new, .old],
|
|
context: nil)
|
|
|
|
addObserver(self,
|
|
forKeyPath: #keyPath(WKWebView.title),
|
|
options: [.new, .old],
|
|
context: nil)
|
|
|
|
if #available(iOS 15.0, *) {
|
|
addObserver(self,
|
|
forKeyPath: #keyPath(WKWebView.cameraCaptureState),
|
|
options: [.new, .old],
|
|
context: nil)
|
|
|
|
addObserver(self,
|
|
forKeyPath: #keyPath(WKWebView.microphoneCaptureState),
|
|
options: [.new, .old],
|
|
context: nil)
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(onCreateContextMenu),
|
|
name: UIMenuController.willShowMenuNotification,
|
|
object: nil)
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(onHideContextMenu),
|
|
name: UIMenuController.didHideMenuNotification,
|
|
object: nil)
|
|
|
|
// TODO: Still not working on iOS 16.0!
|
|
// if #available(iOS 16.0, *) {
|
|
// addObserver(self,
|
|
// forKeyPath: #keyPath(WKWebView.fullscreenState),
|
|
// options: .new,
|
|
// context: nil)
|
|
// } else {
|
|
// listen for videos playing in fullscreen
|
|
NotificationCenter.default.addObserver(self,
|
|
selector: #selector(onEnterFullscreen(_:)),
|
|
name: UIWindow.didBecomeVisibleNotification,
|
|
object: window)
|
|
|
|
// listen for videos stopping to play in fullscreen
|
|
NotificationCenter.default.addObserver(self,
|
|
selector: #selector(onExitFullscreen(_:)),
|
|
name: UIWindow.didBecomeHiddenNotification,
|
|
object: window)
|
|
// }
|
|
|
|
if let settings = settings {
|
|
if settings.transparentBackground {
|
|
isOpaque = false
|
|
backgroundColor = UIColor.clear
|
|
scrollView.backgroundColor = UIColor.clear
|
|
}
|
|
|
|
// prevent webView from bouncing
|
|
if settings.disallowOverScroll {
|
|
if responds(to: #selector(getter: scrollView)) {
|
|
scrollView.bounces = false
|
|
}
|
|
else {
|
|
for subview: UIView in subviews {
|
|
if subview is UIScrollView {
|
|
(subview as! UIScrollView).bounces = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if #available(iOS 11.0, *) {
|
|
accessibilityIgnoresInvertColors = settings.accessibilityIgnoresInvertColors
|
|
scrollView.contentInsetAdjustmentBehavior =
|
|
UIScrollView.ContentInsetAdjustmentBehavior.init(rawValue: settings.contentInsetAdjustmentBehavior)!
|
|
}
|
|
|
|
allowsBackForwardNavigationGestures = settings.allowsBackForwardNavigationGestures
|
|
if #available(iOS 9.0, *) {
|
|
allowsLinkPreview = settings.allowsLinkPreview
|
|
if !settings.userAgent.isEmpty {
|
|
customUserAgent = settings.userAgent
|
|
}
|
|
}
|
|
|
|
if #available(iOS 13.0, *) {
|
|
scrollView.automaticallyAdjustsScrollIndicatorInsets = settings.automaticallyAdjustsScrollIndicatorInsets
|
|
}
|
|
|
|
scrollView.showsVerticalScrollIndicator = !settings.disableVerticalScroll
|
|
scrollView.showsHorizontalScrollIndicator = !settings.disableHorizontalScroll
|
|
scrollView.showsVerticalScrollIndicator = settings.verticalScrollBarEnabled
|
|
scrollView.showsHorizontalScrollIndicator = settings.horizontalScrollBarEnabled
|
|
scrollView.isScrollEnabled = !(settings.disableVerticalScroll && settings.disableHorizontalScroll)
|
|
scrollView.isDirectionalLockEnabled = settings.isDirectionalLockEnabled
|
|
|
|
scrollView.decelerationRate = Util.getDecelerationRate(type: settings.decelerationRate)
|
|
scrollView.alwaysBounceVertical = settings.alwaysBounceVertical
|
|
scrollView.alwaysBounceHorizontal = settings.alwaysBounceHorizontal
|
|
scrollView.scrollsToTop = settings.scrollsToTop
|
|
scrollView.isPagingEnabled = settings.isPagingEnabled
|
|
scrollView.maximumZoomScale = CGFloat(settings.maximumZoomScale)
|
|
scrollView.minimumZoomScale = CGFloat(settings.minimumZoomScale)
|
|
|
|
if #available(iOS 14.0, *) {
|
|
mediaType = settings.mediaType
|
|
pageZoom = CGFloat(settings.pageZoom)
|
|
}
|
|
|
|
if #available(iOS 15.0, *) {
|
|
if let underPageBackgroundColor = settings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty {
|
|
self.underPageBackgroundColor = UIColor(hexString: underPageBackgroundColor)
|
|
}
|
|
}
|
|
|
|
if #available(iOS 15.5, *) {
|
|
if let minViewportInset = settings.minimumViewportInset, let maxViewportInset = settings.maximumViewportInset {
|
|
setMinimumViewportInset(minViewportInset, maximumViewportInset: maxViewportInset)
|
|
}
|
|
}
|
|
|
|
if #available(iOS 16.0, *) {
|
|
isFindInteractionEnabled = settings.isFindInteractionEnabled
|
|
}
|
|
|
|
// debugging is always enabled for iOS,
|
|
// there isn't any option to set about it such as on Android.
|
|
|
|
if settings.clearCache {
|
|
clearCache()
|
|
}
|
|
}
|
|
|
|
prepareAndAddUserScripts()
|
|
|
|
if windowId != nil {
|
|
// The new created window webview has the same WKWebViewConfiguration variable reference.
|
|
// So, we cannot set another WKWebViewConfiguration for it unfortunately!
|
|
// This is a limitation of the official WebKit API.
|
|
return
|
|
}
|
|
|
|
configuration.preferences = WKPreferences()
|
|
if let settings = settings {
|
|
if #available(iOS 9.0, *) {
|
|
configuration.allowsAirPlayForMediaPlayback = settings.allowsAirPlayForMediaPlayback
|
|
configuration.allowsPictureInPictureMediaPlayback = settings.allowsPictureInPictureMediaPlayback
|
|
}
|
|
|
|
configuration.preferences.javaScriptCanOpenWindowsAutomatically = settings.javaScriptCanOpenWindowsAutomatically
|
|
configuration.preferences.minimumFontSize = CGFloat(settings.minimumFontSize)
|
|
|
|
if #available(iOS 13.0, *) {
|
|
configuration.preferences.isFraudulentWebsiteWarningEnabled = settings.isFraudulentWebsiteWarningEnabled
|
|
configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: settings.preferredContentMode)!
|
|
}
|
|
|
|
configuration.preferences.javaScriptEnabled = settings.javaScriptEnabled
|
|
if #available(iOS 14.0, *) {
|
|
configuration.defaultWebpagePreferences.allowsContentJavaScript = settings.javaScriptEnabled
|
|
}
|
|
|
|
if #available(iOS 14.5, *) {
|
|
configuration.preferences.isTextInteractionEnabled = settings.isTextInteractionEnabled
|
|
}
|
|
|
|
if #available(iOS 15.4, *) {
|
|
configuration.preferences.isSiteSpecificQuirksModeEnabled = settings.isSiteSpecificQuirksModeEnabled
|
|
configuration.preferences.isElementFullscreenEnabled = settings.isElementFullscreenEnabled
|
|
}
|
|
}
|
|
}
|
|
|
|
public func prepareAndAddUserScripts() -> Void {
|
|
if windowId != nil {
|
|
// The new created window webview has the same WKWebViewConfiguration variable reference.
|
|
// So, we cannot set another WKWebViewConfiguration for it unfortunately!
|
|
// This is a limitation of the official WebKit API.
|
|
return
|
|
}
|
|
configuration.userContentController = WKUserContentController()
|
|
configuration.userContentController.initialize()
|
|
|
|
if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled {
|
|
return
|
|
}
|
|
|
|
configuration.userContentController.addPluginScript(PROMISE_POLYFILL_JS_PLUGIN_SCRIPT)
|
|
configuration.userContentController.addPluginScript(JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT)
|
|
configuration.userContentController.addPluginScript(CONSOLE_LOG_JS_PLUGIN_SCRIPT)
|
|
configuration.userContentController.addPluginScript(PRINT_JS_PLUGIN_SCRIPT)
|
|
configuration.userContentController.addPluginScript(ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT)
|
|
configuration.userContentController.addPluginScript(ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT)
|
|
configuration.userContentController.addPluginScript(FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT)
|
|
configuration.userContentController.addPluginScript(LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT)
|
|
configuration.userContentController.addPluginScript(FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT)
|
|
configuration.userContentController.addPluginScript(ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT)
|
|
if let settings = settings {
|
|
if settings.useShouldInterceptAjaxRequest {
|
|
configuration.userContentController.addPluginScript(INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT)
|
|
}
|
|
if settings.useShouldInterceptFetchRequest {
|
|
configuration.userContentController.addPluginScript(INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT)
|
|
}
|
|
if settings.useOnLoadResource {
|
|
configuration.userContentController.addPluginScript(ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT)
|
|
}
|
|
if !settings.supportZoom {
|
|
configuration.userContentController.addPluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT)
|
|
} else if settings.enableViewportScale {
|
|
configuration.userContentController.addPluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT)
|
|
}
|
|
}
|
|
configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received")
|
|
configuration.userContentController.add(self, name: "onCallAsyncJavaScriptResultBelowIOS14Received")
|
|
configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessagePortMessageReceived")
|
|
configuration.userContentController.add(self, name: "onWebMessagePortMessageReceived")
|
|
configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessageListenerPostMessageReceived")
|
|
configuration.userContentController.add(self, name: "onWebMessageListenerPostMessageReceived")
|
|
configuration.userContentController.addUserOnlyScripts(initialUserScripts)
|
|
configuration.userContentController.sync(scriptMessageHandler: self)
|
|
}
|
|
|
|
public static func preWKWebViewConfiguration(settings: InAppWebViewSettings?) -> WKWebViewConfiguration {
|
|
let configuration = WKWebViewConfiguration()
|
|
|
|
configuration.processPool = WKProcessPoolManager.sharedProcessPool
|
|
|
|
if let settings = settings {
|
|
configuration.allowsInlineMediaPlayback = settings.allowsInlineMediaPlayback
|
|
configuration.suppressesIncrementalRendering = settings.suppressesIncrementalRendering
|
|
configuration.selectionGranularity = WKSelectionGranularity.init(rawValue: settings.selectionGranularity)!
|
|
|
|
if settings.allowUniversalAccessFromFileURLs {
|
|
configuration.setValue(settings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs")
|
|
}
|
|
|
|
if settings.allowFileAccessFromFileURLs {
|
|
configuration.preferences.setValue(settings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs")
|
|
}
|
|
|
|
if #available(iOS 9.0, *) {
|
|
if settings.incognito {
|
|
configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()
|
|
} else if settings.cacheEnabled {
|
|
configuration.websiteDataStore = WKWebsiteDataStore.default()
|
|
}
|
|
if !settings.applicationNameForUserAgent.isEmpty {
|
|
if let applicationNameForUserAgent = configuration.applicationNameForUserAgent {
|
|
configuration.applicationNameForUserAgent = applicationNameForUserAgent + " " + settings.applicationNameForUserAgent
|
|
}
|
|
}
|
|
}
|
|
|
|
if #available(iOS 10.0, *) {
|
|
configuration.ignoresViewportScaleLimits = settings.ignoresViewportScaleLimits
|
|
|
|
var dataDetectorTypes = WKDataDetectorTypes.init(rawValue: 0)
|
|
for type in settings.dataDetectorTypes {
|
|
let dataDetectorType = Util.getDataDetectorType(type: type)
|
|
dataDetectorTypes = WKDataDetectorTypes(rawValue: dataDetectorTypes.rawValue | dataDetectorType.rawValue)
|
|
}
|
|
configuration.dataDetectorTypes = dataDetectorTypes
|
|
|
|
configuration.mediaTypesRequiringUserActionForPlayback = settings.mediaPlaybackRequiresUserGesture ? .all : []
|
|
} else {
|
|
// Fallback on earlier versions
|
|
configuration.mediaPlaybackRequiresUserAction = settings.mediaPlaybackRequiresUserGesture
|
|
}
|
|
|
|
if #available(iOS 11.0, *) {
|
|
for scheme in settings.resourceCustomSchemes {
|
|
configuration.setURLSchemeHandler(CustomSchemeHandler(), forURLScheme: scheme)
|
|
}
|
|
if settings.sharedCookiesEnabled {
|
|
// More info to sending cookies with WKWebView
|
|
// https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303
|
|
// Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies
|
|
// See also https://forums.developer.apple.com/thread/97194
|
|
// check if websiteDataStore has not been initialized before
|
|
if(!settings.incognito && !settings.cacheEnabled) {
|
|
configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()
|
|
}
|
|
for cookie in HTTPCookieStorage.shared.cookies ?? [] {
|
|
configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
if #available(iOS 14.0, *) {
|
|
configuration.limitsNavigationsToAppBoundDomains = settings.limitsNavigationsToAppBoundDomains
|
|
}
|
|
|
|
if #available(iOS 14.5, *) {
|
|
configuration.upgradeKnownHostsToHTTPS = settings.upgradeKnownHostsToHTTPS
|
|
}
|
|
}
|
|
|
|
return configuration
|
|
}
|
|
|
|
@objc func onCreateContextMenu() {
|
|
let mapSorted = SharedLastTouchPointTimestamp.sorted { $0.value > $1.value }
|
|
if (mapSorted.first?.key != self) {
|
|
return
|
|
}
|
|
|
|
contextMenuIsShowing = true
|
|
|
|
let hitTestResult = HitTestResult(type: .unknownType, extra: nil)
|
|
|
|
if let lastLongPressTouhLocation = lastLongPressTouchPoint {
|
|
if configuration.preferences.javaScriptEnabled {
|
|
self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(lastLongPressTouhLocation.x),\(lastLongPressTouhLocation.y))", completionHandler: {(value, error) in
|
|
if error != nil {
|
|
print("Long press gesture recognizer error: \(error?.localizedDescription ?? "")")
|
|
} else if let value = value as? [String: Any?] {
|
|
self.channelDelegate?.onCreateContextMenu(hitTestResult: HitTestResult.fromMap(map: value) ?? hitTestResult)
|
|
} else {
|
|
self.channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult)
|
|
}
|
|
})
|
|
} else {
|
|
channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult)
|
|
}
|
|
} else {
|
|
channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult)
|
|
}
|
|
}
|
|
|
|
@objc func onHideContextMenu() {
|
|
if contextMenuIsShowing == false {
|
|
return
|
|
}
|
|
contextMenuIsShowing = false
|
|
channelDelegate?.onHideContextMenu()
|
|
}
|
|
|
|
override public func observeValue(forKeyPath keyPath: String?, of object: Any?,
|
|
change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
|
if keyPath == #keyPath(WKWebView.estimatedProgress) {
|
|
initializeWindowIdJS()
|
|
let progress = Int(estimatedProgress * 100)
|
|
channelDelegate?.onProgressChanged(progress: progress)
|
|
inAppBrowserDelegate?.didChangeProgress(progress: estimatedProgress)
|
|
} else if keyPath == #keyPath(WKWebView.url) && change?[.newKey] is URL {
|
|
initializeWindowIdJS()
|
|
let newUrl = change?[NSKeyValueChangeKey.newKey] as? URL
|
|
channelDelegate?.onUpdateVisitedHistory(url: newUrl?.absoluteString, isReload: nil)
|
|
inAppBrowserDelegate?.didUpdateVisitedHistory(url: newUrl)
|
|
} else if keyPath == #keyPath(WKWebView.title) && change?[.newKey] is String {
|
|
let newTitle = change?[.newKey] as? String
|
|
channelDelegate?.onTitleChanged(title: newTitle)
|
|
inAppBrowserDelegate?.didChangeTitle(title: newTitle)
|
|
} else if keyPath == #keyPath(UIScrollView.contentOffset) {
|
|
let newContentOffset = change?[.newKey] as? CGPoint
|
|
let oldContentOffset = change?[.oldKey] as? CGPoint
|
|
let startedByUser = scrollView.isDragging || scrollView.isDecelerating
|
|
if newContentOffset != oldContentOffset {
|
|
DispatchQueue.main.async {
|
|
self.onScrollChanged(startedByUser: startedByUser, oldContentOffset: oldContentOffset)
|
|
}
|
|
}
|
|
}
|
|
else if #available(iOS 15.0, *) {
|
|
if keyPath == #keyPath(WKWebView.cameraCaptureState) || keyPath == #keyPath(WKWebView.microphoneCaptureState) {
|
|
var oldState: WKMediaCaptureState? = nil
|
|
if let oldValue = change?[.oldKey] as? Int {
|
|
oldState = WKMediaCaptureState.init(rawValue: oldValue)
|
|
}
|
|
var newState: WKMediaCaptureState? = nil
|
|
if let newValue = change?[.newKey] as? Int {
|
|
newState = WKMediaCaptureState.init(rawValue: newValue)
|
|
}
|
|
if oldState != newState {
|
|
if keyPath == #keyPath(WKWebView.cameraCaptureState) {
|
|
channelDelegate?.onCameraCaptureStateChanged(oldState: oldState, newState: newState)
|
|
} else {
|
|
channelDelegate?.onMicrophoneCaptureStateChanged(oldState: oldState, newState: newState)
|
|
}
|
|
}
|
|
}
|
|
} else if #available(iOS 16.0, *) {
|
|
// TODO: Still not working on iOS 16.0!
|
|
// if keyPath == #keyPath(WKWebView.fullscreenState) {
|
|
// if fullscreenState == .enteringFullscreen {
|
|
// channelDelegate?.onEnterFullscreen()
|
|
// } else if fullscreenState == .exitingFullscreen {
|
|
// channelDelegate?.onExitFullscreen()
|
|
// }
|
|
// }
|
|
}
|
|
replaceGestureHandlerIfNeeded()
|
|
}
|
|
|
|
public func initializeWindowIdJS() {
|
|
if let windowId = windowId {
|
|
if #available(iOS 14.0, *) {
|
|
let contentWorlds = configuration.userContentController.getContentWorlds(with: windowId)
|
|
for contentWorld in contentWorlds {
|
|
let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId))
|
|
evaluateJavascript(source: source, contentWorld: contentWorld)
|
|
}
|
|
} else {
|
|
let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId))
|
|
evaluateJavascript(source: source)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func goBackOrForward(steps: Int) {
|
|
if canGoBackOrForward(steps: steps) {
|
|
if (steps > 0) {
|
|
let index = steps - 1
|
|
go(to: self.backForwardList.forwardList[index])
|
|
}
|
|
else if (steps < 0){
|
|
let backListLength = self.backForwardList.backList.count
|
|
let index = backListLength + steps
|
|
go(to: self.backForwardList.backList[index])
|
|
}
|
|
}
|
|
}
|
|
|
|
public func canGoBackOrForward(steps: Int) -> Bool {
|
|
let currentIndex = self.backForwardList.backList.count
|
|
return (steps >= 0)
|
|
? steps <= self.backForwardList.forwardList.count
|
|
: currentIndex + steps >= 0
|
|
}
|
|
|
|
@available(iOS 11.0, *)
|
|
public func takeScreenshot (with: [String: Any?]?, completionHandler: @escaping (_ screenshot: Data?) -> Void) {
|
|
var snapshotConfiguration: WKSnapshotConfiguration? = nil
|
|
if let with = with {
|
|
snapshotConfiguration = WKSnapshotConfiguration()
|
|
if let rect = with["rect"] as? [String: Double] {
|
|
snapshotConfiguration!.rect = CGRect.fromMap(map: rect)
|
|
}
|
|
if let snapshotWidth = with["snapshotWidth"] as? Double {
|
|
snapshotConfiguration!.snapshotWidth = NSNumber(value: snapshotWidth)
|
|
}
|
|
if #available(iOS 13.0, *), let afterScreenUpdates = with["afterScreenUpdates"] as? Bool {
|
|
snapshotConfiguration!.afterScreenUpdates = afterScreenUpdates
|
|
}
|
|
}
|
|
takeSnapshot(with: snapshotConfiguration, completionHandler: {(image, error) -> Void in
|
|
var imageData: Data? = nil
|
|
if let screenshot = image {
|
|
if let with = with {
|
|
switch with["compressFormat"] as! String {
|
|
case "JPEG":
|
|
let quality = Float(with["quality"] as! Int) / 100
|
|
imageData = screenshot.jpegData(compressionQuality: CGFloat(quality))
|
|
break
|
|
case "PNG":
|
|
imageData = screenshot.pngData()
|
|
break
|
|
default:
|
|
imageData = screenshot.pngData()
|
|
}
|
|
}
|
|
else {
|
|
imageData = screenshot.pngData()
|
|
}
|
|
}
|
|
completionHandler(imageData)
|
|
})
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
public func createPdf (configuration: [String: Any?]?, completionHandler: @escaping (_ pdf: Data?) -> Void) {
|
|
let pdfConfiguration: WKPDFConfiguration = .init()
|
|
if let configuration = configuration {
|
|
if let rect = configuration["rect"] as? [String: Double] {
|
|
pdfConfiguration.rect = CGRect.fromMap(map: rect)
|
|
}
|
|
}
|
|
createPDF(configuration: pdfConfiguration) { (result) in
|
|
switch (result) {
|
|
case .success(let data):
|
|
completionHandler(data)
|
|
return
|
|
case .failure(let error):
|
|
print(error.localizedDescription)
|
|
completionHandler(nil)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
public func createWebArchiveData (dataCompletionHandler: @escaping (_ webArchiveData: Data?) -> Void) {
|
|
createWebArchiveData(completionHandler: { (result) in
|
|
switch (result) {
|
|
case .success(let data):
|
|
dataCompletionHandler(data)
|
|
return
|
|
case .failure(let error):
|
|
print(error.localizedDescription)
|
|
dataCompletionHandler(nil)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
public func saveWebArchive (filePath: String, autoname: Bool, completionHandler: @escaping (_ path: String?) -> Void) {
|
|
createWebArchiveData(dataCompletionHandler: { (webArchiveData) in
|
|
if let webArchiveData = webArchiveData {
|
|
var localUrl = URL(fileURLWithPath: filePath)
|
|
if autoname {
|
|
if let url = self.url {
|
|
// tries to mimic Android saveWebArchive method
|
|
let invalidCharacters = CharacterSet(charactersIn: "\\/:*?\"<>|")
|
|
.union(.newlines)
|
|
.union(.illegalCharacters)
|
|
.union(.controlCharacters)
|
|
|
|
let currentPageUrlFileName = url.path
|
|
.components(separatedBy: invalidCharacters)
|
|
.joined(separator: "")
|
|
|
|
let fullPath = filePath + "/" + currentPageUrlFileName + ".webarchive"
|
|
localUrl = URL(fileURLWithPath: fullPath)
|
|
} else {
|
|
completionHandler(nil)
|
|
return
|
|
}
|
|
}
|
|
do {
|
|
try webArchiveData.write(to: localUrl)
|
|
completionHandler(localUrl.path)
|
|
} catch {
|
|
// Catch any errors
|
|
print(error.localizedDescription)
|
|
completionHandler(nil)
|
|
}
|
|
} else {
|
|
completionHandler(nil)
|
|
}
|
|
})
|
|
}
|
|
|
|
public func loadUrl(urlRequest: URLRequest, allowingReadAccessTo: URL?) {
|
|
let url = urlRequest.url!
|
|
|
|
if #available(iOS 9.0, *), let allowingReadAccessTo = allowingReadAccessTo, url.scheme == "file", allowingReadAccessTo.scheme == "file" {
|
|
loadFileURL(url, allowingReadAccessTo: allowingReadAccessTo)
|
|
} else {
|
|
load(urlRequest)
|
|
}
|
|
}
|
|
|
|
public func postUrl(url: URL, postData: Data) {
|
|
var request = URLRequest(url: url)
|
|
|
|
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
|
request.httpMethod = "POST"
|
|
request.httpBody = postData
|
|
load(request)
|
|
}
|
|
|
|
public func loadData(data: String, mimeType: String, encoding: String, baseUrl: URL, allowingReadAccessTo: URL?) {
|
|
if #available(iOS 9.0, *), let allowingReadAccessTo = allowingReadAccessTo, baseUrl.scheme == "file", allowingReadAccessTo.scheme == "file" {
|
|
loadFileURL(baseUrl, allowingReadAccessTo: allowingReadAccessTo)
|
|
}
|
|
|
|
if #available(iOS 9.0, *) {
|
|
load(data.data(using: .utf8)!, mimeType: mimeType, characterEncodingName: encoding, baseURL: baseUrl)
|
|
} else {
|
|
loadHTMLString(data, baseURL: baseUrl)
|
|
}
|
|
}
|
|
|
|
public func loadFile(assetFilePath: String) throws {
|
|
let assetURL = try Util.getUrlAsset(assetFilePath: assetFilePath)
|
|
let urlRequest = URLRequest(url: assetURL)
|
|
loadUrl(urlRequest: urlRequest, allowingReadAccessTo: nil)
|
|
}
|
|
|
|
func setSettings(newSettings: InAppWebViewSettings, newSettingsMap: [String: Any]) {
|
|
|
|
// MUST be the first! In this way, all the settings that uses evaluateJavaScript can be applied/blocked!
|
|
if #available(iOS 13.0, *) {
|
|
if newSettingsMap["applePayAPIEnabled"] != nil && settings?.applePayAPIEnabled != newSettings.applePayAPIEnabled {
|
|
if let settings = settings {
|
|
settings.applePayAPIEnabled = newSettings.applePayAPIEnabled
|
|
}
|
|
if !newSettings.applePayAPIEnabled {
|
|
// re-add WKUserScripts for the next page load
|
|
prepareAndAddUserScripts()
|
|
} else {
|
|
configuration.userContentController.removeAllUserScripts()
|
|
}
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["transparentBackground"] != nil && settings?.transparentBackground != newSettings.transparentBackground {
|
|
if newSettings.transparentBackground {
|
|
isOpaque = false
|
|
backgroundColor = UIColor.clear
|
|
scrollView.backgroundColor = UIColor.clear
|
|
} else {
|
|
isOpaque = true
|
|
backgroundColor = nil
|
|
scrollView.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1)
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["disallowOverScroll"] != nil && settings?.disallowOverScroll != newSettings.disallowOverScroll {
|
|
if responds(to: #selector(getter: scrollView)) {
|
|
scrollView.bounces = !newSettings.disallowOverScroll
|
|
}
|
|
else {
|
|
for subview: UIView in subviews {
|
|
if subview is UIScrollView {
|
|
(subview as! UIScrollView).bounces = !newSettings.disallowOverScroll
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if #available(iOS 9.0, *) {
|
|
if (newSettingsMap["incognito"] != nil && settings?.incognito != newSettings.incognito && newSettings.incognito) {
|
|
configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()
|
|
} else if (newSettingsMap["cacheEnabled"] != nil && settings?.cacheEnabled != newSettings.cacheEnabled && newSettings.cacheEnabled) {
|
|
configuration.websiteDataStore = WKWebsiteDataStore.default()
|
|
}
|
|
}
|
|
|
|
if #available(iOS 11.0, *) {
|
|
if (newSettingsMap["sharedCookiesEnabled"] != nil && settings?.sharedCookiesEnabled != newSettings.sharedCookiesEnabled && newSettings.sharedCookiesEnabled) {
|
|
if(!newSettings.incognito && !newSettings.cacheEnabled) {
|
|
configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()
|
|
}
|
|
for cookie in HTTPCookieStorage.shared.cookies ?? [] {
|
|
configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: nil)
|
|
}
|
|
}
|
|
if newSettingsMap["accessibilityIgnoresInvertColors"] != nil && settings?.accessibilityIgnoresInvertColors != newSettings.accessibilityIgnoresInvertColors {
|
|
accessibilityIgnoresInvertColors = newSettings.accessibilityIgnoresInvertColors
|
|
}
|
|
if newSettingsMap["contentInsetAdjustmentBehavior"] != nil && settings?.contentInsetAdjustmentBehavior != newSettings.contentInsetAdjustmentBehavior {
|
|
scrollView.contentInsetAdjustmentBehavior =
|
|
UIScrollView.ContentInsetAdjustmentBehavior.init(rawValue: newSettings.contentInsetAdjustmentBehavior)!
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["enableViewportScale"] != nil && settings?.enableViewportScale != newSettings.enableViewportScale {
|
|
if !newSettings.enableViewportScale {
|
|
if configuration.userContentController.userScripts.contains(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) {
|
|
configuration.userContentController.removePluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT)
|
|
evaluateJavaScript(NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE)
|
|
}
|
|
} else {
|
|
evaluateJavaScript(ENABLE_VIEWPORT_SCALE_JS_SOURCE)
|
|
configuration.userContentController.addUserScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT)
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["supportZoom"] != nil && settings?.supportZoom != newSettings.supportZoom {
|
|
if newSettings.supportZoom {
|
|
if configuration.userContentController.userScripts.contains(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) {
|
|
configuration.userContentController.removePluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT)
|
|
evaluateJavaScript(SUPPORT_ZOOM_JS_SOURCE)
|
|
}
|
|
} else {
|
|
evaluateJavaScript(NOT_SUPPORT_ZOOM_JS_SOURCE)
|
|
configuration.userContentController.addUserScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT)
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["useOnLoadResource"] != nil && settings?.useOnLoadResource != newSettings.useOnLoadResource {
|
|
if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled {
|
|
enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE,
|
|
enable: newSettings.useOnLoadResource,
|
|
pluginScript: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT)
|
|
} else {
|
|
newSettings.useOnLoadResource = false
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["useShouldInterceptAjaxRequest"] != nil && settings?.useShouldInterceptAjaxRequest != newSettings.useShouldInterceptAjaxRequest {
|
|
if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled {
|
|
enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE,
|
|
enable: newSettings.useShouldInterceptAjaxRequest,
|
|
pluginScript: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT)
|
|
} else {
|
|
newSettings.useShouldInterceptFetchRequest = false
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["useShouldInterceptFetchRequest"] != nil && settings?.useShouldInterceptFetchRequest != newSettings.useShouldInterceptFetchRequest {
|
|
if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled {
|
|
enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE,
|
|
enable: newSettings.useShouldInterceptFetchRequest,
|
|
pluginScript: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT)
|
|
} else {
|
|
newSettings.useShouldInterceptFetchRequest = false
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["mediaPlaybackRequiresUserGesture"] != nil && settings?.mediaPlaybackRequiresUserGesture != newSettings.mediaPlaybackRequiresUserGesture {
|
|
if #available(iOS 10.0, *) {
|
|
configuration.mediaTypesRequiringUserActionForPlayback = (newSettings.mediaPlaybackRequiresUserGesture) ? .all : []
|
|
} else {
|
|
// Fallback on earlier versions
|
|
configuration.mediaPlaybackRequiresUserAction = newSettings.mediaPlaybackRequiresUserGesture
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["allowsInlineMediaPlayback"] != nil && settings?.allowsInlineMediaPlayback != newSettings.allowsInlineMediaPlayback {
|
|
configuration.allowsInlineMediaPlayback = newSettings.allowsInlineMediaPlayback
|
|
}
|
|
|
|
if newSettingsMap["suppressesIncrementalRendering"] != nil && settings?.suppressesIncrementalRendering != newSettings.suppressesIncrementalRendering {
|
|
configuration.suppressesIncrementalRendering = newSettings.suppressesIncrementalRendering
|
|
}
|
|
|
|
if newSettingsMap["allowsBackForwardNavigationGestures"] != nil && settings?.allowsBackForwardNavigationGestures != newSettings.allowsBackForwardNavigationGestures {
|
|
allowsBackForwardNavigationGestures = newSettings.allowsBackForwardNavigationGestures
|
|
}
|
|
|
|
if newSettingsMap["javaScriptCanOpenWindowsAutomatically"] != nil && settings?.javaScriptCanOpenWindowsAutomatically != newSettings.javaScriptCanOpenWindowsAutomatically {
|
|
configuration.preferences.javaScriptCanOpenWindowsAutomatically = newSettings.javaScriptCanOpenWindowsAutomatically
|
|
}
|
|
|
|
if newSettingsMap["minimumFontSize"] != nil && settings?.minimumFontSize != newSettings.minimumFontSize {
|
|
configuration.preferences.minimumFontSize = CGFloat(newSettings.minimumFontSize)
|
|
}
|
|
|
|
if newSettingsMap["selectionGranularity"] != nil && settings?.selectionGranularity != newSettings.selectionGranularity {
|
|
configuration.selectionGranularity = WKSelectionGranularity.init(rawValue: newSettings.selectionGranularity)!
|
|
}
|
|
|
|
if #available(iOS 10.0, *) {
|
|
if newSettingsMap["ignoresViewportScaleLimits"] != nil && settings?.ignoresViewportScaleLimits != newSettings.ignoresViewportScaleLimits {
|
|
configuration.ignoresViewportScaleLimits = newSettings.ignoresViewportScaleLimits
|
|
}
|
|
|
|
if newSettingsMap["dataDetectorTypes"] != nil && settings?.dataDetectorTypes != newSettings.dataDetectorTypes {
|
|
var dataDetectorTypes = WKDataDetectorTypes.init(rawValue: 0)
|
|
for type in newSettings.dataDetectorTypes {
|
|
let dataDetectorType = Util.getDataDetectorType(type: type)
|
|
dataDetectorTypes = WKDataDetectorTypes(rawValue: dataDetectorTypes.rawValue | dataDetectorType.rawValue)
|
|
}
|
|
configuration.dataDetectorTypes = dataDetectorTypes
|
|
}
|
|
}
|
|
|
|
if #available(iOS 13.0, *) {
|
|
if newSettingsMap["isFraudulentWebsiteWarningEnabled"] != nil && settings?.isFraudulentWebsiteWarningEnabled != newSettings.isFraudulentWebsiteWarningEnabled {
|
|
configuration.preferences.isFraudulentWebsiteWarningEnabled = newSettings.isFraudulentWebsiteWarningEnabled
|
|
}
|
|
if newSettingsMap["preferredContentMode"] != nil && settings?.preferredContentMode != newSettings.preferredContentMode {
|
|
configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: newSettings.preferredContentMode)!
|
|
}
|
|
if newSettingsMap["automaticallyAdjustsScrollIndicatorInsets"] != nil && settings?.automaticallyAdjustsScrollIndicatorInsets != newSettings.automaticallyAdjustsScrollIndicatorInsets {
|
|
scrollView.automaticallyAdjustsScrollIndicatorInsets = newSettings.automaticallyAdjustsScrollIndicatorInsets
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["disableVerticalScroll"] != nil && settings?.disableVerticalScroll != newSettings.disableVerticalScroll {
|
|
scrollView.showsVerticalScrollIndicator = !newSettings.disableVerticalScroll
|
|
}
|
|
if newSettingsMap["disableHorizontalScroll"] != nil && settings?.disableHorizontalScroll != newSettings.disableHorizontalScroll {
|
|
scrollView.showsHorizontalScrollIndicator = !newSettings.disableHorizontalScroll
|
|
}
|
|
|
|
if newSettingsMap["verticalScrollBarEnabled"] != nil && settings?.verticalScrollBarEnabled != newSettings.verticalScrollBarEnabled {
|
|
scrollView.showsVerticalScrollIndicator = newSettings.verticalScrollBarEnabled
|
|
}
|
|
if newSettingsMap["horizontalScrollBarEnabled"] != nil && settings?.horizontalScrollBarEnabled != newSettings.horizontalScrollBarEnabled {
|
|
scrollView.showsHorizontalScrollIndicator = newSettings.horizontalScrollBarEnabled
|
|
}
|
|
|
|
if newSettingsMap["isDirectionalLockEnabled"] != nil && settings?.isDirectionalLockEnabled != newSettings.isDirectionalLockEnabled {
|
|
scrollView.isDirectionalLockEnabled = newSettings.isDirectionalLockEnabled
|
|
}
|
|
|
|
if newSettingsMap["decelerationRate"] != nil && settings?.decelerationRate != newSettings.decelerationRate {
|
|
scrollView.decelerationRate = Util.getDecelerationRate(type: newSettings.decelerationRate)
|
|
}
|
|
if newSettingsMap["alwaysBounceVertical"] != nil && settings?.alwaysBounceVertical != newSettings.alwaysBounceVertical {
|
|
scrollView.alwaysBounceVertical = newSettings.alwaysBounceVertical
|
|
}
|
|
if newSettingsMap["alwaysBounceHorizontal"] != nil && settings?.alwaysBounceHorizontal != newSettings.alwaysBounceHorizontal {
|
|
scrollView.alwaysBounceHorizontal = newSettings.alwaysBounceHorizontal
|
|
}
|
|
if newSettingsMap["scrollsToTop"] != nil && settings?.scrollsToTop != newSettings.scrollsToTop {
|
|
scrollView.scrollsToTop = newSettings.scrollsToTop
|
|
}
|
|
if newSettingsMap["isPagingEnabled"] != nil && settings?.isPagingEnabled != newSettings.isPagingEnabled {
|
|
scrollView.scrollsToTop = newSettings.isPagingEnabled
|
|
}
|
|
if newSettingsMap["maximumZoomScale"] != nil && settings?.maximumZoomScale != newSettings.maximumZoomScale {
|
|
scrollView.maximumZoomScale = CGFloat(newSettings.maximumZoomScale)
|
|
}
|
|
if newSettingsMap["minimumZoomScale"] != nil && settings?.minimumZoomScale != newSettings.minimumZoomScale {
|
|
scrollView.minimumZoomScale = CGFloat(newSettings.minimumZoomScale)
|
|
}
|
|
|
|
if #available(iOS 9.0, *) {
|
|
if newSettingsMap["allowsLinkPreview"] != nil && settings?.allowsLinkPreview != newSettings.allowsLinkPreview {
|
|
allowsLinkPreview = newSettings.allowsLinkPreview
|
|
}
|
|
if newSettingsMap["allowsAirPlayForMediaPlayback"] != nil && settings?.allowsAirPlayForMediaPlayback != newSettings.allowsAirPlayForMediaPlayback {
|
|
configuration.allowsAirPlayForMediaPlayback = newSettings.allowsAirPlayForMediaPlayback
|
|
}
|
|
if newSettingsMap["allowsPictureInPictureMediaPlayback"] != nil && settings?.allowsPictureInPictureMediaPlayback != newSettings.allowsPictureInPictureMediaPlayback {
|
|
configuration.allowsPictureInPictureMediaPlayback = newSettings.allowsPictureInPictureMediaPlayback
|
|
}
|
|
if newSettingsMap["applicationNameForUserAgent"] != nil && settings?.applicationNameForUserAgent != newSettings.applicationNameForUserAgent && newSettings.applicationNameForUserAgent != "" {
|
|
configuration.applicationNameForUserAgent = newSettings.applicationNameForUserAgent
|
|
}
|
|
if newSettingsMap["userAgent"] != nil && settings?.userAgent != newSettings.userAgent && newSettings.userAgent != "" {
|
|
customUserAgent = newSettings.userAgent
|
|
}
|
|
}
|
|
|
|
if newSettingsMap["allowUniversalAccessFromFileURLs"] != nil && settings?.allowUniversalAccessFromFileURLs != newSettings.allowUniversalAccessFromFileURLs {
|
|
configuration.setValue(newSettings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs")
|
|
}
|
|
|
|
if newSettingsMap["allowFileAccessFromFileURLs"] != nil && settings?.allowFileAccessFromFileURLs != newSettings.allowFileAccessFromFileURLs {
|
|
configuration.preferences.setValue(newSettings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs")
|
|
}
|
|
|
|
if newSettingsMap["clearCache"] != nil && newSettings.clearCache {
|
|
clearCache()
|
|
}
|
|
|
|
if newSettingsMap["javaScriptEnabled"] != nil && settings?.javaScriptEnabled != newSettings.javaScriptEnabled {
|
|
configuration.preferences.javaScriptEnabled = newSettings.javaScriptEnabled
|
|
}
|
|
|
|
if #available(iOS 14.0, *) {
|
|
if settings?.mediaType != newSettings.mediaType {
|
|
mediaType = newSettings.mediaType
|
|
}
|
|
|
|
if newSettingsMap["pageZoom"] != nil && settings?.pageZoom != newSettings.pageZoom {
|
|
pageZoom = CGFloat(newSettings.pageZoom)
|
|
}
|
|
|
|
if newSettingsMap["limitsNavigationsToAppBoundDomains"] != nil && settings?.limitsNavigationsToAppBoundDomains != newSettings.limitsNavigationsToAppBoundDomains {
|
|
configuration.limitsNavigationsToAppBoundDomains = newSettings.limitsNavigationsToAppBoundDomains
|
|
}
|
|
|
|
if newSettingsMap["javaScriptEnabled"] != nil && settings?.javaScriptEnabled != newSettings.javaScriptEnabled {
|
|
configuration.defaultWebpagePreferences.allowsContentJavaScript = newSettings.javaScriptEnabled
|
|
}
|
|
}
|
|
|
|
if #available(iOS 11.0, *), newSettingsMap["contentBlockers"] != nil {
|
|
configuration.userContentController.removeAllContentRuleLists()
|
|
let contentBlockers = newSettings.contentBlockers
|
|
if contentBlockers.count > 0 {
|
|
do {
|
|
let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: [])
|
|
let blockRules = String(data: jsonData, encoding: String.Encoding.utf8)
|
|
WKContentRuleListStore.default().compileContentRuleList(
|
|
forIdentifier: "ContentBlockingRules",
|
|
encodedContentRuleList: blockRules) { (contentRuleList, error) in
|
|
if let error = error {
|
|
print(error.localizedDescription)
|
|
return
|
|
}
|
|
self.configuration.userContentController.add(contentRuleList!)
|
|
}
|
|
} catch {
|
|
print(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
|
|
if #available(iOS 14.5, *) {
|
|
if newSettingsMap["upgradeKnownHostsToHTTPS"] != nil && settings?.upgradeKnownHostsToHTTPS != newSettings.upgradeKnownHostsToHTTPS {
|
|
configuration.upgradeKnownHostsToHTTPS = newSettings.upgradeKnownHostsToHTTPS
|
|
}
|
|
if newSettingsMap["isTextInteractionEnabled"] != nil && settings?.isTextInteractionEnabled != newSettings.isTextInteractionEnabled {
|
|
configuration.preferences.isTextInteractionEnabled = newSettings.isTextInteractionEnabled
|
|
}
|
|
}
|
|
|
|
if #available(iOS 15.0, *) {
|
|
if newSettingsMap["underPageBackgroundColor"] != nil, settings?.underPageBackgroundColor != newSettings.underPageBackgroundColor,
|
|
let underPageBackgroundColor = newSettings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty {
|
|
self.underPageBackgroundColor = UIColor(hexString: underPageBackgroundColor)
|
|
}
|
|
}
|
|
if #available(iOS 15.4, *) {
|
|
if newSettingsMap["isSiteSpecificQuirksModeEnabled"] != nil, settings?.isSiteSpecificQuirksModeEnabled != newSettings.isSiteSpecificQuirksModeEnabled {
|
|
configuration.preferences.isSiteSpecificQuirksModeEnabled = newSettings.isSiteSpecificQuirksModeEnabled
|
|
}
|
|
}
|
|
if #available(iOS 15.5, *) {
|
|
if ((newSettingsMap["minimumViewportInset"] != nil && settings?.minimumViewportInset != newSettings.minimumViewportInset) ||
|
|
(newSettingsMap["maximumViewportInset"] != nil && settings?.maximumViewportInset != newSettings.maximumViewportInset)),
|
|
let minViewportInset = newSettings.minimumViewportInset, let maxViewportInset = newSettings.maximumViewportInset {
|
|
setMinimumViewportInset(minViewportInset, maximumViewportInset: maxViewportInset)
|
|
}
|
|
}
|
|
|
|
if #available(iOS 16.0, *) {
|
|
if newSettingsMap["isFindInteractionEnabled"] != nil, settings?.isFindInteractionEnabled != newSettings.isFindInteractionEnabled {
|
|
isFindInteractionEnabled = newSettings.isFindInteractionEnabled
|
|
}
|
|
}
|
|
|
|
scrollView.isScrollEnabled = !(newSettings.disableVerticalScroll && newSettings.disableHorizontalScroll)
|
|
|
|
self.settings = newSettings
|
|
}
|
|
|
|
func getSettings() -> [String: Any?]? {
|
|
if (self.settings == nil) {
|
|
return nil
|
|
}
|
|
return self.settings!.getRealSettings(obj: self)
|
|
}
|
|
|
|
public func enablePluginScriptAtRuntime(flagVariable: String, enable: Bool, pluginScript: PluginScript) {
|
|
evaluateJavascript(source: flagVariable) { (alreadyLoaded) in
|
|
if let alreadyLoaded = alreadyLoaded as? Bool, alreadyLoaded {
|
|
let enableSource = "\(flagVariable) = \(enable);"
|
|
if #available(iOS 14.0, *), pluginScript.requiredInAllContentWorlds {
|
|
for contentWorld in self.configuration.userContentController.contentWorlds {
|
|
self.evaluateJavaScript(enableSource, frame: nil, contentWorld: contentWorld, completionHandler: nil)
|
|
}
|
|
} else {
|
|
self.evaluateJavaScript(enableSource, completionHandler: nil)
|
|
}
|
|
if !enable {
|
|
self.configuration.userContentController.removePluginScripts(with: pluginScript.groupName!)
|
|
}
|
|
}
|
|
else if enable {
|
|
if #available(iOS 14.0, *), pluginScript.requiredInAllContentWorlds {
|
|
for contentWorld in self.configuration.userContentController.contentWorlds {
|
|
self.evaluateJavaScript(pluginScript.source, frame: nil, contentWorld: contentWorld, completionHandler: nil)
|
|
self.configuration.userContentController.addPluginScript(pluginScript)
|
|
}
|
|
} else {
|
|
self.evaluateJavaScript(pluginScript.source, completionHandler: nil)
|
|
self.configuration.userContentController.addPluginScript(pluginScript)
|
|
}
|
|
self.configuration.userContentController.sync(scriptMessageHandler: self)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func clearCache() {
|
|
if #available(iOS 9.0, *) {
|
|
//let websiteDataTypes = NSSet(array: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache])
|
|
let date = NSDate(timeIntervalSince1970: 0)
|
|
WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: date as Date, completionHandler:{ })
|
|
} else {
|
|
var libraryPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.libraryDirectory, FileManager.SearchPathDomainMask.userDomainMask, false).first!
|
|
libraryPath += "/Cookies"
|
|
|
|
do {
|
|
try FileManager.default.removeItem(atPath: libraryPath)
|
|
} catch {
|
|
print("can't clear cache")
|
|
}
|
|
URLCache.shared.removeAllCachedResponses()
|
|
}
|
|
}
|
|
|
|
public func injectDeferredObject(source: String, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) {
|
|
var jsToInject = source
|
|
if let wrapper = jsWrapper {
|
|
let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: [])
|
|
let sourceArrayString = String(data: jsonData!, encoding: String.Encoding.utf8)
|
|
let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2))
|
|
jsToInject = String(format: wrapper, sourceString!)
|
|
}
|
|
|
|
evaluateJavaScript(jsToInject) { (value, error) in
|
|
guard let completionHandler = completionHandler else {
|
|
return
|
|
}
|
|
|
|
if let error = error {
|
|
let userInfo = (error as NSError).userInfo
|
|
let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ??
|
|
userInfo["NSLocalizedDescription"] as? String ??
|
|
error.localizedDescription
|
|
self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3)
|
|
}
|
|
|
|
if value == nil {
|
|
completionHandler(nil)
|
|
return
|
|
}
|
|
|
|
completionHandler(value)
|
|
}
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
public func injectDeferredObject(source: String, contentWorld: WKContentWorld, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) {
|
|
var jsToInject = source
|
|
if let wrapper = jsWrapper {
|
|
let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: [])
|
|
let sourceArrayString = String(data: jsonData!, encoding: String.Encoding.utf8)
|
|
let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2))
|
|
jsToInject = String(format: wrapper, sourceString!)
|
|
}
|
|
|
|
jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: jsToInject, contentWorld: contentWorld)
|
|
|
|
evaluateJavaScript(jsToInject, frame: nil, contentWorld: contentWorld) { (evalResult) in
|
|
guard let completionHandler = completionHandler else {
|
|
return
|
|
}
|
|
|
|
switch (evalResult) {
|
|
case .success(let value):
|
|
completionHandler(value)
|
|
return
|
|
case .failure(let error):
|
|
let userInfo = (error as NSError).userInfo
|
|
let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ??
|
|
userInfo["NSLocalizedDescription"] as? String ??
|
|
error.localizedDescription
|
|
self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3)
|
|
break
|
|
}
|
|
|
|
completionHandler(nil)
|
|
}
|
|
}
|
|
|
|
public override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
|
|
if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled {
|
|
if let completionHandler = completionHandler {
|
|
completionHandler(nil, nil)
|
|
}
|
|
return
|
|
}
|
|
super.evaluateJavaScript(javaScriptString, completionHandler: completionHandler)
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
public func evaluateJavaScript(_ javaScript: String, frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result<Any, Error>) -> Void)? = nil) {
|
|
if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled {
|
|
return
|
|
}
|
|
super.evaluateJavaScript(javaScript, in: frame, in: contentWorld, completionHandler: completionHandler)
|
|
}
|
|
|
|
public func evaluateJavascript(source: String, completionHandler: ((Any?) -> Void)? = nil) {
|
|
injectDeferredObject(source: source, withWrapper: nil, completionHandler: completionHandler)
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
public func evaluateJavascript(source: String, contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) {
|
|
injectDeferredObject(source: source, contentWorld: contentWorld, withWrapper: nil, completionHandler: completionHandler)
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
public func callAsyncJavaScript(_ functionBody: String, arguments: [String : Any] = [:], frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result<Any, Error>) -> Void)? = nil) {
|
|
if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled {
|
|
return
|
|
}
|
|
super.callAsyncJavaScript(functionBody, arguments: arguments, in: frame, in: contentWorld, completionHandler: completionHandler)
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) {
|
|
let jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: functionBody, contentWorld: contentWorld)
|
|
|
|
callAsyncJavaScript(jsToInject, arguments: arguments, frame: nil, contentWorld: contentWorld) { (evalResult) in
|
|
guard let completionHandler = completionHandler else {
|
|
return
|
|
}
|
|
|
|
var body: [String: Any?] = [
|
|
"value": nil,
|
|
"error": nil
|
|
]
|
|
|
|
switch (evalResult) {
|
|
case .success(let value):
|
|
body["value"] = value
|
|
break
|
|
case .failure(let error):
|
|
let userInfo = (error as NSError).userInfo
|
|
body["error"] = userInfo["WKJavaScriptExceptionMessage"] ??
|
|
userInfo["NSLocalizedDescription"] as? String ??
|
|
error.localizedDescription
|
|
self.channelDelegate?.onConsoleMessage(message: String(describing: body["error"]), messageLevel: 3)
|
|
break
|
|
}
|
|
|
|
completionHandler(body)
|
|
}
|
|
}
|
|
|
|
@available(iOS 10.3, *)
|
|
public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], completionHandler: ((Any?) -> Void)? = nil) {
|
|
if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled {
|
|
completionHandler?(nil)
|
|
}
|
|
|
|
var jsToInject = functionBody
|
|
|
|
let resultUuid = NSUUID().uuidString
|
|
if let completionHandler = completionHandler {
|
|
callAsyncJavaScriptBelowIOS14Results[resultUuid] = completionHandler
|
|
}
|
|
|
|
var functionArgumentNamesList: [String] = []
|
|
var functionArgumentValuesList: [String] = []
|
|
let keys = arguments.keys
|
|
keys.forEach { (key) in
|
|
functionArgumentNamesList.append(key)
|
|
functionArgumentValuesList.append("obj.\(key)")
|
|
}
|
|
|
|
let functionArgumentNames = functionArgumentNamesList.joined(separator: ", ")
|
|
let functionArgumentValues = functionArgumentValuesList.joined(separator: ", ")
|
|
|
|
jsToInject = CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS
|
|
.replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES, with: functionArgumentNames)
|
|
.replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES, with: functionArgumentValues)
|
|
.replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ, with: Util.JSONStringify(value: arguments))
|
|
.replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_BODY, with: jsToInject)
|
|
.replacingOccurrences(of: PluginScriptsUtil.VAR_RESULT_UUID, with: resultUuid)
|
|
|
|
evaluateJavaScript(jsToInject) { (value, error) in
|
|
if let error = error {
|
|
let userInfo = (error as NSError).userInfo
|
|
let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ??
|
|
userInfo["NSLocalizedDescription"] as? String ??
|
|
error.localizedDescription
|
|
self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3)
|
|
completionHandler?(nil)
|
|
self.callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func injectJavascriptFileFromUrl(urlFile: String, scriptHtmlTagAttributes: [String:Any?]?) {
|
|
var scriptAttributes = ""
|
|
if let scriptHtmlTagAttributes = scriptHtmlTagAttributes {
|
|
if let typeAttr = scriptHtmlTagAttributes["type"] as? String {
|
|
scriptAttributes += " script.type = '\(typeAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
if let idAttr = scriptHtmlTagAttributes["id"] as? String {
|
|
let scriptIdEscaped = idAttr.replacingOccurrences(of: "\'", with: "\\'")
|
|
scriptAttributes += " script.id = '\(scriptIdEscaped)'; "
|
|
scriptAttributes += """
|
|
script.onload = function() {
|
|
if (window.\(JAVASCRIPT_BRIDGE_NAME) != null) {
|
|
window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onInjectedScriptLoaded', '\(scriptIdEscaped)');
|
|
}
|
|
};
|
|
"""
|
|
scriptAttributes += """
|
|
script.onerror = function() {
|
|
if (window.\(JAVASCRIPT_BRIDGE_NAME) != null) {
|
|
window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onInjectedScriptError', '\(scriptIdEscaped)');
|
|
}
|
|
};
|
|
"""
|
|
}
|
|
if let asyncAttr = scriptHtmlTagAttributes["async"] as? Bool, asyncAttr {
|
|
scriptAttributes += " script.async = true; "
|
|
}
|
|
if let deferAttr = scriptHtmlTagAttributes["defer"] as? Bool, deferAttr {
|
|
scriptAttributes += " script.defer = true; "
|
|
}
|
|
if let crossOriginAttr = scriptHtmlTagAttributes["crossOrigin"] as? String {
|
|
scriptAttributes += " script.crossOrigin = '\(crossOriginAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
if let integrityAttr = scriptHtmlTagAttributes["integrity"] as? String {
|
|
scriptAttributes += " script.integrity = '\(integrityAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
if let noModuleAttr = scriptHtmlTagAttributes["noModule"] as? Bool, noModuleAttr {
|
|
scriptAttributes += " script.noModule = true; "
|
|
}
|
|
if let nonceAttr = scriptHtmlTagAttributes["nonce"] as? String {
|
|
scriptAttributes += " script.nonce = '\(nonceAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
if let referrerPolicyAttr = scriptHtmlTagAttributes["referrerPolicy"] as? String {
|
|
scriptAttributes += " script.referrerPolicy = '\(referrerPolicyAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
}
|
|
let jsWrapper = "(function(d) { var script = d.createElement('script'); \(scriptAttributes) script.src = %@; d.body.appendChild(script); })(document);"
|
|
injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil)
|
|
}
|
|
|
|
public func injectCSSCode(source: String) {
|
|
let jsWrapper = "(function(d) { var style = d.createElement('style'); style.innerHTML = %@; d.head.appendChild(style); })(document);"
|
|
injectDeferredObject(source: source, withWrapper: jsWrapper, completionHandler: nil)
|
|
}
|
|
|
|
public func injectCSSFileFromUrl(urlFile: String, cssLinkHtmlTagAttributes: [String:Any?]?) {
|
|
var cssLinkAttributes = ""
|
|
var alternateStylesheet = ""
|
|
if let cssLinkHtmlTagAttributes = cssLinkHtmlTagAttributes {
|
|
if let idAttr = cssLinkHtmlTagAttributes["id"] as? String {
|
|
cssLinkAttributes += " link.id = '\(idAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
if let mediaAttr = cssLinkHtmlTagAttributes["media"] as? String {
|
|
cssLinkAttributes += " link.media = '\(mediaAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
if let crossOriginAttr = cssLinkHtmlTagAttributes["crossOrigin"] as? String {
|
|
cssLinkAttributes += " link.crossOrigin = '\(crossOriginAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
if let integrityAttr = cssLinkHtmlTagAttributes["integrity"] as? String {
|
|
cssLinkAttributes += " link.integrity = '\(integrityAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
if let referrerPolicyAttr = cssLinkHtmlTagAttributes["referrerPolicy"] as? String {
|
|
cssLinkAttributes += " link.referrerPolicy = '\(referrerPolicyAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
if let disabledAttr = cssLinkHtmlTagAttributes["disabled"] as? Bool, disabledAttr {
|
|
cssLinkAttributes += " link.disabled = true; "
|
|
}
|
|
if let alternateAttr = cssLinkHtmlTagAttributes["alternate"] as? Bool, alternateAttr {
|
|
alternateStylesheet = "alternate "
|
|
}
|
|
if let titleAttr = cssLinkHtmlTagAttributes["title"] as? String {
|
|
cssLinkAttributes += " link.title = '\(titleAttr.replacingOccurrences(of: "\'", with: "\\'"))'; "
|
|
}
|
|
}
|
|
let jsWrapper = "(function(d) { var link = d.createElement('link'); link.rel='\(alternateStylesheet)stylesheet', link.type='text/css'; \(cssLinkAttributes) link.href = %@; d.head.appendChild(link); })(document);"
|
|
injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil)
|
|
}
|
|
|
|
public func getCopyBackForwardList() -> [String: Any] {
|
|
let currentList = backForwardList
|
|
let currentIndex = currentList.backList.count
|
|
var completeList = currentList.backList
|
|
if currentList.currentItem != nil {
|
|
completeList.append(currentList.currentItem!)
|
|
}
|
|
completeList.append(contentsOf: currentList.forwardList)
|
|
|
|
var history: [[String: String]] = []
|
|
|
|
for historyItem in completeList {
|
|
var historyItemMap: [String: String] = [:]
|
|
historyItemMap["originalUrl"] = historyItem.initialURL.absoluteString
|
|
historyItemMap["title"] = historyItem.title
|
|
historyItemMap["url"] = historyItem.url.absoluteString
|
|
history.append(historyItemMap)
|
|
}
|
|
|
|
var result: [String: Any] = [:]
|
|
result["list"] = history
|
|
result["currentIndex"] = currentIndex
|
|
|
|
return result;
|
|
}
|
|
|
|
@available(iOS 15.0, *)
|
|
@available(macOS 12.0, *)
|
|
@available(macCatalyst 15.0, *)
|
|
public func webView(_ webView: WKWebView,
|
|
requestMediaCapturePermissionFor origin: WKSecurityOrigin,
|
|
initiatedByFrame frame: WKFrameInfo,
|
|
type: WKMediaCaptureType,
|
|
decisionHandler: @escaping (WKPermissionDecision) -> Void) {
|
|
let origin = "\(origin.protocol)://\(origin.host)\(origin.port != 0 ? ":" + String(origin.port) : "")"
|
|
let permissionRequest = PermissionRequest(origin: origin, resources: [type.rawValue], frame: frame)
|
|
|
|
let callback = WebViewChannelDelegate.PermissionRequestCallback()
|
|
callback.nonNullSuccess = { (response: PermissionResponse) in
|
|
if let action = response.action {
|
|
switch action {
|
|
case 1:
|
|
decisionHandler(.grant)
|
|
break
|
|
case 2:
|
|
decisionHandler(.prompt)
|
|
break
|
|
default:
|
|
decisionHandler(.deny)
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
callback.defaultBehaviour = { (response: PermissionResponse?) in
|
|
decisionHandler(.deny)
|
|
}
|
|
callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
callback?.defaultBehaviour(nil)
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.onPermissionRequest(request: permissionRequest, callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
|
|
@available(iOS 15.0, *)
|
|
@available(macOS 12.0, *)
|
|
@available(macCatalyst 15.0, *)
|
|
public func webView(_ webView: WKWebView,
|
|
requestDeviceOrientationAndMotionPermissionFor origin: WKSecurityOrigin,
|
|
initiatedByFrame frame: WKFrameInfo,
|
|
decisionHandler: @escaping (WKPermissionDecision) -> Void) {
|
|
let origin = "\(origin.protocol)://\(origin.host)\(origin.port != 0 ? ":" + String(origin.port) : "")"
|
|
let permissionRequest = PermissionRequest(origin: origin, resources: ["deviceOrientationAndMotion"], frame: frame)
|
|
|
|
let callback = WebViewChannelDelegate.PermissionRequestCallback()
|
|
callback.nonNullSuccess = { (response: PermissionResponse) in
|
|
if let action = response.action {
|
|
switch action {
|
|
case 1:
|
|
decisionHandler(.grant)
|
|
break
|
|
case 2:
|
|
decisionHandler(.prompt)
|
|
break
|
|
default:
|
|
decisionHandler(.deny)
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
callback.defaultBehaviour = { (response: PermissionResponse?) in
|
|
decisionHandler(.deny)
|
|
}
|
|
callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
callback?.defaultBehaviour(nil)
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.onPermissionRequest(request: permissionRequest, callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
|
|
@available(iOS 13.0, *)
|
|
public func webView(_ webView: WKWebView,
|
|
decidePolicyFor navigationAction: WKNavigationAction,
|
|
preferences: WKWebpagePreferences,
|
|
decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
|
|
self.webView(webView, decidePolicyFor: navigationAction, decisionHandler: {(navigationActionPolicy) -> Void in
|
|
decisionHandler(navigationActionPolicy, preferences)
|
|
})
|
|
}
|
|
|
|
@available(iOS 14.5, *)
|
|
public func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {
|
|
if let url = response.url, let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart {
|
|
let downloadStartRequest = DownloadStartRequest(url: url.absoluteString,
|
|
userAgent: nil,
|
|
contentDisposition: nil,
|
|
mimeType: response.mimeType,
|
|
contentLength: response.expectedContentLength,
|
|
suggestedFilename: suggestedFilename,
|
|
textEncodingName: response.textEncodingName)
|
|
channelDelegate?.onDownloadStartRequest(request: downloadStartRequest)
|
|
}
|
|
download.delegate = nil
|
|
// cancel the download
|
|
completionHandler(nil)
|
|
}
|
|
|
|
@available(iOS 14.5, *)
|
|
public func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
|
|
let response = navigationResponse.response
|
|
if let url = response.url, let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart {
|
|
let downloadStartRequest = DownloadStartRequest(url: url.absoluteString,
|
|
userAgent: nil,
|
|
contentDisposition: nil,
|
|
mimeType: response.mimeType,
|
|
contentLength: response.expectedContentLength,
|
|
suggestedFilename: response.suggestedFilename,
|
|
textEncodingName: response.textEncodingName)
|
|
channelDelegate?.onDownloadStartRequest(request: downloadStartRequest)
|
|
}
|
|
download.delegate = nil
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView,
|
|
decidePolicyFor navigationAction: WKNavigationAction,
|
|
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
|
|
|
if windowId != nil, !windowCreated {
|
|
decisionHandler(.cancel)
|
|
return
|
|
}
|
|
|
|
let callback = WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback()
|
|
callback.nonNullSuccess = { (response: WKNavigationActionPolicy) in
|
|
decisionHandler(response)
|
|
return false
|
|
}
|
|
callback.defaultBehaviour = { (response: WKNavigationActionPolicy?) in
|
|
decisionHandler(.allow)
|
|
}
|
|
callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
callback?.defaultBehaviour(nil)
|
|
}
|
|
|
|
if let useShouldOverrideUrlLoading = settings?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading, let channelDelegate = channelDelegate {
|
|
channelDelegate.shouldOverrideUrlLoading(navigationAction: navigationAction, callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView,
|
|
decidePolicyFor navigationResponse: WKNavigationResponse,
|
|
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
|
|
if let response = navigationResponse.response as? HTTPURLResponse, response.statusCode >= 400 {
|
|
let request = WebResourceRequest.init(fromWKNavigationResponse: navigationResponse)
|
|
let errorResponse = WebResourceResponse.init(fromWKNavigationResponse: navigationResponse)
|
|
channelDelegate?.onReceivedHttpError(request: request, errorResponse: errorResponse)
|
|
}
|
|
|
|
let useOnNavigationResponse = settings?.useOnNavigationResponse
|
|
|
|
if useOnNavigationResponse != nil, useOnNavigationResponse! {
|
|
let callback = WebViewChannelDelegate.NavigationResponseCallback()
|
|
callback.nonNullSuccess = { (response: WKNavigationResponsePolicy) in
|
|
decisionHandler(response)
|
|
return false
|
|
}
|
|
callback.defaultBehaviour = { (response: WKNavigationResponsePolicy?) in
|
|
decisionHandler(.allow)
|
|
}
|
|
callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
callback?.defaultBehaviour(nil)
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.onNavigationResponse(navigationResponse: navigationResponse, callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
|
|
if let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart {
|
|
if #available(iOS 14.5, *), !navigationResponse.canShowMIMEType {
|
|
decisionHandler(.download)
|
|
return
|
|
} else {
|
|
let mimeType = navigationResponse.response.mimeType
|
|
if let url = navigationResponse.response.url, navigationResponse.isForMainFrame {
|
|
if url.scheme != "file", mimeType != nil, !mimeType!.starts(with: "text/") {
|
|
let downloadStartRequest = DownloadStartRequest(url: url.absoluteString,
|
|
userAgent: nil,
|
|
contentDisposition: nil,
|
|
mimeType: mimeType,
|
|
contentLength: navigationResponse.response.expectedContentLength,
|
|
suggestedFilename: navigationResponse.response.suggestedFilename,
|
|
textEncodingName: navigationResponse.response.textEncodingName)
|
|
channelDelegate?.onDownloadStartRequest(request: downloadStartRequest)
|
|
if useOnNavigationResponse == nil || !useOnNavigationResponse! {
|
|
decisionHandler(.cancel)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if useOnNavigationResponse == nil || !useOnNavigationResponse! {
|
|
decisionHandler(.allow)
|
|
}
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
|
currentOriginalUrl = url
|
|
lastTouchPoint = nil
|
|
|
|
disposeWebMessageChannels()
|
|
initializeWindowIdJS()
|
|
|
|
if #available(iOS 14.0, *) {
|
|
configuration.userContentController.resetContentWorlds(windowId: windowId)
|
|
}
|
|
|
|
channelDelegate?.onLoadStart(url: url?.absoluteString)
|
|
|
|
inAppBrowserDelegate?.didStartNavigation(url: url)
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
initializeWindowIdJS()
|
|
|
|
InAppWebView.credentialsProposed = []
|
|
evaluateJavaScript(PLATFORM_READY_JS_SOURCE, completionHandler: nil)
|
|
|
|
// sometimes scrollView.contentSize doesn't fit all the frame.size available
|
|
// so, we call setNeedsLayout to redraw the layout
|
|
let webViewFrameSize = frame.size
|
|
let scrollViewSize = scrollView.contentSize
|
|
if (scrollViewSize.width < webViewFrameSize.width || scrollViewSize.height < webViewFrameSize.height) {
|
|
setNeedsLayout()
|
|
}
|
|
|
|
channelDelegate?.onLoadStop(url: url?.absoluteString)
|
|
|
|
inAppBrowserDelegate?.didFinishNavigation(url: url)
|
|
}
|
|
|
|
public func webView(_ view: WKWebView,
|
|
didFailProvisionalNavigation navigation: WKNavigation!,
|
|
withError error: Error) {
|
|
webView(view, didFail: navigation, withError: error)
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
|
InAppWebView.credentialsProposed = []
|
|
|
|
var urlError: URL = url ?? URL(string: "about:blank")!
|
|
var errorCode = -1
|
|
var errorDescription = "domain=\(error._domain), code=\(error._code), \(error.localizedDescription)"
|
|
|
|
if let info = error as? URLError {
|
|
if let failingURL = info.failingURL {
|
|
urlError = failingURL
|
|
}
|
|
errorCode = info.code.rawValue
|
|
errorDescription = info.localizedDescription
|
|
}
|
|
else if let info = error._userInfo as? [String: Any] {
|
|
if let failingUrl = info[NSURLErrorFailingURLErrorKey] as? URL {
|
|
urlError = failingUrl
|
|
}
|
|
if let failingUrlString = info[NSURLErrorFailingURLStringErrorKey] as? String,
|
|
let failingUrl = URL(string: failingUrlString) {
|
|
urlError = failingUrl
|
|
}
|
|
}
|
|
|
|
let webResourceRequest = WebResourceRequest(url: urlError, headers: nil)
|
|
let webResourceError = WebResourceError(type: errorCode, errorDescription: errorDescription)
|
|
|
|
channelDelegate?.onReceivedError(request: webResourceRequest, error: webResourceError)
|
|
|
|
inAppBrowserDelegate?.didFailNavigation(url: url, error: error)
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
|
|
|
if windowId != nil, !windowCreated {
|
|
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
return
|
|
}
|
|
|
|
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic ||
|
|
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault ||
|
|
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest ||
|
|
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNegotiate ||
|
|
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM {
|
|
let host = challenge.protectionSpace.host
|
|
let prot = challenge.protectionSpace.protocol
|
|
let realm = challenge.protectionSpace.realm
|
|
let port = challenge.protectionSpace.port
|
|
|
|
let callback = WebViewChannelDelegate.ReceivedHttpAuthRequestCallback()
|
|
callback.nonNullSuccess = { (response: HttpAuthResponse) in
|
|
if let action = response.action {
|
|
switch action {
|
|
case 0:
|
|
InAppWebView.credentialsProposed = []
|
|
// used .performDefaultHandling to mantain consistency with Android
|
|
// because .cancelAuthenticationChallenge will call webView(_:didFail:withError:)
|
|
completionHandler(.performDefaultHandling, nil)
|
|
//completionHandler(.cancelAuthenticationChallenge, nil)
|
|
break
|
|
case 1:
|
|
let username = response.username
|
|
let password = response.password
|
|
let permanentPersistence = response.permanentPersistence
|
|
let persistence = (permanentPersistence) ? URLCredential.Persistence.permanent : URLCredential.Persistence.forSession
|
|
let credential = URLCredential(user: username, password: password, persistence: persistence)
|
|
completionHandler(.useCredential, credential)
|
|
break
|
|
case 2:
|
|
if InAppWebView.credentialsProposed.count == 0, let credentialStore = CredentialDatabase.credentialStore {
|
|
for (protectionSpace, credentials) in credentialStore.allCredentials {
|
|
if protectionSpace.host == host && protectionSpace.realm == realm &&
|
|
protectionSpace.protocol == prot && protectionSpace.port == port {
|
|
for credential in credentials {
|
|
InAppWebView.credentialsProposed.append(credential.value)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if InAppWebView.credentialsProposed.count == 0, let credential = challenge.proposedCredential {
|
|
InAppWebView.credentialsProposed.append(credential)
|
|
}
|
|
|
|
if let credential = InAppWebView.credentialsProposed.popLast() {
|
|
completionHandler(.useCredential, credential)
|
|
}
|
|
else {
|
|
completionHandler(.performDefaultHandling, nil)
|
|
}
|
|
break
|
|
default:
|
|
InAppWebView.credentialsProposed = []
|
|
completionHandler(.performDefaultHandling, nil)
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
callback.defaultBehaviour = { (response: HttpAuthResponse?) in
|
|
completionHandler(.performDefaultHandling, nil)
|
|
}
|
|
callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
callback?.defaultBehaviour(nil)
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge(fromChallenge: challenge), callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
|
|
guard let serverTrust = challenge.protectionSpace.serverTrust else {
|
|
completionHandler(.performDefaultHandling, nil)
|
|
return
|
|
}
|
|
|
|
if let scheme = challenge.protectionSpace.protocol, scheme == "https",
|
|
let sslCertificate = challenge.protectionSpace.sslCertificate {
|
|
InAppWebView.sslCertificatesMap[challenge.protectionSpace.host] = sslCertificate
|
|
}
|
|
|
|
let callback = WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback()
|
|
callback.nonNullSuccess = { (response: ServerTrustAuthResponse) in
|
|
if let action = response.action {
|
|
switch action {
|
|
case 0:
|
|
InAppWebView.credentialsProposed = []
|
|
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
break
|
|
case 1:
|
|
let exceptions = SecTrustCopyExceptions(serverTrust)
|
|
SecTrustSetExceptions(serverTrust, exceptions)
|
|
let credential = URLCredential(trust: serverTrust)
|
|
completionHandler(.useCredential, credential)
|
|
break
|
|
default:
|
|
InAppWebView.credentialsProposed = []
|
|
completionHandler(.performDefaultHandling, nil)
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
callback.defaultBehaviour = { (response: ServerTrustAuthResponse?) in
|
|
completionHandler(.performDefaultHandling, nil)
|
|
}
|
|
callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
callback?.defaultBehaviour(nil)
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge(fromChallenge: challenge), callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
|
|
let callback = WebViewChannelDelegate.ReceivedClientCertRequestCallback()
|
|
callback.nonNullSuccess = { (response: ClientCertResponse) in
|
|
if let action = response.action {
|
|
switch action {
|
|
case 0:
|
|
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
break
|
|
case 1:
|
|
let certificatePath = response.certificatePath
|
|
let certificatePassword = response.certificatePassword ?? "";
|
|
|
|
var path: String = certificatePath
|
|
do {
|
|
path = try Util.getAbsPathAsset(assetFilePath: certificatePath)
|
|
} catch {}
|
|
|
|
if let PKCS12Data = NSData(contentsOfFile: path),
|
|
let identityAndTrust: IdentityAndTrust = self.extractIdentity(PKCS12Data: PKCS12Data, password: certificatePassword) {
|
|
let urlCredential: URLCredential = URLCredential(
|
|
identity: identityAndTrust.identityRef,
|
|
certificates: identityAndTrust.certArray as? [AnyObject],
|
|
persistence: URLCredential.Persistence.forSession);
|
|
completionHandler(.useCredential, urlCredential)
|
|
} else {
|
|
completionHandler(.performDefaultHandling, nil)
|
|
}
|
|
|
|
break
|
|
case 2:
|
|
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
break
|
|
default:
|
|
completionHandler(.performDefaultHandling, nil)
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
callback.defaultBehaviour = { (response: ClientCertResponse?) in
|
|
completionHandler(.performDefaultHandling, nil)
|
|
}
|
|
callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
callback?.defaultBehaviour(nil)
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.onReceivedClientCertRequest(challenge: ClientCertChallenge(fromChallenge: challenge), callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
else {
|
|
completionHandler(.performDefaultHandling, nil)
|
|
}
|
|
}
|
|
|
|
struct IdentityAndTrust {
|
|
|
|
var identityRef:SecIdentity
|
|
var trust:SecTrust
|
|
var certArray:AnyObject
|
|
}
|
|
|
|
func extractIdentity(PKCS12Data:NSData, password: String) -> IdentityAndTrust? {
|
|
var identityAndTrust:IdentityAndTrust?
|
|
var securityError:OSStatus = errSecSuccess
|
|
|
|
var importResult: CFArray? = nil
|
|
securityError = SecPKCS12Import(
|
|
PKCS12Data as NSData,
|
|
[kSecImportExportPassphrase as String: password] as NSDictionary,
|
|
&importResult
|
|
)
|
|
|
|
if securityError == errSecSuccess {
|
|
let certItems:CFArray = importResult! as CFArray;
|
|
let certItemsArray:Array = certItems as Array
|
|
let dict:AnyObject? = certItemsArray.first;
|
|
if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {
|
|
// grab the identity
|
|
let identityPointer:AnyObject? = certEntry["identity"];
|
|
let secIdentityRef:SecIdentity = (identityPointer as! SecIdentity?)!;
|
|
// grab the trust
|
|
let trustPointer:AnyObject? = certEntry["trust"];
|
|
let trustRef:SecTrust = trustPointer as! SecTrust;
|
|
// grab the cert
|
|
let chainPointer:AnyObject? = certEntry["chain"];
|
|
identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: chainPointer!);
|
|
}
|
|
} else {
|
|
print("Security Error: " + securityError.description)
|
|
if #available(iOS 11.3, *) {
|
|
print(SecCopyErrorMessageString(securityError,nil) ?? "")
|
|
}
|
|
}
|
|
return identityAndTrust;
|
|
}
|
|
|
|
func createAlertDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, completionHandler: @escaping () -> Void) {
|
|
let title = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message
|
|
let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "")
|
|
let alertController = UIAlertController(title: title, message: nil,
|
|
preferredStyle: UIAlertController.Style.alert);
|
|
|
|
alertController.addAction(UIAlertAction(title: okButton, style: UIAlertAction.Style.default) {
|
|
_ in completionHandler()}
|
|
);
|
|
|
|
guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else {
|
|
completionHandler()
|
|
return
|
|
}
|
|
presentingViewController.present(alertController, animated: true, completion: {})
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String,
|
|
initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
|
|
|
|
if (isPausedTimers) {
|
|
isPausedTimersCompletionHandler = completionHandler
|
|
return
|
|
}
|
|
|
|
let callback = WebViewChannelDelegate.JsAlertCallback()
|
|
callback.nonNullSuccess = { (response: JsAlertResponse) in
|
|
if response.handledByClient {
|
|
let action = response.action ?? 1
|
|
switch action {
|
|
case 0:
|
|
completionHandler()
|
|
break
|
|
default:
|
|
completionHandler()
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
callback.defaultBehaviour = { (response: JsAlertResponse?) in
|
|
let responseMessage = response?.message
|
|
let confirmButtonTitle = response?.confirmButtonTitle
|
|
self.createAlertDialog(message: message, responseMessage: responseMessage,
|
|
confirmButtonTitle: confirmButtonTitle, completionHandler: completionHandler)
|
|
}
|
|
callback.error = { (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
completionHandler()
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.onJsAlert(url: frame.request.url, message: message, isMainFrame: frame.isMainFrame, callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
|
|
func createConfirmDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, cancelButtonTitle: String?, completionHandler: @escaping (Bool) -> Void) {
|
|
let dialogMessage = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message
|
|
let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "")
|
|
let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "")
|
|
|
|
let confirmController = UIAlertController(title: nil, message: dialogMessage, preferredStyle: .alert)
|
|
|
|
confirmController.addAction(UIAlertAction(title: okButton, style: .default, handler: { (action) in
|
|
completionHandler(true)
|
|
}))
|
|
|
|
confirmController.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: { (action) in
|
|
completionHandler(false)
|
|
}))
|
|
|
|
guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else {
|
|
completionHandler(false)
|
|
return
|
|
}
|
|
presentingViewController.present(confirmController, animated: true, completion: nil)
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo,
|
|
completionHandler: @escaping (Bool) -> Void) {
|
|
let callback = WebViewChannelDelegate.JsConfirmCallback()
|
|
callback.nonNullSuccess = { (response: JsConfirmResponse) in
|
|
if response.handledByClient {
|
|
let action = response.action ?? 1
|
|
switch action {
|
|
case 0:
|
|
completionHandler(true)
|
|
break
|
|
case 1:
|
|
completionHandler(false)
|
|
break
|
|
default:
|
|
completionHandler(false)
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
callback.defaultBehaviour = { (response: JsConfirmResponse?) in
|
|
let responseMessage = response?.message
|
|
let confirmButtonTitle = response?.confirmButtonTitle
|
|
let cancelButtonTitle = response?.cancelButtonTitle
|
|
self.createConfirmDialog(message: message, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, completionHandler: completionHandler)
|
|
}
|
|
callback.error = { (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
completionHandler(false)
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.onJsConfirm(url: frame.request.url, message: message, isMainFrame: frame.isMainFrame, callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
|
|
func createPromptDialog(message: String, defaultValue: String?, responseMessage: String?, confirmButtonTitle: String?, cancelButtonTitle: String?, value: String?, completionHandler: @escaping (String?) -> Void) {
|
|
let dialogMessage = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message
|
|
let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "")
|
|
let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "")
|
|
|
|
let promptController = UIAlertController(title: nil, message: dialogMessage, preferredStyle: .alert)
|
|
|
|
promptController.addTextField { (textField) in
|
|
textField.text = defaultValue
|
|
}
|
|
|
|
promptController.addAction(UIAlertAction(title: okButton, style: .default, handler: { (action) in
|
|
if let v = value {
|
|
completionHandler(v)
|
|
}
|
|
else if let text = promptController.textFields?.first?.text {
|
|
completionHandler(text)
|
|
} else {
|
|
completionHandler("")
|
|
}
|
|
}))
|
|
|
|
promptController.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: { (action) in
|
|
completionHandler(nil)
|
|
}))
|
|
|
|
guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else {
|
|
completionHandler(nil)
|
|
return
|
|
}
|
|
presentingViewController.present(promptController, animated: true, completion: nil)
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt message: String, defaultText defaultValue: String?, initiatedByFrame frame: WKFrameInfo,
|
|
completionHandler: @escaping (String?) -> Void) {
|
|
let callback = WebViewChannelDelegate.JsPromptCallback()
|
|
callback.nonNullSuccess = { (response: JsPromptResponse) in
|
|
if response.handledByClient {
|
|
let action = response.action ?? 1
|
|
switch action {
|
|
case 0:
|
|
completionHandler(response.value)
|
|
break
|
|
case 1:
|
|
completionHandler(nil)
|
|
break
|
|
default:
|
|
completionHandler(nil)
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
callback.defaultBehaviour = { (response: JsPromptResponse?) in
|
|
let responseMessage = response?.message
|
|
let confirmButtonTitle = response?.confirmButtonTitle
|
|
let cancelButtonTitle = response?.cancelButtonTitle
|
|
let value = response?.value
|
|
self.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle,
|
|
cancelButtonTitle: cancelButtonTitle, value: value, completionHandler: completionHandler)
|
|
}
|
|
callback.error = { (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
completionHandler(nil)
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.onJsPrompt(url: frame.request.url, message: message, defaultValue: defaultValue, isMainFrame: frame.isMainFrame, callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
|
|
/// UIScrollViewDelegate is somehow bugged:
|
|
/// if InAppWebView implements the UIScrollViewDelegate protocol and implement the scrollViewDidScroll event,
|
|
/// then, when the user scrolls the content, the webview content is not rendered (just white space).
|
|
/// Calling setNeedsLayout() resolves this problem, but, for some reason, the bounce effect is canceled.
|
|
///
|
|
/// So, to track the same event, without implementing the scrollViewDidScroll event, we create
|
|
/// an observer that observes the scrollView.contentOffset property.
|
|
/// This way, we don't need to call setNeedsLayout() and all works fine.
|
|
public func onScrollChanged(startedByUser: Bool, oldContentOffset: CGPoint?) {
|
|
let disableVerticalScroll = settings?.disableVerticalScroll ?? false
|
|
let disableHorizontalScroll = settings?.disableHorizontalScroll ?? false
|
|
if startedByUser {
|
|
if disableVerticalScroll && disableHorizontalScroll {
|
|
scrollView.contentOffset = CGPoint(x: lastScrollX, y: lastScrollY);
|
|
}
|
|
else if disableVerticalScroll {
|
|
if scrollView.contentOffset.y >= 0 || scrollView.contentOffset.y < 0 {
|
|
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: lastScrollY);
|
|
}
|
|
}
|
|
else if disableHorizontalScroll {
|
|
if scrollView.contentOffset.x >= 0 || scrollView.contentOffset.x < 0 {
|
|
scrollView.contentOffset = CGPoint(x: lastScrollX, y: scrollView.contentOffset.y);
|
|
}
|
|
}
|
|
}
|
|
if (!disableVerticalScroll && !disableHorizontalScroll) ||
|
|
(disableVerticalScroll && scrollView.contentOffset.x != oldContentOffset?.x) ||
|
|
(disableHorizontalScroll && scrollView.contentOffset.y != oldContentOffset?.y) {
|
|
let x = Int(scrollView.contentOffset.x / scrollView.contentScaleFactor)
|
|
let y = Int(scrollView.contentOffset.y / scrollView.contentScaleFactor)
|
|
channelDelegate?.onScrollChanged(x: x, y: y)
|
|
}
|
|
lastScrollX = scrollView.contentOffset.x
|
|
lastScrollY = scrollView.contentOffset.y
|
|
|
|
let overScrolledHorizontally = lastScrollX < 0 || lastScrollX > (scrollView.contentSize.width - scrollView.frame.size.width)
|
|
let overScrolledVertically = lastScrollY < 0 || lastScrollY > (scrollView.contentSize.height - scrollView.frame.size.height)
|
|
if overScrolledHorizontally || overScrolledVertically {
|
|
let x = Int(lastScrollX / scrollView.contentScaleFactor)
|
|
let y = Int(lastScrollY / scrollView.contentScaleFactor)
|
|
channelDelegate?.onOverScrolled(x: x, y: y,
|
|
clampedX: overScrolledHorizontally,
|
|
clampedY: overScrolledVertically)
|
|
}
|
|
}
|
|
|
|
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
let newScale = Float(scrollView.zoomScale)
|
|
if newScale != oldZoomScale {
|
|
channelDelegate?.onZoomScaleChanged(newScale: newScale, oldScale: oldZoomScale)
|
|
oldZoomScale = newScale
|
|
}
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView,
|
|
createWebViewWith configuration: WKWebViewConfiguration,
|
|
for navigationAction: WKNavigationAction,
|
|
windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
InAppWebView.windowAutoincrementId += 1
|
|
let windowId = InAppWebView.windowAutoincrementId
|
|
|
|
let windowWebView = InAppWebView(id: nil, registrar: nil, frame: CGRect.zero, configuration: configuration, contextMenu: nil)
|
|
windowWebView.windowId = windowId
|
|
|
|
let webViewTransport = WebViewTransport(
|
|
webView: windowWebView,
|
|
request: navigationAction.request
|
|
)
|
|
|
|
InAppWebView.windowWebViews[windowId] = webViewTransport
|
|
windowWebView.stopLoading()
|
|
|
|
let createWindowAction = CreateWindowAction(navigationAction: navigationAction, windowId: windowId, windowFeatures: windowFeatures, isDialog: nil)
|
|
|
|
let callback = WebViewChannelDelegate.CreateWindowCallback()
|
|
callback.nonNullSuccess = { (handledByClient: Bool) in
|
|
return !handledByClient
|
|
}
|
|
callback.defaultBehaviour = { (handledByClient: Bool?) in
|
|
if InAppWebView.windowWebViews[windowId] != nil {
|
|
InAppWebView.windowWebViews.removeValue(forKey: windowId)
|
|
}
|
|
self.loadUrl(urlRequest: navigationAction.request, allowingReadAccessTo: nil)
|
|
}
|
|
callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
callback?.defaultBehaviour(nil)
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.onCreateWindow(createWindowAction: createWindowAction, callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
|
|
return windowWebView
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView,
|
|
authenticationChallenge challenge: URLAuthenticationChallenge,
|
|
shouldAllowDeprecatedTLS decisionHandler: @escaping (Bool) -> Void) {
|
|
if windowId != nil, !windowCreated {
|
|
decisionHandler(false)
|
|
return
|
|
}
|
|
|
|
let callback = WebViewChannelDelegate.ShouldAllowDeprecatedTLSCallback()
|
|
callback.nonNullSuccess = { (action: Bool) in
|
|
decisionHandler(action)
|
|
return false
|
|
}
|
|
callback.defaultBehaviour = { (action: Bool?) in
|
|
decisionHandler(false)
|
|
}
|
|
callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
callback?.defaultBehaviour(nil)
|
|
}
|
|
|
|
if let channelDelegate = channelDelegate {
|
|
channelDelegate.shouldAllowDeprecatedTLS(challenge: challenge, callback: callback)
|
|
} else {
|
|
callback.defaultBehaviour(nil)
|
|
}
|
|
}
|
|
|
|
public func webViewDidClose(_ webView: WKWebView) {
|
|
channelDelegate?.onCloseWindow()
|
|
}
|
|
|
|
public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
|
channelDelegate?.onWebContentProcessDidTerminate()
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView,
|
|
didCommit navigation: WKNavigation!) {
|
|
channelDelegate?.onPageCommitVisible(url: url?.absoluteString)
|
|
}
|
|
|
|
public func webView(_ webView: WKWebView,
|
|
didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
|
|
channelDelegate?.onDidReceiveServerRedirectForProvisionalNavigation()
|
|
}
|
|
|
|
// @available(iOS 13.0, *)
|
|
// public func webView(_ webView: WKWebView,
|
|
// contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo,
|
|
// completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) {
|
|
// print("contextMenuConfigurationForElement")
|
|
// let actionProvider: UIContextMenuActionProvider = { _ in
|
|
// let editMenu = UIMenu(title: "Edit...", children: [
|
|
// UIAction(title: "Copy") { action in
|
|
//
|
|
// },
|
|
// UIAction(title: "Duplicate") { action in
|
|
//
|
|
// }
|
|
// ])
|
|
// return UIMenu(title: "Title", children: [
|
|
// UIAction(title: "Share") { action in
|
|
//
|
|
// },
|
|
// editMenu
|
|
// ])
|
|
// }
|
|
// let contextMenuConfiguration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: actionProvider)
|
|
// //completionHandler(contextMenuConfiguration)
|
|
// completionHandler(nil)
|
|
//// onContextMenuConfigurationForElement(linkURL: elementInfo.linkURL?.absoluteString, result: nil/*{(result) -> Void in
|
|
//// if result is FlutterError {
|
|
//// print((result as! FlutterError).message ?? "")
|
|
//// }
|
|
//// else if (result as? NSObject) == FlutterMethodNotImplemented {
|
|
//// completionHandler(nil)
|
|
//// }
|
|
//// else {
|
|
//// var response: [String: Any]
|
|
//// if let r = result {
|
|
//// response = r as! [String: Any]
|
|
//// var action = response["action"] as? Int
|
|
//// action = action != nil ? action : 0;
|
|
//// switch action {
|
|
//// case 0:
|
|
//// break
|
|
//// case 1:
|
|
//// break
|
|
//// default:
|
|
//// completionHandler(nil)
|
|
//// }
|
|
//// return;
|
|
//// }
|
|
//// completionHandler(nil)
|
|
//// }
|
|
//// }*/)
|
|
// }
|
|
////
|
|
// @available(iOS 13.0, *)
|
|
// public func webView(_ webView: WKWebView,
|
|
// contextMenuDidEndForElement elementInfo: WKContextMenuElementInfo) {
|
|
// print("contextMenuDidEndForElement")
|
|
// print(elementInfo)
|
|
// //onContextMenuDidEndForElement(linkURL: elementInfo.linkURL?.absoluteString)
|
|
// }
|
|
//
|
|
// @available(iOS 13.0, *)
|
|
// public func webView(_ webView: WKWebView,
|
|
// contextMenuForElement elementInfo: WKContextMenuElementInfo,
|
|
// willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) {
|
|
// print("willCommitWithAnimator")
|
|
// print(elementInfo)
|
|
//// onWillCommitWithAnimator(linkURL: elementInfo.linkURL?.absoluteString, result: nil/*{(result) -> Void in
|
|
//// if result is FlutterError {
|
|
//// print((result as! FlutterError).message ?? "")
|
|
//// }
|
|
//// else if (result as? NSObject) == FlutterMethodNotImplemented {
|
|
////
|
|
//// }
|
|
//// else {
|
|
//// var response: [String: Any]
|
|
//// if let r = result {
|
|
//// response = r as! [String: Any]
|
|
//// var action = response["action"] as? Int
|
|
//// action = action != nil ? action : 0;
|
|
////// switch action {
|
|
////// case 0:
|
|
////// break
|
|
////// case 1:
|
|
////// break
|
|
////// default:
|
|
//////
|
|
////// }
|
|
//// return;
|
|
//// }
|
|
////
|
|
//// }
|
|
//// }*/)
|
|
// }
|
|
//
|
|
// @available(iOS 13.0, *)
|
|
// public func webView(_ webView: WKWebView,
|
|
// contextMenuWillPresentForElement elementInfo: WKContextMenuElementInfo) {
|
|
// print("contextMenuWillPresentForElement")
|
|
// print(elementInfo.linkURL)
|
|
// //onContextMenuWillPresentForElement(linkURL: elementInfo.linkURL?.absoluteString)
|
|
// }
|
|
|
|
|
|
// https://stackoverflow.com/a/42840541/4637638
|
|
public func isVideoPlayerWindow(_ notificationObject: AnyObject?) -> Bool {
|
|
let nonVideoClasses = ["_UIAlertControllerShimPresenterWindow",
|
|
"UITextEffectsWindow",
|
|
"UIRemoteKeyboardWindow",
|
|
"PGHostedWindow"]
|
|
|
|
var isVideo = true
|
|
if let obj = notificationObject {
|
|
for nonVideoClass in nonVideoClasses {
|
|
if let clazz = NSClassFromString(nonVideoClass) {
|
|
isVideo = isVideo && !(obj.isKind(of: clazz))
|
|
}
|
|
}
|
|
}
|
|
return isVideo
|
|
}
|
|
|
|
@objc func onEnterFullscreen(_ notification: Notification) {
|
|
// TODO: Still not working on iOS 16.0!
|
|
// if #available(iOS 16.0, *) {
|
|
// channelDelegate?.onEnterFullscreen()
|
|
// inFullscreen = true
|
|
// }
|
|
// else
|
|
if (isVideoPlayerWindow(notification.object as AnyObject?)) {
|
|
channelDelegate?.onEnterFullscreen()
|
|
inFullscreen = true
|
|
}
|
|
}
|
|
|
|
@objc func onExitFullscreen(_ notification: Notification) {
|
|
// TODO: Still not working on iOS 16.0!
|
|
// if #available(iOS 16.0, *) {
|
|
// channelDelegate?.onExitFullscreen()
|
|
// inFullscreen = false
|
|
// }
|
|
// else
|
|
if (isVideoPlayerWindow(notification.object as AnyObject?)) {
|
|
channelDelegate?.onExitFullscreen()
|
|
inFullscreen = false
|
|
}
|
|
}
|
|
|
|
// public func onContextMenuConfigurationForElement(linkURL: String?, result: FlutterResult?) {
|
|
// let arguments: [String: Any?] = ["linkURL": linkURL]
|
|
// channel?.invokeMethod("onContextMenuConfigurationForElement", arguments: arguments, result: result)
|
|
// }
|
|
//
|
|
// public func onContextMenuDidEndForElement(linkURL: String?) {
|
|
// let arguments: [String: Any?] = ["linkURL": linkURL]
|
|
// channel?.invokeMethod("onContextMenuDidEndForElement", arguments: arguments)
|
|
// }
|
|
//
|
|
// public func onWillCommitWithAnimator(linkURL: String?, result: FlutterResult?) {
|
|
// let arguments: [String: Any?] = ["linkURL": linkURL]
|
|
// channel?.invokeMethod("onWillCommitWithAnimator", arguments: arguments, result: result)
|
|
// }
|
|
//
|
|
// public func onContextMenuWillPresentForElement(linkURL: String?) {
|
|
// let arguments: [String: Any?] = ["linkURL": linkURL]
|
|
// channel?.invokeMethod("onContextMenuWillPresentForElement", arguments: arguments)
|
|
// }
|
|
|
|
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
if message.name.starts(with: "console") {
|
|
var messageLevel = 1
|
|
switch (message.name) {
|
|
case "consoleLog":
|
|
messageLevel = 1
|
|
break;
|
|
case "consoleDebug":
|
|
// on Android, console.debug is TIP
|
|
messageLevel = 0
|
|
break;
|
|
case "consoleError":
|
|
messageLevel = 3
|
|
break;
|
|
case "consoleInfo":
|
|
// on Android, console.info is LOG
|
|
messageLevel = 1
|
|
break;
|
|
case "consoleWarn":
|
|
messageLevel = 2
|
|
break;
|
|
default:
|
|
messageLevel = 1
|
|
break;
|
|
}
|
|
let body = message.body as! [String: Any?]
|
|
let consoleMessage = body["message"] as! String
|
|
|
|
let _windowId = body["_windowId"] as? Int64
|
|
var webView = self
|
|
if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] {
|
|
webView = webViewTransport.webView
|
|
}
|
|
webView.channelDelegate?.onConsoleMessage(message: consoleMessage, messageLevel: messageLevel)
|
|
} else if message.name == "callHandler" {
|
|
let body = message.body as! [String: Any?]
|
|
let handlerName = body["handlerName"] as! String
|
|
|
|
if handlerName == "onPrintRequest" {
|
|
let settings = PrintJobSettings()
|
|
settings.handledByClient = true
|
|
if let printJobId = printCurrentPage(settings: settings) {
|
|
let callback = WebViewChannelDelegate.PrintRequestCallback()
|
|
callback.nonNullSuccess = { (handledByClient: Bool) in
|
|
return !handledByClient
|
|
}
|
|
callback.defaultBehaviour = { (handledByClient: Bool?) in
|
|
if let printJob = PrintJobManager.jobs[printJobId] {
|
|
printJob?.disposeNoDismiss()
|
|
}
|
|
}
|
|
callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
callback?.defaultBehaviour(nil)
|
|
}
|
|
channelDelegate?.onPrintRequest(url: url, printJobId: printJobId, callback: callback)
|
|
}
|
|
return
|
|
}
|
|
|
|
let _callHandlerID = body["_callHandlerID"] as! Int64
|
|
let args = body["args"] as! String
|
|
|
|
let _windowId = body["_windowId"] as? Int64
|
|
var webView = self
|
|
if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] {
|
|
webView = webViewTransport.webView
|
|
}
|
|
|
|
let callback = WebViewChannelDelegate.CallJsHandlerCallback()
|
|
callback.defaultBehaviour = { (response: Any?) in
|
|
var json = "null"
|
|
if let r = response as? String {
|
|
json = r
|
|
}
|
|
|
|
self.evaluateJavaScript("""
|
|
if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) {
|
|
window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)](\(json));
|
|
delete window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)];
|
|
}
|
|
""", completionHandler: nil)
|
|
}
|
|
callback.error = { (code: String, message: String?, details: Any?) in
|
|
print(code + ", " + (message ?? ""))
|
|
}
|
|
|
|
if let channelDelegate = webView.channelDelegate {
|
|
channelDelegate.onCallJsHandler(handlerName: handlerName, args: args, callback: callback)
|
|
}
|
|
} else if message.name == "onFindResultReceived" {
|
|
let body = message.body as! [String: Any?]
|
|
let findResult = body["findResult"] as! [String: Any]
|
|
let activeMatchOrdinal = findResult["activeMatchOrdinal"] as! Int
|
|
let numberOfMatches = findResult["numberOfMatches"] as! Int
|
|
let isDoneCounting = findResult["isDoneCounting"] as! Bool
|
|
|
|
let _windowId = body["_windowId"] as? Int64
|
|
var webView = self
|
|
if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] {
|
|
webView = webViewTransport.webView
|
|
}
|
|
webView.findInteractionController?.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting)
|
|
webView.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting)
|
|
} else if message.name == "onCallAsyncJavaScriptResultBelowIOS14Received" {
|
|
let body = message.body as! [String: Any?]
|
|
let resultUuid = body["resultUuid"] as! String
|
|
if let result = callAsyncJavaScriptBelowIOS14Results[resultUuid] {
|
|
result([
|
|
"value": body["value"],
|
|
"error": body["error"]
|
|
])
|
|
callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid)
|
|
}
|
|
} else if message.name == "onWebMessagePortMessageReceived" {
|
|
let body = message.body as! [String: Any?]
|
|
let webMessageChannelId = body["webMessageChannelId"] as! String
|
|
let index = body["index"] as! Int64
|
|
let webMessage = body["message"] as? String
|
|
if let webMessageChannel = webMessageChannels[webMessageChannelId] {
|
|
webMessageChannel.channelDelegate?.onMessage(index: index, message: webMessage)
|
|
}
|
|
} else if message.name == "onWebMessageListenerPostMessageReceived" {
|
|
let body = message.body as! [String: Any?]
|
|
let jsObjectName = body["jsObjectName"] as! String
|
|
let messageData = body["message"] as? String
|
|
if let webMessageListener = webMessageListeners.first(where: ({($0.jsObjectName == jsObjectName)})) {
|
|
let isMainFrame = message.frameInfo.isMainFrame
|
|
|
|
var scheme: String? = nil
|
|
var host: String? = nil
|
|
var port: Int? = nil
|
|
if #available(iOS 9.0, *) {
|
|
let sourceOrigin = message.frameInfo.securityOrigin
|
|
scheme = sourceOrigin.protocol
|
|
host = sourceOrigin.host
|
|
port = sourceOrigin.port
|
|
} else if let url = message.frameInfo.request.url {
|
|
scheme = url.scheme
|
|
host = url.host
|
|
port = url.port
|
|
}
|
|
|
|
if !webMessageListener.isOriginAllowed(scheme: scheme, host: host, port: port) {
|
|
return
|
|
}
|
|
|
|
var sourceOrigin: URL? = nil
|
|
if let scheme = scheme, !scheme.isEmpty, let host = host, !host.isEmpty {
|
|
sourceOrigin = URL(string: "\(scheme)://\(host)\(port != nil && port != 0 ? ":" + String(port!) : "")")
|
|
}
|
|
webMessageListener.channelDelegate?.onPostMessage(message: messageData, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func scrollTo(x: Int, y: Int, animated: Bool) {
|
|
scrollView.setContentOffset(CGPoint(x: x, y: y), animated: animated)
|
|
}
|
|
|
|
public func scrollBy(x: Int, y: Int, animated: Bool) {
|
|
let newX = CGFloat(x) + scrollView.contentOffset.x
|
|
let newY = CGFloat(y) + scrollView.contentOffset.y
|
|
scrollView.setContentOffset(CGPoint(x: newX, y: newY), animated: animated)
|
|
}
|
|
|
|
|
|
public func pauseTimers() {
|
|
if !isPausedTimers {
|
|
isPausedTimers = true
|
|
let script = "alert();";
|
|
self.evaluateJavaScript(script, completionHandler: nil)
|
|
}
|
|
}
|
|
|
|
public func resumeTimers() {
|
|
if isPausedTimers {
|
|
if let completionHandler = isPausedTimersCompletionHandler {
|
|
self.isPausedTimersCompletionHandler = nil
|
|
completionHandler()
|
|
}
|
|
isPausedTimers = false
|
|
}
|
|
}
|
|
|
|
public func printCurrentPage(settings: PrintJobSettings? = nil,
|
|
completionHandler: UIPrintInteractionController.CompletionHandler? = nil) -> String? {
|
|
var printJobId: String? = nil
|
|
if let settings = settings, settings.handledByClient {
|
|
printJobId = NSUUID().uuidString
|
|
}
|
|
|
|
let printController = UIPrintInteractionController.shared
|
|
let printFormatter = self.viewPrintFormatter()
|
|
if let settings = settings {
|
|
if let margins = settings.margins {
|
|
printFormatter.perPageContentInsets = margins
|
|
}
|
|
if let maximumContentHeight = settings.maximumContentHeight {
|
|
printFormatter.maximumContentHeight = maximumContentHeight
|
|
}
|
|
if let maximumContentWidth = settings.maximumContentWidth {
|
|
printFormatter.maximumContentWidth = maximumContentWidth
|
|
}
|
|
}
|
|
printController.printFormatter = printFormatter
|
|
|
|
printController.printInfo = UIPrintInfo(dictionary: nil)
|
|
if let printInfo = printController.printInfo {
|
|
printInfo.jobName = settings?.jobName ?? (title ?? url?.absoluteString ?? "") + " Document"
|
|
if let settings = settings {
|
|
if let orientationValue = settings.orientation,
|
|
let orientation = UIPrintInfo.Orientation.init(rawValue: orientationValue) {
|
|
printInfo.orientation = orientation
|
|
}
|
|
if let duplexModeValue = settings.duplexMode,
|
|
let duplexMode = UIPrintInfo.Duplex.init(rawValue: duplexModeValue) {
|
|
printInfo.duplex = duplexMode
|
|
}
|
|
if let outputTypeValue = settings.outputType,
|
|
let outputType = UIPrintInfo.OutputType.init(rawValue: outputTypeValue) {
|
|
printInfo.outputType = outputType
|
|
}
|
|
}
|
|
}
|
|
|
|
// initialize print renderer and set its formatter
|
|
let printRenderer = CustomUIPrintPageRenderer(numberOfPage: settings?.numberOfPages,
|
|
forceRenderingQuality: settings?.forceRenderingQuality)
|
|
printRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0)
|
|
if let settings = settings {
|
|
if let footerHeight = settings.footerHeight {
|
|
printRenderer.footerHeight = footerHeight
|
|
}
|
|
if let headerHeight = settings.headerHeight {
|
|
printRenderer.headerHeight = headerHeight
|
|
}
|
|
}
|
|
printController.printPageRenderer = printRenderer
|
|
|
|
if let settings = settings {
|
|
printController.showsNumberOfCopies = settings.showsNumberOfCopies
|
|
printController.showsPaperSelectionForLoadedPapers = settings.showsPaperSelectionForLoadedPapers
|
|
if #available(iOS 15.0, *) {
|
|
printController.showsPaperOrientation = settings.showsPaperOrientation
|
|
}
|
|
}
|
|
|
|
let animated = settings?.animated ?? true
|
|
if let id = printJobId {
|
|
let printJob = PrintJobController(id: id, job: printController, settings: settings)
|
|
PrintJobManager.jobs[id] = printJob
|
|
printJob.present(animated: animated, completionHandler: completionHandler)
|
|
} else {
|
|
printController.present(animated: animated, completionHandler: completionHandler)
|
|
}
|
|
|
|
return printJobId
|
|
}
|
|
|
|
public func getContentHeight() -> Int64 {
|
|
return Int64(scrollView.contentSize.height)
|
|
}
|
|
|
|
public func zoomBy(zoomFactor: Float, animated: Bool) {
|
|
let currentZoomScale = scrollView.zoomScale
|
|
scrollView.setZoomScale(currentZoomScale * CGFloat(zoomFactor), animated: animated)
|
|
}
|
|
|
|
public func getOriginalUrl() -> URL? {
|
|
return currentOriginalUrl
|
|
}
|
|
|
|
public func getZoomScale() -> Float {
|
|
return Float(scrollView.zoomScale)
|
|
}
|
|
|
|
public func getSelectedText(completionHandler: @escaping (Any?, Error?) -> Void) {
|
|
if configuration.preferences.javaScriptEnabled {
|
|
evaluateJavaScript(PluginScriptsUtil.GET_SELECTED_TEXT_JS_SOURCE, completionHandler: completionHandler)
|
|
} else {
|
|
completionHandler(nil, nil)
|
|
}
|
|
}
|
|
|
|
public func getHitTestResult(completionHandler: @escaping (HitTestResult) -> Void) {
|
|
if configuration.preferences.javaScriptEnabled, let lastTouchLocation = lastTouchPoint {
|
|
self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(lastTouchLocation.x),\(lastTouchLocation.y))", completionHandler: {(value, error) in
|
|
if error != nil {
|
|
print("getHitTestResult error: \(error?.localizedDescription ?? "")")
|
|
completionHandler(HitTestResult(type: .unknownType, extra: nil))
|
|
} else if let value = value as? [String: Any?] {
|
|
let hitTestResult = HitTestResult.fromMap(map: value)!
|
|
completionHandler(hitTestResult)
|
|
} else {
|
|
completionHandler(HitTestResult(type: .unknownType, extra: nil))
|
|
}
|
|
})
|
|
} else {
|
|
completionHandler(HitTestResult(type: .unknownType, extra: nil))
|
|
}
|
|
}
|
|
|
|
public func requestFocusNodeHref(completionHandler: @escaping ([String: Any?]?, Error?) -> Void) {
|
|
if configuration.preferences.javaScriptEnabled {
|
|
// add some delay to make it sure _lastAnchorOrImageTouched is updated
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched", completionHandler: {(value, error) in
|
|
let lastAnchorOrImageTouched = value as? [String: Any?]
|
|
completionHandler(lastAnchorOrImageTouched, error)
|
|
})
|
|
}
|
|
} else {
|
|
completionHandler(nil, nil)
|
|
}
|
|
}
|
|
|
|
public func requestImageRef(completionHandler: @escaping ([String: Any?]?, Error?) -> Void) {
|
|
if configuration.preferences.javaScriptEnabled {
|
|
// add some delay to make it sure _lastImageTouched is updated
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched", completionHandler: {(value, error) in
|
|
let lastImageTouched = value as? [String: Any?]
|
|
completionHandler(lastImageTouched, error)
|
|
})
|
|
}
|
|
} else {
|
|
completionHandler(nil, nil)
|
|
}
|
|
}
|
|
|
|
public func clearFocus() {
|
|
self.scrollView.subviews.first?.resignFirstResponder()
|
|
}
|
|
|
|
public func getCertificate() -> SslCertificate? {
|
|
guard let scheme = url?.scheme,
|
|
scheme == "https",
|
|
let host = url?.host,
|
|
let sslCertificate = InAppWebView.sslCertificatesMap[host] else {
|
|
return nil
|
|
}
|
|
return sslCertificate
|
|
}
|
|
|
|
public func isSecureContext(completionHandler: @escaping (_ isSecureContext: Bool) -> Void) {
|
|
evaluateJavascript(source: "window.isSecureContext") { (isSecureContext) in
|
|
if let isSecureContext = isSecureContext {
|
|
completionHandler(isSecureContext as? Bool ?? false)
|
|
return
|
|
}
|
|
completionHandler(false)
|
|
}
|
|
}
|
|
|
|
public func canScrollVertically() -> Bool {
|
|
return scrollView.contentSize.height > self.frame.height
|
|
}
|
|
|
|
public func canScrollHorizontally() -> Bool {
|
|
return scrollView.contentSize.width > self.frame.width
|
|
}
|
|
|
|
public func enablePullToRefresh() {
|
|
if let pullToRefreshControl = pullToRefreshControl {
|
|
if #available(iOS 10.0, *) {
|
|
scrollView.refreshControl = pullToRefreshControl
|
|
} else {
|
|
scrollView.addSubview(pullToRefreshControl)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func disablePullToRefresh() {
|
|
pullToRefreshControl?.removeFromSuperview()
|
|
if #available(iOS 10.0, *) {
|
|
scrollView.refreshControl = nil
|
|
}
|
|
}
|
|
|
|
public func isPullToRefreshEnabled() -> Bool {
|
|
if #available(iOS 10.0, *) {
|
|
return scrollView.refreshControl != nil
|
|
} else {
|
|
return pullToRefreshControl?.superview != nil
|
|
}
|
|
}
|
|
|
|
public func createWebMessageChannel(completionHandler: ((WebMessageChannel) -> Void)? = nil) -> WebMessageChannel {
|
|
let id = NSUUID().uuidString
|
|
let webMessageChannel = WebMessageChannel(id: id)
|
|
webMessageChannel.initJsInstance(webView: self, completionHandler: completionHandler)
|
|
webMessageChannels[id] = webMessageChannel
|
|
|
|
return webMessageChannel
|
|
}
|
|
|
|
public func postWebMessage(message: WebMessage, targetOrigin: String, completionHandler: ((Any?) -> Void)? = nil) throws {
|
|
var portsString = "null"
|
|
if let ports = message.ports {
|
|
var portArrayString: [String] = []
|
|
for port in ports {
|
|
if port.isStarted {
|
|
throw NSError(domain: "Port is already started", code: 0)
|
|
}
|
|
if port.isClosed || port.isTransferred {
|
|
throw NSError(domain: "Port is already closed or transferred", code: 0)
|
|
}
|
|
port.isTransferred = true
|
|
portArrayString.append("\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)['\(port.webMessageChannel!.id)'].\(port.name)")
|
|
}
|
|
portsString = "[" + portArrayString.joined(separator: ", ") + "]"
|
|
}
|
|
let data = message.data?.replacingOccurrences(of: "\'", with: "\\'") ?? "null"
|
|
let url = URL(string: targetOrigin)?.absoluteString ?? "*"
|
|
let source = """
|
|
(function() {
|
|
window.postMessage('\(data)', '\(url)', \(portsString));
|
|
})();
|
|
"""
|
|
evaluateJavascript(source: source, completionHandler: completionHandler)
|
|
message.dispose()
|
|
}
|
|
|
|
public func addWebMessageListener(webMessageListener: WebMessageListener) throws {
|
|
if webMessageListeners.map({ ($0.jsObjectName) }).contains(webMessageListener.jsObjectName) {
|
|
throw NSError(domain: "jsObjectName \(webMessageListener.jsObjectName) was already added.", code: 0)
|
|
}
|
|
try webMessageListener.assertOriginRulesValid()
|
|
webMessageListener.initJsInstance(webView: self)
|
|
webMessageListeners.append(webMessageListener)
|
|
}
|
|
|
|
public func disposeWebMessageChannels() {
|
|
for webMessageChannel in webMessageChannels.values {
|
|
webMessageChannel.dispose()
|
|
}
|
|
webMessageChannels.removeAll()
|
|
}
|
|
|
|
// https://stackoverflow.com/a/58001395/4637638
|
|
public override var inputAccessoryView: UIView? {
|
|
return settings?.disableInputAccessoryView ?? false ? nil : super.inputAccessoryView
|
|
}
|
|
|
|
public func dispose() {
|
|
channelDelegate?.dispose()
|
|
channelDelegate = nil
|
|
removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
|
|
removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
|
|
removeObserver(self, forKeyPath: #keyPath(WKWebView.title))
|
|
if #available(iOS 15.0, *) {
|
|
removeObserver(self, forKeyPath: #keyPath(WKWebView.cameraCaptureState))
|
|
removeObserver(self, forKeyPath: #keyPath(WKWebView.microphoneCaptureState))
|
|
}
|
|
// TODO: Still not working on iOS 16.0!
|
|
// if #available(iOS 16.0, *) {
|
|
// removeObserver(self, forKeyPath: #keyPath(WKWebView.fullscreenState))
|
|
// }
|
|
scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset))
|
|
scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.zoomScale))
|
|
resumeTimers()
|
|
stopLoading()
|
|
disposeWebMessageChannels()
|
|
for webMessageListener in webMessageListeners {
|
|
webMessageListener.dispose()
|
|
}
|
|
webMessageListeners.removeAll()
|
|
if windowId == nil {
|
|
configuration.userContentController.removeAllPluginScriptMessageHandlers()
|
|
configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received")
|
|
configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessagePortMessageReceived")
|
|
configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessageListenerPostMessageReceived")
|
|
configuration.userContentController.removeAllUserScripts()
|
|
if #available(iOS 11.0, *) {
|
|
configuration.userContentController.removeAllContentRuleLists()
|
|
}
|
|
} else if let wId = windowId, InAppWebView.windowWebViews[wId] != nil {
|
|
InAppWebView.windowWebViews.removeValue(forKey: wId)
|
|
}
|
|
configuration.userContentController.dispose(windowId: windowId)
|
|
NotificationCenter.default.removeObserver(self)
|
|
for imp in customIMPs {
|
|
imp_removeBlock(imp)
|
|
}
|
|
longPressRecognizer.removeTarget(self, action: #selector(longPressGestureDetected))
|
|
longPressRecognizer.delegate = nil
|
|
scrollView.removeGestureRecognizer(longPressRecognizer)
|
|
recognizerForDisablingContextMenuOnLinks.removeTarget(self, action: #selector(longPressGestureDetected))
|
|
recognizerForDisablingContextMenuOnLinks.delegate = nil
|
|
scrollView.removeGestureRecognizer(recognizerForDisablingContextMenuOnLinks)
|
|
panGestureRecognizer.removeTarget(self, action: #selector(endDraggingDetected))
|
|
panGestureRecognizer.delegate = nil
|
|
scrollView.removeGestureRecognizer(panGestureRecognizer)
|
|
disablePullToRefresh()
|
|
pullToRefreshControl?.dispose()
|
|
pullToRefreshControl = nil
|
|
findInteractionController?.dispose()
|
|
findInteractionController = nil
|
|
uiDelegate = nil
|
|
navigationDelegate = nil
|
|
scrollView.delegate = nil
|
|
isPausedTimersCompletionHandler = nil
|
|
SharedLastTouchPointTimestamp.removeValue(forKey: self)
|
|
callAsyncJavaScriptBelowIOS14Results.removeAll()
|
|
super.removeFromSuperview()
|
|
}
|
|
|
|
deinit {
|
|
debugPrint("InAppWebView - dealloc")
|
|
}
|
|
}
|