chromium/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebApkShareTargetUtil.java

// Copyright 2018 The Chromium Authors
// 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.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.text.TextUtils;

import androidx.browser.trusted.sharing.ShareData;

import org.json.JSONArray;
import org.json.JSONException;

import org.chromium.base.ContextUtils;
import org.chromium.chrome.browser.browserservices.intents.WebApkShareTarget;
import org.chromium.net.MimeTypeFilter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/** 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 ArrayList<String> names;
        public ArrayList<Boolean> isValueFileUri;
        public ArrayList<String> values;
        public ArrayList<String> filenames;
        public ArrayList<String> types;

        public PostData(boolean isMultipartEncoding) {
            this.isMultipartEncoding = isMultipartEncoding;
            names = new ArrayList<>();
            isValueFileUri = new ArrayList<>();
            values = new ArrayList<>();
            filenames = new ArrayList<>();
            types = new ArrayList<>();
        }

        private void addPlainText(String name, String value) {
            names.add(name);
            isValueFileUri.add(false);
            values.add(value);
            filenames.add("");
            types.add("text/plain");
        }

        private void add(
                String name, String value, boolean isValueAFileUri, String fileName, String type) {
            names.add(name);
            values.add(value);
            isValueFileUri.add(isValueAFileUri);
            filenames.add(fileName);
            types.add(type);
        }
    }

    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.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
                    if (result != null) {
                        return result;
                    }
                }
            }
        }

        return uri.getPath();
    }

    public static String[] decodeJsonStringArray(String encodedJsonArray) {
        if (encodedJsonArray == null) {
            return null;
        }

        try {
            JSONArray jsonArray = new JSONArray(encodedJsonArray);
            String[] originalData = new String[jsonArray.length()];
            for (int i = 0; i < jsonArray.length(); i++) {
                originalData[i] = jsonArray.getString(i);
            }
            return originalData;
        } catch (JSONException e) {
        }
        return null;
    }

    public static String[][] decodeJsonAccepts(String encodedAcceptsArray) {
        if (encodedAcceptsArray == null) {
            return null;
        }
        try {
            JSONArray jsonArray = new JSONArray(encodedAcceptsArray);
            String[][] originalData = new String[jsonArray.length()][];
            for (int i = 0; i < jsonArray.length(); i++) {
                String[] childArr = new String[jsonArray.getJSONArray(i).length()];
                for (int j = 0; j < childArr.length; j++) {
                    childArr[j] = jsonArray.getJSONArray(i).getString(j);
                }
                originalData[i] = childArr;
            }
            return originalData;
        } catch (JSONException e) {
        }

        return null;
    }

    /**
     * Given a list of share target params file names, and the mime types each file name can
     * accept, returns the first share target params file name which accepts the passed-in file URI.
     */
    private static String findFormFieldToShareFile(
            Uri fileUri,
            String fileType,
            String[] shareTargetParamsFileNames,
            String[][] shareTargetParamsFileAccepts) {
        if (shareTargetParamsFileNames == null
                || shareTargetParamsFileAccepts == null
                || shareTargetParamsFileNames.length != shareTargetParamsFileAccepts.length) {
            return null;
        }
        for (int i = 0; i < shareTargetParamsFileNames.length; i++) {
            String[] mimeTypeList = shareTargetParamsFileAccepts[i];
            MimeTypeFilter mimeTypeFilter = new MimeTypeFilter(Arrays.asList(mimeTypeList), false);
            if (mimeTypeFilter.accept(fileUri, fileType)) {
                return shareTargetParamsFileNames[i];
            }
        }
        return null;
    }

    protected static void addFilesToMultipartPostData(
            PostData postData,
            String fallbackNameForPlainTextFile,
            String[] shareTargetParamsFileNames,
            String[][] shareTargetParamsFileAccepts,
            List<Uri> shareFiles) {
        if (shareFiles == null) {
            return;
        }

        for (Uri fileUri : shareFiles) {
            String fileType;
            String fileName;

            fileType = getFileTypeFromContentUri(fileUri);
            fileName = getFileNameFromContentUri(fileUri);

            if (fileType == null || fileName == null) {
                continue;
            }

            String fieldName =
                    findFormFieldToShareFile(
                            fileUri,
                            fileType,
                            shareTargetParamsFileNames,
                            shareTargetParamsFileAccepts);

            if (fieldName != null) {
                postData.add(
                        fieldName,
                        fileUri.toString(),
                        /* isValueAFileUri= */ true,
                        fileName,
                        fileType);
            } else if (fallbackNameForPlainTextFile != null && fileType.equals("text/plain")) {
                postData.add(
                        fallbackNameForPlainTextFile, fileUri.toString(), true, "", "text/plain");
                // we should only add one text file as a fake text selection
                fallbackNameForPlainTextFile = null;
            }
        }
    }

    /**
     * If a WebAPK has a share target parameter file name that receives sharing of text files, adds
     * the share text selection as if it's a text file.
     */
    private static void tryAddShareTextAsFakeFile(
            PostData postData,
            String[] shareTargetParamsFileNames,
            String[][] shareTargetParamsFileAccepts,
            String shareText) {
        if (TextUtils.isEmpty(shareText)) {
            return;
        }

        String fieldName =
                findFormFieldToShareFile(
                        null,
                        "text/plain",
                        shareTargetParamsFileNames,
                        shareTargetParamsFileAccepts);
        if (fieldName != null) {
            postData.add(
                    fieldName, shareText, /* isValueAFileUri= */ false, "shared.txt", "text/plain");
        }
    }

    protected static PostData computePostData(WebApkShareTarget shareTarget, ShareData shareData) {
        if (shareTarget == null || !shareTarget.isShareMethodPost() || shareData == null) {
            return null;
        }

        PostData postData = new PostData(shareTarget.isShareEncTypeMultipart());

        if (!TextUtils.isEmpty(shareTarget.getParamTitle())
                && !TextUtils.isEmpty(shareData.title)) {
            postData.addPlainText(shareTarget.getParamTitle(), shareData.title);
        }

        if (!TextUtils.isEmpty(shareTarget.getParamText()) && !TextUtils.isEmpty(shareData.text)) {
            postData.addPlainText(shareTarget.getParamText(), shareData.text);
        }

        if (!postData.isMultipartEncoding) {
            return postData;
        }

        // When a WebAPK doesn't expect a shared text selection, but receives one (because Android
        // intent filters don't distinguish between text selections and text files), we send the
        // text selection as if it's a text file.
        if (TextUtils.isEmpty(shareTarget.getParamText()) && !TextUtils.isEmpty(shareData.text)) {
            tryAddShareTextAsFakeFile(
                    postData,
                    shareTarget.getFileNames(),
                    shareTarget.getFileAccepts(),
                    shareData.text);
        }

        boolean enableAddingFileAsFakePlainText =
                !TextUtils.isEmpty(shareTarget.getParamText()) && TextUtils.isEmpty(shareData.text);

        // We allow adding a file as fake shared text only when shared text is absent, because the
        // web page expects a single value (not an array) in the "param text" field.
        addFilesToMultipartPostData(
                postData,
                enableAddingFileAsFakePlainText ? shareTarget.getParamText() : null,
                shareTarget.getFileNames(),
                shareTarget.getFileAccepts(),
                shareData.uris);

        return postData;
    }
}