// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser; import android.app.KeyguardManager; import android.app.PendingIntent; import android.app.SearchManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; import android.provider.Browser; import android.provider.MediaStore; import android.speech.RecognizerResultsIntent; import android.text.TextUtils; import android.util.Pair; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.Log; import org.chromium.base.VisibleForTesting; import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordUserAction; import org.chromium.chrome.browser.externalauth.ExternalAuthUtils; import org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl; import org.chromium.chrome.browser.externalnav.IntentWithGesturesHandler; import org.chromium.chrome.browser.omnibox.AutocompleteController; import org.chromium.chrome.browser.rappor.RapporServiceBridge; import org.chromium.chrome.browser.search_engines.TemplateUrlService; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tabmodel.document.ActivityDelegate; import org.chromium.chrome.browser.util.IntentUtils; import org.chromium.content_public.browser.LoadUrlParams; import org.chromium.content_public.common.Referrer; import org.chromium.ui.base.PageTransition; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; /** * Handles all browser-related Intents. */ public class IntentHandler { private static final String TAG = "IntentHandler"; /** * Document mode: If true, Chrome is launched into the same Task. * Note: used by first-party applications, do not rename. */ public static final String EXTRA_APPEND_TASK = "com.android.chrome.append_task"; /** * Document mode: If true, keep tasks in Recents when a user hits back at the root URL. * Note: used by first-party applications, do not rename. */ public static final String EXTRA_PRESERVE_TASK = "com.android.chrome.preserve_task"; /** * Document mode: If true, opens the document in background. * Note: used by first-party applications, do not rename. */ public static final String EXTRA_OPEN_IN_BG = "com.android.chrome.open_with_affiliation"; /** * Document mode: Records what caused a document to be created. */ public static final String EXTRA_STARTED_BY = "com.android.chrome.started_by"; /** * Tab ID to use when creating a new Tab. */ public static final String EXTRA_TAB_ID = "com.android.chrome.tab_id"; /** * The tab id of the parent tab, if any. */ public static final String EXTRA_PARENT_TAB_ID = "com.android.chrome.parent_tab_id"; /** * Intent to bring the parent Activity back, if the parent Tab lives in a different Activity. */ public static final String EXTRA_PARENT_INTENT = "com.android.chrome.parent_intent"; /** * ComponentName of the parent Activity. Can be used by an Activity launched on top of another * Activity (e.g. BookmarkActivity) to intent back into the Activity it sits on top of. */ public static final String EXTRA_PARENT_COMPONENT = "org.chromium.chrome.browser.parent_component"; /** * Transition type is only set internally by a first-party app and has to be signed. */ public static final String EXTRA_PAGE_TRANSITION_TYPE = "com.google.chrome.transition_type"; /** * The original intent of the given intent before it was modified. */ public static final String EXTRA_ORIGINAL_INTENT = "com.android.chrome.original_intent"; /** * An extra to indicate that a particular intent was triggered from the first run experience * flow. */ public static final String EXTRA_INVOKED_FROM_FRE = "com.android.chrome.invoked_from_fre"; /** * An extra to indicate that the intent was triggered from a launcher shortcut. */ public static final String EXTRA_INVOKED_FROM_SHORTCUT = "com.android.chrome.invoked_from_shortcut"; /** * Intent extra used to identify the sending application. */ private static final String TRUSTED_APPLICATION_CODE_EXTRA = "trusted_application_code_extra"; /** * The scheme for referrer coming from an application. */ public static final String ANDROID_APP_REFERRER_SCHEME = "android-app"; /** * A referrer id used for Chrome to Chrome referrer passing. */ public static final String EXTRA_REFERRER_ID = "org.chromium.chrome.browser.referrer_id"; /** * Key to associate a timestamp with an intent. */ private static final String EXTRA_TIMESTAMP_MS = "org.chromium.chrome.browser.timestamp"; /** * For multi-window, passes the id of the window. */ public static final String EXTRA_WINDOW_ID = "org.chromium.chrome.browser.window_id"; /** * Records package names of other applications in the system that could have handled * this intent. */ public static final String EXTRA_EXTERNAL_NAV_PACKAGES = "org.chromium.chrome.browser.eenp"; /** * A hash code for the URL to verify intent data hasn't been modified. */ public static final String EXTRA_DATA_HASH_CODE = "org.chromium.chrome.browser.data_hash"; /** * Fake ComponentName used in constructing TRUSTED_APPLICATION_CODE_EXTRA. */ private static ComponentName sFakeComponentName = null; private static final Object LOCK = new Object(); private static Pair<Integer, String> sPendingReferrer; private static int sReferrerId; private static String sPendingIncognitoUrl; private static final String PACKAGE_GSA = "com.google.android.googlequicksearchbox"; private static final String PACKAGE_GMAIL = "com.google.android.gm"; private static final String PACKAGE_PLUS = "com.google.android.apps.plus"; private static final String PACKAGE_HANGOUTS = "com.google.android.talk"; private static final String PACKAGE_MESSENGER = "com.google.android.apps.messaging"; private static final String PACKAGE_LINE = "jp.naver.line.android"; private static final String PACKAGE_WHATSAPP = "com.whatsapp"; private static final String FACEBOOK_LINK_PREFIX = "http://m.facebook.com/l.php?"; private static final String TWITTER_LINK_PREFIX = "http://t.co/"; private static final String NEWS_LINK_PREFIX = "http://news.google.com/news/url?"; /** * Represents popular external applications that can load a page in Chrome via intent. */ public static enum ExternalAppId { OTHER, GMAIL, FACEBOOK, PLUS, TWITTER, CHROME, HANGOUTS, MESSENGER, NEWS, LINE, WHATSAPP, GSA, INDEX_BOUNDARY } private static ComponentName getFakeComponentName(String packageName) { synchronized (LOCK) { if (sFakeComponentName == null) { sFakeComponentName = new ComponentName(packageName, "FakeClass"); } } return sFakeComponentName; } /** Intent extra to open an incognito tab. */ public static final String EXTRA_OPEN_NEW_INCOGNITO_TAB = "com.google.android.apps.chrome.EXTRA_OPEN_NEW_INCOGNITO_TAB"; /** Schemes used by web pages to start up Chrome without an explicit Intent. */ public static final String GOOGLECHROME_SCHEME = "googlechrome"; public static final String GOOGLECHROME_NAVIGATE_PREFIX = GOOGLECHROME_SCHEME + "://navigate?url="; /** * The class name to be specified in the ComponentName for Intents that are creating a new * tab (regardless of whether the user is in document or tabbed mode). */ // TODO(tedchoc): Remove this and directly reference the Launcher activity when that becomes // publicly available. private static final String TAB_ACTIVITY_COMPONENT_CLASS_NAME = "com.google.android.apps.chrome.Main"; private static boolean sTestIntentsEnabled; private final IntentHandlerDelegate mDelegate; private final String mPackageName; private KeyguardManager mKeyguardManager; public static enum TabOpenType { OPEN_NEW_TAB, // Tab is reused only if the URLs perfectly match. REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB, // Tab is reused only if there's an existing tab opened by the same app ID. REUSE_APP_ID_MATCHING_TAB_ELSE_NEW_TAB, CLOBBER_CURRENT_TAB, BRING_TAB_TO_FRONT, // Opens a new incognito tab. OPEN_NEW_INCOGNITO_TAB, } /** * A delegate interface for users of IntentHandler. */ public static interface IntentHandlerDelegate { /** * Processes a URL VIEW Intent. */ void processUrlViewIntent(String url, String referer, String headers, TabOpenType tabOpenType, String externalAppId, int tabIdToBringToFront, boolean hasUserGesture, Intent intent); void processWebSearchIntent(String query); } /** Sets whether or not test intents are enabled. */ @VisibleForTesting public static void setTestIntentsEnabled(boolean enabled) { sTestIntentsEnabled = enabled; } public IntentHandler(IntentHandlerDelegate delegate, String packageName) { mDelegate = delegate; mPackageName = packageName; } /** * Determines what App was used to fire this Intent. * @param packageName Package name of this application. * @param intent Intent that was used to launch Chrome. * @return ExternalAppId representing the app. */ public static ExternalAppId determineExternalIntentSource(String packageName, Intent intent) { String appId = IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID); ExternalAppId externalId = ExternalAppId.OTHER; if (appId == null) { String url = getUrlFromIntent(intent); if (url != null && url.startsWith(TWITTER_LINK_PREFIX)) { externalId = ExternalAppId.TWITTER; } else if (url != null && url.startsWith(FACEBOOK_LINK_PREFIX)) { externalId = ExternalAppId.FACEBOOK; } else if (url != null && url.startsWith(NEWS_LINK_PREFIX)) { externalId = ExternalAppId.NEWS; } } else { if (appId.equals(PACKAGE_PLUS)) { externalId = ExternalAppId.PLUS; } else if (appId.equals(PACKAGE_GMAIL)) { externalId = ExternalAppId.GMAIL; } else if (appId.equals(PACKAGE_HANGOUTS)) { externalId = ExternalAppId.HANGOUTS; } else if (appId.equals(PACKAGE_MESSENGER)) { externalId = ExternalAppId.MESSENGER; } else if (appId.equals(PACKAGE_LINE)) { externalId = ExternalAppId.LINE; } else if (appId.equals(PACKAGE_WHATSAPP)) { externalId = ExternalAppId.WHATSAPP; } else if (appId.equals(PACKAGE_GSA)) { externalId = ExternalAppId.GSA; } else if (appId.equals(packageName)) { externalId = ExternalAppId.CHROME; } } return externalId; } private void recordExternalIntentSourceUMA(Intent intent) { ExternalAppId externalId = determineExternalIntentSource(mPackageName, intent); RecordHistogram.recordEnumeratedHistogram("MobileIntent.PageLoadDueToExternalApp", externalId.ordinal(), ExternalAppId.INDEX_BOUNDARY.ordinal()); if (externalId == ExternalAppId.OTHER) { String appId = IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID); if (!TextUtils.isEmpty(appId)) { RapporServiceBridge.sampleString("Android.PageLoadDueToExternalApp", appId); } } } /** * Records an action when a user chose to handle a URL in Chrome that could have been handled * by an application installed on the phone. Also records the name of that application. * This doesn't include generic URL handlers, such as browsers. */ private void recordAppHandlersForIntent(Intent intent) { List<String> packages = IntentUtils.safeGetStringArrayListExtra(intent, IntentHandler.EXTRA_EXTERNAL_NAV_PACKAGES); if (packages != null && packages.size() > 0) { RecordUserAction.record("MobileExternalNavigationReceived"); for (String name : packages) { RapporServiceBridge.sampleString("Android.ExternalNavigationNotChosen", name); } } } /** * Handles an Intent after the ChromeTabbedActivity decides that it shouldn't ignore the * Intent. * * @return Whether the Intent was successfully handled. */ boolean onNewIntent(Context context, Intent intent) { assert intentHasValidUrl(intent); String url = getUrlFromIntent(intent); boolean hasUserGesture = IntentWithGesturesHandler.getInstance().getUserGestureAndClear(intent); TabOpenType tabOpenType = getTabOpenType(intent); int tabIdToBringToFront = IntentUtils.safeGetIntExtra( intent, TabOpenType.BRING_TAB_TO_FRONT.name(), Tab.INVALID_TAB_ID); if (url == null && tabIdToBringToFront == Tab.INVALID_TAB_ID && tabOpenType != TabOpenType.OPEN_NEW_INCOGNITO_TAB) { return handleWebSearchIntent(intent); } String referrerUrl = getReferrerUrlIncludingExtraHeaders(intent, context); String extraHeaders = getExtraHeadersFromIntent(intent); // TODO(joth): Presumably this should check the action too. mDelegate.processUrlViewIntent(url, referrerUrl, extraHeaders, tabOpenType, IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID), tabIdToBringToFront, hasUserGesture, intent); recordExternalIntentSourceUMA(intent); recordAppHandlersForIntent(intent); return true; } /** * Extracts referrer Uri from intent, if supplied. * @param intent The intent to use. * @return The referrer Uri. */ private static Uri getReferrer(Intent intent) { Uri referrer = IntentUtils.safeGetParcelableExtra(intent, Intent.EXTRA_REFERRER); if (referrer != null) { return referrer; } String referrerName = IntentUtils.safeGetStringExtra(intent, Intent.EXTRA_REFERRER_NAME); if (referrerName != null) { return Uri.parse(referrerName); } return null; } /** * Extracts referrer URL string. The extra is used if we received it from a first party app or * if the referrer_extra is specified as android-app://package style URL. * @param intent The intent from which to extract the URL. * @param context The activity that received the intent. * @return The URL string or null if none should be used. */ private static String getReferrerUrl(Intent intent, Context context) { Uri referrerExtra = getReferrer(intent); if (referrerExtra == null) return null; String referrerUrl = IntentHandler.getPendingReferrerUrl( IntentUtils.safeGetIntExtra(intent, EXTRA_REFERRER_ID, 0)); if (!TextUtils.isEmpty(referrerUrl)) { return referrerUrl; } else if (isValidReferrerHeader(referrerExtra.toString())) { return referrerExtra.toString(); } else if (IntentHandler.isIntentChromeOrFirstParty(intent, context)) { return referrerExtra.toString(); } return null; } /** * Gets the referrer, looking in the Intent extra and in the extra headers extra. * * The referrer extra takes priority over the "extra headers" one. * * @param intent The Intent containing the extras. * @param context The application context. * @return The referrer, or null. */ public static String getReferrerUrlIncludingExtraHeaders(Intent intent, Context context) { String referrerUrl = getReferrerUrl(intent, context); if (referrerUrl != null) return referrerUrl; Bundle bundleExtraHeaders = IntentUtils.safeGetBundleExtra(intent, Browser.EXTRA_HEADERS); if (bundleExtraHeaders == null) return null; for (String key : bundleExtraHeaders.keySet()) { String value = bundleExtraHeaders.getString(key); if ("referer".equals(key.toLowerCase(Locale.US)) && isValidReferrerHeader(value)) { return value; } } return null; } /** * Add referrer and extra headers to a {@link LoadUrlParams}, if we managed to parse them from * the intent. * @param params The {@link LoadUrlParams} to add referrer and headers. * @param intent The intent we use to parse the extras. */ public static void addReferrerAndHeaders(LoadUrlParams params, Intent intent, Context context) { String referrer = getReferrerUrlIncludingExtraHeaders(intent, context); if (referrer != null) { params.setReferrer(new Referrer(referrer, Referrer.REFERRER_POLICY_DEFAULT)); } String headers = getExtraHeadersFromIntent(intent); if (headers != null) params.setVerbatimHeaders(headers); } /** * @return Whether that the given referrer is of the format that Chrome allows external * apps to specify. */ private static boolean isValidReferrerHeader(String referrer) { return referrer != null && referrer.toLowerCase(Locale.US).startsWith(ANDROID_APP_REFERRER_SCHEME + "://"); } /** * Constructs a valid referrer using the given authority. * @param authority The authority to use. * @return Referrer with default policy that uses the valid android app scheme. */ public static Referrer constructValidReferrerForAuthority(String authority) { return new Referrer(new Uri.Builder().scheme(ANDROID_APP_REFERRER_SCHEME) .authority(authority).build().toString(), Referrer.REFERRER_POLICY_DEFAULT); } /** * Extracts the URL from voice search result intent. * @return URL if it was found, null otherwise. */ static String getUrlFromVoiceSearchResult(Intent intent) { if (!RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS.equals(intent.getAction())) { return null; } ArrayList<String> results = IntentUtils.safeGetStringArrayListExtra( intent, RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS); // Allow specifying a single voice result via the command line during testing (as the // 'am' command does not allow specifying an array of strings). if (results == null && sTestIntentsEnabled) { String testResult = IntentUtils.safeGetStringExtra( intent, RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS); if (testResult != null) { results = new ArrayList<String>(); results.add(testResult); } } if (results == null || results.size() == 0) return null; String query = results.get(0); String url = AutocompleteController.nativeQualifyPartialURLQuery(query); if (url == null) { List<String> urls = IntentUtils.safeGetStringArrayListExtra( intent, RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_URLS); if (urls != null && urls.size() > 0) { url = urls.get(0); } else { url = TemplateUrlService.getInstance().getUrlForVoiceSearchQuery(query); } } return url; } public boolean handleWebSearchIntent(Intent intent) { if (intent == null) return false; String query = null; final String action = intent.getAction(); if (Intent.ACTION_SEARCH.equals(action) || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)) { query = IntentUtils.safeGetStringExtra(intent, SearchManager.QUERY); } if (query == null || TextUtils.isEmpty(query)) return false; mDelegate.processWebSearchIntent(query); return true; } private static PendingIntent getAuthenticationToken(Context appContext) { Intent fakeIntent = new Intent(); fakeIntent.setComponent(getFakeComponentName(appContext.getPackageName())); return PendingIntent.getActivity(appContext, 0, fakeIntent, 0); } /** * Start activity for the given trusted Intent. * * To make sure the intent is not dropped by Chrome, we send along an authentication token to * identify ourselves as a trusted sender. The method {@link #shouldIgnoreIntent} validates the * token. */ public static void startActivityForTrustedIntent(Intent intent, Context context) { startActivityForTrustedIntentInternal(intent, context, null); } /** * Start the activity that handles launching tabs in Chrome given the trusted intent. * * This allows specifying URLs that chrome:// handles internally, but does not expose in * intent-filters for global use. * * To make sure the intent is not dropped by Chrome, we send along an authentication token to * identify ourselves as a trusted sender. The method {@link #shouldIgnoreIntent} validates the * token. */ public static void startChromeLauncherActivityForTrustedIntent(Intent intent, Context context) { // Specify the exact component that will handle creating a new tab. This allows specifying // URLs that are not exposed in the intent filters (i.e. chrome://). startActivityForTrustedIntentInternal(intent, context, new ComponentName( context.getPackageName(), TAB_ACTIVITY_COMPONENT_CLASS_NAME)); } private static void startActivityForTrustedIntentInternal( Intent intent, Context context, ComponentName componentName) { // The caller might want to re-use the Intent, so we'll use a copy. Intent copiedIntent = new Intent(intent); if (componentName != null) { assert copiedIntent.getComponent() == null; // Specify the exact component that will handle creating a new tab. This allows // specifying URLs that are not exposed in the intent filters (i.e. chrome://). copiedIntent.setComponent(componentName); } addTrustedIntentExtras(copiedIntent, context); // Make sure we use the application context. Context appContext = context.getApplicationContext(); appContext.startActivity(copiedIntent); } /** * Sets TRUSTED_APPLICATION_CODE_EXTRA on the provided intent to identify it as coming from * a trusted source. */ public static void addTrustedIntentExtras(Intent intent, Context context) { if (ExternalNavigationDelegateImpl.willChromeHandleIntent(context, intent, true)) { // The PendingIntent functions as an authentication token --- it could only have come // from us. Stash it in the real Intent as an extra. shouldIgnoreIntent will retrieve it // and check it with isIntentChromeInternal. intent.putExtra(TRUSTED_APPLICATION_CODE_EXTRA, getAuthenticationToken(context.getApplicationContext())); // It is crucial that we never leak the authentication token to other packages, because // then the other package could be used to impersonate us/do things as us. Therefore, // scope the real Intent to our package. intent.setPackage(context.getApplicationContext().getPackageName()); } } /** * Returns a String (or null) containing the extra headers sent by the intent, if any. * * This methods skips the referrer header. * * @param intent The intent containing the bundle extra with the HTTP headers. */ public static String getExtraHeadersFromIntent(Intent intent) { Bundle bundleExtraHeaders = IntentUtils.safeGetBundleExtra(intent, Browser.EXTRA_HEADERS); if (bundleExtraHeaders == null) return null; StringBuilder extraHeaders = new StringBuilder(); Iterator<String> keys = bundleExtraHeaders.keySet().iterator(); while (keys.hasNext()) { String key = keys.next(); String value = bundleExtraHeaders.getString(key); if ("referer".equals(key.toLowerCase(Locale.US))) continue; if (extraHeaders.length() != 0) extraHeaders.append("\n"); extraHeaders.append(key); extraHeaders.append(": "); extraHeaders.append(value); } return extraHeaders.length() == 0 ? null : extraHeaders.toString(); } /** * Adds a timestamp to an intent, as returned by {@link SystemClock#elapsedRealtime()}. * * To track page load time, this needs to be called as close as possible to * the entry point (in {@link Activity#onCreate()} for instance). */ public static void addTimestampToIntent(Intent intent) { intent.putExtra(EXTRA_TIMESTAMP_MS, SystemClock.elapsedRealtime()); } /** * @return the timestamp associated with an intent, or -1. */ public static long getTimestampFromIntent(Intent intent) { return intent.getLongExtra(EXTRA_TIMESTAMP_MS, -1); } /** * Returns true if the app should ignore a given intent. * * @param context Android Context. * @param intent Intent to check. * @return true if the intent should be ignored. */ public boolean shouldIgnoreIntent(Context context, Intent intent) { // Although not documented to, many/most methods that retrieve values from an Intent may // throw. Because we can't control what packages might send to us, we should catch any // Throwable and then fail closed (safe). This is ugly, but resolves top crashers in the // wild. try { // Ignore all invalid URLs, regardless of what the intent was. if (!intentHasValidUrl(intent)) { return true; } // Determine if this intent came from a trustworthy source (either Chrome or Google // first party applications). boolean isInternal = isIntentChromeOrFirstParty(intent, context); // "Open new incognito tab" is currently limited to Chrome or first parties. if (!isInternal && IntentUtils.safeGetBooleanExtra( intent, EXTRA_OPEN_NEW_INCOGNITO_TAB, false) && (getPendingIncognitoUrl() == null || !getPendingIncognitoUrl().equals(intent.getDataString()))) { return true; } // Now if we have an empty URL and the intent was ACTION_MAIN, // we are pretty sure it is the launcher calling us to show up. // We can safely ignore the screen state. String url = getUrlFromIntent(intent); if (url == null && Intent.ACTION_MAIN.equals(intent.getAction())) { return false; } // Ignore all intents that specify a Chrome internal scheme if they did not come from // a trustworthy source. String scheme = getSanitizedUrlScheme(url); if (!isInternal && scheme != null && (intent.hasCategory(Intent.CATEGORY_BROWSABLE) || intent.hasCategory(Intent.CATEGORY_DEFAULT) || intent.getCategories() == null)) { String lowerCaseScheme = scheme.toLowerCase(Locale.US); if ("chrome".equals(lowerCaseScheme) || "chrome-native".equals(lowerCaseScheme) || "about".equals(lowerCaseScheme)) { // Allow certain "safe" internal URLs to be launched by external // applications. String lowerCaseUrl = url.toLowerCase(Locale.US); if ("about:blank".equals(lowerCaseUrl) || "about://blank".equals(lowerCaseUrl)) { return false; } Log.w(TAG, "Ignoring internal Chrome URL from untrustworthy source."); return true; } } // We must check for screen state at this point. // These might be slow. boolean internalOrVisible = isInternal || isIntentUserVisible(context); return !internalOrVisible; } catch (Throwable t) { return true; } } @VisibleForTesting boolean intentHasValidUrl(Intent intent) { String url = getUrlFromIntent(intent); // Always drop insecure urls. if (url != null && isJavascriptSchemeOrInvalidUrl(url)) { return false; } return true; } /** * Fetch the authentication token (a PendingIntent) created by startActivityForTrustedIntent, * if any. If anything goes wrong trying to retrieve the token (examples include * BadParcelableException or ClassNotFoundException), fail closed. */ private static PendingIntent fetchAuthenticationTokenFromIntent(Intent intent) { return (PendingIntent) IntentUtils.safeGetParcelableExtra( intent, TRUSTED_APPLICATION_CODE_EXTRA); } private static boolean isChromeToken(PendingIntent token, Context context) { // Fetch what should be a matching token. Context appContext = context.getApplicationContext(); PendingIntent pending = getAuthenticationToken(appContext); return pending.equals(token); } /** * @param intent An Intent to be checked. * @param context A context. * @return Whether an intent originates from Chrome. */ public static boolean wasIntentSenderChrome(Intent intent, Context context) { if (intent == null) return false; PendingIntent token = fetchAuthenticationTokenFromIntent(intent); if (token == null) return false; // Do not ignore a valid URL Intent if the sender is Chrome. (If the PendingIntents are // equal, we know that the sender was us.) return isChromeToken(token, context); } /** * @param intent An Intent to be checked. * @param context A context. * @return Whether an intent originates from Chrome or a first-party app. */ public static boolean isIntentChromeOrFirstParty(Intent intent, Context context) { if (intent == null) return false; PendingIntent token = fetchAuthenticationTokenFromIntent(intent); if (token == null) return false; // Do not ignore a valid URL Intent if the sender is Chrome. (If the PendingIntents are // equal, we know that the sender was us.) if (isChromeToken(token, context)) { return true; } if (ExternalAuthUtils.getInstance().isGoogleSigned( context, ApiCompatibilityUtils.getCreatorPackage(token))) { return true; } return false; } private boolean isIntentUserVisible(Context context) { // Only process Intents if the screen is on and the device is unlocked; // i.e. the user will see what is going on. if (mKeyguardManager == null) { mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); } if (!ApiCompatibilityUtils.isInteractive(context)) return false; return !ApiCompatibilityUtils.isDeviceProvisioned(context) || !mKeyguardManager.inKeyguardRestrictedInputMode(); } /* * The default behavior here is to open in a new tab. If this is changed, ensure * intents with action NDEF_DISCOVERED (links beamed over NFC) are handled properly. */ private TabOpenType getTabOpenType(Intent intent) { if (IntentUtils.safeGetBooleanExtra( intent, ShortcutHelper.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB, false)) { return TabOpenType.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB; } if (IntentUtils.safeGetBooleanExtra(intent, EXTRA_OPEN_NEW_INCOGNITO_TAB, false)) { return TabOpenType.OPEN_NEW_INCOGNITO_TAB; } if (IntentUtils.safeGetIntExtra(intent, TabOpenType.BRING_TAB_TO_FRONT.name(), Tab.INVALID_TAB_ID) != Tab.INVALID_TAB_ID) { return TabOpenType.BRING_TAB_TO_FRONT; } String appId = IntentUtils.safeGetStringExtra(intent, Browser.EXTRA_APPLICATION_ID); // Due to users complaints, we are NOT reusing tabs for apps that do not specify an appId. if (appId == null || IntentUtils.safeGetBooleanExtra(intent, Browser.EXTRA_CREATE_NEW_TAB, false)) { return TabOpenType.OPEN_NEW_TAB; } // Intents from chrome open in the same tab by default, all others only clobber // tabs created by the same app. return mPackageName.equals(appId) ? TabOpenType.CLOBBER_CURRENT_TAB : TabOpenType.REUSE_APP_ID_MATCHING_TAB_ELSE_NEW_TAB; } private boolean isInvalidScheme(String scheme) { return scheme != null && (scheme.toLowerCase(Locale.US).equals("javascript") || scheme.toLowerCase(Locale.US).equals("jar")); } /** * Parses the scheme out of the URL if possible, trimming and getting rid of unsafe characters. * This is useful for determining if a URL has a sneaky, unsafe scheme, e.g. "java script" or * "j$a$r". See: http://crbug.com/248398 * @return The sanitized URL scheme or null if no scheme is specified. */ private String getSanitizedUrlScheme(String url) { if (url == null) { return null; } int colonIdx = url.indexOf(":"); if (colonIdx < 0) { // No scheme specified for the url return null; } String scheme = url.substring(0, colonIdx).toLowerCase(Locale.US).trim(); // Check for the presence of and get rid of all non-alphanumeric characters in the scheme, // except dash, plus and period. Those are the only valid scheme chars: // https://tools.ietf.org/html/rfc3986#section-3.1 boolean nonAlphaNum = false; for (char ch : scheme.toCharArray()) { if (!Character.isLetterOrDigit(ch) && ch != '-' && ch != '+' && ch != '.') { nonAlphaNum = true; break; } } if (nonAlphaNum) { scheme = scheme.replaceAll("[^a-z0-9.+-]", ""); } return scheme; } private boolean isJavascriptSchemeOrInvalidUrl(String url) { String urlScheme = getSanitizedUrlScheme(url); return isInvalidScheme(urlScheme); } /** * Retrieve the URL from the Intent, which may be in multiple locations. * @param intent Intent to examine. * @return URL from the Intent, or null if a valid URL couldn't be found. */ public static String getUrlFromIntent(Intent intent) { if (intent == null) return null; String url = getUrlFromVoiceSearchResult(intent); if (url == null) url = ActivityDelegate.getInitialUrlForDocument(intent); if (url == null) url = getUrlForCustomTab(intent); if (url == null) url = intent.getDataString(); if (url == null) return null; url = url.trim(); if (isGoogleChromeScheme(url)) { url = getUrlFromGoogleChromeSchemeUrl(url); } return TextUtils.isEmpty(url) ? null : url; } private static String getUrlForCustomTab(Intent intent) { if (intent == null || intent.getData() == null) return null; Uri data = intent.getData(); return TextUtils.equals(data.getScheme(), UrlConstants.CUSTOM_TAB_SCHEME) ? data.getQuery() : null; } /** * Adjusts the URL to account for the googlechrome:// scheme. * Currently, its only use is to handle navigations. * @param url URL to be processed * @return The string with the scheme and prefixes chopped off, if a valid prefix was used. * Otherwise returns null. */ public static String getUrlFromGoogleChromeSchemeUrl(String url) { if (url.toLowerCase(Locale.US).startsWith(GOOGLECHROME_NAVIGATE_PREFIX)) { return url.substring(GOOGLECHROME_NAVIGATE_PREFIX.length()); } return null; } /** * @param url URL to be tested * @return Whether the given URL adheres to the googlechrome:// scheme definition. */ public static boolean isGoogleChromeScheme(String url) { if (url == null) return false; String urlScheme = Uri.parse(url).getScheme(); return urlScheme != null && urlScheme.equals(GOOGLECHROME_SCHEME); } // TODO(mariakhomenko): pending referrer and pending incognito intent could potentially // not work correctly in multi-window. Store per-window information instead. /** * Records a pending referrer URL that we may be sending to ourselves through an intent. * @param intent The intent to which we add a referrer. * @param url The referrer URL. */ public static void setPendingReferrer(Intent intent, String url) { intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse(url)); intent.putExtra(IntentHandler.EXTRA_REFERRER_ID, ++sReferrerId); sPendingReferrer = new Pair<Integer, String>(sReferrerId, url); } /** * Clears any pending referrer data. */ public static void clearPendingReferrer() { sPendingReferrer = null; } /** * Retrieves pending referrer URL based on the given id. * @param id The referrer id. * @return The URL for the referrer or null if none found. */ public static String getPendingReferrerUrl(int id) { if (sPendingReferrer != null && (sPendingReferrer.first == id)) { return sPendingReferrer.second; } return null; } /** * Keeps track of pending incognito URL to be loaded and ensures we allow to load it if it * comes back to us. This is a method for dispatching incognito URL intents from Chrome that * may or may not end up in Chrome. * @param intent The intent that will be sent. */ public static void setPendingIncognitoUrl(Intent intent) { if (intent.getData() != null) { intent.putExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, true); sPendingIncognitoUrl = intent.getDataString(); } } /** * Clears the pending incognito URL. */ public static void clearPendingIncognitoUrl() { sPendingIncognitoUrl = null; } /** * @return Pending incognito URL that is allowed to be loaded without system token. */ public static String getPendingIncognitoUrl() { return sPendingIncognitoUrl; } /** * Some applications may request to load the URL with a particular transition type. * @param context The application context. * @param intent Intent causing the URL load, may be null. * @param defaultTransition The transition to return if none specified in the intent. * @return The transition type to use for loading the URL. */ public static int getTransitionTypeFromIntent(Context context, Intent intent, int defaultTransition) { if (intent == null) return defaultTransition; int transitionType = IntentUtils.safeGetIntExtra( intent, IntentHandler.EXTRA_PAGE_TRANSITION_TYPE, PageTransition.LINK); if (transitionType == PageTransition.TYPED) { return transitionType; } else if (transitionType != PageTransition.LINK && isIntentChromeOrFirstParty(intent, context)) { // 1st party applications may specify any transition type. return transitionType; } return defaultTransition; } }