blob: 8a0728c01591b784e769c93bfa30e70dfbec93af [file] [log] [blame]
// 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.chrome.browser.webshare;
import android.app.Activity;
import android.content.ComponentName;
import android.net.Uri;
import android.support.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.chrome.browser.safe_browsing.FileTypePolicies;
import org.chromium.chrome.browser.share.ShareHelper;
import org.chromium.chrome.browser.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 Activity mActivity;
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);
public ShareServiceImpl(@Nullable WebContents webContents) {
mActivity = activityFromWebContents(webContents);
}
@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 (mActivity == null) {
RecordHistogram.recordEnumeratedHistogram("WebShare.ShareOutcome",
WEBSHARE_OUTCOME_UNKNOWN_FAILURE, WEBSHARE_OUTCOME_COUNT);
callback.call(ShareError.INTERNAL_ERROR);
return;
}
ShareHelper.TargetChosenCallback innerCallback = new ShareHelper.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(mActivity, title, url.url)
.setText(text)
.setCallback(innerCallback);
if (files == null || files.length == 0) {
ShareHelper.share(paramsBuilder.build());
return;
}
if (files.length > MAX_SHARED_FILE_COUNT) {
callback.call(ShareError.PERMISSION_DENIED);
return;
}
for (SharedFile file : files) {
RecordHistogram.recordSparseHistogram(
"WebShare.Unverified", FileTypePolicies.umaValueForFile(file.name));
}
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 = ShareHelper.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(paramsBuilder.build(), callback);
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();
}
}