1 /* 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, you can obtain one at http://mozilla.org/MPL/2.0/. 5 */ 6 7 package org.mozilla.gecko.util; 8 9 import android.annotation.TargetApi; 10 import android.content.Intent; 11 import android.net.Uri; 12 import android.os.Bundle; 13 import android.support.annotation.CheckResult; 14 import android.support.annotation.NonNull; 15 import android.text.TextUtils; 16 17 import org.mozilla.gecko.mozglue.SafeIntent; 18 19 import java.net.URISyntaxException; 20 import java.util.HashMap; 21 import java.util.Locale; 22 import java.util.regex.Matcher; 23 import java.util.regex.Pattern; 24 25 /** 26 * Utilities for Intents. 27 */ 28 public class IntentUtils { 29 public static final String ENV_VAR_IN_AUTOMATION = "MOZ_IN_AUTOMATION"; 30 31 private static final String ENV_VAR_REGEX = "(.+)=(.*)"; 32 IntentUtils()33 private IntentUtils() {} 34 35 /** 36 * Returns a list of environment variables and their values. These are parsed from an Intent extra 37 * with the key -> value format: env# -> ENV_VAR=VALUE, where # is an integer starting at 0. 38 * 39 * @return A Map of environment variable name to value, e.g. ENV_VAR -> VALUE 40 */ getEnvVarMap(@onNull final SafeIntent intent)41 public static HashMap<String, String> getEnvVarMap(@NonNull final SafeIntent intent) { 42 // Optimization: get matcher for re-use. Pattern.matcher creates a new object every time so it'd be great 43 // to avoid the unnecessary allocation, particularly because we expect to be called on the startup path. 44 final Pattern envVarPattern = Pattern.compile(ENV_VAR_REGEX); 45 final Matcher matcher = envVarPattern.matcher(""); // argument does not matter here. 46 47 // This is expected to be an external intent so we should use SafeIntent to prevent crashing. 48 final HashMap<String, String> out = new HashMap<>(); 49 int i = 0; 50 while (true) { 51 final String envKey = "env" + i; 52 i += 1; 53 if (!intent.hasExtra(envKey)) { 54 break; 55 } 56 57 maybeAddEnvVarToEnvVarMap(out, intent, envKey, matcher); 58 } 59 return out; 60 } 61 62 /** 63 * @param envVarMap the map to add the env var to 64 * @param intent the intent from which to extract the env var 65 * @param envKey the key at which the env var resides 66 * @param envVarMatcher a matcher initialized with the env var pattern to extract 67 */ maybeAddEnvVarToEnvVarMap(@onNull final HashMap<String, String> envVarMap, @NonNull final SafeIntent intent, @NonNull final String envKey, @NonNull final Matcher envVarMatcher)68 private static void maybeAddEnvVarToEnvVarMap(@NonNull final HashMap<String, String> envVarMap, 69 @NonNull final SafeIntent intent, @NonNull final String envKey, @NonNull final Matcher envVarMatcher) { 70 final String envValue = intent.getStringExtra(envKey); 71 if (envValue == null) { 72 return; // nothing to do here! 73 } 74 75 envVarMatcher.reset(envValue); 76 if (envVarMatcher.matches()) { 77 final String envVarName = envVarMatcher.group(1); 78 final String envVarValue = envVarMatcher.group(2); 79 envVarMap.put(envVarName, envVarValue); 80 } 81 } 82 getBundleExtraSafe(final Intent intent, final String name)83 public static Bundle getBundleExtraSafe(final Intent intent, final String name) { 84 return new SafeIntent(intent).getBundleExtra(name); 85 } 86 getStringExtraSafe(final Intent intent, final String name)87 public static String getStringExtraSafe(final Intent intent, final String name) { 88 return new SafeIntent(intent).getStringExtra(name); 89 } 90 getBooleanExtraSafe(final Intent intent, final String name, final boolean defaultValue)91 public static boolean getBooleanExtraSafe(final Intent intent, final String name, final boolean defaultValue) { 92 return new SafeIntent(intent).getBooleanExtra(name, defaultValue); 93 } 94 95 /** 96 * Gets whether or not we're in automation from the passed in environment variables. 97 * 98 * We need to read environment variables from the intent string 99 * extra because environment variables from our test harness aren't set 100 * until Gecko is loaded, and we need to know this before then. 101 * 102 * The return value of this method should be used early since other 103 * initialization may depend on its results. 104 */ 105 @CheckResult getIsInAutomationFromEnvironment(final SafeIntent intent)106 public static boolean getIsInAutomationFromEnvironment(final SafeIntent intent) { 107 final HashMap<String, String> envVars = IntentUtils.getEnvVarMap(intent); 108 return !TextUtils.isEmpty(envVars.get(IntentUtils.ENV_VAR_IN_AUTOMATION)); 109 } 110 111 /** 112 * Return a Uri instance which is equivalent to uri, 113 * but with a guaranteed-lowercase scheme as if the API level 16 method 114 * Uri.normalizeScheme had been called. 115 * 116 * @param uri The URI string to normalize. 117 * @return The corresponding normalized Uri. 118 */ normalizeUriScheme(final Uri uri)119 private static Uri normalizeUriScheme(final Uri uri) { 120 final String scheme = uri.getScheme(); 121 final String lower = scheme.toLowerCase(Locale.US); 122 if (lower.equals(scheme)) { 123 return uri; 124 } 125 126 // Otherwise, return a new URI with a normalized scheme. 127 return uri.buildUpon().scheme(lower).build(); 128 } 129 130 131 /** 132 * Return a normalized Uri instance that corresponds to the given URI string 133 * with cross-API-level compatibility. 134 * 135 * @param aUri The URI string to normalize. 136 * @return The corresponding normalized Uri. 137 */ normalizeUri(final String aUri)138 public static Uri normalizeUri(final String aUri) { 139 final Uri normUri = normalizeUriScheme( 140 aUri.indexOf(':') >= 0 141 ? Uri.parse(aUri) 142 : new Uri.Builder().scheme(aUri).build()); 143 return normUri; 144 } 145 isUriSafeForScheme(final String aUri)146 public static boolean isUriSafeForScheme(final String aUri) { 147 return isUriSafeForScheme(normalizeUri(aUri)); 148 } 149 150 /** 151 * Verify whether the given URI is considered safe to load in respect to 152 * its scheme. 153 * Unsafe URIs should be blocked from further handling. 154 * 155 * @param aUri The URI instance to test. 156 * @return Whether the provided URI is considered safe in respect to its 157 * scheme. 158 */ isUriSafeForScheme(final Uri aUri)159 public static boolean isUriSafeForScheme(final Uri aUri) { 160 final String scheme = aUri.getScheme(); 161 if ("tel".equals(scheme) || "sms".equals(scheme)) { 162 // Bug 794034 - We don't want to pass MWI or USSD codes to the 163 // dialer, and ensure the Uri class doesn't parse a URI 164 // containing a fragment ('#') 165 final String number = aUri.getSchemeSpecificPart(); 166 if (number.contains("#") || number.contains("*") || 167 aUri.getFragment() != null) { 168 return false; 169 } 170 } 171 172 if (("intent".equals(scheme) || "android-app".equals(scheme))) { 173 // Bug 1356893 - Rject intents with file data schemes. 174 return getSafeIntent(aUri) != null; 175 } 176 177 return true; 178 } 179 180 /** 181 * Create a safe intent for the given URI. 182 * Intents with file data schemes are considered unsafe. 183 * 184 * @param aUri The URI for the intent. 185 * @return A safe intent for the given URI or null if URI is considered 186 * unsafe. 187 */ getSafeIntent(final Uri aUri)188 public static Intent getSafeIntent(final Uri aUri) { 189 final Intent intent; 190 try { 191 intent = Intent.parseUri(aUri.toString(), 0); 192 } catch (final URISyntaxException e) { 193 return null; 194 } 195 196 final Uri data = intent.getData(); 197 if (data != null && 198 "file".equals(normalizeUriScheme(data).getScheme())) { 199 return null; 200 } 201 202 // Only open applications which can accept arbitrary data from a browser. 203 intent.addCategory(Intent.CATEGORY_BROWSABLE); 204 205 // Prevent site from explicitly opening our internal activities, 206 // which can leak data. 207 intent.setComponent(null); 208 nullIntentSelector(intent); 209 210 return intent; 211 } 212 213 // We create a separate method to better encapsulate the @TargetApi use. 214 @TargetApi(15) nullIntentSelector(final Intent intent)215 private static void nullIntentSelector(final Intent intent) { 216 intent.setSelector(null); 217 } 218 219 } 220