Bug 1480757 - [1.0] Move safe intent handling utilities from IntentHelper to IntentUtils to make them accessible in GeckoView. r=jchen
authorEugen Sawin <esawin@mozilla.com>
Fri, 03 Aug 2018 15:03:46 +0200
changeset 485933 4a9bb2b62b72ce22b21520451dba85ecc5e1626e
parent 485932 09fbeb118af3db3c5092603339f025164a01b337
child 485934 0f5e0ddc5ce11c9e7b538b1928e3c899f5dc4efa
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjchen
bugs1480757
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1480757 - [1.0] Move safe intent handling utilities from IntentHelper to IntentUtils to make them accessible in GeckoView. r=jchen
mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
--- a/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
@@ -7,19 +7,19 @@ package org.mozilla.gecko;
 
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
+import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.widget.ExternalIntentDuringPrivateBrowsingPromptFragment;
 
-import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.ClipData;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.provider.Browser;
@@ -313,51 +313,31 @@ public final class IntentHelper implemen
                                                 final String mimeType, final String action, final String title) {
 
         if (action.equalsIgnoreCase(Intent.ACTION_SEND)) {
             Intent shareIntent = getShareIntent(context, targetURI, mimeType, title);
             return Intent.createChooser(shareIntent,
                                         context.getResources().getString(R.string.share_title));
         }
 
-        Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
+        Uri uri = IntentUtils.normalizeUri(targetURI);
+
         if (!TextUtils.isEmpty(mimeType)) {
             Intent intent = getIntentForActionString(action);
             intent.setDataAndType(uri, mimeType);
             return intent;
         }
 
-        if (!GeckoAppShell.isUriSafeForScheme(uri)) {
+        if (!IntentUtils.isUriSafeForScheme(targetURI)) {
             return null;
         }
 
         final String scheme = uri.getScheme();
         if ("intent".equals(scheme) || "android-app".equals(scheme)) {
-            final Intent intent;
-            try {
-                intent = Intent.parseUri(targetURI, 0);
-            } catch (final URISyntaxException e) {
-                Log.e(LOGTAG, "Unable to parse URI - " + e);
-                return null;
-            }
-
-            final Uri data = intent.getData();
-            if (data != null && "file".equals(normalizeUriScheme(data).getScheme())) {
-                Log.w(LOGTAG, "Blocked intent with \"file://\" data scheme.");
-                return null;
-            }
-
-            // Only open applications which can accept arbitrary data from a browser.
-            intent.addCategory(Intent.CATEGORY_BROWSABLE);
-
-            // Prevent site from explicitly opening our internal activities, which can leak data.
-            intent.setComponent(null);
-            nullIntentSelector(intent);
-
-            return intent;
+            return IntentUtils.getSafeIntent(uri);
         }
 
         // Compute our most likely intent, then check to see if there are any
         // custom handlers that would apply.
         // Start with the original URI. If we end up modifying it, we'll
         // overwrite it.
         final String extension = MimeTypeMap.getFileExtensionFromUrl(targetURI);
         final Intent intent = getIntentForActionString(action);
@@ -422,41 +402,16 @@ public final class IntentHelper implemen
         // push that into the new Intent.
         final String newQuery = resultQuery.length() > 0 ? "?" + resultQuery : "";
         final Uri pruned = uri.buildUpon().encodedQuery(newQuery).build();
         intent.setData(pruned);
 
         return intent;
     }
 
-    // We create a separate method to better encapsulate the @TargetApi use.
-    @TargetApi(15)
-    private static void nullIntentSelector(final Intent intent) {
-        intent.setSelector(null);
-    }
-
-    /**
-     * Return a <code>Uri</code> instance which is equivalent to <code>u</code>,
-     * but with a guaranteed-lowercase scheme as if the API level 16 method
-     * <code>u.normalizeScheme</code> had been called.
-     *
-     * @param u the <code>Uri</code> to normalize.
-     * @return a <code>Uri</code>, which might be <code>u</code>.
-     */
-    private static Uri normalizeUriScheme(final Uri u) {
-        final String scheme = u.getScheme();
-        final String lower  = scheme.toLowerCase(Locale.US);
-        if (lower.equals(scheme)) {
-            return u;
-        }
-
-        // Otherwise, return a new URI with a normalized scheme.
-        return u.buildUpon().scheme(lower).build();
-    }
-
     @Override // BundleEventHandler
     public void handleMessage(final String event, final GeckoBundle message,
                               final EventCallback callback) {
         if ("Intent:OpenNoHandler".equals(event)) {
             openNoHandler(message, callback);
 
         } else if ("Intent:GetHandlers".equals(event)) {
             getHandlers(message, callback);
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
@@ -931,30 +931,16 @@ public class GeckoAppShell
         }
         if (type == null)
             type = "*";
         if (subType == null)
             subType = "*";
         return type + "/" + subType;
     }
 
-    static boolean isUriSafeForScheme(Uri aUri) {
-        // Bug 794034 - We don't want to pass MWI or USSD codes to the
-        // dialer, and ensure the Uri class doesn't parse a URI
-        // containing a fragment ('#')
-        final String scheme = aUri.getScheme();
-        if ("tel".equals(scheme) || "sms".equals(scheme)) {
-            final String number = aUri.getSchemeSpecificPart();
-            if (number.contains("#") || number.contains("*") || aUri.getFragment() != null) {
-                return false;
-            }
-        }
-        return true;
-    }
-
     @WrapForJNI(calledFrom = "gecko")
     private static boolean openUriExternal(String targetURI,
                                            String mimeType,
                                            String packageName,
                                            String className,
                                            String action,
                                            String title) {
         final GeckoInterface geckoInterface = getGeckoInterface();
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
@@ -1,26 +1,30 @@
 /*
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, you can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
 package org.mozilla.gecko.util;
 
+import android.annotation.TargetApi;
 import android.content.ComponentName;
 import android.content.Intent;
+import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.CheckResult;
 import android.support.annotation.NonNull;
 import android.text.TextUtils;
 
 import org.mozilla.gecko.mozglue.SafeIntent;
 
+import java.net.URISyntaxException;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
  * Utilities for Intents.
  */
 public class IntentUtils {
     public static final String ENV_VAR_IN_AUTOMATION = "MOZ_IN_AUTOMATION";
@@ -99,9 +103,118 @@ public class IntentUtils {
      * The return value of this method should be used early since other
      * initialization may depend on its results.
      */
     @CheckResult
     public static boolean getIsInAutomationFromEnvironment(final SafeIntent intent) {
         final HashMap<String, String> envVars = IntentUtils.getEnvVarMap(intent);
         return !TextUtils.isEmpty(envVars.get(IntentUtils.ENV_VAR_IN_AUTOMATION));
     }
+
+    /**
+     * Return a Uri instance which is equivalent to uri,
+     * but with a guaranteed-lowercase scheme as if the API level 16 method
+     * Uri.normalizeScheme had been called.
+     *
+     * @param uri The URI string to normalize.
+     * @return The corresponding normalized Uri.
+     */
+    private static Uri normalizeUriScheme(final Uri uri) {
+        final String scheme = uri.getScheme();
+        final String lower  = scheme.toLowerCase(Locale.US);
+        if (lower.equals(scheme)) {
+            return uri;
+        }
+
+        // Otherwise, return a new URI with a normalized scheme.
+        return uri.buildUpon().scheme(lower).build();
+    }
+
+
+    /**
+     * Return a normalized Uri instance that corresponds to the given URI string
+     * with cross-API-level compatibility.
+     *
+     * @param aUri The URI string to normalize.
+     * @return The corresponding normalized Uri.
+     */
+    public static Uri normalizeUri(final String aUri) {
+        final Uri normUri = normalizeUriScheme(
+            aUri.indexOf(':') >= 0
+            ? Uri.parse(aUri)
+            : new Uri.Builder().scheme(aUri).build());
+        return normUri;
+    }
+
+    public static boolean isUriSafeForScheme(final String aUri) {
+        return isUriSafeForScheme(normalizeUri(aUri));
+    }
+
+    /**
+     * Verify whether the given URI is considered safe to load in respect to
+     * its scheme.
+     * Unsafe URIs should be blocked from further handling.
+     *
+     * @param aUri The URI instance to test.
+     * @return Whether the provided URI is considered safe in respect to its
+     *         scheme.
+     */
+    public static boolean isUriSafeForScheme(final Uri aUri) {
+        final String scheme = aUri.getScheme();
+        if ("tel".equals(scheme) || "sms".equals(scheme)) {
+            // Bug 794034 - We don't want to pass MWI or USSD codes to the
+            // dialer, and ensure the Uri class doesn't parse a URI
+            // containing a fragment ('#')
+            final String number = aUri.getSchemeSpecificPart();
+            if (number.contains("#") || number.contains("*") ||
+                aUri.getFragment() != null) {
+                return false;
+            }
+        }
+
+        if (("intent".equals(scheme) || "android-app".equals(scheme))) {
+            // Bug 1356893 - Rject intents with file data schemes.
+            return getSafeIntent(aUri) != null;
+        }
+
+        return true;
+    }
+
+    /**
+     * Create a safe intent for the given URI.
+     * Intents with file data schemes are considered unsafe.
+     *
+     * @param aUri The URI for the intent.
+     * @return A safe intent for the given URI or null if URI is considered
+     *         unsafe.
+     */
+    public static Intent getSafeIntent(final Uri aUri) {
+        final Intent intent;
+        try {
+            intent = Intent.parseUri(aUri.toString(), 0);
+        } catch (final URISyntaxException e) {
+            return null;
+        }
+
+        final Uri data = intent.getData();
+        if (data != null &&
+            "file".equals(normalizeUriScheme(data).getScheme())) {
+            return null;
+        }
+
+        // Only open applications which can accept arbitrary data from a browser.
+        intent.addCategory(Intent.CATEGORY_BROWSABLE);
+
+        // Prevent site from explicitly opening our internal activities,
+        // which can leak data.
+        intent.setComponent(null);
+        nullIntentSelector(intent);
+
+        return intent;
+    }
+
+    // We create a separate method to better encapsulate the @TargetApi use.
+    @TargetApi(15)
+    private static void nullIntentSelector(final Intent intent) {
+        intent.setSelector(null);
+    }
+
 }