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