Commit c4740eae authored by Ethan Xu's avatar Ethan Xu Committed by Commit Bot

[WebShareTargetV2] Support launching the new web share target with POST request.

This is the second CL for web share target V2.

The goal of this CL is to implement the new web share target launcher according to the new web share target spec.
https://pr-preview.s3.amazonaws.com/ewilligers/web-share-target/pull/53.html#launching-the-web-share-target

There're three main parts of this CL that are put together:
1) Parse the fields received from the android manifest on the server-side
2) Perform the files-matching, which involves matching the share files with
   with share target files entries that accept the same mime type.
3) Launch the web share target. Specifically, this involves the additional step of
   generating the POST request. There're two possible encoding types for the POST request:
   Multipart/form-data encoding and application/x-www-form-urlencoded, both of which
   are implemented inside a new file named webapk_post_share_target_navigator.cc.
   It performs all the encoding/escaping needed for the two encoding methods.

One additional function is added to mime_util.cc in order to support the multipart/form-data POST request.

Change-Id: I719af0dc33fe64561963ae9aecc9d01192f1c17a
Reviewed-on: https://chromium-review.googlesource.com/c/1271776
Commit-Queue: Ethan Xu <xuethan@google.com>
Reviewed-by: default avatarBence Béky <bnc@chromium.org>
Reviewed-by: default avatarPeter Kotwicz <pkotwicz@chromium.org>
Reviewed-by: default avatarDominick Ng <dominickn@chromium.org>
Reviewed-by: default avatarEric Willigers <ericwilligers@chromium.org>
Cr-Commit-Position: refs/heads/master@{#611877}
parent 43363a69
......@@ -732,9 +732,9 @@ public class ShortcutHelper {
if (WebApkValidator.isValidWebApk(context, packageInfo.packageName)) {
// Pass non-null URL parameter so that {@link WebApkInfo#create()}
// return value is non-null
WebApkInfo webApkInfo =
WebApkInfo.create(packageInfo.packageName, "", ShortcutSource.UNKNOWN,
false /* forceNavigation */, false /* useTransparentSplash */);
WebApkInfo webApkInfo = WebApkInfo.create(packageInfo.packageName, "",
ShortcutSource.UNKNOWN, false /* forceNavigation */,
false /* useTransparentSplash */, null /* shareData */);
if (webApkInfo != null) {
names.add(webApkInfo.name());
shortNames.add(webApkInfo.shortName());
......
......@@ -165,4 +165,11 @@ public class WebApkActivity extends WebappActivity {
mWebApkSplashscreenMetrics = new WebApkSplashscreenMetrics();
addSplashscreenObserver(mWebApkSplashscreenMetrics);
}
@Override
protected boolean loadUrlIfPostShareTarget(WebappInfo webappInfo) {
WebApkInfo webApkInfo = (WebApkInfo) webappInfo;
return WebApkPostShareTargetNavigator.navigateIfPostShareTarget(
webApkInfo, getActivityTab().getWebContents());
}
}
......@@ -13,6 +13,7 @@ import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.IntDef;
......@@ -33,6 +34,7 @@ import org.chromium.webapk.lib.common.WebApkMetaDataUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
......@@ -41,6 +43,14 @@ import java.util.Map;
* Stores info for WebAPK.
*/
public class WebApkInfo extends WebappInfo {
// A class that stores share information from share intent.
protected static class ShareData {
public String subject;
public String text;
public ArrayList<Uri> files;
public String shareActivityClassName;
}
public static final String RESOURCE_NAME = "name";
public static final String RESOURCE_SHORT_NAME = "short_name";
public static final String RESOURCE_STRING_TYPE = "string";
......@@ -67,6 +77,8 @@ public class WebApkInfo extends WebappInfo {
private Map<String, String> mIconUrlToMurmur2HashMap;
private boolean mUseTransparentSplash;
private ShareData mShareData;
public static WebApkInfo createEmpty() {
return new WebApkInfo();
}
......@@ -79,6 +91,7 @@ public class WebApkInfo extends WebappInfo {
public static WebApkInfo create(Intent intent) {
String webApkPackageName =
IntentUtils.safeGetStringExtra(intent, WebApkConstants.EXTRA_WEBAPK_PACKAGE_NAME);
if (TextUtils.isEmpty(webApkPackageName)) {
return null;
}
......@@ -99,11 +112,32 @@ public class WebApkInfo extends WebappInfo {
boolean forceNavigation = IntentUtils.safeGetBooleanExtra(
intent, ShortcutHelper.EXTRA_FORCE_NAVIGATION, true);
ShareData shareData = null;
String shareActivityClassName = IntentUtils.safeGetStringExtra(
intent, WebApkConstants.EXTRA_WEBAPK_SELECTED_SHARE_TARGET_ACTIVITY_CLASS_NAME);
// Share Target when shareActivityClassName is present.
if (!TextUtils.isEmpty(shareActivityClassName)) {
shareData = new ShareData();
shareData.shareActivityClassName = shareActivityClassName;
shareData.subject = IntentUtils.safeGetStringExtra(intent, Intent.EXTRA_SUBJECT);
shareData.text = IntentUtils.safeGetStringExtra(intent, Intent.EXTRA_TEXT);
shareData.files = IntentUtils.getParcelableArrayListExtra(intent, Intent.EXTRA_STREAM);
if (shareData.files == null) {
Uri file = IntentUtils.safeGetParcelableExtra(intent, Intent.EXTRA_STREAM);
if (file != null) {
shareData.files = new ArrayList<>();
shareData.files.add(file);
}
}
}
boolean useTransparentSplash = !IntentUtils.isIntentForNewTaskOrNewDocument(intent)
&& IntentUtils.safeGetBooleanExtra(
intent, WebApkConstants.EXTRA_USE_TRANSPARENT_SPLASH, false);
return create(webApkPackageName, url, source, forceNavigation, useTransparentSplash);
return create(
webApkPackageName, url, source, forceNavigation, useTransparentSplash, shareData);
}
private static @WebApkDistributor int getDistributor(Bundle bundle, String packageName) {
......@@ -133,9 +167,10 @@ public class WebApkInfo extends WebappInfo {
* running.
* @param useTransparentSplash Whether the WebApkActivity should be fully transparent while the
* page is loading.
* @param shareData Shared information from the share intent.
*/
public static WebApkInfo create(String webApkPackageName, String url, int source,
boolean forceNavigation, boolean useTransparentSplash) {
boolean forceNavigation, boolean useTransparentSplash, ShareData shareData) {
// Unlike non-WebAPK web apps, WebAPK ids are predictable. A malicious actor may send an
// intent with a valid start URL and arbitrary other data. Only use the start URL, the
// package name and the ShortcutSource from the launch intent and extract the remaining data
......@@ -202,7 +237,7 @@ public class WebApkInfo extends WebappInfo {
displayMode, orientation, source, themeColor, backgroundColor, webApkPackageName,
shellApkVersion, manifestUrl, manifestStartUrl, distributor,
iconUrlToMurmur2HashMap, serializedShareTarget, forceNavigation,
useTransparentSplash);
useTransparentSplash, shareData);
}
/**
......@@ -236,6 +271,7 @@ public class WebApkInfo extends WebappInfo {
* WebAPK is already open.
* @param useTransparentSplash Whether the WebApkActivity should be fully transparent while
* the page is loading.
* @param shareData Shared information from the share intent.
*/
public static WebApkInfo create(String id, String url, String scope, Icon primaryIcon,
Icon badgeIcon, Icon splashIcon, String name, String shortName,
......@@ -243,7 +279,7 @@ public class WebApkInfo extends WebappInfo {
long backgroundColor, String webApkPackageName, int shellApkVersion, String manifestUrl,
String manifestStartUrl, @WebApkDistributor int distributor,
Map<String, String> iconUrlToMurmur2HashMap, String serializedShareTarget,
boolean forceNavigation, boolean useTransparentSplash) {
boolean forceNavigation, boolean useTransparentSplash, ShareData shareData) {
if (id == null || url == null || manifestStartUrl == null || webApkPackageName == null) {
Log.e(TAG,
"Incomplete data provided: " + id + ", " + url + ", " + manifestStartUrl + ", "
......@@ -262,7 +298,7 @@ public class WebApkInfo extends WebappInfo {
displayMode, orientation, source, themeColor, backgroundColor, webApkPackageName,
shellApkVersion, manifestUrl, manifestStartUrl, distributor,
iconUrlToMurmur2HashMap, serializedShareTarget, forceNavigation,
useTransparentSplash);
useTransparentSplash, shareData);
}
protected WebApkInfo(String id, String url, String scope, Icon primaryIcon, Icon badgeIcon,
......@@ -271,7 +307,7 @@ public class WebApkInfo extends WebappInfo {
String webApkPackageName, int shellApkVersion, String manifestUrl,
String manifestStartUrl, @WebApkDistributor int distributor,
Map<String, String> iconUrlToMurmur2HashMap, String serializedShareTarget,
boolean forceNavigation, boolean useTransparentSplash) {
boolean forceNavigation, boolean useTransparentSplash, ShareData shareData) {
super(id, url, scope, primaryIcon, name, shortName, displayMode, orientation, source,
themeColor, backgroundColor, null /* splash_screen_url */,
false /* isIconGenerated */, forceNavigation);
......@@ -285,6 +321,7 @@ public class WebApkInfo extends WebappInfo {
mIconUrlToMurmur2HashMap = iconUrlToMurmur2HashMap;
mSerializedShareTarget = serializedShareTarget;
mUseTransparentSplash = useTransparentSplash;
mShareData = shareData;
}
protected WebApkInfo() {}
......@@ -345,6 +382,10 @@ public class WebApkInfo extends WebappInfo {
return mIconUrlToMurmur2HashMap;
}
public ShareData shareData() {
return mShareData;
}
@Override
public void setWebappIntentExtras(Intent intent) {
// For launching a {@link WebApkActivity}.
......@@ -489,19 +530,26 @@ public class WebApkInfo extends WebappInfo {
Bundle metaData = activityInfo.metaData;
return getSerializedShareTarget(metaData.getString(WebApkMetaDataKeys.SHARE_ACTION),
metaData.getString(WebApkMetaDataKeys.SHARE_METHOD),
metaData.getString(WebApkMetaDataKeys.SHARE_ENCTYPE),
metaData.getString(WebApkMetaDataKeys.SHARE_PARAM_TITLE),
metaData.getString(WebApkMetaDataKeys.SHARE_PARAM_TEXT),
metaData.getString(WebApkMetaDataKeys.SHARE_PARAM_URL));
metaData.getString(WebApkMetaDataKeys.SHARE_PARAM_URL),
metaData.getString(WebApkMetaDataKeys.SHARE_PARAM_NAMES),
metaData.getString(WebApkMetaDataKeys.SHARE_PARAM_ACCEPTS));
}
/**
* Returns the serialized Share Target String.
*/
static String getSerializedShareTarget(String shareAction, String shareParamsTitle,
String shareParamsText, String shareParamsUrl) {
static String getSerializedShareTarget(String shareAction, String shareMethod,
String shareEnctype, String shareParamsTitle, String shareParamsText,
String shareParamsUrl, String shareParamsNames, String shareParamsAccepts) {
if (shareAction == null) return null;
return String.format("action: \"%s\", title: \"%s\", text: \"%s\", url: \"%s\"",
shareAction, shareParamsTitle, shareParamsText, shareParamsUrl);
return String.format("action: \"%s\", method: \"%s\", enctype: \"%s\", title: \"%s\""
+ "text: \"%s\", url: \"%s\", names: \"%s\", accepts: \"%s\"",
shareAction, shareMethod, shareEnctype, shareParamsTitle, shareParamsText,
shareParamsUrl, shareParamsNames, shareParamsAccepts);
}
}
// 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.
package org.chromium.chrome.browser.webapps;
import org.chromium.content_public.browser.WebContents;
/**
* Perform navigation for share target with POST request.
*/
public class WebApkPostShareTargetNavigator {
public static boolean navigateIfPostShareTarget(
WebApkInfo webApkInfo, WebContents webContents) {
WebApkShareTargetUtil.PostData postData = WebApkShareTargetUtil.computePostData(
webApkInfo.webApkPackageName(), webApkInfo.shareData());
if (postData == null) {
return false;
}
nativeLoadViewForShareTargetPost(postData.isMultipartEncoding, postData.names,
postData.values, postData.filenames, postData.types, webApkInfo.uri().toString(),
webContents);
return true;
}
private static native void nativeLoadViewForShareTargetPost(boolean isMultipartEncoding,
String[] names, byte[][] values, String[] filenames, String[] types, String startUrl,
WebContents webContents);
}
\ No newline at end of file
// 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.
package org.chromium.chrome.browser.webapps;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import org.json.JSONArray;
import org.json.JSONException;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.net.MimeTypeFilter;
import org.chromium.webapk.lib.common.WebApkMetaDataKeys;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Computes data for Post Share Target.
*/
public class WebApkShareTargetUtil {
// A class containing data required to generate a share target post request.
protected static class PostData {
public boolean isMultipartEncoding;
public String[] names;
public byte[][] values;
public String[] filenames;
public String[] types;
}
private static Bundle computeShareTargetMetaData(
String apkPackageName, WebApkInfo.ShareData shareData) {
if (shareData == null) {
return null;
}
ActivityInfo shareActivityInfo;
try {
shareActivityInfo =
ContextUtils.getApplicationContext().getPackageManager().getActivityInfo(
new ComponentName(apkPackageName, shareData.shareActivityClassName),
PackageManager.GET_META_DATA);
} catch (PackageManager.NameNotFoundException e) {
return null;
}
if (shareActivityInfo == null) {
return null;
}
return shareActivityInfo.metaData;
}
private static boolean methodFromMetaDataIsPost(Bundle metaData) {
String method = IntentUtils.safeGetString(metaData, WebApkMetaDataKeys.SHARE_METHOD);
return method != null && "POST".equals(method.toUpperCase(Locale.ENGLISH));
}
private static boolean enctypeFromMetaDataIsMultipart(Bundle metaData) {
String enctype = IntentUtils.safeGetString(metaData, WebApkMetaDataKeys.SHARE_ENCTYPE);
return enctype != null && "multipart/form-data".equals(enctype.toLowerCase(Locale.ENGLISH));
}
private static byte[] readStringFromContentUri(Uri uri) {
try (InputStream inputStream =
ContextUtils.getApplicationContext().getContentResolver().openInputStream(
uri)) {
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
byteBuffer.write(buffer, 0, len);
}
return byteBuffer.toByteArray();
} catch (IOException e) {
return null;
}
}
private static String getFileTypeFromContentUri(Uri uri) {
return ContextUtils.getApplicationContext().getContentResolver().getType(uri);
}
private static String getFileNameFromContentUri(Uri uri) {
if (uri.getScheme().equals("content")) {
try (Cursor cursor = ContextUtils.getApplicationContext().getContentResolver().query(
uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
String result =
cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
if (result != null) {
return result;
}
}
}
}
return uri.getPath();
}
private static ArrayList<String> decodeJsonStringArray(JSONArray jsonArray)
throws JSONException {
ArrayList<String> originalData = new ArrayList<>();
for (int i = 0; i < jsonArray.length(); i++) {
originalData.add(jsonArray.getString(i));
}
return originalData;
}
private static ArrayList<ArrayList<String>> decodeJsonAccepts(String string)
throws JSONException {
JSONArray jsonArray = new JSONArray(string);
ArrayList<ArrayList<String>> originalData = new ArrayList<>();
for (int i = 0; i < jsonArray.length(); i++) {
originalData.add(decodeJsonStringArray(jsonArray.getJSONArray(i)));
}
return originalData;
}
protected static PostData computeMultipartPostData(
Bundle shareTargetMetaData, WebApkInfo.ShareData shareData) {
String nameString = IntentUtils.safeGetString(
shareTargetMetaData, WebApkMetaDataKeys.SHARE_PARAM_NAMES);
String acceptString = IntentUtils.safeGetString(
shareTargetMetaData, WebApkMetaDataKeys.SHARE_PARAM_ACCEPTS);
ArrayList<Uri> fileUris = shareData.files;
if (nameString == null || acceptString == null) {
return null;
}
if (fileUris == null) {
return null;
}
ArrayList<String> names;
ArrayList<ArrayList<String>> accepts;
try {
names = decodeJsonStringArray(new JSONArray(nameString));
} catch (JSONException e) {
return null;
}
try {
accepts = decodeJsonAccepts(acceptString);
} catch (JSONException e) {
return null;
}
if (names.size() != accepts.size()) {
return null;
}
PostData postData = new PostData();
postData.isMultipartEncoding = true;
ArrayList<String> shareNames = new ArrayList<>();
ArrayList<byte[]> shareValues = new ArrayList<>();
ArrayList<String> shareFilenames = new ArrayList<>();
ArrayList<String> shareTypes = new ArrayList<>();
for (Uri fileUri : fileUris) {
String fileType = getFileTypeFromContentUri(fileUri);
String fileName = getFileNameFromContentUri(fileUri);
if (fileType == null || fileName == null) {
continue;
}
for (int i = 0; i < names.size(); i++) {
List<String> mimeTypeList = accepts.get(i);
MimeTypeFilter mimeTypeFilter = new MimeTypeFilter(mimeTypeList, false);
if (mimeTypeFilter.accept(fileUri, fileType)) {
byte[] fileContent = readStringFromContentUri(fileUri);
if (fileContent != null) {
shareNames.add(names.get(i));
shareValues.add(fileContent);
shareFilenames.add(fileName);
shareTypes.add(fileType);
}
break;
}
}
}
postData.names = shareNames.toArray(new String[0]);
postData.values = shareValues.toArray(new byte[0][]);
postData.filenames = shareFilenames.toArray(new String[0]);
postData.types = shareTypes.toArray(new String[0]);
return postData;
}
protected static PostData computeUrlEncodedPostData(
Bundle shareTargetMetaData, WebApkInfo.ShareData shareData) {
PostData postData = new PostData();
postData.isMultipartEncoding = false;
postData.filenames = new String[0];
postData.types = new String[0];
ArrayList<String> names = new ArrayList<>();
ArrayList<byte[]> values = new ArrayList<>();
String shareTitleName = IntentUtils.safeGetString(
shareTargetMetaData, WebApkMetaDataKeys.SHARE_PARAM_TITLE);
String shareTextName =
IntentUtils.safeGetString(shareTargetMetaData, WebApkMetaDataKeys.SHARE_PARAM_TEXT);
if (shareTitleName != null && shareData.subject != null) {
names.add(shareTitleName);
values.add(ApiCompatibilityUtils.getBytesUtf8(shareData.subject));
}
if (shareTextName != null && shareData.text != null) {
names.add(shareTextName);
values.add(ApiCompatibilityUtils.getBytesUtf8(shareData.text));
}
postData.names = names.toArray(new String[0]);
postData.values = values.toArray(new byte[0][]);
return postData;
}
protected static PostData computePostData(
String apkPackageName, WebApkInfo.ShareData shareData) {
Bundle shareTargetMetaData = computeShareTargetMetaData(apkPackageName, shareData);
if (shareTargetMetaData == null || !methodFromMetaDataIsPost(shareTargetMetaData)) {
return null;
}
if (enctypeFromMetaDataIsMultipart(shareTargetMetaData)) {
return computeMultipartPostData(shareTargetMetaData, shareData);
}
return computeUrlEncodedPostData(shareTargetMetaData, shareData);
}
}
......@@ -113,7 +113,7 @@ public class WebApkUpdateDataFetcher extends EmptyTabObserver {
}
String serializedShareTarget = WebApkInfo.getSerializedShareTarget(
shareAction, shareParamsTitle, shareParamsText, shareParamsUrl);
shareAction, "", "", shareParamsTitle, shareParamsText, shareParamsUrl, "", "");
WebApkInfo info = WebApkInfo.create(mOldInfo.id(), mOldInfo.uri().toString(), scopeUrl,
new WebApkInfo.Icon(primaryIconBitmap), new WebApkInfo.Icon(badgeIconBitmap), null,
......@@ -121,7 +121,7 @@ public class WebApkUpdateDataFetcher extends EmptyTabObserver {
backgroundColor, mOldInfo.webApkPackageName(), mOldInfo.shellApkVersion(),
mOldInfo.manifestUrl(), manifestStartUrl, WebApkInfo.WebApkDistributor.BROWSER,
iconUrlToMurmur2HashMap, serializedShareTarget, mOldInfo.shouldForceNavigation(),
mOldInfo.useTransparentSplash());
mOldInfo.useTransparentSplash(), null);
mObserver.onGotManifestData(info, primaryIconUrl, badgeIconUrl);
}
......
......@@ -135,12 +135,6 @@ public class WebappActivity extends SingleTabActivity {
mNotificationManager = new WebappActionsNotificationManager(this);
}
private static LoadUrlParams createLoadUrlParams(WebappInfo info, Intent intent) {
LoadUrlParams params =
new LoadUrlParams(info.uri().toString(), PageTransition.AUTO_TOPLEVEL);
return params;
}
@Override
protected void onNewIntent(Intent intent) {
if (intent == null) return;
......@@ -156,12 +150,25 @@ public class WebappActivity extends SingleTabActivity {
Log.e(TAG, "Failed to parse new Intent: " + intent);
ApiCompatibilityUtils.finishAndRemoveTask(this);
} else if (newWebappInfo.shouldForceNavigation() && mIsInitialized) {
LoadUrlParams params = createLoadUrlParams(newWebappInfo, intent);
params.setShouldClearHistoryList(true);
getActivityTab().loadUrl(params);
loadUrl(newWebappInfo, getActivityTab());
}
}
protected boolean loadUrlIfPostShareTarget(WebappInfo webappInfo) {
return false;
}
protected void loadUrl(WebappInfo webappInfo, Tab tab) {
if (loadUrlIfPostShareTarget(webappInfo)) {
// Web Share Target Post was successful, so don't load anything.
return;
}
LoadUrlParams params =
new LoadUrlParams(webappInfo.uri().toString(), PageTransition.AUTO_TOPLEVEL);
params.setShouldClearHistoryList(true);
tab.loadUrl(params);
}
protected boolean isInitialized() {
return mIsInitialized;
}
......@@ -223,8 +230,7 @@ public class WebappActivity extends SingleTabActivity {
// we saved instance state before loading a URL, so even after restoring from
// SavedInstanceState we might not have a URL and should initialize from the intent.
if (tab.getUrl().isEmpty()) {
LoadUrlParams params = createLoadUrlParams(mWebappInfo, getIntent());
tab.loadUrl(params);
loadUrl(mWebappInfo, tab);
} else {
if (getActivityType() != ActivityType.WEBAPK && NetworkChangeNotifier.isOnline()) {
tab.reloadIgnoringCache();
......
......@@ -1671,8 +1671,10 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/webapps/WebApkInstaller.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkInstallService.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkManagedActivity.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkPostShareTargetNavigator.java",
"java/src/org/chromium/chrome/browser/webapps/WebappOfflineDialog.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkServiceClient.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkShareTargetUtil.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkUpdateDataFetcher.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkUpdateManager.java",
"java/src/org/chromium/chrome/browser/webapps/WebApkUpdateTask.java",
......@@ -2476,6 +2478,7 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/webapps/WebappRegistryTest.java",
"junit/src/org/chromium/chrome/browser/webapps/WebappInfoTest.java",
"junit/src/org/chromium/chrome/browser/webapps/WebApkInfoTest.java",
"junit/src/org/chromium/chrome/browser/webapps/WebApkShareTargetUtilTest.java",
"junit/src/org/chromium/chrome/browser/webapps/WebApkUpdateManagerUnitTest.java",
"junit/src/org/chromium/chrome/browser/widget/bottomsheet/BottomSheetSwipeDetectorTest.java",
"junit/src/org/chromium/chrome/browser/widget/selection/SelectionDelegateTest.java",
......
......@@ -106,7 +106,7 @@ public class WebApkUpdateDataFetcherTest {
WebApkInfo oldInfo = WebApkInfo.create("", "", scopeUrl, null, null, null, null,
null, -1, -1, -1, -1, -1, "random.package", -1, manifestUrl, "",
WebApkInfo.WebApkDistributor.BROWSER, new HashMap<String, String>(), null,
false /* forceNavigation */, false /* useTransparentSplash */);
false /* forceNavigation */, false /* useTransparentSplash */, null /* shareData */);
fetcher.start(mTab, oldInfo, observer);
}
});
......
......@@ -160,7 +160,7 @@ public class WebApkUpdateManagerTest {
WebApkVersion.REQUEST_UPDATE_FOR_SHELL_APK_VERSION,
creationData.manifestUrl, creationData.startUrl,
WebApkInfo.WebApkDistributor.BROWSER, creationData.iconUrlToMurmur2HashMap,
null, false /* forceNavigation */, false /* useTransparentSplash */);
null, false /* forceNavigation */, false /* useTransparentSplash */, null /* shareData */);
updateManager.updateIfNeeded(mTab, info);
}
});
......
......@@ -135,6 +135,6 @@ public class WebappVisibilityTest {
: WebApkInfo.create("", "", webappStartUrlOrScopeUrl, null, null, null, null, null,
displayMode, 0, 0, 0, 0, "", 0, null, "",
WebApkInfo.WebApkDistributor.BROWSER, null, null,
false /* forceNavigation */, false /* useTransparentSplash */);
false /* forceNavigation */, false /* useTransparentSplash */, null /* shareData */);
}
}
......@@ -114,6 +114,7 @@ public class WebApkInfoTest {
bundle.putString(WebApkMetaDataKeys.START_URL, START_URL);
bundle.putString(WebApkMetaDataKeys.ICON_URLS_AND_ICON_MURMUR2_HASHES,
ICON_URL + " " + ICON_MURMUR2_HASH);
bundle.putString(WebApkMetaDataKeys.SHARE_METHOD, "GET");
WebApkTestHelper.registerWebApkWithMetaData(WEBAPK_PACKAGE_NAME, bundle);
Intent intent = new Intent();
......@@ -417,4 +418,14 @@ public class WebApkInfoTest {
info = WebApkInfo.create(intent);
Assert.assertEquals(WebApkInfo.WebApkDistributor.OTHER, info.distributor());
}
// Test whether getSerializedShareTarget can handle special characters
@Test
public void testGetSerializedShareTarget() {
String serializedShareTarget =
WebApkInfo.getSerializedShareTarget("\n", "\\", "", "", "", "", "", "");
Assert.assertEquals("action: \"\n\", method: \"\\\", enctype: \"\", title: \"\""
+ "text: \"\", url: \"\", names: \"\", accepts: \"\"",
serializedShareTarget);
}
}
......@@ -258,7 +258,7 @@ public class WebApkUpdateManagerUnitTest {
manifestData.themeColor, manifestData.backgroundColor, kPackageName, -1,
WEB_MANIFEST_URL, manifestData.startUrl, WebApkInfo.WebApkDistributor.BROWSER,
manifestData.iconUrlToMurmur2HashMap, null, false /* forceNavigation */,
false /* useTransparentSplash */);
false /* useTransparentSplash */, null);
}
/**
......
......@@ -13,6 +13,8 @@ import android.util.Log;
import org.chromium.webapk.lib.common.WebApkConstants;
import java.util.ArrayList;
/** Contains methods for launching host browser. */
public class HostBrowserLauncher {
private static final String TAG = "cr_HostBrowserLauncher";
......@@ -28,6 +30,22 @@ public class HostBrowserLauncher {
private static final String REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB =
"REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB";
private static void grantUriPermissionToHostBrowser(
Context context, Intent launchIntent, String hostBrowserPackageName) {
ArrayList<Uri> uris = launchIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
if (uris == null) {
uris = new ArrayList<>();
Uri uri = launchIntent.getParcelableExtra(Intent.EXTRA_STREAM);
if (uri != null) {
uris.add(uri);
}
}
for (Uri uri : uris) {
context.grantUriPermission(
hostBrowserPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
/**
* Launches host browser in WebAPK mode if the browser is WebAPK-compatible.
* Otherwise, launches the host browser in tabbed mode.
......@@ -42,6 +60,10 @@ public class HostBrowserLauncher {
}
Intent launchIntent = createLaunchInWebApkModeIntent(context, params);
if (params.getSelectedShareTargetActivityClassName() != null) {
grantUriPermissionToHostBrowser(
context, launchIntent, params.getHostBrowserPackageName());
}
try {
context.startActivity(launchIntent);
} catch (ActivityNotFoundException e) {
......
......@@ -2405,6 +2405,8 @@ jumbo_split_static_library("browser") {
"android/webapk/webapk_installer.h",
"android/webapk/webapk_metrics.cc",
"android/webapk/webapk_metrics.h",
"android/webapk/webapk_post_share_target_navigator.cc",
"android/webapk/webapk_post_share_target_navigator.h",
"android/webapk/webapk_types.h",
"android/webapk/webapk_update_data_fetcher.cc",
"android/webapk/webapk_update_data_fetcher.h",
......@@ -4874,6 +4876,7 @@ if (is_android) {
"../android/java/src/org/chromium/chrome/browser/webapps/AddToHomescreenManager.java",
"../android/java/src/org/chromium/chrome/browser/webapps/WebApkInstallService.java",
"../android/java/src/org/chromium/chrome/browser/webapps/WebApkInstaller.java",
"../android/java/src/org/chromium/chrome/browser/webapps/WebApkPostShareTargetNavigator.java",
"../android/java/src/org/chromium/chrome/browser/webapps/WebApkUpdateDataFetcher.java",
"../android/java/src/org/chromium/chrome/browser/webapps/WebApkUpdateManager.java",
"../android/java/src/org/chromium/chrome/browser/webapps/WebappRegistry.java",
......
// 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.
#include "chrome/browser/android/webapk/webapk_post_share_target_navigator.h"
#include <sstream>
#include <jni.h>
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/strings/utf_string_conversion_utils.h"
#include "content/public/browser/web_contents.h"
#include "jni/WebApkPostShareTargetNavigator_jni.h"
#include "net/base/escape.h"
#include "net/base/mime_util.h"
#include "services/network/public/cpp/resource_request_body.h"
#include "ui/base/window_open_disposition.h"
#include "url/gurl.h"
using base::android::JavaParamRef;
namespace webapk {
std::string PercentEscapeString(const std::string& unescaped_string) {
std::ostringstream escaped_oss;
for (size_t i = 0; i < unescaped_string.length(); ++i) {
if (unescaped_string[i] == '"') {
escaped_oss << "%22";
} else if (unescaped_string[i] == 0x0a) {
escaped_oss << "%0A";
} else if (unescaped_string[i] == 0x0d) {
escaped_oss << "%0D";
} else {
escaped_oss << unescaped_string[i];
}
}
return escaped_oss.str();
}
std::string ComputeMultipartBody(const std::vector<std::string>& names,
const std::vector<std::string>& values,
const std::vector<std::string>& filenames,
const std::vector<std::string>& types,
const std::string& boundary) {
size_t num_files = names.size();
if (num_files != values.size() || num_files != filenames.size() ||
num_files != types.size()) {
// The length of |names|, |values|, |filenames|, and |types| should always
// be the same for multipart POST. This should never happen.
return "";
}
std::string body;
for (size_t i = 0; i < num_files; i++)
net::AddMultipartValueForUploadWithFileName(
PercentEscapeString(names[i]), PercentEscapeString(filenames[i]),
values[i], boundary, types[i], &body);
net::AddMultipartFinalDelimiterForUpload(boundary, &body);
return body;
}
std::string ComputeUrlEncodedBody(const std::vector<std::string>& names,
const std::vector<std::string>& values) {
if (names.size() != values.size() || names.size() == 0)
return "";
std::ostringstream application_body_oss;
application_body_oss << net::EscapeUrlEncodedData(names[0], true) << "="
<< net::EscapeUrlEncodedData(values[0], true);
for (size_t i = 1; i < names.size(); i++)
application_body_oss << "&" << net::EscapeUrlEncodedData(names[i], true)
<< "=" << net::EscapeUrlEncodedData(values[i], true);
return application_body_oss.str();
}
void NavigateShareTargetPost(const std::string& body,
const std::string& header_list,
const GURL& share_target_gurl,
content::WebContents* web_contents) {
content::OpenURLParams open_url_params(
share_target_gurl, content::Referrer(),
WindowOpenDisposition::CURRENT_TAB,
ui::PageTransition::PAGE_TRANSITION_AUTO_TOPLEVEL,
false /* is_renderer_initiated */);
open_url_params.post_data = network::ResourceRequestBody::CreateFromBytes(
body.c_str(), body.length());
open_url_params.uses_post = true;
open_url_params.extra_headers = header_list;
web_contents->OpenURL(open_url_params);
}
} // namespace webapk
void JNI_WebApkPostShareTargetNavigator_LoadViewForShareTargetPost(
JNIEnv* env,
const JavaParamRef<jclass>& clazz,
const jboolean java_is_multipart_encoding,
const JavaParamRef<jobjectArray>& java_names,
const JavaParamRef<jobjectArray>& java_values,
const JavaParamRef<jobjectArray>& java_filenames,
const JavaParamRef<jobjectArray>& java_types,
const JavaParamRef<jstring>& java_url,
const JavaParamRef<jobject>& java_web_contents) {
std::vector<std::string> names;
std::vector<std::string> values;
std::vector<std::string> filenames;
std::vector<std::string> types;
bool is_multipart_encoding = static_cast<bool>(java_is_multipart_encoding);
base::android::AppendJavaStringArrayToStringVector(env, java_names, &names);
base::android::JavaArrayOfByteArrayToStringVector(env, java_values, &values);
base::android::AppendJavaStringArrayToStringVector(env, java_filenames,
&filenames);
base::android::AppendJavaStringArrayToStringVector(env, java_types, &types);
GURL share_target_gurl(base::android::ConvertJavaStringToUTF8(java_url));
std::string body;
std::string header_list;
if (is_multipart_encoding) {
std::string boundary = net::GenerateMimeMultipartBoundary();
body =
webapk::ComputeMultipartBody(names, values, filenames, types, boundary);
header_list = base::StringPrintf(
"Content-Type: multipart/form-data; boundary=%s\r\n", boundary.c_str());
} else {
body = webapk::ComputeUrlEncodedBody(names, values);
header_list = "Content-Type: application/x-www-form-urlencoded\r\n";
}
content::WebContents* web_contents =
content::WebContents::FromJavaWebContents(java_web_contents);
webapk::NavigateShareTargetPost(body, header_list, share_target_gurl,
web_contents);
}
// 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.
#ifndef CHROME_BROWSER_ANDROID_WEBAPK_WEBAPK_POST_SHARE_TARGET_NAVIGATOR_H_
#define CHROME_BROWSER_ANDROID_WEBAPK_WEBAPK_POST_SHARE_TARGET_NAVIGATOR_H_
#include <string>
namespace content {
class WebContents;
}
class GURL;
namespace webapk {
// Return a string as a quoted value, escaping quotes and line breaks.
std::string PercentEscapeString(const std::string& unescaped_string);
// Compute and return multipart/form-data POST body for share target.
std::string ComputeMultipartBody(const std::vector<std::string>& names,
const std::vector<std::string>& values,
const std::vector<std::string>& filenames,
const std::vector<std::string>& types,
const std::string& boundary);
// Compute and return application/x-www-form-urlencoded POST body for share
// target.
std::string ComputeUrlEncodedBody(const std::vector<std::string>& names,
const std::vector<std::string>& values);
// Navigate to share target gurl with post |body| and |header_list|.
void NavigateShareTargetPost(const std::string& body,
const std::string& header_list,
const GURL& share_target_gurl,
content::WebContents* web_contents);
} // namespace webapk
#endif // CHROME_BROWSER_ANDROID_WEBAPK_WEBAPK_POST_SHARE_TARGET_NAVIGATOR_H_
// 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.
#include "chrome/browser/android/webapk/webapk_post_share_target_navigator.h"
#include <string>
#include "testing/gtest/include/gtest/gtest.h"
// Test that multipart/form-data body is empty if inputs are of different sizes.
TEST(WebApkActivityTest, InvalidMultipartBody) {
std::vector<std::string> names = {"name"};
std::vector<std::string> values;
std::vector<std::string> filenames;
std::vector<std::string> types;
std::string boundary = "boundary";
std::string multipart_body =
webapk::ComputeMultipartBody(names, values, filenames, types, boundary);
EXPECT_EQ("", multipart_body);
}
// Test that multipart/form-data body is correctly computed for accepted inputs.
TEST(WebApkActivityTest, ValidMultipartBody) {
std::vector<std::string> names = {"name\""};
std::vector<std::string> values = {"value"};
std::vector<std::string> filenames = {"filename\r\n"};
std::vector<std::string> types = {"type"};
std::string boundary = "boundary";
std::string multipart_body =
webapk::ComputeMultipartBody(names, values, filenames, types, boundary);
std::string expected_multipart_body =
"--boundary\r\nContent-Disposition: form-data;"
" name=\"name%22\"; filename=\"filename%0D%0A\"\r\nContent-Type: type"
"\r\n\r\nvalue\r\n"
"--boundary--\r\n";
EXPECT_EQ(expected_multipart_body, multipart_body);
}
// Test that multipart/form-data body is properly percent-escaped.
TEST(WebApkActivityTest, MultipartBodyWithPercentEncoding) {
std::vector<std::string> names = {"name"};
std::vector<std::string> values = {"value"};
std::vector<std::string> filenames = {"filename"};
std::vector<std::string> types = {"type"};
std::string boundary = "boundary";
std::string multipart_body =
webapk::ComputeMultipartBody(names, values, filenames, types, boundary);
std::string expected_multipart_body =
"--boundary\r\nContent-Disposition: form-data;"
" name=\"name\"; filename=\"filename\"\r\nContent-Type: type"
"\r\n\r\nvalue\r\n"
"--boundary--\r\n";
EXPECT_EQ(expected_multipart_body, multipart_body);
}
// Test that application/x-www-form-urlencoded body is empty if inputs are of
// different sizes.
TEST(WebApkActivityTest, InvalidApplicationBody) {
std::vector<std::string> names = {"name1", "name2"};
std::vector<std::string> values = {"value1"};
std::string application_body = webapk::ComputeUrlEncodedBody(names, values);
EXPECT_EQ("", application_body);
}
// Test that application/x-www-form-urlencoded body is correctly computed for
// accepted inputs.
TEST(WebApkActivityTest, ValidApplicationBody) {
std::vector<std::string> names = {"name1", "name2"};
std::vector<std::string> values = {"value1", "value2"};
std::string application_body = webapk::ComputeUrlEncodedBody(names, values);
EXPECT_EQ("name1=value1&name2=value2", application_body);
}
// Test that PercentEscapeString correctly escapes quotes to %22.
TEST(WebApkActivityTest, NeedsPercentEscapeQuote) {
EXPECT_EQ("hello%22", webapk::PercentEscapeString("hello\""));
}
// Test that PercentEscapeString correctly escapes newline to %0A.
TEST(WebApkActivityTest, NeedsPercentEscape0A) {
EXPECT_EQ("%0A", webapk::PercentEscapeString("\n"));
}
// Test that PercentEscapeString correctly escapes \r to %0D.
TEST(WebApkActivityTest, NeedsPercentEscape0D) {
EXPECT_EQ("%0D", webapk::PercentEscapeString("\r"));
}
// Test that Percent Escape is not performed on strings that don't need to be
// escaped.
TEST(WebApkActivityTest, NoPercentEscape) {
EXPECT_EQ("helloworld", webapk::PercentEscapeString("helloworld"));
}
......@@ -2372,6 +2372,7 @@ test("unit_tests") {
"../browser/android/thumbnail/scoped_ptr_expiring_cache_unittest.cc",
"../browser/android/webapk/webapk_icon_hasher_unittest.cc",
"../browser/android/webapk/webapk_installer_unittest.cc",
"../browser/android/webapk/webapk_post_share_target_navigator_unittest.cc",
"../browser/android/webapk/webapk_web_manifest_checker_unittest.cc",
"../browser/android/webapps/add_to_homescreen_data_fetcher_unittest.cc",
"../browser/app_controller_mac_unittest.mm",
......
......@@ -762,6 +762,26 @@ void AddMultipartValueForUpload(const std::string& value_name,
post_data->append("\r\n" + value + "\r\n");
}
void AddMultipartValueForUploadWithFileName(const std::string& value_name,
const std::string& file_name,
const std::string& value,
const std::string& mime_boundary,
const std::string& content_type,
std::string* post_data) {
DCHECK(post_data);
// First line is the boundary.
post_data->append("--" + mime_boundary + "\r\n");
// Next line is the Content-disposition.
post_data->append("Content-Disposition: form-data; name=\"" + value_name +
"\"; filename=\"" + file_name + "\"\r\n");
if (!content_type.empty()) {
// If Content-type is specified, the next line is that.
post_data->append("Content-Type: " + content_type + "\r\n");
}
// Leave an empty line and append the value.
post_data->append("\r\n" + value + "\r\n");
}
void AddMultipartFinalDelimiterForUpload(const std::string& mime_boundary,
std::string* post_data) {
DCHECK(post_data);
......
......@@ -104,6 +104,16 @@ NET_EXPORT void AddMultipartValueForUpload(const std::string& value_name,
const std::string& content_type,
std::string* post_data);
// Prepares one value as part of a multi-part upload request, with file name as
// an additional parameter.
NET_EXPORT void AddMultipartValueForUploadWithFileName(
const std::string& value_name,
const std::string& file_name,
const std::string& value,
const std::string& mime_boundary,
const std::string& content_type,
std::string* post_data);
// Adds the final delimiter to a multi-part upload request.
NET_EXPORT void AddMultipartFinalDelimiterForUpload(
const std::string& mime_boundary,
......
......@@ -375,4 +375,22 @@ TEST(MimeUtilTest, TestAddMultipartValueForUpload) {
EXPECT_STREQ(ref_output, post_data.c_str());
}
TEST(MimeUtilTest, TestAddMultipartValueForUploadWithFileName) {
const char ref_output[] =
"--boundary\r\nContent-Disposition: form-data;"
" name=\"value name\"; filename=\"file name\"\r\nContent-Type: content "
"type"
"\r\n\r\nvalue\r\n"
"--boundary\r\nContent-Disposition: form-data;"
" name=\"value name\"; filename=\"file name\"\r\n\r\nvalue\r\n"
"--boundary--\r\n";
std::string post_data;
AddMultipartValueForUploadWithFileName("value name", "file name", "value",
"boundary", "content type",
&post_data);
AddMultipartValueForUploadWithFileName("value name", "file name", "value",
"boundary", "", &post_data);
AddMultipartFinalDelimiterForUpload("boundary", &post_data);
EXPECT_STREQ(ref_output, post_data.c_str());
}
} // namespace net
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment