From 562afedf2fefb2f47dbbed514cb80b6b5a993f67 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 21 Apr 2021 10:26:45 -0400 Subject: [PATCH] [url_launcher] Add a workaround for Uri encoding `Uri`'s constructor doesn't handle query parameters correctly for non-http(s) schemes, so the `mailto` example in the README is misleading. This adds a new utility method to do query string construction correctly, and updates the README to show using it and warning about the need to use it in general. If/when `Uri` is fixed to handle generic URI query parameters correctly, this utility method can be deprecated. Fixes https://github.com/flutter/flutter/issues/75552 Fixes https://github.com/flutter/flutter/issues/73717 --- .../url_launcher/url_launcher/CHANGELOG.md | 8 ++- packages/url_launcher/url_launcher/README.md | 38 ++++++++----- .../flutter/generated_plugin_registrant.cc | 3 +- .../url_launcher/lib/url_launcher.dart | 18 +++++++ .../url_launcher/url_launcher/pubspec.yaml | 2 +- .../url_launcher/test/url_launcher_test.dart | 54 +++++++++++++++++++ 6 files changed, 106 insertions(+), 17 deletions(-) diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index f732fa58f8c6..e1dd18f18f6e 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,6 +1,12 @@ +## 6.1.0 + +* Add a utility method for query encoding when constructing + URIs, and update the README accordingly, to work around limitations + in `Uri` behavior. + ## 6.0.3 -* Updat README notes about URL schemes on iOS +* Update README notes about URL schemes on iOS ## 6.0.2 diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index 31fed9a833f1..d8638f5ef2c7 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -10,10 +10,10 @@ To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml fil ## Installation -### iOS +### iOS Add any URL schemes passed to `canLaunch` as `LSApplicationQueriesSchemes` entries in your Info.plist file. -Example: +Example: ``` LSApplicationQueriesSchemes @@ -73,25 +73,37 @@ apps installed, so can't open `tel:` or `mailto:` links. ### Encoding URLs -URLs must be properly encoded, especially when including spaces or other special characters. This can be done using the [`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html): +URLs must be properly encoded, especially when including spaces or other special +characters. This can be done using the +[`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html). +For example: ```dart -import 'dart:core'; -import 'package:url_launcher/url_launcher.dart'; - -final Uri _emailLaunchUri = Uri( +final Uri emailLaunchUri = Uri( scheme: 'mailto', path: 'smith@example.com', - queryParameters: { + query: encodeQueryParameters({ 'subject': 'Example Subject & Symbols are allowed!' - } + }), ); -// ... +launch(emailLaunchUri.toString()); +``` + +**Warning**: For any scheme other than `http` or `https`, you should use this +package's utility method for query parameters: -// mailto:smith@example.com?subject=Example+Subject+%26+Symbols+are+allowed%21 -launch(_emailLaunchUri.toString()); +```dart +Uri( + // ... + query: encodeQueryParameters(yourParameters), +); ``` +rather than `Uri`'s `queryParameters` constructor argument, due to +[a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri` +encodes query parameters. Using `queryParameters` will result in spaces being +converted to `+` in many cases. + ## Handling missing URL receivers A particular mobile device may not be able to receive all supported URL schemes. @@ -113,4 +125,4 @@ By default, Android opens up a browser when handling URLs. You can pass If you do this for a URL of a page containing JavaScript, make sure to pass in `enableJavaScript: true`, or else the launch method will not work properly. On iOS, the default behavior is to open all web URLs within the app. Everything -else is redirected to the app handler. \ No newline at end of file +else is redirected to the app handler. diff --git a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc index 36185a63f2fd..026851fa2f96 100644 --- a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc +++ b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc @@ -8,7 +8,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, - "UrlLauncherPlugin"); + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/packages/url_launcher/url_launcher/lib/url_launcher.dart b/packages/url_launcher/url_launcher/lib/url_launcher.dart index b59c91d02a1a..d32aa81f36f5 100644 --- a/packages/url_launcher/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/url_launcher/lib/url_launcher.dart @@ -9,6 +9,24 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +/// Encodes [params] as a query parameter string suitable to be passed as the +/// 'query' parameter to a [Uri] constructor. +/// +/// This exists to work around the fact that the 'queryParameters' argument of +/// the [Uri] constructor does encoding as HTML form parameters rather than +/// generic URI query parameters, and thus does not work correctly for schemes +/// other than http(s). See https://github.com/dart-lang/sdk/issues/43838 for +/// details. +String? encodeQueryParameters(Map params) { + if (params.isEmpty) { + return null; + } + return params.entries + .map((MapEntry e) => + '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') + .join('&'); +} + /// Parses the specified URL string and delegates handling of it to the /// underlying platform. /// diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index c8c5163d0e97..ed146cf08031 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher description: Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher -version: 6.0.3 +version: 6.1.0 flutter: plugin: diff --git a/packages/url_launcher/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/url_launcher/test/url_launcher_test.dart index 9b2d167483cd..b0289e4a21cd 100644 --- a/packages/url_launcher/url_launcher/test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/test/url_launcher_test.dart @@ -282,4 +282,58 @@ void main() { expect(binding.renderView.automaticSystemUiAdjustment, true); }); }); + + group('encodeQueryParameters', () { + test('handles empty dictionary', () async { + final String? result = encodeQueryParameters({}); + + expect(result, isNull); + }); + + test('handles parameters without special characters', () async { + final Map parameters = { + 'key1': 'value1', + 'key2': 'value2', + }; + final String? result = encodeQueryParameters(parameters); + + expect(Uri.splitQueryString(result!), parameters); + }); + + test('handles & correctly', () async { + final Map parameters = { + 'key1': 'this & that', + 'key&': 'foo & bar', + }; + final String? result = encodeQueryParameters(parameters); + + expect(Uri.splitQueryString(result!), parameters); + // There should be exactly one unencoded & in the string, joining the + // two parameters. + expect(result.split('&').length, 2); + }); + + test('handles spaces correctly', () async { + final Map parameters = { + 'a key': 'a value', + }; + final String? result = encodeQueryParameters(parameters); + + expect(Uri.splitQueryString(result!), parameters); + // Spaces should be encoded as %20, not +. + expect(result.contains('+'), isFalse); + expect(result.contains('%20'), isTrue); + }); + + test('handles + correctly', () async { + final Map parameters = { + 'key+': 'value+', + }; + final String? result = encodeQueryParameters(parameters); + + expect(Uri.splitQueryString(result!), parameters); + // + should be encoded. + expect(result.contains('+'), isFalse); + }); + }); }