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