1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser;
6 
7 import static org.chromium.components.webapk.lib.common.WebApkConstants.WEBAPK_PACKAGE_PREFIX;
8 
9 import android.app.Activity;
10 import android.app.KeyguardManager;
11 import android.app.PendingIntent;
12 import android.app.SearchManager;
13 import android.content.ComponentName;
14 import android.content.Context;
15 import android.content.Intent;
16 import android.net.Uri;
17 import android.os.Bundle;
18 import android.os.PowerManager;
19 import android.os.SystemClock;
20 import android.provider.Browser;
21 import android.provider.MediaStore;
22 import android.provider.Settings;
23 import android.speech.RecognizerResultsIntent;
24 import android.text.TextUtils;
25 import android.util.Pair;
26 
27 import androidx.annotation.IntDef;
28 import androidx.annotation.Nullable;
29 import androidx.annotation.VisibleForTesting;
30 import androidx.browser.customtabs.CustomTabsSessionToken;
31 
32 import org.chromium.base.ContextUtils;
33 import org.chromium.base.FileUtils;
34 import org.chromium.base.IntentUtils;
35 import org.chromium.base.Log;
36 import org.chromium.base.annotations.JNINamespace;
37 import org.chromium.base.annotations.NativeMethods;
38 import org.chromium.base.metrics.RecordHistogram;
39 import org.chromium.base.metrics.RecordUserAction;
40 import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
41 import org.chromium.chrome.browser.document.ChromeLauncherActivity;
42 import org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl;
43 import org.chromium.chrome.browser.externalnav.IntentWithRequestMetadataHandler;
44 import org.chromium.chrome.browser.externalnav.IntentWithRequestMetadataHandler.RequestMetadata;
45 import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
46 import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteCoordinator;
47 import org.chromium.chrome.browser.profiles.Profile;
48 import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
49 import org.chromium.chrome.browser.tab.Tab;
50 import org.chromium.chrome.browser.tab.TabLaunchType;
51 import org.chromium.chrome.browser.translate.TranslateIntentHandler;
52 import org.chromium.chrome.browser.webapps.WebappActivity;
53 import org.chromium.components.embedder_support.util.UrlConstants;
54 import org.chromium.components.embedder_support.util.UrlUtilities;
55 import org.chromium.components.external_intents.ExternalNavigationHandler;
56 import org.chromium.content_public.browser.BrowserStartupController;
57 import org.chromium.content_public.browser.LoadUrlParams;
58 import org.chromium.content_public.common.ContentUrlConstants;
59 import org.chromium.content_public.common.Referrer;
60 import org.chromium.net.HttpUtil;
61 import org.chromium.network.mojom.ReferrerPolicy;
62 import org.chromium.ui.base.PageTransition;
63 import org.chromium.url.Origin;
64 
65 import java.lang.annotation.Retention;
66 import java.lang.annotation.RetentionPolicy;
67 import java.util.ArrayList;
68 import java.util.List;
69 import java.util.Locale;
70 import java.util.Map;
71 
72 /**
73  * Handles all browser-related Intents.
74  */
75 @JNINamespace("chrome::android")
76 public class IntentHandler {
77     private static final String TAG = "IntentHandler";
78 
79     /**
80      * Document mode: If true, Chrome is launched into the same Task.
81      * Note: used by first-party applications, do not rename.
82      */
83     public static final String EXTRA_APPEND_TASK = "com.android.chrome.append_task";
84 
85     /**
86      * Document mode: If true, keep tasks in Recents when a user hits back at the root URL.
87      * Note: used by first-party applications, do not rename.
88      */
89     public static final String EXTRA_PRESERVE_TASK = "com.android.chrome.preserve_task";
90 
91     /**
92      * Document mode: If true, opens the document in background.
93      * Note: used by first-party applications, do not rename.
94      */
95     public static final String EXTRA_OPEN_IN_BG = "com.android.chrome.open_with_affiliation";
96 
97     /**
98      * Document mode: Records what caused a document to be created.
99      */
100     public static final String EXTRA_STARTED_BY = "com.android.chrome.started_by";
101 
102     /**
103      * Tab ID to use when creating a new Tab.
104      */
105     public static final String EXTRA_TAB_ID = "com.android.chrome.tab_id";
106 
107     /**
108      * The tab id of the parent tab, if any.
109      */
110     public static final String EXTRA_PARENT_TAB_ID = "com.android.chrome.parent_tab_id";
111 
112     /**
113      * Intent to bring the parent Activity back, if the parent Tab lives in a different Activity.
114      */
115     public static final String EXTRA_PARENT_INTENT = "com.android.chrome.parent_intent";
116 
117     /**
118      * ComponentName of the parent Activity. Can be used by an Activity launched on top of another
119      * Activity (e.g. BookmarkActivity) to intent back into the Activity it sits on top of.
120      */
121     public static final String EXTRA_PARENT_COMPONENT =
122             "org.chromium.chrome.browser.parent_component";
123 
124     /**
125      * Transition type is only set internally by a first-party app and has to be signed.
126      */
127     public static final String EXTRA_PAGE_TRANSITION_TYPE = "com.google.chrome.transition_type";
128 
129     /**
130      * The original intent of the given intent before it was modified.
131      */
132     public static final String EXTRA_ORIGINAL_INTENT = "com.android.chrome.original_intent";
133 
134     /**
135      * An extra to indicate that a particular intent was triggered from the first run experience
136      * flow.
137      */
138     public static final String EXTRA_INVOKED_FROM_FRE = "com.android.chrome.invoked_from_fre";
139 
140     /**
141      * An extra to indicate that the intent was triggered from a launcher shortcut.
142      */
143     public static final String EXTRA_INVOKED_FROM_SHORTCUT =
144             "com.android.chrome.invoked_from_shortcut";
145 
146     /**
147      * An extra to indicate that the intent was triggered by the launch new incognito tab feature.
148      * See {@link org.chromium.chrome.browser.incognito.IncognitoTabLauncher}.
149      */
150     public static final String EXTRA_INVOKED_FROM_LAUNCH_NEW_INCOGNITO_TAB =
151             "org.chromium.chrome.browser.incognito.invoked_from_launch_new_incognito_tab";
152 
153     /**
154      * Intent extra used to identify the sending application.
155      */
156     private static final String TRUSTED_APPLICATION_CODE_EXTRA = "trusted_application_code_extra";
157 
158     /**
159      * A referrer id used for Chrome to Chrome referrer passing.
160      */
161     public static final String EXTRA_REFERRER_ID = "org.chromium.chrome.browser.referrer_id";
162 
163     /**
164      * An extra for identifying the referrer policy to be used.
165      * TODO(yusufo): Move this to support library.
166      */
167     public static final String EXTRA_REFERRER_POLICY =
168             "android.support.browser.extra.referrer_policy";
169 
170     /**
171      * Key to associate a timestamp with an intent.
172      */
173     private static final String EXTRA_TIMESTAMP_MS = "org.chromium.chrome.browser.timestamp";
174 
175     /**
176      * For multi-window, passes the id of the window.
177      */
178     public static final String EXTRA_WINDOW_ID = "org.chromium.chrome.browser.window_id";
179 
180     /**
181      * Extra to indicate the launch type of the tab to be created.
182      */
183     private static final String EXTRA_TAB_LAUNCH_TYPE =
184             "org.chromium.chrome.browser.tab_launch_type";
185 
186     /**
187      * A hash code for the URL to verify intent data hasn't been modified.
188      */
189     public static final String EXTRA_DATA_HASH_CODE = "org.chromium.chrome.browser.data_hash";
190 
191     /**
192      * A boolean to indicate whether incognito mode is currently selected.
193      */
194     public static final String EXTRA_INCOGNITO_MODE = "org.chromium.chrome.browser.incognito_mode";
195 
196     /**
197      * Byte array for the POST data when load a url, only Intents sent by Chrome can use this.
198      */
199     public static final String EXTRA_POST_DATA = "com.android.chrome.post_data";
200 
201     /**
202      * The type of the POST data, need to be added to the HTTP request header, only Intents sent by
203      * Chrome can use this.
204      */
205     public static final String EXTRA_POST_DATA_TYPE = "com.android.chrome.post_data_type";
206 
207     /**
208      * Fake ComponentName used in constructing TRUSTED_APPLICATION_CODE_EXTRA.
209      */
210     private static ComponentName sFakeComponentName;
211 
212     private static final Object LOCK = new Object();
213 
214     private static Pair<Integer, String> sPendingReferrer;
215     private static int sReferrerId;
216     private static String sPendingIncognitoUrl;
217 
218     public static final String PACKAGE_GSA = "com.google.android.googlequicksearchbox";
219     private static final String PACKAGE_GMAIL = "com.google.android.gm";
220     private static final String PACKAGE_PLUS = "com.google.android.apps.plus";
221     private static final String PACKAGE_HANGOUTS = "com.google.android.talk";
222     private static final String PACKAGE_MESSENGER = "com.google.android.apps.messaging";
223     private static final String PACKAGE_LINE = "jp.naver.line.android";
224     private static final String PACKAGE_WHATSAPP = "com.whatsapp";
225     private static final String PACKAGE_YAHOO_MAIL = "com.yahoo.mobile.client.android.mail";
226     private static final String PACKAGE_VIBER = "com.viber.voip";
227     private static final String FACEBOOK_REFERRER_URL = "android-app://m.facebook.com";
228     private static final String FACEBOOK_INTERNAL_BROWSER_REFERRER = "http://m.facebook.com";
229     private static final String TWITTER_LINK_PREFIX = "http://t.co/";
230     private static final String NEWS_LINK_PREFIX = "http://news.google.com/news/url?";
231     private static final String YOUTUBE_LINK_PREFIX_HTTPS = "https://www.youtube.com/redirect?";
232     private static final String YOUTUBE_LINK_PREFIX_HTTP = "http://www.youtube.com/redirect?";
233 
234     /**
235      * Represents popular external applications that can load a page in Chrome via intent.
236      * DO NOT reorder items in this interface, because it's mirrored to UMA (as ClientAppId).
237      * Values should be enumerated from 0 and can't have gaps. When removing items,
238      * comment them out and keep existing numeric values stable.
239      */
240     @IntDef({ExternalAppId.OTHER, ExternalAppId.GMAIL, ExternalAppId.FACEBOOK, ExternalAppId.PLUS,
241             ExternalAppId.TWITTER, ExternalAppId.CHROME, ExternalAppId.HANGOUTS,
242             ExternalAppId.MESSENGER, ExternalAppId.NEWS, ExternalAppId.LINE, ExternalAppId.WHATSAPP,
243             ExternalAppId.GSA, ExternalAppId.WEBAPK, ExternalAppId.YAHOO_MAIL, ExternalAppId.VIBER,
244             ExternalAppId.YOUTUBE})
245     @Retention(RetentionPolicy.SOURCE)
246     public @interface ExternalAppId {
247         int OTHER = 0;
248         int GMAIL = 1;
249         int FACEBOOK = 2;
250         int PLUS = 3;
251         int TWITTER = 4;
252         int CHROME = 5;
253         int HANGOUTS = 6;
254         int MESSENGER = 7;
255         int NEWS = 8;
256         int LINE = 9;
257         int WHATSAPP = 10;
258         int GSA = 11;
259         int WEBAPK = 12;
260         int YAHOO_MAIL = 13;
261         int VIBER = 14;
262         int YOUTUBE = 15;
263         // Update ClientAppId in enums.xml when adding new items.
264         int NUM_ENTRIES = 16;
265     }
266 
getFakeComponentName(String packageName)267     private static ComponentName getFakeComponentName(String packageName) {
268         synchronized (LOCK) {
269             if (sFakeComponentName == null) {
270                 sFakeComponentName = new ComponentName(packageName, "FakeClass");
271             }
272         }
273 
274         return sFakeComponentName;
275     }
276 
277     /** Intent extra to open an incognito tab. */
278     public static final String EXTRA_OPEN_NEW_INCOGNITO_TAB =
279             "com.google.android.apps.chrome.EXTRA_OPEN_NEW_INCOGNITO_TAB";
280 
281     /** Schemes used by web pages to start up Chrome without an explicit Intent. */
282     public static final String GOOGLECHROME_SCHEME = "googlechrome";
283     public static final String GOOGLECHROME_NAVIGATE_PREFIX =
284             GOOGLECHROME_SCHEME + "://navigate?url=";
285 
286     private static boolean sTestIntentsEnabled;
287 
288     private final IntentHandlerDelegate mDelegate;
289     private final Activity mActivity;
290 
291     /**
292      * Receiver for screen unlock broadcast.
293      */
294     private static DelayedScreenLockIntentHandler sDelayedScreenIntentHandler;
295 
296     @IntDef({TabOpenType.OPEN_NEW_TAB, TabOpenType.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB,
297             TabOpenType.REUSE_APP_ID_MATCHING_TAB_ELSE_NEW_TAB, TabOpenType.CLOBBER_CURRENT_TAB,
298             TabOpenType.BRING_TAB_TO_FRONT, TabOpenType.OPEN_NEW_INCOGNITO_TAB,
299             TabOpenType.REUSE_TAB_MATCHING_ID_ELSE_NEW_TAB})
300     @Retention(RetentionPolicy.SOURCE)
301     public @interface TabOpenType {
302         int OPEN_NEW_TAB = 0;
303         // Tab is reused only if the URLs perfectly match.
304         int REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB = 1;
305         // Tab is reused only if there's an existing tab opened by the same app ID.
306         int REUSE_APP_ID_MATCHING_TAB_ELSE_NEW_TAB = 2;
307         int CLOBBER_CURRENT_TAB = 3;
308         int BRING_TAB_TO_FRONT = 4;
309         // Opens a new incognito tab.
310         int OPEN_NEW_INCOGNITO_TAB = 5;
311         // Tab is reused only if the tab ID exists (tab ID is specified with the integer extra
312         // REUSE_TAB_MATCHING_ID_STRING), and if the tab matches either the requested URL, or
313         // the URL provided in the REUSE_TAB_ORIGINAL_URL_STRING extra.
314         // Otherwise, the URL is opened in a new tab. REUSE_TAB_ORIGINAL_URL_STRING can be used if
315         // the intent url is a result of a redirect, so that a tab pointing at the original URL can
316         // be reused.
317         int REUSE_TAB_MATCHING_ID_ELSE_NEW_TAB = 6;
318 
319         String BRING_TAB_TO_FRONT_STRING = "BRING_TAB_TO_FRONT";
320         String REUSE_TAB_MATCHING_ID_STRING = "REUSE_TAB_MATCHING_ID";
321         String REUSE_TAB_ORIGINAL_URL_STRING = "REUSE_TAB_ORIGINAL_URL";
322     }
323 
324     /**
325      * A delegate interface for users of IntentHandler.
326      */
327     public static interface IntentHandlerDelegate {
328         /**
329          * Processes a URL VIEW Intent.
330          */
processUrlViewIntent(String url, String referer, String headers, @TabOpenType int tabOpenType, String externalAppId, int tabIdToBringToFront, boolean hasUserGesture, boolean isRendererInitiated, Origin initiatorOrigin, Intent intent)331         void processUrlViewIntent(String url, String referer, String headers,
332                 @TabOpenType int tabOpenType, String externalAppId, int tabIdToBringToFront,
333                 boolean hasUserGesture, boolean isRendererInitiated, Origin initiatorOrigin,
334                 Intent intent);
335 
processWebSearchIntent(String query)336         void processWebSearchIntent(String query);
337 
338         /**
339          * Processes a TRANSLATE_TAB intent.
340          * @param targetLanguageCode The language code that the page should be translated into.
341          *         Optional.
342          * @param expectedUrl The URL of the page that should be translated. If this doesn't match
343          *         the current tab, no translate will be performed.
344          */
processTranslateTabIntent( @ullable String targetLanguageCode, @Nullable String expectedUrl)345         void processTranslateTabIntent(
346                 @Nullable String targetLanguageCode, @Nullable String expectedUrl);
347     }
348 
349     /** Sets whether or not test intents are enabled. */
350     @VisibleForTesting
setTestIntentsEnabled(boolean enabled)351     public static void setTestIntentsEnabled(boolean enabled) {
352         sTestIntentsEnabled = enabled;
353     }
354 
IntentHandler(Activity activity, IntentHandlerDelegate delegate)355     public IntentHandler(Activity activity, IntentHandlerDelegate delegate) {
356         mDelegate = delegate;
357         mActivity = activity;
358     }
359 
360     /**
361      * Determines what App was used to fire this Intent.
362      * @param intent Intent that was used to launch Chrome.
363      * @return ExternalAppId representing the app.
364      */
determineExternalIntentSource(Intent intent)365     public static @ExternalAppId int determineExternalIntentSource(Intent intent) {
366         if (wasIntentSenderChrome(intent)) return ExternalAppId.CHROME;
367 
368         String appId = IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID);
369         @ExternalAppId
370         int externalId = ExternalAppId.OTHER;
371         if (appId == null) {
372             String url = getUrlFromIntent(intent);
373             String referrer = getReferrerUrl(intent);
374             if (url != null && url.startsWith(TWITTER_LINK_PREFIX)) {
375                 externalId = ExternalAppId.TWITTER;
376             } else if (FACEBOOK_REFERRER_URL.equals(referrer)) {
377                 // This happens when "Links Open Externally" is checked in the Facebook app.
378                 externalId = ExternalAppId.FACEBOOK;
379             } else if (url != null && url.startsWith(NEWS_LINK_PREFIX)) {
380                 externalId = ExternalAppId.NEWS;
381             } else if (url != null
382                     && (url.startsWith(YOUTUBE_LINK_PREFIX_HTTPS)
383                             || url.startsWith(YOUTUBE_LINK_PREFIX_HTTP))) {
384                 externalId = ExternalAppId.YOUTUBE;
385             } else {
386                 Bundle headers = IntentUtils.safeGetBundleExtra(intent, Browser.EXTRA_HEADERS);
387                 if (headers != null
388                         && FACEBOOK_INTERNAL_BROWSER_REFERRER.equals(headers.get("Referer"))) {
389                     // This happens when "Links Open Externally" is unchecked in the Facebook app,
390                     // and we use "Open With..." from the internal browser.
391                     externalId = ExternalAppId.FACEBOOK;
392                 }
393             }
394         } else {
395             externalId = mapPackageToExternalAppId(appId);
396         }
397         return externalId;
398     }
399 
400     /**
401      * Returns the appropriate entry of the ExteranAppId enum based on the supplied package name.
402      * @param packageName String The application package name to map.
403      * @return ExternalAppId representing the app.
404      */
mapPackageToExternalAppId(String packageName)405     public static @ExternalAppId int mapPackageToExternalAppId(String packageName) {
406         if (packageName.equals(PACKAGE_PLUS)) {
407             return ExternalAppId.PLUS;
408         } else if (packageName.equals(PACKAGE_GMAIL)) {
409             return ExternalAppId.GMAIL;
410         } else if (packageName.equals(PACKAGE_HANGOUTS)) {
411             return ExternalAppId.HANGOUTS;
412         } else if (packageName.equals(PACKAGE_MESSENGER)) {
413             return ExternalAppId.MESSENGER;
414         } else if (packageName.equals(PACKAGE_LINE)) {
415             return ExternalAppId.LINE;
416         } else if (packageName.equals(PACKAGE_WHATSAPP)) {
417             return ExternalAppId.WHATSAPP;
418         } else if (packageName.equals(PACKAGE_GSA)) {
419             return ExternalAppId.GSA;
420         } else if (packageName.equals(ContextUtils.getApplicationContext().getPackageName())) {
421             return ExternalAppId.CHROME;
422         } else if (packageName.startsWith(WEBAPK_PACKAGE_PREFIX)) {
423             return ExternalAppId.WEBAPK;
424         } else if (packageName.equals(PACKAGE_YAHOO_MAIL)) {
425             return ExternalAppId.YAHOO_MAIL;
426         } else if (packageName.equals(PACKAGE_VIBER)) {
427             return ExternalAppId.VIBER;
428         }
429         return ExternalAppId.OTHER;
430     }
431 
recordExternalIntentSourceUMA(Intent intent)432     private void recordExternalIntentSourceUMA(Intent intent) {
433         @ExternalAppId
434         int externalId = determineExternalIntentSource(intent);
435 
436         // Don't record external app page loads for intents we sent.
437         if (externalId == ExternalAppId.CHROME) return;
438         RecordHistogram.recordEnumeratedHistogram(
439                 "MobileIntent.PageLoadDueToExternalApp", externalId, ExternalAppId.NUM_ENTRIES);
440     }
441 
442     /**
443      * Records an action when a user chose to handle a URL in Chrome that could have been handled
444      * by an application installed on the phone. Also records the name of that application.
445      * This doesn't include generic URL handlers, such as browsers.
446      */
recordAppHandlersForIntent(Intent intent)447     private void recordAppHandlersForIntent(Intent intent) {
448         List<String> packages = IntentUtils.safeGetStringArrayListExtra(
449                 intent, ExternalNavigationHandler.EXTRA_EXTERNAL_NAV_PACKAGES);
450         if (packages != null && packages.size() > 0) {
451             RecordUserAction.record("MobileExternalNavigationReceived");
452         }
453     }
454 
updateDeferredIntent(Intent intent)455     private void updateDeferredIntent(Intent intent) {
456         if (sDelayedScreenIntentHandler == null && intent != null) {
457             sDelayedScreenIntentHandler = new DelayedScreenLockIntentHandler(mActivity);
458         }
459 
460         if (sDelayedScreenIntentHandler != null) {
461             sDelayedScreenIntentHandler.updateDeferredIntent(intent);
462         }
463     }
464 
465     /**
466      * Handles an Intent after the ChromeTabbedActivity decides that it shouldn't ignore the
467      * Intent.
468      * @param intent Target intent.
469      * @return Whether the Intent was successfully handled.
470      */
onNewIntent(Intent intent)471     public boolean onNewIntent(Intent intent) {
472         updateDeferredIntent(null);
473 
474         assert intentHasValidUrl(intent);
475         String url = getUrlFromIntent(intent);
476         RequestMetadata metadata =
477                 IntentWithRequestMetadataHandler.getInstance().getRequestMetadataAndClear(intent);
478         @TabOpenType
479         int tabOpenType = getTabOpenType(intent);
480         int tabIdToBringToFront = IntentUtils.safeGetIntExtra(
481                 intent, TabOpenType.BRING_TAB_TO_FRONT_STRING, Tab.INVALID_TAB_ID);
482         if (url == null && tabIdToBringToFront == Tab.INVALID_TAB_ID
483                 && tabOpenType != TabOpenType.OPEN_NEW_INCOGNITO_TAB) {
484             return handleWebSearchIntent(intent)
485                     || TranslateIntentHandler.handleTranslateTabIntent(intent, mDelegate);
486         }
487 
488         String referrerUrl = getReferrerUrlIncludingExtraHeaders(intent);
489         String extraHeaders = getExtraHeadersFromIntent(intent);
490 
491         if (isIntentForMhtmlFileOrContent(intent) && tabOpenType == TabOpenType.OPEN_NEW_TAB
492                 && referrerUrl == null && extraHeaders == null) {
493             handleMhtmlFileOrContentIntent(url, intent);
494             return true;
495         }
496 
497         processUrlViewIntent(url, referrerUrl, extraHeaders, tabOpenType,
498                 IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID),
499                 tabIdToBringToFront, metadata == null ? false : metadata.hasUserGesture(),
500                 metadata == null ? false : metadata.isRendererInitiated(),
501                 metadata == null ? null : metadata.getInitiatorOrigin(), intent);
502         return true;
503     }
504 
processUrlViewIntent(String url, String referrerUrl, String extraHeaders, @TabOpenType int tabOpenType, String externalAppId, int tabIdToBringToFront, boolean hasUserGesture, boolean isRendererInitiated, Origin initiatorOrigin, Intent intent)505     private void processUrlViewIntent(String url, String referrerUrl, String extraHeaders,
506             @TabOpenType int tabOpenType, String externalAppId, int tabIdToBringToFront,
507             boolean hasUserGesture, boolean isRendererInitiated, Origin initiatorOrigin,
508             Intent intent) {
509         extraHeaders = maybeAddAdditionalExtraHeaders(intent, url, extraHeaders);
510 
511         // TODO(joth): Presumably this should check the action too.
512         mDelegate.processUrlViewIntent(url, referrerUrl, extraHeaders, tabOpenType,
513                 IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID),
514                 tabIdToBringToFront, hasUserGesture, isRendererInitiated, initiatorOrigin, intent);
515         recordExternalIntentSourceUMA(intent);
516         recordAppHandlersForIntent(intent);
517     }
518 
519     /**
520      * Extracts referrer Uri from intent, if supplied.
521      * @param intent The intent to use.
522      * @return The referrer Uri.
523      */
getReferrer(Intent intent)524     private static Uri getReferrer(Intent intent) {
525         Uri referrer = IntentUtils.safeGetParcelableExtra(intent, Intent.EXTRA_REFERRER);
526         if (referrer != null) {
527             String pendingReferrer = IntentHandler.getPendingReferrerUrl(
528                     IntentUtils.safeGetIntExtra(intent, EXTRA_REFERRER_ID, 0));
529             return TextUtils.isEmpty(pendingReferrer) ? referrer : Uri.parse(pendingReferrer);
530         }
531         String referrerName = IntentUtils.safeGetStringExtra(intent, Intent.EXTRA_REFERRER_NAME);
532         if (referrerName != null) {
533             return Uri.parse(referrerName);
534         }
535         return null;
536     }
537 
538     /**
539      * Extracts referrer URL string. The extra is used if we received it from a first party app or
540      * if the referrer_extra is specified as android-app://package style URL.
541      * @param intent The intent from which to extract the URL.
542      * @return The URL string or null if none should be used.
543      */
getReferrerUrl(Intent intent)544     private static String getReferrerUrl(Intent intent) {
545         Uri referrerExtra = getReferrer(intent);
546         CustomTabsSessionToken customTabsSession =
547                 CustomTabsSessionToken.getSessionTokenFromIntent(intent);
548         if (referrerExtra == null && customTabsSession != null) {
549             Referrer referrer = CustomTabsConnection.getInstance().getDefaultReferrerForSession(
550                     customTabsSession);
551             if (referrer != null) {
552                 referrerExtra = Uri.parse(referrer.getUrl());
553             }
554         }
555 
556         if (referrerExtra == null) return null;
557         if (isValidReferrerHeader(referrerExtra)) {
558             return referrerExtra.toString();
559         } else if (IntentHandler.notSecureIsIntentChromeOrFirstParty(intent)
560                 || ChromeApplication.getComponent()
561                            .resolveSessionDataHolder()
562                            .canActiveHandlerUseReferrer(customTabsSession, referrerExtra)) {
563             return referrerExtra.toString();
564         }
565         return null;
566     }
567 
568     /**
569      * Gets the referrer, looking in the Intent extra and in the extra headers extra.
570      *
571      * The referrer extra takes priority over the "extra headers" one.
572      *
573      * @param intent The Intent containing the extras.
574      * @return The referrer, or null.
575      */
getReferrerUrlIncludingExtraHeaders(Intent intent)576     public static String getReferrerUrlIncludingExtraHeaders(Intent intent) {
577         String referrerUrl = getReferrerUrl(intent);
578         if (referrerUrl != null) return referrerUrl;
579 
580         Bundle bundleExtraHeaders = IntentUtils.safeGetBundleExtra(intent, Browser.EXTRA_HEADERS);
581         if (bundleExtraHeaders == null) return null;
582         for (String key : bundleExtraHeaders.keySet()) {
583             String value = bundleExtraHeaders.getString(key);
584             if (value != null && "referer".equals(key.toLowerCase(Locale.US))) {
585                 Uri referrer = Uri.parse(value).normalizeScheme();
586                 if (isValidReferrerHeader(referrer)) return referrer.toString();
587             }
588         }
589         return null;
590     }
591 
592     /**
593      * Add referrer and extra headers to a {@link LoadUrlParams}, if we managed to parse them from
594      * the intent.
595      * @param params The {@link LoadUrlParams} to add referrer and headers.
596      * @param intent The intent we use to parse the extras.
597      */
addReferrerAndHeaders(LoadUrlParams params, Intent intent)598     public static void addReferrerAndHeaders(LoadUrlParams params, Intent intent) {
599         String referrer = getReferrerUrlIncludingExtraHeaders(intent);
600         if (referrer != null) {
601             params.setReferrer(new Referrer(referrer, getReferrerPolicyFromIntent(intent)));
602         }
603         String headers = getExtraHeadersFromIntent(intent);
604         if (headers != null) params.setVerbatimHeaders(headers);
605     }
606 
getReferrerPolicyFromIntent(Intent intent)607     public static int getReferrerPolicyFromIntent(Intent intent) {
608         int policy =
609                 IntentUtils.safeGetIntExtra(intent, EXTRA_REFERRER_POLICY, ReferrerPolicy.DEFAULT);
610         if (policy < ReferrerPolicy.MIN_VALUE || policy >= ReferrerPolicy.MAX_VALUE) {
611             policy = ReferrerPolicy.DEFAULT;
612         }
613         return policy;
614     }
615 
616     /**
617      * @return Whether that the given referrer is of the format that Chrome allows external
618      * apps to specify.
619      */
isValidReferrerHeader(Uri referrer)620     private static boolean isValidReferrerHeader(Uri referrer) {
621         if (referrer == null) return false;
622         Uri normalized = referrer.normalizeScheme();
623         return TextUtils.equals(normalized.getScheme(), IntentUtils.ANDROID_APP_REFERRER_SCHEME)
624                 && !TextUtils.isEmpty(normalized.getHost());
625     }
626 
627     /**
628      * Constructs a valid referrer using the given authority.
629      * @param authority The authority to use.
630      * @return Referrer with default policy that uses the valid android app scheme, or null.
631      */
constructValidReferrerForAuthority(String authority)632     public static Referrer constructValidReferrerForAuthority(String authority) {
633         if (TextUtils.isEmpty(authority)) return null;
634         return new Referrer(new Uri.Builder()
635                                     .scheme(IntentUtils.ANDROID_APP_REFERRER_SCHEME)
636                                     .authority(authority)
637                                     .build()
638                                     .toString(),
639                 ReferrerPolicy.DEFAULT);
640     }
641 
642     /**
643      * Extracts the URL from voice search result intent.
644      * @return URL if it was found, null otherwise.
645      */
646     // TODO(https://crbug.com/783819): Investigate whether this function can return a GURL instead,
647     // or split into formatted/unformatted getUrl.
getUrlFromVoiceSearchResult(Intent intent)648     static String getUrlFromVoiceSearchResult(Intent intent) {
649         if (!RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS.equals(intent.getAction())) {
650             return null;
651         }
652         ArrayList<String> results = IntentUtils.safeGetStringArrayListExtra(
653                 intent, RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS);
654 
655         // Allow specifying a single voice result via the command line during testing (as the
656         // 'am' command does not allow specifying an array of strings).
657         if (results == null && sTestIntentsEnabled) {
658             String testResult = IntentUtils.safeGetStringExtra(
659                     intent, RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS);
660             if (testResult != null) {
661                 results = new ArrayList<String>();
662                 results.add(testResult);
663             }
664         }
665         // The logic in this method should be moved to ChromeTabbedActivity eventually. We should
666         // support async handling of voice search when native finishes initializing.
667         if (results == null || results.size() == 0
668                 || !BrowserStartupController.getInstance().isFullBrowserStarted()) {
669             return null;
670         }
671         String query = results.get(0);
672         String url = AutocompleteCoordinator.qualifyPartialURLQuery(query);
673         if (url == null) {
674             List<String> urls = IntentUtils.safeGetStringArrayListExtra(
675                     intent, RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_URLS);
676             if (urls != null && urls.size() > 0) {
677                 url = urls.get(0);
678             } else {
679                 url = TemplateUrlServiceFactory.get().getUrlForVoiceSearchQuery(query).getSpec();
680             }
681         }
682         return url;
683     }
684 
handleWebSearchIntent(Intent intent)685     public boolean handleWebSearchIntent(Intent intent) {
686         if (intent == null) return false;
687 
688         String query = null;
689         final String action = intent.getAction();
690         if (Intent.ACTION_SEARCH.equals(action)
691                 || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)) {
692             query = IntentUtils.safeGetStringExtra(intent, SearchManager.QUERY);
693         }
694 
695         if (query == null || TextUtils.isEmpty(query)) return false;
696 
697         mDelegate.processWebSearchIntent(query);
698         return true;
699     }
700 
handleMhtmlFileOrContentIntent(final String url, final Intent intent)701     private void handleMhtmlFileOrContentIntent(final String url, final Intent intent) {
702         OfflinePageUtils.getLoadUrlParamsForOpeningMhtmlFileOrContent(url, (loadUrlParams) -> {
703             processUrlViewIntent(loadUrlParams.getUrl(), null, loadUrlParams.getVerbatimHeaders(),
704                     TabOpenType.OPEN_NEW_TAB, null, 0, false, false, null, intent);
705         }, Profile.getLastUsedRegularProfile());
706     }
707 
getAuthenticationToken()708     private static PendingIntent getAuthenticationToken() {
709         Intent fakeIntent = new Intent();
710         Context appContext = ContextUtils.getApplicationContext();
711         fakeIntent.setComponent(getFakeComponentName(appContext.getPackageName()));
712         return PendingIntent.getActivity(appContext, 0, fakeIntent, 0);
713     }
714 
715     /**
716      * Start activity for the given trusted Intent.
717      *
718      * To make sure the intent is not dropped by Chrome, we send along an authentication token to
719      * identify ourselves as a trusted sender. The method {@link #shouldIgnoreIntent} validates the
720      * token.
721      */
startActivityForTrustedIntent(Intent intent)722     public static void startActivityForTrustedIntent(Intent intent) {
723         startActivityForTrustedIntentInternal(intent, null);
724     }
725 
726     /**
727      * Start the activity that handles launching tabs in Chrome given the trusted intent.
728      *
729      * This allows specifying URLs that chrome:// handles internally, but does not expose in
730      * intent-filters for global use.
731      *
732      * To make sure the intent is not dropped by Chrome, we send along an authentication token to
733      * identify ourselves as a trusted sender. The method {@link #shouldIgnoreIntent} validates the
734      * token.
735      */
startChromeLauncherActivityForTrustedIntent(Intent intent)736     public static void startChromeLauncherActivityForTrustedIntent(Intent intent) {
737         // Specify the exact component that will handle creating a new tab.  This allows specifying
738         // URLs that are not exposed in the intent filters (i.e. chrome://).
739         startActivityForTrustedIntentInternal(intent, ChromeLauncherActivity.class.getName());
740     }
741 
startActivityForTrustedIntentInternal( Intent intent, String componentClassName)742     private static void startActivityForTrustedIntentInternal(
743             Intent intent, String componentClassName) {
744         Context appContext = ContextUtils.getApplicationContext();
745         // The caller might want to re-use the Intent, so we'll use a copy.
746         Intent copiedIntent = new Intent(intent);
747 
748         if (componentClassName != null) {
749             assert copiedIntent.getComponent() == null;
750             // Specify the exact component that will handle creating a new tab.  This allows
751             // specifying URLs that are not exposed in the intent filters (i.e. chrome://).
752             copiedIntent.setComponent(
753                     new ComponentName(appContext.getPackageName(), componentClassName));
754         }
755 
756         // Because we are starting this activity from the application context, we need
757         // FLAG_ACTIVITY_NEW_TASK on pre-N versions of Android.  On N+ we can get away with
758         // specifying a task ID or not specifying an options bundle.
759         assert (copiedIntent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0;
760         addTrustedIntentExtras(copiedIntent);
761         appContext.startActivity(copiedIntent);
762     }
763 
764     /**
765      * Sets TRUSTED_APPLICATION_CODE_EXTRA on the provided intent to identify it as coming from
766      * a trusted source.
767      */
addTrustedIntentExtras(Intent intent)768     public static void addTrustedIntentExtras(Intent intent) {
769         if (ExternalNavigationDelegateImpl.willChromeHandleIntent(intent, true)) {
770             addTrustedIntentExtrasInternal(intent);
771         }
772     }
773 
774     @VisibleForTesting
addTrustedIntentExtrasInternal(Intent intent)775     static void addTrustedIntentExtrasInternal(Intent intent) {
776         // It is crucial that we never leak the authentication token to other packages, because
777         // then the other package could be used to impersonate us/do things as us. Therefore,
778         // scope the real Intent to our package.
779         intent.setPackage(ContextUtils.getApplicationContext().getPackageName());
780         // The PendingIntent functions as an authentication token --- it could only have come
781         // from us. Stash it in the real Intent as an extra. shouldIgnoreIntent will retrieve it
782         // and check it with isIntentChromeInternal.
783         intent.putExtra(TRUSTED_APPLICATION_CODE_EXTRA, getAuthenticationToken());
784     }
785 
786     /**
787      * Sets the Extra field 'EXTRA_HEADERS' on intent. If |extraHeaders| is empty or null,
788      * removes 'EXTRA_HEADERS' from intent.
789      *
790      * @param extraHeaders   A map containing the set of headers. May be null.
791      * @param intent         The intent to modify.
792      */
setIntentExtraHeaders( @ullable Map<String, String> extraHeaders, Intent intent)793     public static void setIntentExtraHeaders(
794             @Nullable Map<String, String> extraHeaders, Intent intent) {
795         if (extraHeaders == null || extraHeaders.isEmpty()) {
796             intent.removeExtra(Browser.EXTRA_HEADERS);
797         } else {
798             Bundle bundle = new Bundle();
799             for (Map.Entry<String, String> header : extraHeaders.entrySet()) {
800                 bundle.putString(header.getKey(), header.getValue());
801             }
802             intent.putExtra(Browser.EXTRA_HEADERS, bundle);
803         }
804     }
805 
806     /**
807      * Returns a String (or null) containing the extra headers sent by the intent, if any.
808      *
809      * This methods skips the referrer header.
810      *
811      * @param intent The intent containing the bundle extra with the HTTP headers.
812      */
getExtraHeadersFromIntent(Intent intent)813     public static String getExtraHeadersFromIntent(Intent intent) {
814         Bundle bundleExtraHeaders = IntentUtils.safeGetBundleExtra(intent, Browser.EXTRA_HEADERS);
815         if (bundleExtraHeaders == null) return null;
816         StringBuilder extraHeaders = new StringBuilder();
817 
818         boolean fromChrome = IntentHandler.wasIntentSenderChrome(intent);
819         boolean shouldAllowNonSafelistedHeaders =
820                 CustomTabsConnection.getInstance().isFirstPartyOriginForIntent(intent);
821 
822         for (String key : bundleExtraHeaders.keySet()) {
823             String value = bundleExtraHeaders.getString(key);
824 
825             if (!HttpUtil.isAllowedHeader(key, value)) {
826                 Log.w(TAG, "Ignoring forbidden header " + key + " in EXTRA_HEADERS.");
827             }
828 
829             // Strip the custom header that can only be added by ourselves.
830             if ("x-chrome-intent-type".equals(key.toLowerCase(Locale.US))) continue;
831 
832             if (!fromChrome) {
833                 if (key.toLowerCase(Locale.US).startsWith("x-chrome-")) {
834                     Log.w(TAG, "Ignoring x-chrome header " + key + " in EXTRA_HEADERS.");
835                     continue;
836                 }
837 
838                 if (!shouldAllowNonSafelistedHeaders
839                         && !IntentHandlerJni.get().isCorsSafelistedHeader(key, value)) {
840                     Log.w(TAG, "Ignoring non-CORS-safelisted header " + key + " in EXTRA_HEADERS.");
841                     continue;
842                 }
843             }
844 
845             if (extraHeaders.length() != 0) extraHeaders.append("\n");
846             extraHeaders.append(key);
847             extraHeaders.append(": ");
848             extraHeaders.append(value);
849         }
850 
851         return extraHeaders.length() == 0 ? null : extraHeaders.toString();
852     }
853 
854     /**
855      * Adds a timestamp to an intent, as returned by {@link SystemClock#elapsedRealtime()}.
856      *
857      * To track page load time, this needs to be called as close as possible to
858      * the entry point (in {@link Activity#onCreate()} for instance).
859      */
addTimestampToIntent(Intent intent)860     public static void addTimestampToIntent(Intent intent) {
861         addTimestampToIntent(intent, SystemClock.elapsedRealtime());
862     }
863 
864     /**
865      * Adds provided timestamp to an intent.
866      *
867      * To track page load time, the value passed in should be as close as possible to
868      * the entry point (in {@link Activity#onCreate()} for instance).
869      */
addTimestampToIntent(Intent intent, long timeStamp)870     public static void addTimestampToIntent(Intent intent, long timeStamp) {
871         intent.putExtra(EXTRA_TIMESTAMP_MS, timeStamp);
872     }
873 
874     /**
875      * @return the timestamp associated with an intent, or -1.
876      */
getTimestampFromIntent(Intent intent)877     public static long getTimestampFromIntent(Intent intent) {
878         return intent.getLongExtra(EXTRA_TIMESTAMP_MS, -1);
879     }
880 
881     /**
882      * Returns true if the app should ignore a given intent.
883      *
884      * @param intent Intent to check.
885      * @param startedActivity True if the Activity was not running prior to receiving the Intent.
886      * @return true if the intent should be ignored.
887      */
shouldIgnoreIntent(Intent intent, boolean startedActivity)888     public boolean shouldIgnoreIntent(Intent intent, boolean startedActivity) {
889         // Although not documented to, many/most methods that retrieve values from an Intent may
890         // throw. Because we can't control what packages might send to us, we should catch any
891         // Throwable and then fail closed (safe). This is ugly, but resolves top crashers in the
892         // wild.
893         try {
894             // Ignore all invalid URLs, regardless of what the intent was.
895             if (!intentHasValidUrl(intent)) {
896                 return true;
897             }
898 
899             // Determine if this intent came from a trustworthy source (either Chrome or Google
900             // first party applications).
901             boolean isInternal = notSecureIsIntentChromeOrFirstParty(intent);
902             boolean isFromChrome = wasIntentSenderChrome(intent);
903 
904             // "Open new incognito tab" is currently limited to Chrome.
905             //
906             // The pending incognito URL check is to handle the case where the user is shown an
907             // Android intent picker while in incognito and they select the current Chrome instance
908             // from the list.  In this case, we do not apply our Chrome token as the user has the
909             // option to select apps outside of our control, so we rely on this in memory check
910             // instead.
911             if (!isFromChrome
912                     && IntentUtils.safeGetBooleanExtra(
913                             intent, EXTRA_OPEN_NEW_INCOGNITO_TAB, false)
914                     && (getPendingIncognitoUrl() == null
915                             || !getPendingIncognitoUrl().equals(intent.getDataString()))) {
916                 return true;
917             }
918 
919             // Now if we have an empty URL and the intent was ACTION_MAIN,
920             // we are pretty sure it is the launcher calling us to show up.
921             // We can safely ignore the screen state.
922             String url = getUrlFromIntent(intent);
923             if (url == null && Intent.ACTION_MAIN.equals(intent.getAction())) {
924                 return false;
925             }
926 
927             // Ignore Translate intents if they were the intent that started the activity.
928             if (startedActivity && intent != null
929                     && TranslateIntentHandler.ACTION_TRANSLATE_TAB.equals(intent.getAction())) {
930                 return true;
931             }
932 
933             // Ignore all intents that specify a Chrome internal scheme if they did not come from
934             // a trustworthy source.
935             String scheme = getSanitizedUrlScheme(url);
936             recordFirstPartyToInternalScheme(scheme, url, intent, isInternal, isFromChrome);
937             if (!isInternal) {
938                 if (intentHasUnsafeInternalScheme(scheme, url, intent)) {
939                     Log.w(TAG, "Ignoring internal Chrome URL from untrustworthy source.");
940                     return true;
941                 }
942 
943                 return false;
944             }
945 
946             // We must check for screen state at this point.
947             // These might be slow.
948             boolean internalOrVisible = isInternal || isIntentUserVisible();
949             if (!internalOrVisible) {
950                 updateDeferredIntent(intent);
951                 return true;
952             }
953             return false;
954         } catch (Throwable t) {
955             return true;
956         }
957     }
958 
intentHasUnsafeInternalScheme(String scheme, String url, Intent intent)959     private static boolean intentHasUnsafeInternalScheme(String scheme, String url, Intent intent) {
960         if (scheme != null
961                 && (intent.hasCategory(Intent.CATEGORY_BROWSABLE)
962                         || intent.hasCategory(Intent.CATEGORY_DEFAULT)
963                         || intent.getCategories() == null)) {
964             String lowerCaseScheme = scheme.toLowerCase(Locale.US);
965             if (UrlConstants.CHROME_SCHEME.equals(lowerCaseScheme)
966                     || UrlConstants.CHROME_NATIVE_SCHEME.equals(lowerCaseScheme)
967                     || ContentUrlConstants.ABOUT_SCHEME.equals(lowerCaseScheme)) {
968                 // Allow certain "safe" internal URLs to be launched by external
969                 // applications.
970                 String lowerCaseUrl = url.toLowerCase(Locale.US);
971                 if (ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL.equals(lowerCaseUrl)
972                         || ContentUrlConstants.ABOUT_BLANK_URL.equals(lowerCaseUrl)
973                         || UrlConstants.CHROME_DINO_URL.equals(lowerCaseUrl)) {
974                     return false;
975                 }
976 
977                 return true;
978             }
979         }
980         return false;
981     }
982 
983     @VisibleForTesting
intentHasValidUrl(Intent intent)984     static boolean intentHasValidUrl(Intent intent) {
985         String url = extractUrlFromIntent(intent);
986 
987         // Check if this is a valid googlechrome:// URL.
988         if (isGoogleChromeScheme(url)) {
989             url = getUrlFromGoogleChromeSchemeUrl(url);
990             if (url == null) return false;
991         }
992 
993         // Always drop insecure urls.
994         if (url != null && isJavascriptSchemeOrInvalidUrl(url)) {
995             return false;
996         }
997 
998         return true;
999     }
1000 
1001     /**
1002      * Fetch the authentication token (a PendingIntent) created by startActivityForTrustedIntent,
1003      * if any. If anything goes wrong trying to retrieve the token (examples include
1004      * BadParcelableException or ClassNotFoundException), fail closed.
1005      */
fetchAuthenticationTokenFromIntent(Intent intent)1006     private static PendingIntent fetchAuthenticationTokenFromIntent(Intent intent) {
1007         return (PendingIntent) IntentUtils.safeGetParcelableExtra(
1008                 intent, TRUSTED_APPLICATION_CODE_EXTRA);
1009     }
1010 
isChromeToken(PendingIntent token)1011     private static boolean isChromeToken(PendingIntent token) {
1012         // Fetch what should be a matching token.
1013         PendingIntent pending = getAuthenticationToken();
1014         return pending.equals(token);
1015     }
1016 
1017     /**
1018      * @param intent An Intent to be checked.
1019      * @return Whether an intent originates from Chrome.
1020      */
wasIntentSenderChrome(Intent intent)1021     public static boolean wasIntentSenderChrome(Intent intent) {
1022         if (intent == null) return false;
1023 
1024         PendingIntent token = fetchAuthenticationTokenFromIntent(intent);
1025         if (token == null) return false;
1026 
1027         // Do not ignore a valid URL Intent if the sender is Chrome. (If the PendingIntents are
1028         // equal, we know that the sender was us.)
1029         return isChromeToken(token);
1030     }
1031 
1032     /**
1033      * Attempts to verify that an Intent was sent from either Chrome or a first-
1034      * party app by evaluating a PendingIntent token within the passed Intent.
1035      *
1036      * This method of verifying first-party apps is not secure, as it is not
1037      * possible to determine the sender of an Intent. This method only verifies
1038      * the creator of the PendingIntent token. But a malicious app may be able
1039      * to obtain a PendingIntent from another application and use it to
1040      * masquerade as it for the purposes of this check. Do not use this method.
1041      *
1042      * @param intent An Intent to be checked.
1043      * @return Whether an intent originates from Chrome or a first-party app.
1044      *
1045      * @deprecated This method is not reliable, see https://crbug.com/832124
1046      */
1047     @Deprecated
notSecureIsIntentChromeOrFirstParty(Intent intent)1048     public static boolean notSecureIsIntentChromeOrFirstParty(Intent intent) {
1049         if (intent == null) return false;
1050 
1051         PendingIntent token = fetchAuthenticationTokenFromIntent(intent);
1052         if (token == null) return false;
1053 
1054         // Do not ignore a valid URL Intent if the sender is Chrome. (If the PendingIntents are
1055         // equal, we know that the sender was us.)
1056         if (isChromeToken(token)) {
1057             return true;
1058         }
1059         if (AppHooks.get().getExternalAuthUtils().isGoogleSigned(token.getCreatorPackage())) {
1060             return true;
1061         }
1062         return false;
1063     }
1064 
1065     @VisibleForTesting
isIntentUserVisible()1066     static boolean isIntentUserVisible() {
1067         // Only process Intents if the screen is on and the device is unlocked;
1068         // i.e. the user will see what is going on.
1069         Context appContext = ContextUtils.getApplicationContext();
1070         PowerManager powerManager =
1071                 (PowerManager) appContext.getSystemService(Context.POWER_SERVICE);
1072 
1073         if (!powerManager.isInteractive()) return false;
1074         if (!isDeviceProvisioned(appContext)) return true;
1075 
1076         return !((KeyguardManager) appContext.getSystemService(Context.KEYGUARD_SERVICE))
1077                 .inKeyguardRestrictedInputMode();
1078     }
1079 
isDeviceProvisioned(Context context)1080     private static boolean isDeviceProvisioned(Context context) {
1081         if (context == null || context.getContentResolver() == null) return true;
1082         return Settings.Global.getInt(
1083                        context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0)
1084                 != 0;
1085     }
1086 
1087     /*
1088      * The default behavior here is to open in a new tab.  If this is changed, ensure
1089      * intents with action NDEF_DISCOVERED (links beamed over NFC) are handled properly.
1090      */
getTabOpenType(Intent intent)1091     private @TabOpenType int getTabOpenType(Intent intent) {
1092         if (IntentUtils.safeGetBooleanExtra(
1093                     intent, ShortcutHelper.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB, false)) {
1094             return TabOpenType.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB;
1095         }
1096         if (IntentUtils.safeGetBooleanExtra(intent, EXTRA_OPEN_NEW_INCOGNITO_TAB, false)) {
1097             return TabOpenType.OPEN_NEW_INCOGNITO_TAB;
1098         }
1099         if (IntentUtils.safeGetIntExtra(
1100                     intent, TabOpenType.BRING_TAB_TO_FRONT_STRING, Tab.INVALID_TAB_ID)
1101                 != Tab.INVALID_TAB_ID) {
1102             return TabOpenType.BRING_TAB_TO_FRONT;
1103         }
1104 
1105         String appId = IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID);
1106         // Due to users complaints, we are NOT reusing tabs for apps that do not specify an appId.
1107         if (appId == null
1108                 || IntentUtils.safeGetBooleanExtra(intent, Browser.EXTRA_CREATE_NEW_TAB, false)) {
1109             return TabOpenType.OPEN_NEW_TAB;
1110         }
1111 
1112         int tabId = IntentUtils.safeGetIntExtra(
1113                 intent, TabOpenType.REUSE_TAB_MATCHING_ID_STRING, Tab.INVALID_TAB_ID);
1114         if (tabId != Tab.INVALID_TAB_ID) {
1115             return TabOpenType.REUSE_TAB_MATCHING_ID_ELSE_NEW_TAB;
1116         }
1117 
1118         // Intents from chrome open in the same tab by default, all others only clobber
1119         // tabs created by the same app.
1120         return mActivity.getPackageName().equals(appId)
1121                 ? TabOpenType.CLOBBER_CURRENT_TAB
1122                 : TabOpenType.REUSE_APP_ID_MATCHING_TAB_ELSE_NEW_TAB;
1123     }
1124 
isInvalidScheme(String scheme)1125     private static boolean isInvalidScheme(String scheme) {
1126         return scheme != null
1127             && (scheme.toLowerCase(Locale.US).equals(UrlConstants.JAVASCRIPT_SCHEME)
1128                 || scheme.toLowerCase(Locale.US).equals(UrlConstants.JAR_SCHEME));
1129     }
1130 
1131     /**
1132      * Parses the scheme out of the URL if possible, trimming and getting rid of unsafe characters.
1133      * This is useful for determining if a URL has a sneaky, unsafe scheme, e.g. "java  script" or
1134      * "j$a$r". See: http://crbug.com/248398
1135      * @return The sanitized URL scheme or null if no scheme is specified.
1136      */
getSanitizedUrlScheme(String url)1137     private static String getSanitizedUrlScheme(String url) {
1138         if (url == null) {
1139             return null;
1140         }
1141 
1142         int colonIdx = url.indexOf(":");
1143         if (colonIdx < 0) {
1144             // No scheme specified for the url
1145             return null;
1146         }
1147 
1148         String scheme = url.substring(0, colonIdx).toLowerCase(Locale.US).trim();
1149 
1150         // Check for the presence of and get rid of all non-alphanumeric characters in the scheme,
1151         // except dash, plus and period. Those are the only valid scheme chars:
1152         // https://tools.ietf.org/html/rfc3986#section-3.1
1153         boolean nonAlphaNum = false;
1154         for (char ch : scheme.toCharArray()) {
1155             if (!Character.isLetterOrDigit(ch) && ch != '-' && ch != '+' && ch != '.') {
1156                 nonAlphaNum = true;
1157                 break;
1158             }
1159         }
1160 
1161         if (nonAlphaNum) {
1162             scheme = scheme.replaceAll("[^a-z0-9.+-]", "");
1163         }
1164         return scheme;
1165     }
1166 
isJavascriptSchemeOrInvalidUrl(String url)1167     private static boolean isJavascriptSchemeOrInvalidUrl(String url) {
1168         String urlScheme = getSanitizedUrlScheme(url);
1169         return isInvalidScheme(urlScheme);
1170     }
1171 
1172     /**
1173      * Retrieve the URL from the Intent, which may be in multiple locations.
1174      * If the URL is googlechrome:// scheme, parse the actual navigation URL.
1175      * @param intent Intent to examine.
1176      * @return URL from the Intent, or null if a valid URL couldn't be found.
1177      */
getUrlFromIntent(Intent intent)1178     public static String getUrlFromIntent(Intent intent) {
1179         String url = extractUrlFromIntent(intent);
1180         if (isGoogleChromeScheme(url)) {
1181             url = getUrlFromGoogleChromeSchemeUrl(url);
1182         }
1183         return url;
1184     }
1185 
1186     /**
1187      * Helper method to extract the raw URL from the intent, without further processing.
1188      * The URL may be in multiple locations.
1189      * @param intent Intent to examine.
1190      * @return Raw URL from the intent, or null if raw URL could't be found.
1191      */
extractUrlFromIntent(Intent intent)1192     private static String extractUrlFromIntent(Intent intent) {
1193         if (intent == null) return null;
1194         String url = getUrlFromVoiceSearchResult(intent);
1195         if (url == null) url = getUrlForCustomTab(intent);
1196         if (url == null) url = getUrlForWebapp(intent);
1197         if (url == null) url = intent.getDataString();
1198         if (url == null) return null;
1199         url = url.trim();
1200         return TextUtils.isEmpty(url) ? null : url;
1201     }
1202 
getUrlForCustomTab(Intent intent)1203     private static String getUrlForCustomTab(Intent intent) {
1204         if (intent == null || intent.getData() == null) return null;
1205         Uri data = intent.getData();
1206         return TextUtils.equals(data.getScheme(), UrlConstants.CUSTOM_TAB_SCHEME)
1207                 ? data.getQuery() : null;
1208     }
1209 
getUrlForWebapp(Intent intent)1210     private static String getUrlForWebapp(Intent intent) {
1211         if (intent == null || intent.getData() == null) return null;
1212         Uri data = intent.getData();
1213         return TextUtils.equals(data.getScheme(), WebappActivity.WEBAPP_SCHEME)
1214                 ? IntentUtils.safeGetStringExtra(intent, ShortcutHelper.EXTRA_URL)
1215                 : null;
1216     }
1217 
1218     @VisibleForTesting
maybeAddAdditionalExtraHeaders(Intent intent, String url, String extraHeaders)1219     static String maybeAddAdditionalExtraHeaders(Intent intent, String url, String extraHeaders) {
1220         // For some apps, ContentResolver.getType(contentUri) returns "application/octet-stream",
1221         // instead of the registered MIME type when opening a document from Downloads. To work
1222         // around this, we pass the intent type in extra headers such that content request job can
1223         // get it.
1224         if (intent == null || url == null) return extraHeaders;
1225 
1226         String scheme = getSanitizedUrlScheme(url);
1227         if (!TextUtils.equals(scheme, UrlConstants.CONTENT_SCHEME)) return extraHeaders;
1228 
1229         String type = intent.getType();
1230         if (type == null || type.isEmpty()) return extraHeaders;
1231 
1232         // Only override the type for MHTML related types, which some applications get wrong.
1233         if (!isMhtmlMimeType(type)) return extraHeaders;
1234 
1235         String typeHeader = "X-Chrome-intent-type: " + type;
1236         return (extraHeaders == null) ? typeHeader : (extraHeaders + "\n" + typeHeader);
1237     }
1238 
1239     /** Return true if the type is one of the Mime types used for MHTML */
isMhtmlMimeType(String type)1240     static boolean isMhtmlMimeType(String type) {
1241         return type.equals("multipart/related") || type.equals("message/rfc822");
1242     }
1243 
1244     /**
1245      * @param intent An Intent to be checked.
1246      * @return Whether the intent has an file:// or content:// URL with MHTML MIME type.
1247      */
1248     @VisibleForTesting
isIntentForMhtmlFileOrContent(Intent intent)1249     static boolean isIntentForMhtmlFileOrContent(Intent intent) {
1250         String url = getUrlFromIntent(intent);
1251         if (url == null) return false;
1252         String scheme = getSanitizedUrlScheme(url);
1253         boolean isContentUriScheme = TextUtils.equals(scheme, UrlConstants.CONTENT_SCHEME);
1254         boolean isFileUriScheme = TextUtils.equals(scheme, UrlConstants.FILE_SCHEME);
1255         if (!isContentUriScheme && !isFileUriScheme) return false;
1256         String type = intent.getType();
1257         if (type != null && isMhtmlMimeType(type)) {
1258             return true;
1259         }
1260         // Note that "application/octet-stream" type may be passed by some apps that do not know
1261         // about MHTML file types.
1262         if (!isFileUriScheme
1263                 || (!TextUtils.isEmpty(type) && !type.equals("application/octet-stream"))) {
1264             return false;
1265         }
1266 
1267         // Get the file extension. We can't use MimeTypeMap.getFileExtensionFromUrl because it will
1268         // reject urls with characters that are valid in filenames (such as "!").
1269         String extension = FileUtils.getExtension(url);
1270 
1271         return extension.equals("mhtml") || extension.equals("mht");
1272     }
1273 
1274     /**
1275      * Adjusts the URL to account for the googlechrome:// scheme.
1276      * Currently, its only use is to handle navigations, only http and https URL is allowed.
1277      * @param url URL to be processed
1278      * @return The string with the scheme and prefixes chopped off, if a valid prefix was used.
1279      *         Otherwise returns null.
1280      */
getUrlFromGoogleChromeSchemeUrl(String url)1281     public static String getUrlFromGoogleChromeSchemeUrl(String url) {
1282         if (url.toLowerCase(Locale.US).startsWith(GOOGLECHROME_NAVIGATE_PREFIX)) {
1283             String parsedUrl = url.substring(GOOGLECHROME_NAVIGATE_PREFIX.length());
1284             if (!TextUtils.isEmpty(parsedUrl)) {
1285                 String scheme = getSanitizedUrlScheme(parsedUrl);
1286                 if (scheme == null) {
1287                     // If no scheme, assuming this is an http url.
1288                     parsedUrl = UrlConstants.HTTP_URL_PREFIX + parsedUrl;
1289                 }
1290             }
1291             if (UrlUtilities.isHttpOrHttps(parsedUrl)) return parsedUrl;
1292         }
1293 
1294         return null;
1295     }
1296 
1297     /**
1298      * @param url URL to be tested
1299      * @return Whether the given URL adheres to the googlechrome:// scheme definition.
1300      */
isGoogleChromeScheme(String url)1301     public static boolean isGoogleChromeScheme(String url) {
1302         if (url == null) return false;
1303         String urlScheme = Uri.parse(url).getScheme();
1304         return urlScheme != null && urlScheme.equals(GOOGLECHROME_SCHEME);
1305     }
1306 
1307     // TODO(mariakhomenko): pending referrer and pending incognito intent could potentially
1308     // not work correctly in multi-window. Store per-window information instead.
1309 
1310     /**
1311      * Records a pending referrer URL that we may be sending to ourselves through an intent.
1312      * @param intent The intent to which we add a referrer.
1313      * @param url The referrer URL.
1314      */
setPendingReferrer(Intent intent, String url)1315     public static void setPendingReferrer(Intent intent, String url) {
1316         intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse(url));
1317         intent.putExtra(IntentHandler.EXTRA_REFERRER_ID, ++sReferrerId);
1318         sPendingReferrer = new Pair<Integer, String>(sReferrerId, url);
1319     }
1320 
1321     /**
1322      * Clears any pending referrer data.
1323      */
clearPendingReferrer()1324     public static void clearPendingReferrer() {
1325         sPendingReferrer = null;
1326     }
1327 
1328     /**
1329      * Retrieves pending referrer URL based on the given id.
1330      * @param id The referrer id.
1331      * @return The URL for the referrer or null if none found.
1332      */
getPendingReferrerUrl(int id)1333     public static String getPendingReferrerUrl(int id) {
1334         if (sPendingReferrer != null && (sPendingReferrer.first == id)) {
1335             return sPendingReferrer.second;
1336         }
1337         return null;
1338     }
1339 
1340     /**
1341      * Keeps track of pending incognito URL to be loaded and ensures we allow to load it if it
1342      * comes back to us. This is a method for dispatching incognito URL intents from Chrome that
1343      * may or may not end up in Chrome.
1344      * @param intent The intent that will be sent.
1345      */
setPendingIncognitoUrl(Intent intent)1346     public static void setPendingIncognitoUrl(Intent intent) {
1347         if (intent.getData() != null) {
1348             intent.putExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, true);
1349             sPendingIncognitoUrl = intent.getDataString();
1350         }
1351     }
1352 
1353     /**
1354      * Clears the pending incognito URL.
1355      */
clearPendingIncognitoUrl()1356     public static void clearPendingIncognitoUrl() {
1357         sPendingIncognitoUrl = null;
1358     }
1359 
1360     /**
1361      * @return Pending incognito URL that is allowed to be loaded without system token.
1362      */
getPendingIncognitoUrl()1363     public static String getPendingIncognitoUrl() {
1364         return sPendingIncognitoUrl;
1365     }
1366 
1367     /**
1368      * Some applications may request to load the URL with a particular transition type.
1369      * @param intent Intent causing the URL load, may be null.
1370      * @param defaultTransition The transition to return if none specified in the intent.
1371      * @return The transition type to use for loading the URL.
1372      */
getTransitionTypeFromIntent(Intent intent, int defaultTransition)1373     public static int getTransitionTypeFromIntent(Intent intent, int defaultTransition) {
1374         if (intent == null) return defaultTransition;
1375         int transitionType = IntentUtils.safeGetIntExtra(
1376                 intent, IntentHandler.EXTRA_PAGE_TRANSITION_TYPE, PageTransition.LINK);
1377         if (transitionType == PageTransition.TYPED) {
1378             return transitionType;
1379         } else if (transitionType != PageTransition.LINK
1380                 && notSecureIsIntentChromeOrFirstParty(intent)) {
1381             // 1st party applications may specify any transition type.
1382             return transitionType;
1383         }
1384         return defaultTransition;
1385     }
1386 
1387     /**
1388      * Sets the launch type in a tab creation intent.
1389      * @param intent The Intent to be set.
1390      */
setTabLaunchType(Intent intent, @TabLaunchType int type)1391     public static void setTabLaunchType(Intent intent, @TabLaunchType int type) {
1392         intent.putExtra(EXTRA_TAB_LAUNCH_TYPE, type);
1393     }
1394 
1395     /**
1396      * @param intent An Intent to be checked.
1397      * @return The launch type of the tab to be created.
1398      */
getTabLaunchType(Intent intent)1399     public static @Nullable @TabLaunchType Integer getTabLaunchType(Intent intent) {
1400         return IntentUtils.safeGetSerializableExtra(intent, EXTRA_TAB_LAUNCH_TYPE);
1401     }
1402 
1403     /**
1404      * Creates an Intent that will launch a ChromeTabbedActivity on the new tab page. The Intent
1405      * will be trusted and therefore able to launch Incognito tabs.
1406      * @param context A {@link Context} to access class and package information.
1407      * @param incognito Whether the tab should be opened in Incognito.
1408      * @return The {@link Intent} to launch.
1409      */
createTrustedOpenNewTabIntent(Context context, boolean incognito)1410     public static Intent createTrustedOpenNewTabIntent(Context context, boolean incognito) {
1411         Intent newIntent = new Intent();
1412         newIntent.setAction(Intent.ACTION_VIEW);
1413         newIntent.setData(Uri.parse(UrlConstants.NTP_URL));
1414         newIntent.setClass(context, ChromeLauncherActivity.class);
1415         newIntent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true);
1416         newIntent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
1417         newIntent.putExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, incognito);
1418         IntentHandler.addTrustedIntentExtras(newIntent);
1419 
1420         return newIntent;
1421     }
1422 
1423     /**
1424      * Records whether the intent comes from a non-Chrome first party and contains a Chrome internal
1425      * scheme. This is so we can determine whether we can cut the feature.
1426      */
recordFirstPartyToInternalScheme( String scheme, String url, Intent intent, boolean isInternal, boolean isChrome)1427     private static void recordFirstPartyToInternalScheme(
1428             String scheme, String url, Intent intent, boolean isInternal, boolean isChrome) {
1429         if (!isInternal || isChrome) return;
1430 
1431         RecordHistogram.recordBooleanHistogram("MobileIntent.FirstPartyToInternalScheme",
1432                 intentHasUnsafeInternalScheme(scheme, url, intent));
1433     }
1434 
1435     @NativeMethods
1436     interface Natives {
isCorsSafelistedHeader(String name, String value)1437         boolean isCorsSafelistedHeader(String name, String value);
1438     }
1439 }
1440