summaryrefslogtreecommitdiff
path: root/chromium/components/browser_ui/webshare
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/components/browser_ui/webshare')
-rw-r--r--chromium/components/browser_ui/webshare/OWNERS5
-rw-r--r--chromium/components/browser_ui/webshare/android/BUILD.gn42
-rw-r--r--chromium/components/browser_ui/webshare/android/DEPS5
-rw-r--r--chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java177
-rw-r--r--chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java290
-rw-r--r--chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImplTest.java84
-rw-r--r--chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollator.java74
-rw-r--r--chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollatorTest.java108
8 files changed, 785 insertions, 0 deletions
diff --git a/chromium/components/browser_ui/webshare/OWNERS b/chromium/components/browser_ui/webshare/OWNERS
new file mode 100644
index 00000000000..c880f89d823
--- /dev/null
+++ b/chromium/components/browser_ui/webshare/OWNERS
@@ -0,0 +1,5 @@
+ericwilligers@chromium.org
+raymes@chromium.org
+
+# COMPONENT: Blink>WebShare
+# OS: Android
diff --git a/chromium/components/browser_ui/webshare/android/BUILD.gn b/chromium/components/browser_ui/webshare/android/BUILD.gn
new file mode 100644
index 00000000000..05149697937
--- /dev/null
+++ b/chromium/components/browser_ui/webshare/android/BUILD.gn
@@ -0,0 +1,42 @@
+# Copyright 2020 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("//build/config/android/rules.gni")
+
+android_library("java") {
+ sources = [
+ "java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java",
+ "java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java",
+ "java/src/org/chromium/components/browser_ui/webshare/SharedFileCollator.java",
+ ]
+ deps = [
+ "//base:base_java",
+ "//components/browser_ui/share/android:java",
+ "//content/public/android:content_java",
+ "//mojo/public/java:system_java",
+ "//mojo/public/java/system:system_impl_java",
+ "//third_party/blink/public/mojom:android_mojo_bindings_java",
+ "//ui/android:ui_java",
+ "//url/mojom:url_mojom_gurl_java",
+ ]
+}
+
+java_library("junit") {
+ # Skip platform checks since Robolectric depends on requires_android targets.
+ bypass_platform_checks = true
+ testonly = true
+ sources = [
+ "java/src/org/chromium/components/browser_ui/webshare/ShareServiceImplTest.java",
+ "java/src/org/chromium/components/browser_ui/webshare/SharedFileCollatorTest.java",
+ ]
+ deps = [
+ ":java",
+ "//base:base_java",
+ "//base:base_java_test_support",
+ "//base:base_junit_test_support",
+ "//base/test:test_support_java",
+ "//third_party/blink/public/mojom:android_mojo_bindings_java",
+ "//third_party/junit",
+ ]
+}
diff --git a/chromium/components/browser_ui/webshare/android/DEPS b/chromium/components/browser_ui/webshare/android/DEPS
new file mode 100644
index 00000000000..bb311370365
--- /dev/null
+++ b/chromium/components/browser_ui/webshare/android/DEPS
@@ -0,0 +1,5 @@
+include_rules = [
+ "+mojo/public/java/system",
+ "+content/public/android/java",
+ "+ui/android/java",
+]
diff --git a/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java
new file mode 100644
index 00000000000..e1008c3c68f
--- /dev/null
+++ b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java
@@ -0,0 +1,177 @@
+// Copyright 2019 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.components.browser_ui.webshare;
+
+import org.chromium.base.Callback;
+import org.chromium.base.Log;
+import org.chromium.base.StreamUtil;
+import org.chromium.blink.mojom.Blob;
+import org.chromium.blink.mojom.BlobReaderClient;
+import org.chromium.mojo.system.Core;
+import org.chromium.mojo.system.DataPipe;
+import org.chromium.mojo.system.MojoException;
+import org.chromium.mojo.system.MojoResult;
+import org.chromium.mojo.system.Pair;
+import org.chromium.mojo.system.ResultAnd;
+import org.chromium.mojo.system.Watcher;
+import org.chromium.mojo.system.impl.CoreImpl;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Receives a blob over mojom and writes it to the stream.
+ */
+public class BlobReceiver implements BlobReaderClient {
+ private static final String TAG = "share";
+ private static final int CHUNK_SIZE = 64 * 1024;
+ private static final int PIPE_CAPACITY = 2 * CHUNK_SIZE;
+
+ private final ByteBuffer mBuffer;
+ private final OutputStream mOutputStream;
+ private long mMaximumContentSize;
+ private long mExpectedContentSize;
+ private long mReceivedContentSize;
+ private DataPipe.ConsumerHandle mConsumerHandle;
+ private Callback<Integer> mCallback;
+
+ /**
+ * Constructs a BlobReceiver.
+ *
+ * @param outputStream the destination for the blob contents.
+ * @param maximumContentSize the maximum permitted length of the blob.
+ */
+ public BlobReceiver(OutputStream outputStream, long maximumContentSize) {
+ mBuffer = ByteBuffer.allocateDirect(CHUNK_SIZE);
+ mOutputStream = outputStream;
+ mMaximumContentSize = maximumContentSize;
+ }
+
+ /**
+ * Initiates reading of the blob contents.
+ *
+ * @param blob the blob to read.
+ * @param callback the callback to call when reading is complete.
+ */
+ public void start(Blob blob, Callback<Integer> callback) {
+ mCallback = callback;
+ DataPipe.CreateOptions options = new DataPipe.CreateOptions();
+ options.setElementNumBytes(1);
+ options.setCapacityNumBytes(PIPE_CAPACITY);
+
+ Pair<DataPipe.ProducerHandle, DataPipe.ConsumerHandle> pipe =
+ CoreImpl.getInstance().createDataPipe(options);
+ mConsumerHandle = pipe.second;
+ blob.readAll(pipe.first, this);
+ }
+
+ // Interface
+ @Override
+ public void close() {}
+
+ // ConnectionErrorHandler
+ @Override
+ public void onConnectionError(MojoException e) {
+ if (mCallback == null) return;
+ reportError(e.getMojoResult(), "Connection error detected.");
+ }
+
+ // BlobReaderClient
+ @Override
+ public void onCalculatedSize(long totalSize, long expectedContentSize) {
+ if (mCallback == null) return;
+ if (expectedContentSize > mMaximumContentSize) {
+ reportError(MojoResult.RESOURCE_EXHAUSTED, "Stream exceeds permitted size");
+ return;
+ }
+ mExpectedContentSize = expectedContentSize;
+ if (mReceivedContentSize >= mExpectedContentSize) {
+ complete();
+ return;
+ }
+
+ Watcher watcher = CoreImpl.getInstance().getWatcher();
+ watcher.start(mConsumerHandle, Core.HandleSignals.READABLE, new Watcher.Callback() {
+ @Override
+ public void onResult(int result) {
+ if (mCallback == null) return;
+ if (result == MojoResult.OK) {
+ read();
+ } else {
+ reportError(result, "Watcher reported error.");
+ }
+ }
+ });
+ }
+
+ // BlobReaderClient
+ @Override
+ public void onComplete(int status, long dataLength) {
+ if (mCallback == null) return;
+ read();
+ }
+
+ private void read() {
+ try {
+ while (true) {
+ ResultAnd<Integer> result =
+ mConsumerHandle.readData(mBuffer, DataPipe.ReadFlags.NONE);
+
+ if (result.getMojoResult() == MojoResult.SHOULD_WAIT) return;
+
+ if (result.getMojoResult() != MojoResult.OK) {
+ reportError(result.getMojoResult(), "Failed to read from blob.");
+ return;
+ }
+
+ Integer bytesRead = result.getValue();
+ if (bytesRead <= 0) {
+ reportError(MojoResult.SHOULD_WAIT, "No data available");
+ return;
+ }
+ try {
+ mOutputStream.write(mBuffer.array(), mBuffer.arrayOffset(), bytesRead);
+ } catch (IOException e) {
+ reportError(MojoResult.DATA_LOSS, "Failed to write to stream.");
+ return;
+ }
+ mReceivedContentSize += bytesRead;
+ if (mReceivedContentSize >= mExpectedContentSize) {
+ if (mReceivedContentSize == mExpectedContentSize) {
+ complete();
+ } else {
+ reportError(
+ MojoResult.OUT_OF_RANGE, "Received more bytes than expected size.");
+ }
+ return;
+ }
+ }
+ } catch (MojoException e) {
+ reportError(e.getMojoResult(), "Failed to receive blob.");
+ }
+ }
+
+ private void complete() {
+ try {
+ mOutputStream.close();
+ } catch (IOException e) {
+ reportError(MojoResult.CANCELLED, "Failed to close stream.");
+ return;
+ }
+ mCallback.onResult(MojoResult.OK);
+ mCallback = null;
+ }
+
+ private void reportError(int result, String message) {
+ if (result == MojoResult.OK) {
+ result = MojoResult.INVALID_ARGUMENT;
+ }
+ Log.w(TAG, message);
+ StreamUtil.closeQuietly(mOutputStream);
+ mCallback.onResult(result);
+ mCallback = null;
+ }
+}
diff --git a/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java
new file mode 100644
index 00000000000..dcdc8ddc798
--- /dev/null
+++ b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java
@@ -0,0 +1,290 @@
+// Copyright 2016 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.components.browser_ui.webshare;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.net.Uri;
+
+import androidx.annotation.Nullable;
+
+import org.chromium.base.CollectionUtil;
+import org.chromium.base.ContentUriUtils;
+import org.chromium.base.FileUtils;
+import org.chromium.base.Log;
+import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.base.task.AsyncTask;
+import org.chromium.base.task.PostTask;
+import org.chromium.base.task.TaskRunner;
+import org.chromium.base.task.TaskTraits;
+import org.chromium.components.browser_ui.share.ShareImageFileUtils;
+import org.chromium.components.browser_ui.share.ShareParams;
+import org.chromium.content_public.browser.WebContents;
+import org.chromium.mojo.system.MojoException;
+import org.chromium.ui.base.WindowAndroid;
+import org.chromium.url.mojom.Url;
+import org.chromium.webshare.mojom.ShareError;
+import org.chromium.webshare.mojom.ShareService;
+import org.chromium.webshare.mojom.SharedFile;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Android implementation of the ShareService service defined in
+ * third_party/blink/public/mojom/webshare/webshare.mojom.
+ */
+public class ShareServiceImpl implements ShareService {
+ private final WindowAndroid mWindow;
+ private final WebShareDelegate mDelegate;
+
+ private static final String TAG = "share";
+
+ // These numbers are written to histograms. Keep in sync with WebShareMethod enum in
+ // histograms.xml, and don't reuse or renumber entries (except for the _COUNT entry).
+ private static final int WEBSHARE_METHOD_SHARE = 0;
+ // Count is technically 1, but recordEnumeratedHistogram requires a boundary of at least 2
+ // (https://crbug.com/645032).
+ private static final int WEBSHARE_METHOD_COUNT = 2;
+
+ // These numbers are written to histograms. Keep in sync with WebShareOutcome enum in
+ // histograms.xml, and don't reuse or renumber entries (except for the _COUNT entry).
+ private static final int WEBSHARE_OUTCOME_SUCCESS = 0;
+ private static final int WEBSHARE_OUTCOME_UNKNOWN_FAILURE = 1;
+ private static final int WEBSHARE_OUTCOME_CANCELED = 2;
+ private static final int WEBSHARE_OUTCOME_COUNT = 3;
+
+ // These protect us if the renderer is compromised.
+ private static final int MAX_SHARED_FILE_COUNT = 10;
+ private static final int MAX_SHARED_FILE_BYTES = 50 * 1024 * 1024;
+
+ // clang-format off
+ private static final Set<String> PERMITTED_EXTENSIONS =
+ Collections.unmodifiableSet(CollectionUtil.newHashSet(
+ "bmp", // image/bmp / image/x-ms-bmp
+ "css", // text/css
+ "csv", // text/csv / text/comma-separated-values
+ "ehtml", // text/html
+ "flac", // audio/flac
+ "gif", // image/gif
+ "htm", // text/html
+ "html", // text/html
+ "ico", // image/x-icon
+ "jfif", // image/jpeg
+ "jpeg", // image/jpeg
+ "jpg", // image/jpeg
+ "m4a", // audio/x-m4a
+ "m4v", // video/mp4
+ "mp3", // audio/mp3
+ "mp4", // video/mp4
+ "mpeg", // video/mpeg
+ "mpg", // video/mpeg
+ "oga", // audio/ogg
+ "ogg", // audio/ogg
+ "ogm", // video/ogg
+ "ogv", // video/ogg
+ "opus", // audio/ogg
+ "pjp", // image/jpeg
+ "pjpeg", // image/jpeg
+ "png", // image/png
+ "shtm", // text/html
+ "shtml", // text/html
+ "svg", // image/svg+xml
+ "svgz", // image/svg+xml
+ "text", // text/plain
+ "tif", // image/tiff
+ "tiff", // image/tiff
+ "txt", // text/plain
+ "wav", // audio/wav
+ "weba", // audio/webm
+ "webm", // video/webm
+ "webp", // image/webp
+ "xbm" // image/x-xbitmap
+ ));
+
+ private static final Set<String> PERMITTED_MIME_TYPES =
+ Collections.unmodifiableSet(CollectionUtil.newHashSet(
+ "audio/flac",
+ "audio/mp3",
+ "audio/ogg",
+ "audio/wav",
+ "audio/webm",
+ "audio/x-m4a",
+ "image/bmp",
+ "image/gif",
+ "image/jpeg",
+ "image/png",
+ "image/svg+xml",
+ "image/tiff",
+ "image/webp",
+ "image/x-icon",
+ "image/x-ms-bmp",
+ "image/x-xbitmap",
+ "text/comma-separated-values",
+ "text/css",
+ "text/csv",
+ "text/html",
+ "text/plain",
+ "video/mp4",
+ "video/mpeg",
+ "video/ogg",
+ "video/webm"
+ ));
+ // clang-format on
+
+ private static final TaskRunner TASK_RUNNER =
+ PostTask.createSequencedTaskRunner(TaskTraits.USER_BLOCKING);
+
+ /** Delegate class that provides embedder-specific functionality. */
+ public interface WebShareDelegate {
+ /**
+ * @return true if sharing is currently possible.
+ */
+ public boolean canShare();
+
+ /**
+ * Overridden by the embedder to execute the share.
+ * @param params the share data.
+ */
+ public void share(ShareParams params);
+ }
+
+ public ShareServiceImpl(@Nullable WebContents webContents, WebShareDelegate delegate) {
+ mWindow = webContents.getTopLevelNativeWindow();
+ mDelegate = delegate;
+ }
+
+ @Override
+ public void close() {}
+
+ @Override
+ public void onConnectionError(MojoException e) {}
+
+ @Override
+ public void share(String title, String text, Url url, final SharedFile[] files,
+ final ShareResponse callback) {
+ RecordHistogram.recordEnumeratedHistogram(
+ "WebShare.ApiCount", WEBSHARE_METHOD_SHARE, WEBSHARE_METHOD_COUNT);
+
+ if (!mDelegate.canShare()) {
+ RecordHistogram.recordEnumeratedHistogram("WebShare.ShareOutcome",
+ WEBSHARE_OUTCOME_UNKNOWN_FAILURE, WEBSHARE_OUTCOME_COUNT);
+ callback.call(ShareError.INTERNAL_ERROR);
+ return;
+ }
+
+ ShareParams.TargetChosenCallback innerCallback = new ShareParams.TargetChosenCallback() {
+ @Override
+ public void onTargetChosen(ComponentName chosenComponent) {
+ RecordHistogram.recordEnumeratedHistogram(
+ "WebShare.ShareOutcome", WEBSHARE_OUTCOME_SUCCESS, WEBSHARE_OUTCOME_COUNT);
+ callback.call(ShareError.OK);
+ }
+
+ @Override
+ public void onCancel() {
+ RecordHistogram.recordEnumeratedHistogram(
+ "WebShare.ShareOutcome", WEBSHARE_OUTCOME_CANCELED, WEBSHARE_OUTCOME_COUNT);
+ callback.call(ShareError.CANCELED);
+ }
+ };
+
+ final ShareParams.Builder paramsBuilder = new ShareParams.Builder(mWindow, title, url.url)
+ .setText(text)
+ .setCallback(innerCallback);
+ if (files == null || files.length == 0) {
+ mDelegate.share(paramsBuilder.build());
+ return;
+ }
+
+ if (files.length > MAX_SHARED_FILE_COUNT) {
+ callback.call(ShareError.PERMISSION_DENIED);
+ return;
+ }
+
+ for (SharedFile file : files) {
+ if (isDangerousFilename(file.name) || isDangerousMimeType(file.blob.contentType)) {
+ Log.i(TAG,
+ "Cannot share potentially dangerous \"" + file.blob.contentType
+ + "\" file \"" + file.name + "\".");
+ callback.call(ShareError.PERMISSION_DENIED);
+ return;
+ }
+ }
+
+ new AsyncTask<Boolean>() {
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result.equals(Boolean.FALSE)) {
+ callback.call(ShareError.INTERNAL_ERROR);
+ }
+ }
+
+ @Override
+ protected Boolean doInBackground() {
+ ArrayList<Uri> fileUris = new ArrayList<>(files.length);
+ ArrayList<BlobReceiver> blobReceivers = new ArrayList<>(files.length);
+ try {
+ File sharePath = ShareImageFileUtils.getSharedFilesDirectory();
+
+ if (!sharePath.exists() && !sharePath.mkdir()) {
+ throw new IOException("Failed to create directory for shared file.");
+ }
+
+ for (int index = 0; index < files.length; ++index) {
+ File tempFile = File.createTempFile("share",
+ "." + FileUtils.getExtension(files[index].name), sharePath);
+ fileUris.add(ContentUriUtils.getContentUriFromFile(tempFile));
+ blobReceivers.add(new BlobReceiver(
+ new FileOutputStream(tempFile), MAX_SHARED_FILE_BYTES));
+ }
+
+ } catch (IOException ie) {
+ Log.w(TAG, "Error creating shared file", ie);
+ return false;
+ }
+
+ paramsBuilder.setFileContentType(SharedFileCollator.commonMimeType(files));
+ paramsBuilder.setFileUris(fileUris);
+ SharedFileCollator collator = new SharedFileCollator(files.length, success -> {
+ if (success) {
+ mDelegate.share(paramsBuilder.build());
+ } else {
+ callback.call(ShareError.INTERNAL_ERROR);
+ }
+ });
+
+ for (int index = 0; index < files.length; ++index) {
+ blobReceivers.get(index).start(files[index].blob.blob, collator);
+ }
+ return true;
+ }
+ }.executeOnTaskRunner(TASK_RUNNER);
+ }
+
+ static boolean isDangerousFilename(String name) {
+ // Reject filenames without a permitted extension.
+ return name.indexOf('.') <= 0
+ || !PERMITTED_EXTENSIONS.contains(FileUtils.getExtension(name));
+ }
+
+ static boolean isDangerousMimeType(String contentType) {
+ return !PERMITTED_MIME_TYPES.contains(contentType);
+ }
+
+ @Nullable
+ private static Activity activityFromWebContents(@Nullable WebContents webContents) {
+ if (webContents == null) return null;
+
+ WindowAndroid window = webContents.getTopLevelNativeWindow();
+ if (window == null) return null;
+
+ return window.getActivity().get();
+ }
+}
diff --git a/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImplTest.java b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImplTest.java
new file mode 100644
index 00000000000..4c9d3359870
--- /dev/null
+++ b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImplTest.java
@@ -0,0 +1,84 @@
+// Copyright 2019 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.components.browser_ui.webshare;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
+/**
+ * Unit tests for {@link ShareServiceImpl}.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class ShareServiceImplTest {
+ @Test
+ @SmallTest
+ public void testExtensionFormatting() {
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("foo/bar.txt"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("foo\\bar\u03C0.txt"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename("foo\\bar.tx\u03C0t"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("https://example.com/a/b.html"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename("foo/bar.txt/"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename("foobar.tx\\t"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename("hello"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename("hellotxt"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename(".txt"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("https://example.com/a/.txt"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("/.txt"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename(".."));
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename(".hello.txt"));
+ }
+
+ @Test
+ @SmallTest
+ public void testExecutable() {
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename("application.apk"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename("application.dex"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousFilename("application.sh"));
+ }
+
+ @Test
+ @SmallTest
+ public void testContent() {
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("diagram.svg"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("greeting.txt"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("movie.mpeg"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("photo.jpeg"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("recording.wav"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("statistics.csv"));
+ }
+
+ @Test
+ @SmallTest
+ public void testCompound() {
+ Assert.assertFalse(ShareServiceImpl.isDangerousFilename("powerless.sh.txt"));
+ }
+
+ @Test
+ @SmallTest
+ public void testUnsupportedMime() {
+ Assert.assertTrue(ShareServiceImpl.isDangerousMimeType("application/x-shockwave-flash"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousMimeType("image/wmf"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousMimeType("text/calendar"));
+ Assert.assertTrue(ShareServiceImpl.isDangerousMimeType("video/H264"));
+ }
+
+ @Test
+ @SmallTest
+ public void testSupportedMime() {
+ Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("audio/wav"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("image/jpeg"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("image/svg+xml"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("text/csv"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("text/plain"));
+ Assert.assertFalse(ShareServiceImpl.isDangerousMimeType("video/mpeg"));
+ }
+}
diff --git a/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollator.java b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollator.java
new file mode 100644
index 00000000000..268903ba59e
--- /dev/null
+++ b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollator.java
@@ -0,0 +1,74 @@
+// Copyright 2019 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.components.browser_ui.webshare;
+
+import org.chromium.base.Callback;
+import org.chromium.base.task.PostTask;
+import org.chromium.content_public.browser.UiThreadTaskTraits;
+import org.chromium.mojo.system.MojoResult;
+import org.chromium.webshare.mojom.SharedFile;
+
+/**
+ * Initiates the share dialog when all files have been received.
+ */
+public class SharedFileCollator implements Callback<Integer> {
+ private static final String WILDCARD = "*/*";
+
+ private int mPending;
+ private Callback<Boolean> mCallback;
+
+ /**
+ * Constructs a SharedFileCollator.
+ *
+ * @param params the share request to issue if blobs are successfully received.
+ * @param callback the callback to call if any blob is not successfully received.
+ */
+ public SharedFileCollator(int pendingFileCount, Callback<Boolean> callback) {
+ mPending = pendingFileCount;
+ mCallback = callback;
+
+ assert mPending > 0;
+ }
+
+ /**
+ * Call with a MojoResult each time a blob has been received.
+ *
+ * @param result a MojoResult indicating if a blob was successfully received.
+ */
+ @Override
+ public void onResult(final Integer result) {
+ if (mCallback == null) return;
+
+ if (result == MojoResult.OK && --mPending > 0) return;
+
+ final Callback<Boolean> callback = mCallback;
+ mCallback = null;
+
+ PostTask.postTask(
+ UiThreadTaskTraits.DEFAULT, () -> { callback.onResult(result == MojoResult.OK); });
+ }
+
+ /**
+ * If the files have a common type and subtype, returns type / subtype
+ * Otherwise if the files have a common type, returns type / *
+ * Otherwise returns * / *
+ *
+ * @param files an array of files being shared.
+ */
+ public static String commonMimeType(SharedFile[] files) {
+ if (files == null || files.length == 0) return WILDCARD;
+ String[] common = files[0].blob.contentType.split("/");
+ if (common.length != 2) return WILDCARD;
+ for (int index = 1; index < files.length; ++index) {
+ String[] current = files[index].blob.contentType.split("/");
+ if (current.length != 2) return WILDCARD;
+ if (!current[0].equals(common[0])) return WILDCARD;
+ if (!current[1].equals(common[1])) {
+ common[1] = "*";
+ }
+ }
+ return common[0] + "/" + common[1];
+ }
+}
diff --git a/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollatorTest.java b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollatorTest.java
new file mode 100644
index 00000000000..3d1e9a2ebf1
--- /dev/null
+++ b/chromium/components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/SharedFileCollatorTest.java
@@ -0,0 +1,108 @@
+// Copyright 2019 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.components.browser_ui.webshare;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.blink.mojom.SerializedBlob;
+import org.chromium.webshare.mojom.SharedFile;
+
+/**
+ * Unit tests for {@link SharedFileCollator}.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class SharedFileCollatorTest {
+ @Test
+ @SmallTest
+ public void testDissimilar() {
+ Assert.assertEquals("*/*", SharedFileCollator.commonMimeType(new SharedFile[0]));
+ Assert.assertEquals(
+ "*/*", SharedFileCollator.commonMimeType(createFiles("text/plain", "image/jpeg")));
+ Assert.assertEquals("*/*",
+ SharedFileCollator.commonMimeType(
+ createFiles("video/mpeg", "video/ogg", "text/html")));
+ }
+
+ @Test
+ @SmallTest
+ public void testMalformed() {
+ Assert.assertEquals("*/*", SharedFileCollator.commonMimeType(createFiles("invalid")));
+ Assert.assertEquals(
+ "*/*", SharedFileCollator.commonMimeType(createFiles("text/xml/svg", "text/xml")));
+ Assert.assertEquals("*/*",
+ SharedFileCollator.commonMimeType(createFiles("image/webp", "image/webp/jpeg")));
+ }
+
+ @Test
+ @SmallTest
+ public void testApplication() {
+ Assert.assertEquals("application/*",
+ SharedFileCollator.commonMimeType(
+ createFiles("application/rtf", "application/x-bzip2")));
+ }
+
+ @Test
+ @SmallTest
+ public void testAudio() {
+ Assert.assertEquals("audio/*",
+ SharedFileCollator.commonMimeType(createFiles("audio/mp3", "audio/wav")));
+ }
+
+ @Test
+ @SmallTest
+ public void testImage() {
+ Assert.assertEquals(
+ "image/jpeg", SharedFileCollator.commonMimeType(createFiles("image/jpeg")));
+ Assert.assertEquals("image/gif",
+ SharedFileCollator.commonMimeType(
+ createFiles("image/gif", "image/gif", "image/gif")));
+ Assert.assertEquals("image/*",
+ SharedFileCollator.commonMimeType(createFiles("image/gif", "image/jpeg")));
+ Assert.assertEquals("image/*",
+ SharedFileCollator.commonMimeType(
+ createFiles("image/gif", "image/gif", "image/jpeg")));
+ }
+
+ @Test
+ @SmallTest
+ public void testText() {
+ Assert.assertEquals("text/css", SharedFileCollator.commonMimeType(createFiles("text/css")));
+ Assert.assertEquals(
+ "text/csv", SharedFileCollator.commonMimeType(createFiles("text/csv", "text/csv")));
+ Assert.assertEquals("text/*",
+ SharedFileCollator.commonMimeType(
+ createFiles("text/csv", "text/html", "text/csv")));
+ }
+
+ @Test
+ @SmallTest
+ public void testVideo() {
+ Assert.assertEquals(
+ "video/webm", SharedFileCollator.commonMimeType(createFiles("video/webm")));
+ Assert.assertEquals("video/*",
+ SharedFileCollator.commonMimeType(createFiles("video/mpeg", "video/webm")));
+ Assert.assertEquals("video/*",
+ SharedFileCollator.commonMimeType(
+ createFiles("video/mpeg", "video/webm", "video/webm")));
+ }
+
+ private static SharedFile[] createFiles(String... mimeTypeList) {
+ SharedFile[] result = new SharedFile[mimeTypeList.length];
+ for (int i = 0; i < mimeTypeList.length; ++i) {
+ SerializedBlob blob = new SerializedBlob();
+ blob.contentType = mimeTypeList[i];
+ result[i] = new SharedFile();
+ result[i].blob = blob;
+ }
+ return result;
+ }
+}