123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602 |
- // Copyright 2018 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- import 'dart:async';
-
- import 'package:flutter/foundation.dart';
- import 'package:flutter/gestures.dart';
- import 'package:flutter/widgets.dart';
-
- import 'platform_interface.dart';
- import 'src/webview_android.dart';
- import 'src/webview_cupertino.dart';
-
- typedef void WebViewCreatedCallback(WebViewController controller);
-
- enum JavascriptMode {
- /// JavaScript execution is disabled.
- disabled,
-
- /// JavaScript execution is not restricted.
- unrestricted,
- }
-
- /// A message that was sent by JavaScript code running in a [WebView].
- class JavascriptMessage {
- /// Constructs a JavaScript message object.
- ///
- /// The `message` parameter must not be null.
- const JavascriptMessage(this.message) : assert(message != null);
-
- /// The contents of the message that was sent by the JavaScript code.
- final String message;
- }
-
- /// Callback type for handling messages sent from Javascript running in a web view.
- typedef void JavascriptMessageHandler(JavascriptMessage message);
-
- /// Information about a navigation action that is about to be executed.
- class NavigationRequest {
- NavigationRequest._({this.url, this.isForMainFrame});
-
- /// The URL that will be loaded if the navigation is executed.
- final String url;
-
- /// Whether the navigation request is to be loaded as the main frame.
- final bool isForMainFrame;
-
- @override
- String toString() {
- return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)';
- }
- }
-
- /// A decision on how to handle a navigation request.
- enum NavigationDecision {
- /// Prevent the navigation from taking place.
- prevent,
-
- /// Allow the navigation to take place.
- navigate,
- }
-
- /// Decides how to handle a specific navigation request.
- ///
- /// The returned [NavigationDecision] determines how the navigation described by
- /// `navigation` should be handled.
- ///
- /// See also: [WebView.navigationDelegate].
- typedef NavigationDecision NavigationDelegate(NavigationRequest navigation);
-
- /// Signature for when a [WebView] has finished loading a page.
- typedef void PageFinishedCallback(String url);
-
- final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9]*\$');
-
- /// A named channel for receiving messaged from JavaScript code running inside a web view.
- class JavascriptChannel {
- /// Constructs a Javascript channel.
- ///
- /// The parameters `name` and `onMessageReceived` must not be null.
- JavascriptChannel({
- @required this.name,
- @required this.onMessageReceived,
- }) : assert(name != null),
- assert(onMessageReceived != null),
- assert(_validChannelNames.hasMatch(name));
-
- /// The channel's name.
- ///
- /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to
- /// the Javascript window object's property named `name`.
- ///
- /// The name must start with a letter or underscore(_), followed by any combination of those
- /// characters plus digits.
- ///
- /// Note that any JavaScript existing `window` property with this name will be overriden.
- ///
- /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism.
- final String name;
-
- /// A callback that's invoked when a message is received through the channel.
- final JavascriptMessageHandler onMessageReceived;
- }
-
- /// A web view widget for showing html content.
- class WebView extends StatefulWidget {
- /// Creates a new web view.
- ///
- /// The web view can be controlled using a `WebViewController` that is passed to the
- /// `onWebViewCreated` callback once the web view is created.
- ///
- /// The `javascriptMode` parameter must not be null.
- const WebView({
- Key key,
- this.onWebViewCreated,
- this.initialUrl,
- this.javascriptMode = JavascriptMode.disabled,
- this.javascriptChannels,
- this.navigationDelegate,
- this.gestureRecognizers,
- this.onPageFinished,
- this.debuggingEnabled = false,
- }) : assert(javascriptMode != null),
- super(key: key);
-
- static WebViewPlatform _platform;
-
- /// Sets a custom [WebViewPlatform].
- ///
- /// This property can be set to use a custom platform implementation for WebViews.
- ///
- /// Setting `platform` doesn't affect [WebView]s that were already created.
- ///
- /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS.
- static set platform(WebViewPlatform platform) {
- _platform = platform;
- }
-
- /// The WebView platform that's used by this WebView.
- ///
- /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS.
- static WebViewPlatform get platform {
- if (_platform == null) {
- switch (defaultTargetPlatform) {
- case TargetPlatform.android:
- _platform = AndroidWebView();
- break;
- case TargetPlatform.iOS:
- _platform = CupertinoWebView();
- break;
- default:
- throw UnsupportedError(
- "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one");
- }
- }
- return _platform;
- }
-
- /// If not null invoked once the web view is created.
- final WebViewCreatedCallback onWebViewCreated;
-
- /// Which gestures should be consumed by the web view.
- ///
- /// It is possible for other gesture recognizers to be competing with the web view on pointer
- /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle
- /// vertical drags. The web view will claim gestures that are recognized by any of the
- /// recognizers on this list.
- ///
- /// When this set is empty or null, the web view will only handle pointer events for gestures that
- /// were not claimed by any other gesture recognizer.
- final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
-
- /// The initial URL to load.
- final String initialUrl;
-
- /// Whether Javascript execution is enabled.
- final JavascriptMode javascriptMode;
-
- /// The set of [JavascriptChannel]s available to JavaScript code running in the web view.
- ///
- /// For each [JavascriptChannel] in the set, a channel object is made available for the
- /// JavaScript code in a window property named [JavascriptChannel.name].
- /// The JavaScript code can then call `postMessage` on that object to send a message that will be
- /// passed to [JavascriptChannel.onMessageReceived].
- ///
- /// For example for the following JavascriptChannel:
- ///
- /// ```dart
- /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); });
- /// ```
- ///
- /// JavaScript code can call:
- ///
- /// ```javascript
- /// Print.postMessage('Hello');
- /// ```
- ///
- /// To asynchronously invoke the message handler which will print the message to standard output.
- ///
- /// Adding a new JavaScript channel only takes affect after the next page is loaded.
- ///
- /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple
- /// channels in the list.
- ///
- /// A null value is equivalent to an empty set.
- final Set<JavascriptChannel> javascriptChannels;
-
- /// A delegate function that decides how to handle navigation actions.
- ///
- /// When a navigation is initiated by the WebView (e.g when a user clicks a link)
- /// this delegate is called and has to decide how to proceed with the navigation.
- ///
- /// See [NavigationDecision] for possible decisions the delegate can take.
- ///
- /// When null all navigation actions are allowed.
- ///
- /// Caveats on Android:
- ///
- /// * Navigation actions targeted to the main frame can be intercepted,
- /// navigation actions targeted to subframes are allowed regardless of the value
- /// returned by this delegate.
- /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were
- /// triggered by a user gesture, this disables some of Chromium's security mechanisms.
- /// A navigationDelegate should only be set when loading trusted content.
- /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have
- /// a later version):
- /// * When a navigationDelegate is set pages with frames are not properly handled by the
- /// webview, and frames will be opened in the main frame.
- /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header.
- final NavigationDelegate navigationDelegate;
-
- /// Invoked when a page has finished loading.
- ///
- /// This is invoked only for the main frame.
- ///
- /// When [onPageFinished] is invoked on Android, the page being rendered may
- /// not be updated yet.
- ///
- /// When invoked on iOS or Android, any Javascript code that is embedded
- /// directly in the HTML has been loaded and code injected with
- /// [WebViewController.evaluateJavascript] can assume this.
- final PageFinishedCallback onPageFinished;
-
- /// Controls whether WebView debugging is enabled.
- ///
- /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/).
- ///
- /// WebView debugging is enabled by default in dev builds on iOS.
- ///
- /// To debug WebViews on iOS:
- /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.)
- /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> <your webview page>
- ///
- /// By default `debuggingEnabled` is false.
- final bool debuggingEnabled;
-
- @override
- State<StatefulWidget> createState() => _WebViewState();
- }
-
- class _WebViewState extends State<WebView> {
- final Completer<WebViewController> _controller =
- Completer<WebViewController>();
-
- _PlatformCallbacksHandler _platformCallbacksHandler;
-
- @override
- Widget build(BuildContext context) {
- return WebView.platform.build(
- context: context,
- onWebViewPlatformCreated: _onWebViewPlatformCreated,
- webViewPlatformCallbacksHandler: _platformCallbacksHandler,
- gestureRecognizers: widget.gestureRecognizers,
- creationParams: _creationParamsfromWidget(widget),
- );
- }
-
- @override
- void initState() {
- super.initState();
- _assertJavascriptChannelNamesAreUnique();
- _platformCallbacksHandler = _PlatformCallbacksHandler(widget);
- }
-
- @override
- void didUpdateWidget(WebView oldWidget) {
- super.didUpdateWidget(oldWidget);
- _assertJavascriptChannelNamesAreUnique();
- _controller.future.then((WebViewController controller) {
- _platformCallbacksHandler._widget = widget;
- controller._updateWidget(widget);
- });
- }
-
- void _onWebViewPlatformCreated(WebViewPlatformController webViewPlatform) {
- final WebViewController controller =
- WebViewController._(widget, webViewPlatform, _platformCallbacksHandler);
- _controller.complete(controller);
- if (widget.onWebViewCreated != null) {
- widget.onWebViewCreated(controller);
- }
- }
-
- void _assertJavascriptChannelNamesAreUnique() {
- if (widget.javascriptChannels == null ||
- widget.javascriptChannels.isEmpty) {
- return;
- }
- assert(_extractChannelNames(widget.javascriptChannels).length ==
- widget.javascriptChannels.length);
- }
- }
-
- CreationParams _creationParamsfromWidget(WebView widget) {
- return CreationParams(
- initialUrl: widget.initialUrl,
- webSettings: _webSettingsFromWidget(widget),
- javascriptChannelNames: _extractChannelNames(widget.javascriptChannels),
- );
- }
-
- WebSettings _webSettingsFromWidget(WebView widget) {
- return WebSettings(
- javascriptMode: widget.javascriptMode,
- hasNavigationDelegate: widget.navigationDelegate != null,
- debuggingEnabled: widget.debuggingEnabled,
- );
- }
-
- // This method assumes that no fields in `currentValue` are null.
- WebSettings _clearUnchangedWebSettings(
- WebSettings currentValue, WebSettings newValue) {
- assert(currentValue.javascriptMode != null);
- assert(currentValue.hasNavigationDelegate != null);
- assert(currentValue.debuggingEnabled != null);
- assert(newValue.javascriptMode != null);
- assert(newValue.hasNavigationDelegate != null);
- assert(newValue.debuggingEnabled != null);
- JavascriptMode javascriptMode;
- bool hasNavigationDelegate;
- bool debuggingEnabled;
- if (currentValue.javascriptMode != newValue.javascriptMode) {
- javascriptMode = newValue.javascriptMode;
- }
- if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) {
- hasNavigationDelegate = newValue.hasNavigationDelegate;
- }
- if (currentValue.debuggingEnabled != newValue.debuggingEnabled) {
- debuggingEnabled = newValue.debuggingEnabled;
- }
-
- return WebSettings(
- javascriptMode: javascriptMode,
- hasNavigationDelegate: hasNavigationDelegate,
- debuggingEnabled: debuggingEnabled);
- }
-
- Set<String> _extractChannelNames(Set<JavascriptChannel> channels) {
- final Set<String> channelNames = channels == null
- // TODO(iskakaushik): Remove this when collection literals makes it to stable.
- // ignore: prefer_collection_literals
- ? Set<String>()
- : channels.map((JavascriptChannel channel) => channel.name).toSet();
- return channelNames;
- }
-
- class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler {
- _PlatformCallbacksHandler(this._widget) {
- _updateJavascriptChannelsFromSet(_widget.javascriptChannels);
- }
-
- WebView _widget;
-
- // Maps a channel name to a channel.
- final Map<String, JavascriptChannel> _javascriptChannels =
- <String, JavascriptChannel>{};
-
- @override
- void onJavaScriptChannelMessage(String channel, String message) {
- _javascriptChannels[channel].onMessageReceived(JavascriptMessage(message));
- }
-
- @override
- bool onNavigationRequest({String url, bool isForMainFrame}) {
- final NavigationRequest request =
- NavigationRequest._(url: url, isForMainFrame: isForMainFrame);
- final bool allowNavigation = _widget.navigationDelegate == null ||
- _widget.navigationDelegate(request) == NavigationDecision.navigate;
- return allowNavigation;
- }
-
- @override
- void onPageFinished(String url) {
- if (_widget.onPageFinished != null) {
- _widget.onPageFinished(url);
- }
- }
-
- void _updateJavascriptChannelsFromSet(Set<JavascriptChannel> channels) {
- _javascriptChannels.clear();
- if (channels == null) {
- return;
- }
- for (JavascriptChannel channel in channels) {
- _javascriptChannels[channel.name] = channel;
- }
- }
- }
-
- /// Controls a [WebView].
- ///
- /// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated]
- /// callback for a [WebView] widget.
- class WebViewController {
- WebViewController._(
- this._widget,
- this._webViewPlatformController,
- this._platformCallbacksHandler,
- ) : assert(_webViewPlatformController != null) {
- _settings = _webSettingsFromWidget(_widget);
- }
-
- final WebViewPlatformController _webViewPlatformController;
-
- final _PlatformCallbacksHandler _platformCallbacksHandler;
-
- WebSettings _settings;
-
- WebView _widget;
-
- /// Loads the specified URL.
- ///
- /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will
- /// be added as key value pairs of HTTP headers for the request.
- ///
- /// `url` must not be null.
- ///
- /// Throws an ArgumentError if `url` is not a valid URL string.
- Future<void> loadUrl(
- String url, {
- Map<String, String> headers,
- }) async {
- assert(url != null);
- _validateUrlString(url);
- return _webViewPlatformController.loadUrl(url, headers);
- }
-
- /// Accessor to the current URL that the WebView is displaying.
- ///
- /// If [WebView.initialUrl] was never specified, returns `null`.
- /// Note that this operation is asynchronous, and it is possible that the
- /// current URL changes again by the time this function returns (in other
- /// words, by the time this future completes, the WebView may be displaying a
- /// different URL).
- Future<String> currentUrl() {
- return _webViewPlatformController.currentUrl();
- }
-
- /// Checks whether there's a back history item.
- ///
- /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has
- /// changed by the time the future completed.
- Future<bool> canGoBack() {
- return _webViewPlatformController.canGoBack();
- }
-
- /// Checks whether there's a forward history item.
- ///
- /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has
- /// changed by the time the future completed.
- Future<bool> canGoForward() {
- return _webViewPlatformController.canGoForward();
- }
-
- /// Goes back in the history of this WebView.
- ///
- /// If there is no back history item this is a no-op.
- Future<void> goBack() {
- return _webViewPlatformController.goBack();
- }
-
- /// Goes forward in the history of this WebView.
- ///
- /// If there is no forward history item this is a no-op.
- Future<void> goForward() {
- return _webViewPlatformController.goForward();
- }
-
- /// Reloads the current URL.
- Future<void> reload() {
- return _webViewPlatformController.reload();
- }
-
- /// Clears all caches used by the [WebView].
- ///
- /// The following caches are cleared:
- /// 1. Browser HTTP Cache.
- /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches.
- /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache.
- /// 3. Application cache.
- /// 4. Local Storage.
- ///
- /// Note: Calling this method also triggers a reload.
- Future<void> clearCache() async {
- await _webViewPlatformController.clearCache();
- return reload();
- }
-
- Future<void> _updateWidget(WebView widget) async {
- _widget = widget;
- await _updateSettings(_webSettingsFromWidget(widget));
- await _updateJavascriptChannels(widget.javascriptChannels);
- }
-
- Future<void> _updateSettings(WebSettings newSettings) {
- final WebSettings update =
- _clearUnchangedWebSettings(_settings, newSettings);
- _settings = newSettings;
- return _webViewPlatformController.updateSettings(update);
- }
-
- Future<void> _updateJavascriptChannels(
- Set<JavascriptChannel> newChannels) async {
- final Set<String> currentChannels =
- _platformCallbacksHandler._javascriptChannels.keys.toSet();
- final Set<String> newChannelNames = _extractChannelNames(newChannels);
- final Set<String> channelsToAdd =
- newChannelNames.difference(currentChannels);
- final Set<String> channelsToRemove =
- currentChannels.difference(newChannelNames);
- if (channelsToRemove.isNotEmpty) {
- _webViewPlatformController.removeJavascriptChannels(channelsToRemove);
- }
- if (channelsToAdd.isNotEmpty) {
- _webViewPlatformController.addJavascriptChannels(channelsToAdd);
- }
- _platformCallbacksHandler._updateJavascriptChannelsFromSet(newChannels);
- }
-
- /// Evaluates a JavaScript expression in the context of the current page.
- ///
- /// On Android returns the evaluation result as a JSON formatted string.
- ///
- /// On iOS depending on the value type the return value would be one of:
- ///
- /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100').
- /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.').
- /// - Other non-primitive types are not supported on iOS and will complete the Future with an error.
- ///
- /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the
- /// evaluated expression is not supported as described above.
- ///
- /// When evaluating Javascript in a [WebView], it is best practice to wait for
- /// the [WebView.onPageFinished] callback. This guarantees all the Javascript
- /// embedded in the main frame HTML has been loaded.
- Future<String> evaluateJavascript(String javascriptString) {
- if (_settings.javascriptMode == JavascriptMode.disabled) {
- return Future<String>.error(FlutterError(
- 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.'));
- }
- if (javascriptString == null) {
- return Future<String>.error(
- ArgumentError('The argument javascriptString must not be null.'));
- }
- // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter.
- // https://github.com/flutter/flutter/issues/26431
- // ignore: strong_mode_implicit_dynamic_method
- return _webViewPlatformController.evaluateJavascript(javascriptString);
- }
- }
-
- /// Manages cookies pertaining to all [WebView]s.
- class CookieManager {
- /// Creates a [CookieManager] -- returns the instance if it's already been called.
- factory CookieManager() {
- return _instance ??= CookieManager._();
- }
-
- CookieManager._();
-
- static CookieManager _instance;
-
- /// Clears all cookies for all [WebView] instances.
- ///
- /// This is a no op on iOS version smaller than 9.
- ///
- /// Returns true if cookies were present before clearing, else false.
- Future<bool> clearCookies() => WebView.platform.clearCookies();
- }
-
- // Throws an ArgumentError if `url` is not a valid URL string.
- void _validateUrlString(String url) {
- try {
- final Uri uri = Uri.parse(url);
- if (uri.scheme.isEmpty) {
- throw ArgumentError('Missing scheme in URL string: "$url"');
- }
- } on FormatException catch (e) {
- throw ArgumentError(e);
- }
- }
|