diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml deleted file mode 100644 index 08cb7b47..00000000 --- a/.idea/libraries/Dart_Packages.xml +++ /dev/null @@ -1,738 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index 31799730..65bb3679 100755 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -1,6 +1,8 @@ - + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 667dce09..2d1d2144 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 5.2.0 + +- Added `WebMessageChannel` and `WebMessageListener` features +- `AndroidInAppWebViewController.getCurrentWebViewPackage` is available now starting from Android API 21+. +- Updated Android Gradle distributionUrl version to `5.6.4` +- Attempt to fix "InAppBrowserActivity.onCreate NullPointerException - Attempt to invoke virtual method 'java.lang.String android.os.Bundle.getString(java.lang.String)' on a null object reference" [#665](https://github.com/pichillilorenzo/flutter_inappwebview/issues/665) +- Fixed "[iOS] Application crashes when processing onCreateWindow" [#579](https://github.com/pichillilorenzo/flutter_inappwebview/issues/579) +- Fixed wrong mapping of `NavigationAction` class on Android for `androidHasGesture` and `androidIsRedirect` properties + ## 5.1.0+4 - Fixed "IOS scrolling crash the application" [#707](https://github.com/pichillilorenzo/flutter_inappwebview/issues/707) diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 01a286e9..fd50c3ca 100755 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip \ No newline at end of file diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index aa9e7a21..40dc1267 100755 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -2,8 +2,13 @@ - - + + = Build.VERSION_CODES.M && + WebViewFeature.isFeatureSupported(WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) { + result.success(webView.createCompatWebMessageChannel().toMap()); + } else { + result.success(null); + } + break; + case "postWebMessage": + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) { + Map message = (Map) call.argument("message"); + String targetOrigin = (String) call.argument("targetOrigin"); + List ports = new ArrayList<>(); + List> portsMap = (List>) message.get("ports"); + if (portsMap != null) { + for (Map portMap : portsMap) { + String webMessageChannelId = (String) portMap.get("webMessageChannelId"); + Integer index = (Integer) portMap.get("index"); + WebMessageChannel webMessageChannel = webView.webMessageChannels.get(webMessageChannelId); + if (webMessageChannel != null) { + ports.add(webMessageChannel.ports.get(index)); + } + } + } + WebMessageCompat webMessage = new WebMessageCompat((String) message.get("data"), ports.toArray(new WebMessagePortCompat[0])); + try { + WebViewCompat.postWebMessage(webView, webMessage, Uri.parse(targetOrigin)); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + break; + case "addWebMessageListener": + if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + Map webMessageListenerMap = (Map) call.argument("webMessageListener"); + WebMessageListener webMessageListener = WebMessageListener.fromMap(webMessageListenerMap); + try { + webView.addWebMessageListener(webMessageListener); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + break; default: result.notImplemented(); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewStatic.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewStatic.java index db74d146..2ce006ab 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewStatic.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewStatic.java @@ -1,6 +1,7 @@ package com.pichillilorenzo.flutter_inappwebview; import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.os.Build; import android.webkit.ValueCallback; import android.webkit.WebSettings; @@ -9,6 +10,7 @@ import android.webkit.WebView; import androidx.webkit.WebViewCompat; import androidx.webkit.WebViewFeature; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -66,8 +68,20 @@ public class InAppWebViewStatic implements MethodChannel.MethodCallHandler { break; case "getCurrentWebViewPackage": if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - result.success( - convertWebViewPackageToMap(WebViewCompat.getCurrentWebViewPackage(Shared.activity))); + result.success(convertWebViewPackageToMap(WebViewCompat.getCurrentWebViewPackage(Shared.activity))); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + //with Android Lollipop (API 21) they started to update the WebView + //as a separate APK with the PlayStore and they added the + //getLoadedPackageInfo() method to the WebViewFactory class and this + //should handle the Android 7.0 behaviour changes too + try { + Class webViewFactory = Class.forName("android.webkit.WebViewFactory"); + Method method = webViewFactory.getMethod("getLoadedPackageInfo"); + PackageInfo pInfo = (PackageInfo) method.invoke(null); + result.success(convertWebViewPackageToMap(pInfo)); + } catch (Exception e) { + result.success(null); + } } else { result.success(null); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java index 786826ef..0d0ea118 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java @@ -65,11 +65,8 @@ public class InAppBrowserActivity extends AppCompatActivity implements InAppBrow protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - return; - } - Bundle b = getIntent().getExtras(); + assert b != null; id = b.getString("id"); windowId = b.getInt("windowId"); @@ -77,7 +74,7 @@ public class InAppBrowserActivity extends AppCompatActivity implements InAppBrow setContentView(R.layout.activity_web_view); - Map pullToRefreshInitialOptions = (Map) b.getSerializable("pullToRefreshInitialOptions"); + Map pullToRefreshInitialOptions = (Map) b.getSerializable("pullToRefreshInitialOptions"); MethodChannel pullToRefreshLayoutChannel = new MethodChannel(Shared.messenger, "com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_" + id); PullToRefreshOptions pullToRefreshOptions = new PullToRefreshOptions(); pullToRefreshOptions.parse(pullToRefreshInitialOptions); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java index 4e54c8ed..0cca32ff 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserManager.java @@ -32,6 +32,8 @@ import android.os.Bundle; import android.webkit.MimeTypeMap; import android.util.Log; +import androidx.annotation.NonNull; + import com.pichillilorenzo.flutter_inappwebview.Shared; import java.io.Serializable; @@ -58,7 +60,7 @@ public class InAppBrowserManager implements MethodChannel.MethodCallHandler { } @Override - public void onMethodCall(final MethodCall call, final Result result) { + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { final Activity activity = Shared.activity; switch (call.method) { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java index 30af8d08..e88e6506 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java @@ -9,7 +9,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Point; import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -41,30 +41,28 @@ import android.webkit.WebBackForwardList; import android.webkit.WebHistoryItem; import android.webkit.WebSettings; import android.webkit.WebStorage; +import android.webkit.WebView; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.webkit.JavaScriptReplyProxy; +import androidx.webkit.WebMessageCompat; import androidx.webkit.WebViewCompat; import androidx.webkit.WebViewFeature; -import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; -import com.pichillilorenzo.flutter_inappwebview.types.PreferredContentModeOptionType; -import com.pichillilorenzo.flutter_inappwebview.types.UserContentController; +import com.pichillilorenzo.flutter_inappwebview.JavaScriptBridgeInterface; +import com.pichillilorenzo.flutter_inappwebview.R; +import com.pichillilorenzo.flutter_inappwebview.Shared; +import com.pichillilorenzo.flutter_inappwebview.Util; import com.pichillilorenzo.flutter_inappwebview.content_blocker.ContentBlocker; import com.pichillilorenzo.flutter_inappwebview.content_blocker.ContentBlockerAction; import com.pichillilorenzo.flutter_inappwebview.content_blocker.ContentBlockerHandler; import com.pichillilorenzo.flutter_inappwebview.content_blocker.ContentBlockerTrigger; -import com.pichillilorenzo.flutter_inappwebview.types.ContentWorld; import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserDelegate; -import com.pichillilorenzo.flutter_inappwebview.JavaScriptBridgeInterface; -import com.pichillilorenzo.flutter_inappwebview.R; -import com.pichillilorenzo.flutter_inappwebview.Shared; -import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; -import com.pichillilorenzo.flutter_inappwebview.types.UserScript; -import com.pichillilorenzo.flutter_inappwebview.Util; import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.ConsoleLogJS; import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.InterceptAjaxRequestJS; import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.InterceptFetchRequestJS; @@ -75,19 +73,25 @@ import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.OnWindowFocusE import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.PluginScriptsUtil; import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.PrintJS; import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.PromisePolyfillJS; +import com.pichillilorenzo.flutter_inappwebview.types.ContentWorld; +import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview.types.PreferredContentModeOptionType; +import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview.types.UserContentController; +import com.pichillilorenzo.flutter_inappwebview.types.UserScript; +import com.pichillilorenzo.flutter_inappwebview.types.WebMessageChannel; +import com.pichillilorenzo.flutter_inappwebview.types.WebMessageListener; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.UUID; import java.util.regex.Pattern; @@ -140,6 +144,8 @@ final public class InAppWebView extends InputAwareWebView { public Map> callAsyncJavaScriptCallbacks = new HashMap<>(); public Map> evaluateJavaScriptContentWorldCallbacks = new HashMap<>(); + public Map webMessageChannels = new HashMap<>(); + public InAppWebView(Context context) { super(context); } @@ -1587,6 +1593,30 @@ final public class InAppWebView extends InputAwareWebView { }); } + @TargetApi(Build.VERSION_CODES.M) + public WebMessageChannel createCompatWebMessageChannel() { + String id = UUID.randomUUID().toString(); + WebMessageChannel webMessageChannel = new WebMessageChannel(id, this); + webMessageChannels.put(id, webMessageChannel); + return webMessageChannel; + } + + public void addWebMessageListener(@NonNull WebMessageListener webMessageListener) { + WebViewCompat.addWebMessageListener(this, webMessageListener.jsObjectName, webMessageListener.allowedOriginRules, webMessageListener.listener); + } + + public void disposeWebMessageChannels() { + for (WebMessageChannel webMessageChannel : webMessageChannels.values()) { + webMessageChannel.dispose(); + } + webMessageChannels.clear(); + } + +// @Override +// protected void onWindowVisibilityChanged(int visibility) { +// if (visibility != View.GONE) super.onWindowVisibilityChanged(View.VISIBLE); +// } + @Override public void dispose() { if (windowId != null) { @@ -1594,6 +1624,7 @@ final public class InAppWebView extends InputAwareWebView { } headlessHandler.removeCallbacksAndMessages(null); mHandler.removeCallbacksAndMessages(null); + disposeWebMessageChannels(); removeAllViews(); if (checkContextMenuShouldBeClosedTask != null) removeCallbacks(checkContextMenuShouldBeClosedTask); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java index 2cb50139..7616bc5b 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebViewClient.java @@ -190,7 +190,7 @@ public class InAppWebViewClient extends WebViewClient { public void onPageStarted(WebView view, String url, Bitmap favicon) { final InAppWebView webView = (InAppWebView) view; webView.isLoading = true; - + webView.disposeWebMessageChannels(); webView.userContentController.resetContentWorlds(); loadCustomJavaScriptOnPageStarted(webView); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/NavigationAction.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/NavigationAction.java index 31e82563..115ec8e9 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/NavigationAction.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/NavigationAction.java @@ -20,8 +20,8 @@ public class NavigationAction { Map navigationActionMap = new HashMap<>(); navigationActionMap.put("request", request.toMap()); navigationActionMap.put("isForMainFrame", isForMainFrame); - navigationActionMap.put("hasGesture", hasGesture); - navigationActionMap.put("isRedirect", isRedirect); + navigationActionMap.put("androidHasGesture", hasGesture); + navigationActionMap.put("androidIsRedirect", isRedirect); return navigationActionMap; } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessageChannel.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessageChannel.java new file mode 100644 index 00000000..288ade3a --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessageChannel.java @@ -0,0 +1,133 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebMessagePortCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview.Shared; +import com.pichillilorenzo.flutter_inappwebview.in_app_webview.InAppWebView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class WebMessageChannel implements MethodChannel.MethodCallHandler { + static final String LOG_TAG = "WebMessageChannel"; + + public String id; + public MethodChannel channel; + public final List ports; + private InAppWebView webView; + + public WebMessageChannel(@NonNull String id, @NonNull InAppWebView webView) { + this.id = id; + this.channel = new MethodChannel(Shared.messenger, "com.pichillilorenzo/flutter_inappwebview_web_message_channel_" + id); + this.channel.setMethodCallHandler(this); + this.ports = new ArrayList<>(Arrays.asList(WebViewCompat.createWebMessageChannel(webView))); + this.webView = webView; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "setWebMessageCallback": + if (webView != null && ports.size() > 0 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK)) { + final Integer index = (Integer) call.argument("index"); + final WebMessagePortCompat webMessagePort = ports.get(index); + try { + webMessagePort.setWebMessageCallback(new WebMessagePortCompat.WebMessageCallbackCompat() { + @Override + public void onMessage(@NonNull WebMessagePortCompat port, @Nullable WebMessageCompat message) { + super.onMessage(port, message); + + Map obj = new HashMap<>(); + obj.put("index", index); + obj.put("message", message != null ? message.getData() : null); + channel.invokeMethod("onMessage", obj); + } + }); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + break; + case "postMessage": + if (webView != null && ports.size() > 0 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE)) { + final Integer index = (Integer) call.argument("index"); + WebMessagePortCompat port = ports.get(index); + Map message = (Map) call.argument("message"); + List webMessagePorts = new ArrayList<>(); + List> portsMap = (List>) message.get("ports"); + if (portsMap != null) { + for (Map portMap : portsMap) { + String webMessageChannelId = (String) portMap.get("webMessageChannelId"); + Integer portIndex = (Integer) portMap.get("index"); + WebMessageChannel webMessageChannel = webView.webMessageChannels.get(webMessageChannelId); + if (webMessageChannel != null) { + webMessagePorts.add(webMessageChannel.ports.get(portIndex)); + } + } + } + WebMessageCompat webMessage = new WebMessageCompat((String) message.get("data"), webMessagePorts.toArray(new WebMessagePortCompat[0])); + try { + port.postMessage(webMessage); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + break; + case "close": + if (webView != null && ports.size() > 0 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_CLOSE)) { + Integer index = (Integer) call.argument("index"); + WebMessagePortCompat port = ports.get(index); + try { + port.close(); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + break; + default: + result.notImplemented(); + } + } + + public Map toMap() { + Map webMessageChannelMap = new HashMap<>(); + webMessageChannelMap.put("id", id); + return webMessageChannelMap; + } + + public void dispose() { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_CLOSE)) { + for (WebMessagePortCompat port : ports) { + try { + port.close(); + } catch (Exception ignored) {} + } + } + this.channel.setMethodCallHandler(null); + this.ports.clear(); + this.webView = null; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessageListener.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessageListener.java new file mode 100644 index 00000000..bec5cadc --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessageListener.java @@ -0,0 +1,84 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import android.net.Uri; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.JavaScriptReplyProxy; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview.Shared; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class WebMessageListener implements MethodChannel.MethodCallHandler { + static final String LOG_TAG = "WebMessageListener"; + + public String jsObjectName; + public Set allowedOriginRules; + public WebViewCompat.WebMessageListener listener; + public JavaScriptReplyProxy replyProxy; + public MethodChannel channel; + + public WebMessageListener(@NonNull String jsObjectName, @NonNull Set allowedOriginRules) { + this.jsObjectName = jsObjectName; + this.allowedOriginRules = allowedOriginRules; + this.channel = new MethodChannel(Shared.messenger, "com.pichillilorenzo/flutter_inappwebview_web_message_listener_" + this.jsObjectName); + this.channel.setMethodCallHandler(this); + this.listener = new WebViewCompat.WebMessageListener() { + @Override + public void onPostMessage(@NonNull WebView view, @NonNull WebMessageCompat message, @NonNull Uri sourceOrigin, boolean isMainFrame, @NonNull JavaScriptReplyProxy javaScriptReplyProxy) { + replyProxy = javaScriptReplyProxy; + Map obj = new HashMap<>(); + obj.put("message", message.getData()); + obj.put("sourceOrigin", sourceOrigin.toString().equals("null") ? null : sourceOrigin.toString()); + obj.put("isMainFrame", isMainFrame); + channel.invokeMethod("onPostMessage", obj); + } + }; + } + + @Nullable + public static WebMessageListener fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String jsObjectName = (String) map.get("jsObjectName"); + assert jsObjectName != null; + List allowedOriginRuleList = (List) map.get("allowedOriginRules"); + assert allowedOriginRuleList != null; + Set allowedOriginRules = new HashSet<>(allowedOriginRuleList); + return new WebMessageListener(jsObjectName, allowedOriginRules); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "postMessage": + if (replyProxy != null && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + String message = (String) call.argument("message"); + replyProxy.postMessage(message); + } + result.success(true); + break; + default: + result.notImplemented(); + } + } + + public void dispose() { + this.channel.setMethodCallHandler(null); + this.listener = null; + this.replyProxy = null; + } +} diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 49cb725f..bf36e700 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"android":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.5-nullsafety/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.1.0-nullsafety.2/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.2.0-nullsafety/","dependencies":[]},{"name":"url_launcher_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-0.1.0-nullsafety.3/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-0.1.0-nullsafety.3/","dependencies":[]},{"name":"url_launcher_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-0.1.0-nullsafety.2/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_linux","url_launcher_macos","url_launcher_windows"]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2021-03-08 17:17:50.745041","version":"2.1.0-10.0.pre"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"android":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.5-nullsafety/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.1.0-nullsafety.2/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.2.0-nullsafety/","dependencies":[]},{"name":"url_launcher_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-0.1.0-nullsafety.3/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-0.1.0-nullsafety.3/","dependencies":[]},{"name":"url_launcher_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-0.1.0-nullsafety.2/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_linux","url_launcher_macos","url_launcher_windows"]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2021-03-11 22:30:27.356562","version":"2.1.0-10.0.pre"} \ No newline at end of file diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index ff2151af..b7fc9f56 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -2,13 +2,12 @@ # This is a generated file; do not edit or check into version control. export "FLUTTER_ROOT=/Users/lorenzopichilli/flutter" export "FLUTTER_APPLICATION_PATH=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example" -export "FLUTTER_TARGET=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/lib/main.dart" +export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build/ios" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" -export "DART_DEFINES=Zmx1dHRlci5pbnNwZWN0b3Iuc3RydWN0dXJlZEVycm9ycz10cnVl,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==" export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" +export "TRACK_WIDGET_CREATION=false" export "TREE_SHAKE_ICONS=false" export "PACKAGE_CONFIG=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/.dart_tool/package_config.json" diff --git a/example/lib/main.dart b/example/lib/main.dart index f2b89844..895042e5 100755 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -17,7 +17,7 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); // await Permission.camera.request(); // await Permission.microphone.request(); - //await Permission.storage.request(); + // await Permission.storage.request(); if (Platform.isAndroid) { await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true); diff --git a/flutter_inappwebview.iml b/flutter_inappwebview.iml index 4cb39159..0adae5aa 100755 --- a/flutter_inappwebview.iml +++ b/flutter_inappwebview.iml @@ -80,6 +80,5 @@ - \ No newline at end of file diff --git a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift index f3026c01..21589f01 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -75,6 +75,7 @@ public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelega pullToRefreshControl.prepare() prepareWebView() + webView.windowCreated = true progressBar = UIProgressView(progressViewStyle: .bar) diff --git a/ios/Classes/InAppWebView/FlutterWebViewController.swift b/ios/Classes/InAppWebView/FlutterWebViewController.swift index b927e5b1..0ebe03e9 100755 --- a/ios/Classes/InAppWebView/FlutterWebViewController.swift +++ b/ios/Classes/InAppWebView/FlutterWebViewController.swift @@ -82,6 +82,7 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView { webView!.options = options webView!.prepare() + webView!.windowCreated = true if windowId == nil { if #available(iOS 11.0, *) { diff --git a/ios/Classes/InAppWebView/InAppWebView.swift b/ios/Classes/InAppWebView/InAppWebView.swift index 683994ac..e51db2b2 100755 --- a/ios/Classes/InAppWebView/InAppWebView.swift +++ b/ios/Classes/InAppWebView/InAppWebView.swift @@ -12,10 +12,13 @@ import WebKit public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler, UIGestureRecognizerDelegate, PullToRefreshDelegate { var windowId: Int64? + var windowCreated = false var inAppBrowserDelegate: InAppBrowserDelegate? var channel: FlutterMethodChannel? var options: InAppWebViewOptions? var pullToRefreshControl: PullToRefreshControl? + var webMessageChannels: [String:WebMessageChannel] = [:] + var webMessageListeners: [WebMessageListener] = [] static var sslCertificatesMap: [String: SslCertificate] = [:] // [URL host name : SslCertificate] static var credentialsProposed: [URLCredential] = [] @@ -450,6 +453,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } 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) } @@ -1418,6 +1425,11 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if windowId != nil, !windowCreated { + decisionHandler(.cancel) + return + } + if navigationAction.request.url != nil { if let useShouldOverrideUrlLoading = options?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading { @@ -1513,6 +1525,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + disposeWebMessageChannels() initializeWindowIdJS() if #available(iOS 14.0, *) { @@ -1561,6 +1574,11 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi 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 || @@ -1573,6 +1591,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi onReceivedHttpAuthRequest(challenge: challenge, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") + completionHandler(.performDefaultHandling, nil) } else if (result as? NSObject) == FlutterMethodNotImplemented { completionHandler(.performDefaultHandling, nil) @@ -1642,6 +1661,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi onReceivedServerTrustAuthRequest(challenge: challenge, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") + completionHandler(.performDefaultHandling, nil) } else if (result as? NSObject) == FlutterMethodNotImplemented { completionHandler(.performDefaultHandling, nil) @@ -1677,6 +1697,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi onReceivedClientCertRequest(challenge: challenge, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") + completionHandler(.performDefaultHandling, nil) } else if (result as? NSObject) == FlutterMethodNotImplemented { completionHandler(.performDefaultHandling, nil) @@ -1801,6 +1822,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi onJsAlert(frame: frame, message: message, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") + completionHandler() } else if (result as? NSObject) == FlutterMethodNotImplemented { self.createAlertDialog(message: message, responseMessage: nil, confirmButtonTitle: nil, completionHandler: completionHandler) @@ -1862,6 +1884,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi onJsConfirm(frame: frame, message: message, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") + completionHandler(false) } else if (result as? NSObject) == FlutterMethodNotImplemented { self.createConfirmDialog(message: message, responseMessage: nil, confirmButtonTitle: nil, cancelButtonTitle: nil, completionHandler: completionHandler) @@ -1937,6 +1960,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi onJsPrompt(frame: frame, message: message, defaultValue: defaultValue, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") + completionHandler(nil) } else if (result as? NSObject) == FlutterMethodNotImplemented { self.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: nil, confirmButtonTitle: nil, cancelButtonTitle: nil, value: nil, completionHandler: completionHandler) @@ -2068,9 +2092,15 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi public func webView(_ webView: WKWebView, authenticationChallenge challenge: URLAuthenticationChallenge, shouldAllowDeprecatedTLS decisionHandler: @escaping (Bool) -> Void) { + if windowId != nil, !windowCreated { + decisionHandler(false) + return + } + shouldAllowDeprecatedTLS(challenge: challenge, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") + decisionHandler(false) } else if (result as? NSObject) == FlutterMethodNotImplemented { decisionHandler(false) @@ -2511,6 +2541,45 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { ]) 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.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.onPostMessage(message: messageData, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame) + } } } @@ -2685,15 +2754,74 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { } } + 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() + } + public func dispose() { if isPausedTimers, let completionHandler = isPausedTimersCompletionHandler { isPausedTimersCompletionHandler = nil completionHandler() } 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() diff --git a/ios/Classes/InAppWebViewMethodHandler.swift b/ios/Classes/InAppWebViewMethodHandler.swift index f8437fd6..5d416b3a 100644 --- a/ios/Classes/InAppWebViewMethodHandler.swift +++ b/ios/Classes/InAppWebViewMethodHandler.swift @@ -474,6 +474,57 @@ public class InAppWebViewMethodHandler: FlutterMethodCallDelegate { result(false) } break + case "createWebMessageChannel": + if let webView = webView { + let _ = webView.createWebMessageChannel { (webMessageChannel) in + result(webMessageChannel.toMap()) + } + } else { + result(nil) + } + break + case "postWebMessage": + if let webView = webView { + let message = arguments!["message"] as! [String: Any?] + let targetOrigin = arguments!["targetOrigin"] as! String + + var ports: [WebMessagePort] = [] + let portsMap = message["ports"] as? [[String: Any?]] + if let portsMap = portsMap { + for portMap in portsMap { + let webMessageChannelId = portMap["webMessageChannelId"] as! String + let index = portMap["index"] as! Int + if let webMessageChannel = webView.webMessageChannels[webMessageChannelId] { + ports.append(webMessageChannel.ports[index]) + } + } + } + let webMessage = WebMessage(data: message["data"] as? String, ports: ports) + do { + try webView.postWebMessage(message: webMessage, targetOrigin: targetOrigin) { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "InAppWebViewMethodHandler", message: error.domain, details: nil)) + } + } else { + result(false) + } + break + case "addWebMessageListener": + if let webView = webView { + let webMessageListenerMap = arguments!["webMessageListener"] as! [String: Any?] + let webMessageListener = WebMessageListener.fromMap(map: webMessageListenerMap)! + do { + try webView.addWebMessageListener(webMessageListener: webMessageListener) + result(false) + } catch let error as NSError { + result(FlutterError(code: "InAppWebViewMethodHandler", message: error.domain, details: nil)) + } + } else { + result(false) + } + break default: result(FlutterMethodNotImplemented) break diff --git a/ios/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift b/ios/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift index f26f2e92..2d4b7f89 100644 --- a/ios/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift +++ b/ios/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift @@ -20,6 +20,7 @@ let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT = PluginScript( let JAVASCRIPT_BRIDGE_JS_SOURCE = """ window.\(JAVASCRIPT_BRIDGE_NAME) = {}; +\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME) = {}; window.\(JAVASCRIPT_BRIDGE_NAME).callHandler = function() { var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); var _callHandlerID = setTimeout(function(){}); @@ -28,6 +29,7 @@ window.\(JAVASCRIPT_BRIDGE_NAME).callHandler = function() { window.\(JAVASCRIPT_BRIDGE_NAME)[_callHandlerID] = resolve; }); }; +\(WEB_MESSAGE_LISTENER_JS_SOURCE) """ let PLATFORM_READY_JS_SOURCE = "window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));"; diff --git a/ios/Classes/PluginScriptsJS/WebMessageChannelJS.swift b/ios/Classes/PluginScriptsJS/WebMessageChannelJS.swift new file mode 100644 index 00000000..18445841 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/WebMessageChannelJS.swift @@ -0,0 +1,10 @@ +// +// WebMessageChannelJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +let WEB_MESSAGE_CHANNELS_VARIABLE_NAME = "window.\(JAVASCRIPT_BRIDGE_NAME)._webMessageChannels" diff --git a/ios/Classes/PluginScriptsJS/WebMessageListenerJS.swift b/ios/Classes/PluginScriptsJS/WebMessageListenerJS.swift new file mode 100644 index 00000000..369ac913 --- /dev/null +++ b/ios/Classes/PluginScriptsJS/WebMessageListenerJS.swift @@ -0,0 +1,112 @@ +// +// WebMessageListenerJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +let WEB_MESSAGE_LISTENER_JS_SOURCE = """ +function FlutterInAppWebViewWebMessageListener(jsObjectName) { + this.jsObjectName = jsObjectName; + this.listeners = []; + this.onmessage = null; +} +FlutterInAppWebViewWebMessageListener.prototype.postMessage = function(message) { + window.webkit.messageHandlers['onWebMessageListenerPostMessageReceived'].postMessage({jsObjectName: this.jsObjectName, message: message}); +}; +FlutterInAppWebViewWebMessageListener.prototype.addEventListener = function(type, listener) { + if (listener == null) { + return; + } + this.listeners.push(listener); +}; +FlutterInAppWebViewWebMessageListener.prototype.removeEventListener = function(type, listener) { + if (listener == null) { + return; + } + var index = this.listeners.indexOf(listener); + if (index >= 0) { + this.listeners.splice(index, 1); + } +}; + +window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6 = function(ip_string) { + // replace ipv4 address if any + var ipv4 = ip_string.match(/(.*:)([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$)/); + if (ipv4) { + var ip_string = ipv4[1]; + ipv4 = ipv4[2].match(/[0-9]+/g); + for (var i = 0;i < 4;i ++) { + var byte = parseInt(ipv4[i],10); + ipv4[i] = ("0" + byte.toString(16)).substr(-2); + } + ip_string += ipv4[0] + ipv4[1] + ':' + ipv4[2] + ipv4[3]; + } + + // take care of leading and trailing :: + ip_string = ip_string.replace(/^:|:$/g, ''); + + var ipv6 = ip_string.split(':'); + + for (var i = 0; i < ipv6.length; i ++) { + var hex = ipv6[i]; + if (hex != "") { + // normalize leading zeros + ipv6[i] = ("0000" + hex).substr(-4); + } + else { + // normalize grouped zeros :: + hex = []; + for (var j = ipv6.length; j <= 8; j ++) { + hex.push('0000'); + } + ipv6[i] = hex.join(':'); + } + } + + return ipv6.join(':'); +} + +window.\(JAVASCRIPT_BRIDGE_NAME)._isOriginAllowed = function(allowedOriginRules, scheme, host, port) { + for (var rule of allowedOriginRules) { + if (rule === "*") { + return true; + } + if (scheme == null || scheme === "") { + continue; + } + if ((scheme == null || scheme === "") && (host == null || host === "") && (port === 0 || port === "" || port == null)) { + continue; + } + var rulePort = rule.port == null || rule.port === 0 ? (rule.scheme == "https" ? 443 : 80) : rule.port; + var currentPort = port === 0 || port === "" || port == null ? (scheme == "https" ? 443 : 80) : port; + var IPv6 = null; + if (rule.host != null && rule.host[0] === "[") { + try { + IPv6 = normalizeIPv6(rule.host.substring(1, rule.host.length - 1)); + } catch {} + } + var hostIPv6 = null; + try { + hostIPv6 = normalizeIPv6(host); + } catch {} + + var schemeAllowed = scheme == rule.scheme; + + var hostAllowed = rule.host == null || + rule.host === "" || + host === rule.host || + (rule.host[0] === "*" && host != null && host.indexOf(rule.host.split("*")[1]) >= 0) || + (hostIPv6 != null && IPv6 != null && hostIPv6 === IPv6); + + var portAllowed = rulePort === currentPort + + if (schemeAllowed && hostAllowed && portAllowed) { + return true; + } + } + return false +} +""" diff --git a/ios/Classes/Types/WebMessage.swift b/ios/Classes/Types/WebMessage.swift new file mode 100644 index 00000000..169926b2 --- /dev/null +++ b/ios/Classes/Types/WebMessage.swift @@ -0,0 +1,27 @@ +// +// WebMessage.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessage : NSObject { + var data: String? + var ports: [WebMessagePort]? + + public init(data: String?, ports: [WebMessagePort]?) { + super.init() + self.data = data + self.ports = ports + } + + public func dispose() { + ports?.removeAll() + } + + deinit { + print("WebMessage - dealloc") + } +} diff --git a/ios/Classes/Types/WebMessageChannel.swift b/ios/Classes/Types/WebMessageChannel.swift new file mode 100644 index 00000000..de9e75aa --- /dev/null +++ b/ios/Classes/Types/WebMessageChannel.swift @@ -0,0 +1,150 @@ +// +// WebMessageChannel.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessageChannel : FlutterMethodCallDelegate { + var id: String + var channel: FlutterMethodChannel? + var webView: InAppWebView? + var ports: [WebMessagePort] = [] + + public init(id: String) { + self.id = id + super.init() + self.channel = FlutterMethodChannel(name: "com.pichillilorenzo/flutter_inappwebview_web_message_channel_" + id, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger()) + self.channel?.setMethodCallHandler(self.handle) + self.ports = [ + WebMessagePort(name: "port1", webMessageChannel: self), + WebMessagePort(name: "port2", webMessageChannel: self) + ] + } + + public func initJsInstance(webView: InAppWebView, completionHandler: ((WebMessageChannel) -> Void)? = nil) { + self.webView = webView + if let webView = self.webView { + webView.evaluateJavascript(source: """ + (function() { + \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"] = new MessageChannel(); + })(); + """) { (_) in + completionHandler?(self) + } + } else { + completionHandler?(self) + } + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "setWebMessageCallback": + if let _ = webView, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + do { + try port.setWebMessageCallback { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + + } else { + result(true) + } + break + case "postMessage": + if let webView = webView, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + let message = arguments!["message"] as! [String: Any?] + + var webMessagePorts: [WebMessagePort] = [] + let portsMap = message["ports"] as? [[String: Any?]] + if let portsMap = portsMap { + for portMap in portsMap { + let webMessageChannelId = portMap["webMessageChannelId"] as! String + let index = portMap["index"] as! Int + if let webMessageChannel = webView.webMessageChannels[webMessageChannelId] { + webMessagePorts.append(webMessageChannel.ports[index]) + } + } + } + let webMessage = WebMessage(data: message["data"] as? String, ports: webMessagePorts) + do { + try port.postMessage(message: webMessage) { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + } else { + result(true) + } + break + case "close": + if let _ = webView, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + do { + try port.close { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + } else { + result(true) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onMessage(index: Int64, message: String?) { + let arguments: [String:Any?] = [ + "index": index, + "message": message + ] + channel?.invokeMethod("onMessage", arguments: arguments) + } + + public func toMap () -> [String:Any?] { + return [ + "id": id + ] + } + + public func dispose() { + channel?.setMethodCallHandler(nil) + for port in ports { + port.dispose() + } + ports.removeAll() + webView?.evaluateJavascript(source: """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"]; + if (webMessageChannel != null) { + webMessageChannel.port1.close(); + webMessageChannel.port2.close(); + delete \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"]; + } + })(); + """) + channel = nil + webView = nil + } + + deinit { + print("WebMessageChannel - dealloc") + } +} diff --git a/ios/Classes/Types/WebMessageListener.swift b/ios/Classes/Types/WebMessageListener.swift new file mode 100644 index 00000000..6447a680 --- /dev/null +++ b/ios/Classes/Types/WebMessageListener.swift @@ -0,0 +1,226 @@ +// +// WebMessageListener.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation +import WebKit + +public class WebMessageListener : FlutterMethodCallDelegate { + + var jsObjectName: String + var allowedOriginRules: Set + var channel: FlutterMethodChannel? + var webView: InAppWebView? + + public init(jsObjectName: String, allowedOriginRules: Set) { + self.jsObjectName = jsObjectName + self.allowedOriginRules = allowedOriginRules + super.init() + self.channel = FlutterMethodChannel(name: "com.pichillilorenzo/flutter_inappwebview_web_message_listener_" + self.jsObjectName, + binaryMessenger: SwiftFlutterPlugin.instance!.registrar!.messenger()) + self.channel?.setMethodCallHandler(self.handle) + } + + public func assertOriginRulesValid() throws { + for (index, originRule) in allowedOriginRules.enumerated() { + if originRule.isEmpty { + throw NSError(domain: "allowedOriginRules[\(index)] is empty", code: 0) + } + if originRule == "*" { + continue + } + if let url = URL(string: originRule) { + guard let scheme = url.scheme else { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if scheme == "http" || scheme == "https", url.host == nil || url.host!.isEmpty { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if scheme != "http", scheme != "https", url.host != nil || url.port != nil { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if url.host == nil || url.host!.isEmpty, url.port != nil { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if !url.path.isEmpty { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if let hostname = url.host { + if let firstIndex = hostname.firstIndex(of: "*") { + let distance = hostname.distance(from: hostname.startIndex, to: firstIndex) + if distance != 0 || (distance == 0 && hostname.prefix(2) != "*.") { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + if hostname.hasPrefix("[") { + if !hostname.hasSuffix("]") { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + let fromIndex = hostname.index(hostname.startIndex, offsetBy: 1) + let toIndex = hostname.index(hostname.startIndex, offsetBy: hostname.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + let ipv6 = String(hostname[indexRange]) + if !Util.isIPv6(address: ipv6) { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + } + } else { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + } + + public func initJsInstance(webView: InAppWebView) { + self.webView = webView + if let webView = self.webView { + let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") + let allowedOriginRulesString = allowedOriginRules.map { (allowedOriginRule) -> String in + if allowedOriginRule == "*" { + return "'*'" + } + let rule = URL(string: allowedOriginRule)! + return """ + {scheme: '\(rule.scheme!)', host: '\(rule.host?.replacingOccurrences(of: "\'", with: "\\'") ?? "null")', port: \(rule.port != nil ? String(rule.port!) : "null")} + """ + }.joined(separator: ", ") + let source = """ + (function() { + var allowedOriginRules = [\(allowedOriginRulesString)]; + var isPageBlank = window.location.href === "about:blank"; + var scheme = !isPageBlank ? window.location.protocol.replace(":", "") : null; + var host = !isPageBlank ? window.location.hostname : null; + var port = !isPageBlank ? window.location.port : null; + if (window.\(JAVASCRIPT_BRIDGE_NAME)._isOriginAllowed(allowedOriginRules, scheme, host, port)) { + window['\(jsObjectNameEscaped)'] = new FlutterInAppWebViewWebMessageListener('\(jsObjectNameEscaped)'); + } + })(); + """ + webView.configuration.userContentController.addPluginScript(PluginScript( + groupName: "WebMessageListener-" + jsObjectName, + source: source, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onWebMessageListenerPostMessageReceived"] + )) + webView.configuration.userContentController.sync(scriptMessageHandler: webView) + } + } + + public static func fromMap(map: [String:Any?]?) -> WebMessageListener? { + guard let map = map else { + return nil + } + return WebMessageListener( + jsObjectName: map["jsObjectName"] as! String, + allowedOriginRules: Set(map["allowedOriginRules"] as! [String]) + ) + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "postMessage": + if let webView = webView { + let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") + let messageEscaped = (arguments!["message"] as! String).replacingOccurrences(of: "\'", with: "\\'") + let source = """ + (function() { + var webMessageListener = window['\(jsObjectNameEscaped)']; + if (webMessageListener != null) { + var event = {data: '\(messageEscaped)'}; + if (webMessageListener.onmessage != null) { + webMessageListener.onmessage(event); + } else { + for (var listener of webMessageListener.listeners) { + listener(event); + } + } + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + result(true) + } + } else { + result(true) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func isOriginAllowed(scheme: String?, host: String?, port: Int?) -> Bool { + for allowedOriginRule in allowedOriginRules { + if allowedOriginRule == "*" { + return true + } + if scheme == nil || scheme!.isEmpty { + continue + } + if scheme == nil || scheme!.isEmpty, host == nil || host!.isEmpty, port == nil || port == 0 { + continue + } + if let rule = URL(string: allowedOriginRule) { + let rulePort = rule.port == nil || rule.port == 0 ? (rule.scheme == "https" ? 443 : 80) : rule.port! + let currentPort = port == nil || port == 0 ? (scheme == "https" ? 443 : 80) : port! + var IPv6: String? = nil + if let hostname = rule.host, hostname.hasPrefix("[") { + let fromIndex = hostname.index(hostname.startIndex, offsetBy: 1) + let toIndex = hostname.index(hostname.startIndex, offsetBy: hostname.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + do { + IPv6 = try Util.normalizeIPv6(address: String(hostname[indexRange])) + } catch {} + } + var hostIPv6: String? = nil + if let host = host, Util.isIPv6(address: host) { + do { + hostIPv6 = try Util.normalizeIPv6(address: host) + } catch {} + } + + let schemeAllowed = scheme != nil && !scheme!.isEmpty && scheme == rule.scheme + + let hostAllowed = rule.host == nil || + rule.host!.isEmpty || + host == rule.host || + (rule.host!.hasPrefix("*") && host != nil && host!.hasSuffix(rule.host!.split(separator: "*", omittingEmptySubsequences: false)[1])) || + (hostIPv6 != nil && IPv6 != nil && hostIPv6 == IPv6) + + let portAllowed = rulePort == currentPort + + if schemeAllowed, hostAllowed, portAllowed { + return true + } + } + } + return false + } + + public func onPostMessage(message: String?, sourceOrigin: URL?, isMainFrame: Bool) { + let arguments: [String:Any?] = [ + "message": message, + "sourceOrigin": sourceOrigin?.absoluteString, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onPostMessage", arguments: arguments) + } + + public func dispose() { + channel?.setMethodCallHandler(nil) + channel = nil + webView = nil + } + + deinit { + print("WebMessageListener - dealloc") + } +} diff --git a/ios/Classes/Types/WebMessagePort.swift b/ios/Classes/Types/WebMessagePort.swift new file mode 100644 index 00000000..dff16845 --- /dev/null +++ b/ios/Classes/Types/WebMessagePort.swift @@ -0,0 +1,122 @@ +// +// WebMessagePort.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessagePort : NSObject { + var name: String + var webMessageChannel: WebMessageChannel? + var isClosed = false + var isTransferred = false + var isStarted = false + + public init(name: String, webMessageChannel: WebMessageChannel) { + self.name = name + super.init() + self.webMessageChannel = webMessageChannel + } + + public func setWebMessageCallback(completionHandler: ((Any?) -> Void)? = nil) throws { + if isClosed || isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + self.isStarted = true + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + let index = name == "port1" ? 0 : 1 + webView.evaluateJavascript(source: """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).onmessage = function (event) { + window.webkit.messageHandlers["onWebMessagePortMessageReceived"].postMessage({ + "webMessageChannelId": "\(webMessageChannel.id)", + "index": \(String(index)), + "message": event.data + }); + } + } + })(); + """) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + } + + public func postMessage(message: WebMessage, completionHandler: ((Any?) -> Void)? = nil) throws { + if isClosed || isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + var portsString = "null" + if let ports = message.ports { + var portArrayString: [String] = [] + for port in ports { + if port == self { + throw NSError(domain: "Source port cannot be transferred", code: 0) + } + 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 source = """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).postMessage('\(data)', \(portsString)); + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + message.dispose() + } + + public func close(completionHandler: ((Any?) -> Void)? = nil) throws { + if isTransferred { + throw NSError(domain: "Port is already transferred", code: 0) + } + isClosed = true + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + let source = """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).close(); + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + } + + public func dispose() { + isClosed = true + webMessageChannel = nil + } + + deinit { + print("WebMessagePort - dealloc") + } +} diff --git a/ios/Classes/Util.swift b/ios/Classes/Util.swift index 4899c12f..121930ab 100644 --- a/ios/Classes/Util.swift +++ b/ios/Classes/Util.swift @@ -149,4 +149,78 @@ public class Util { return "NORMAL" } } + + public static func isIPv4(address: String) -> Bool { + var sin = sockaddr_in() + return address.withCString({ cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) }) == 1 + } + + public static func isIPv6(address: String) -> Bool { + var sin6 = sockaddr_in6() + return address.withCString({ cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) }) == 1 + } + + public static func isIpAddress(address: String) -> Bool { + return Util.isIPv6(address: address) || Util.isIPv4(address: address) + } + + public static func normalizeIPv6(address: String) throws -> String { + if !Util.isIPv6(address: address) { + throw NSError(domain: "Invalid address: \(address)", code: 0) + } + var ipString = address + // replace ipv4 address if any + let ipv4Regex = try! NSRegularExpression(pattern: "(.*:)([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$)") + if let match = ipv4Regex.firstMatch(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count)) { + if let ipv6PartRange = Range(match.range(at: 1), in: address) { + ipString = String(address[ipv6PartRange]) + } + if let ipv4Range = Range(match.range(at: 2), in: address) { + let ipv4 = address[ipv4Range] + let ipv4Splitted = ipv4.split(separator: ".") + var ipv4Converted = Array(repeating: "0000", count: 4) + for i in 0...3 { + let byte = Int(ipv4Splitted[i])! + let hex = ("0" + String(byte, radix: 16)) + var offset = hex.count - 3 + offset = offset < 0 ? 0 : offset + let fromIndex = hex.index(hex.startIndex, offsetBy: offset) + let toIndex = hex.index(hex.startIndex, offsetBy: hex.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + ipv4Converted[i] = String(hex[indexRange]) + } + ipString += ipv4Converted[0] + ipv4Converted[1] + ":" + ipv4Converted[2] + ipv4Converted[3] + } + } + + // take care of leading and trailing :: + let regex = try! NSRegularExpression(pattern: "^:|:$") + ipString = regex.stringByReplacingMatches(in: ipString, options: [], range: NSRange(location: 0, length: ipString.count), withTemplate: "") + + let ipv6 = ipString.split(separator: ":", omittingEmptySubsequences: false) + var fullIPv6 = Array(repeating: "0000", count: ipv6.count) + + for (i, hex) in ipv6.enumerated() { + if !hex.isEmpty { + // normalize leading zeros + let hexString = String("0000" + hex) + var offset = hexString.count - 5 + offset = offset < 0 ? 0 : offset + let fromIndex = hexString.index(hexString.startIndex, offsetBy: offset) + let toIndex = hexString.index(hexString.startIndex, offsetBy: hexString.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + fullIPv6[i] = String(hexString[indexRange]) + } else { + // normalize grouped zeros :: + var zeros: [String] = [] + for j in ipv6.count...8 { + zeros.append("0000") + } + fullIPv6[i] = zeros.joined(separator: ":") + } + } + + return fullIPv6.joined(separator: ":") + } + } diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/lib/src/chrome_safari_browser/chrome_safari_browser.dart index 47818b2c..a95b18a2 100755 --- a/lib/src/chrome_safari_browser/chrome_safari_browser.dart +++ b/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -45,7 +45,7 @@ class ChromeSafariBrowser { const MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser'); ChromeSafariBrowser() { - id = ViewIdGenerator.generateId(); + id = IdGenerator.generate(); this._channel = MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser_$id'); this._channel.setMethodCallHandler(handleMethod); diff --git a/lib/src/in_app_browser/in_app_browser.dart b/lib/src/in_app_browser/in_app_browser.dart index e15f29db..f6a70682 100755 --- a/lib/src/in_app_browser/in_app_browser.dart +++ b/lib/src/in_app_browser/in_app_browser.dart @@ -66,7 +66,7 @@ class InAppBrowser { /// InAppBrowser({this.windowId, this.initialUserScripts}) { - id = ViewIdGenerator.generateId(); + id = IdGenerator.generate(); this._channel = MethodChannel('com.pichillilorenzo/flutter_inappbrowser_$id'); this._channel.setMethodCallHandler(handleMethod); diff --git a/lib/src/in_app_webview/android/in_app_webview_controller.dart b/lib/src/in_app_webview/android/in_app_webview_controller.dart index b2411d98..4df59218 100644 --- a/lib/src/in_app_webview/android/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/android/in_app_webview_controller.dart @@ -176,7 +176,7 @@ class AndroidInAppWebViewController { ///has loaded WebView will be killed. ///The next time the app starts and loads WebView it will use the new WebView package instead. /// - ///**NOTE**: available only on Android 26+. + ///**NOTE**: available only on Android 21+. /// ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#getCurrentWebViewPackage(android.content.Context) static Future getCurrentWebViewPackage() async { diff --git a/lib/src/in_app_webview/headless_in_app_webview.dart b/lib/src/in_app_webview/headless_in_app_webview.dart index ffaae21d..754ddc3e 100644 --- a/lib/src/in_app_webview/headless_in_app_webview.dart +++ b/lib/src/in_app_webview/headless_in_app_webview.dart @@ -91,7 +91,7 @@ class HeadlessInAppWebView implements WebView { this.contextMenu, this.initialUserScripts, this.pullToRefreshController}) { - id = ViewIdGenerator.generateId(); + id = IdGenerator.generate(); webViewController = new InAppWebViewController(id, this); } diff --git a/lib/src/in_app_webview/in_app_webview_controller.dart b/lib/src/in_app_webview/in_app_webview_controller.dart index 33118654..59319230 100644 --- a/lib/src/in_app_webview/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/in_app_webview_controller.dart @@ -18,6 +18,8 @@ import '../types.dart'; import '../in_app_browser/in_app_browser.dart'; import '../web_storage/web_storage.dart'; import '../util.dart'; +import '../web_message/web_message_channel.dart'; +import '../web_message/web_message_listener.dart'; import 'headless_in_app_webview.dart'; import 'in_app_webview.dart'; @@ -51,6 +53,7 @@ class InAppWebViewController { Map javaScriptHandlersMap = HashMap(); List _userScripts = []; + Set _webMessageListenerObjNames = Set(); // ignore: unused_field dynamic _id; @@ -1455,7 +1458,7 @@ class InAppWebViewController { void addJavaScriptHandler( {required String handlerName, required JavaScriptHandlerCallback callback}) { - assert(!_JAVASCRIPT_HANDLER_FORBIDDEN_NAMES.contains(handlerName)); + assert(!_JAVASCRIPT_HANDLER_FORBIDDEN_NAMES.contains(handlerName), '"$handlerName" is a forbidden name!'); this.javaScriptHandlersMap[handlerName] = (callback); } @@ -1467,7 +1470,7 @@ class InAppWebViewController { return this.javaScriptHandlersMap.remove(handlerName); } - ///Takes a screenshot (in PNG format) of the WebView's visible viewport and returns a [Uint8List]. Returns `null` if it wasn't be able to take it. + ///Takes a screenshot of the WebView's visible viewport and returns a [Uint8List]. Returns `null` if it wasn't be able to take it. /// ///[screenshotConfiguration] represents the configuration data to use when generating an image from a web view’s contents. /// @@ -2087,6 +2090,32 @@ class InAppWebViewController { return await _channel.invokeMethod('isSecureContext', args); } + Future createWebMessageChannel() async { + Map args = {}; + Map? result = (await _channel.invokeMethod('createWebMessageChannel', args)) + ?.cast(); + return WebMessageChannel.fromMap(result); + } + + Future postWebMessage({required WebMessage message, Uri? targetOrigin}) async { + if (targetOrigin == null) { + targetOrigin = Uri.parse(""); + } + Map args = {}; + args.putIfAbsent('message', () => message.toMap()); + args.putIfAbsent('targetOrigin', () => targetOrigin.toString()); + await _channel.invokeMethod('postWebMessage', args); + } + + Future addWebMessageListener(WebMessageListener webMessageListener) async { + assert(!_webMessageListenerObjNames.contains(webMessageListener.jsObjectName), "jsObjectName ${webMessageListener.jsObjectName} was already added."); + _webMessageListenerObjNames.add(webMessageListener.jsObjectName); + + Map args = {}; + args.putIfAbsent('webMessageListener', () => webMessageListener.toMap()); + await _channel.invokeMethod('addWebMessageListener', args); + } + ///Gets the default user agent. /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebSettings#getDefaultUserAgent(android.content.Context) diff --git a/lib/src/main.dart b/lib/src/main.dart index d692b27d..d3a77fd3 100644 --- a/lib/src/main.dart +++ b/lib/src/main.dart @@ -15,3 +15,4 @@ export 'content_blocker.dart'; export 'http_auth_credentials_database.dart'; export 'context_menu.dart'; export 'pull_to_refresh/main.dart'; +export 'web_message/main.dart'; diff --git a/lib/src/types.dart b/lib/src/types.dart index ff9651c5..2cd130a0 100755 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -6147,8 +6147,10 @@ class IOSURLResponse { ///The name of the text encoding provided by the response’s originating source. String? textEncodingName; + ///All HTTP header fields of the response. Map? headers; + ///The response’s HTTP status code. int? statusCode; IOSURLResponse( diff --git a/lib/src/util.dart b/lib/src/util.dart index c6bfc3c1..9cf8b9dd 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -class ViewIdGenerator { +class IdGenerator { static int _count = 0; /// Math.Random()-based RNG. All platforms, fast, not cryptographically strong. Optional Seed passable. @@ -31,7 +31,7 @@ class ViewIdGenerator { return b; } - static String generateId() { + static String generate() { _count++; return _count.toString() + cryptoRNG().map((e) => e.toString()).join(''); } diff --git a/lib/src/web_message/main.dart b/lib/src/web_message/main.dart new file mode 100644 index 00000000..036f08b1 --- /dev/null +++ b/lib/src/web_message/main.dart @@ -0,0 +1,2 @@ +export 'web_message_channel.dart'; +export 'web_message_listener.dart'; \ No newline at end of file diff --git a/lib/src/web_message/web_message_channel.dart b/lib/src/web_message/web_message_channel.dart new file mode 100644 index 00000000..9938c617 --- /dev/null +++ b/lib/src/web_message/web_message_channel.dart @@ -0,0 +1,115 @@ +import 'package:flutter/services.dart'; + +class WebMessageChannel { + String id; + WebMessagePort port1; + WebMessagePort port2; + + late MethodChannel _channel; + + WebMessageChannel({required this.id, required this.port1, required this.port2}) { + this._channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_web_message_channel_$id'); + this._channel.setMethodCallHandler(handleMethod); + } + + static WebMessageChannel? fromMap(Map? map) { + if (map == null) { + return null; + } + var webMessageChannel = WebMessageChannel( + id: map["id"], + port1: WebMessagePort(index: 0), + port2: WebMessagePort(index: 1) + ); + webMessageChannel.port1._webMessageChannel = webMessageChannel; + webMessageChannel.port2._webMessageChannel = webMessageChannel; + return webMessageChannel; + } + + Future handleMethod(MethodCall call) async { + switch (call.method) { + case "onMessage": + int index = call.arguments["index"]; + var port = index == 0 ? this.port1 : this.port2; + if (port._onMessage != null) { + String? message = call.arguments["message"]; + port._onMessage!(message); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } +} + +class WebMessagePort { + late final int _index; + + Function(String? message)? _onMessage; + late WebMessageChannel _webMessageChannel; + + WebMessagePort({required int index}) { + this._index = index; + } + + Future setWebMessageCallback(Function(String? message)? onMessage) async { + Map args = {}; + args.putIfAbsent('index', () => this._index); + await _webMessageChannel._channel.invokeMethod('setWebMessageCallback', args); + this._onMessage = onMessage; + } + + Future postMessage(WebMessage message) async { + Map args = {}; + args.putIfAbsent('index', () => this._index); + args.putIfAbsent('message', () => message.toMap()); + await _webMessageChannel._channel.invokeMethod('postMessage', args); + } + + Future close() async { + Map args = {}; + args.putIfAbsent('index', () => this._index); + await _webMessageChannel._channel.invokeMethod('close', args); + } + + Map toMap() { + return { + "index": this._index, + "webMessageChannelId": this._webMessageChannel.id + }; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + +class WebMessage { + String? data; + List? ports; + + WebMessage({this.data, this.ports}); + + Map toMap() { + return { + "data": this.data, + "ports": this.ports?.map((e) => e.toMap()).toList(), + }; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} \ No newline at end of file diff --git a/lib/src/web_message/web_message_listener.dart b/lib/src/web_message/web_message_listener.dart new file mode 100644 index 00000000..91b811c3 --- /dev/null +++ b/lib/src/web_message/web_message_listener.dart @@ -0,0 +1,67 @@ +import 'package:flutter/services.dart'; + +class WebMessageListener { + String jsObjectName; + late Set allowedOriginRules; + JavaScriptReplyProxy? _replyProxy; + Function(String? message, Uri? sourceOrigin, bool isMainFrame, JavaScriptReplyProxy replyProxy)? onPostMessage; + + late MethodChannel _channel; + + WebMessageListener({required this.jsObjectName, Set? allowedOriginRules, this.onPostMessage}) { + this.allowedOriginRules = allowedOriginRules != null ? allowedOriginRules : Set.from(["*"]); + assert(!this.allowedOriginRules.contains(""), "allowedOriginRules cannot contain empty strings"); + this._channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_web_message_listener_$jsObjectName'); + this._channel.setMethodCallHandler(handleMethod); + } + + Future handleMethod(MethodCall call) async { + switch (call.method) { + case "onPostMessage": + if (_replyProxy == null) { + _replyProxy = new JavaScriptReplyProxy(this); + } + if (onPostMessage != null) { + String? message = call.arguments["message"]; + Uri? sourceOrigin = call.arguments["sourceOrigin"] != null ? Uri.parse(call.arguments["sourceOrigin"]) : null; + bool isMainFrame = call.arguments["isMainFrame"]; + onPostMessage!(message, sourceOrigin, isMainFrame, _replyProxy!); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + Map toMap() { + return { + "jsObjectName": jsObjectName, + "allowedOriginRules": allowedOriginRules.toList(), + }; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + +class JavaScriptReplyProxy { + late WebMessageListener _webMessageListener; + + JavaScriptReplyProxy(WebMessageListener webMessageListener) { + this._webMessageListener = webMessageListener; + } + + Future postMessage(String message) async { + Map args = {}; + args.putIfAbsent('message', () => message); + await _webMessageListener._channel.invokeMethod('postMessage', args); + } +} diff --git a/nodejs_server_test_auth_basic_and_ssl/index.js b/nodejs_server_test_auth_basic_and_ssl/index.js index 02741082..87ac1ef7 100755 --- a/nodejs_server_test_auth_basic_and_ssl/index.js +++ b/nodejs_server_test_auth_basic_and_ssl/index.js @@ -10,10 +10,10 @@ const appAuthBasic = express() const fs = require('fs') const path = require('path') -var options = { - key: fs.readFileSync('server-key.pem'), - cert: fs.readFileSync('server-crt.pem'), - ca: fs.readFileSync('ca-crt.pem'), +var options = { + key: fs.readFileSync('server-key.pem'), + cert: fs.readFileSync('server-crt.pem'), + ca: fs.readFileSync('ca-crt.pem'), requestCert: true, rejectUnauthorized: false }; diff --git a/pubspec.yaml b/pubspec.yaml index af801015..592b6056 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_inappwebview description: A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. -version: 5.1.0+4 +version: 5.2.0 homepage: https://github.com/pichillilorenzo/flutter_inappwebview environment: