1 // Copyright 2012 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 #include "chrome/browser/ui/search/search_tab_helper.h" 6 7 #include <set> 8 9 #include "base/memory/scoped_ptr.h" 10 #include "base/metrics/histogram.h" 11 #include "base/strings/string16.h" 12 #include "base/strings/string_util.h" 13 #include "build/build_config.h" 14 #include "chrome/browser/history/most_visited_tiles_experiment.h" 15 #include "chrome/browser/history/top_sites.h" 16 #include "chrome/browser/profiles/profile.h" 17 #include "chrome/browser/search/instant_service.h" 18 #include "chrome/browser/search/instant_service_factory.h" 19 #include "chrome/browser/search/search.h" 20 #include "chrome/browser/signin/signin_manager.h" 21 #include "chrome/browser/signin/signin_manager_factory.h" 22 #include "chrome/browser/ui/app_list/app_list_util.h" 23 #include "chrome/browser/ui/browser.h" 24 #include "chrome/browser/ui/browser_finder.h" 25 #include "chrome/browser/ui/browser_navigator.h" 26 #include "chrome/browser/ui/browser_window.h" 27 #include "chrome/browser/ui/omnibox/location_bar.h" 28 #include "chrome/browser/ui/omnibox/omnibox_edit_model.h" 29 #include "chrome/browser/ui/omnibox/omnibox_popup_model.h" 30 #include "chrome/browser/ui/omnibox/omnibox_view.h" 31 #include "chrome/browser/ui/search/search_ipc_router_policy_impl.h" 32 #include "chrome/browser/ui/tabs/tab_strip_model.h" 33 #include "chrome/browser/ui/tabs/tab_strip_model_utils.h" 34 #include "chrome/browser/ui/webui/ntp/ntp_user_data_logger.h" 35 #include "chrome/common/url_constants.h" 36 #include "content/public/browser/navigation_controller.h" 37 #include "content/public/browser/navigation_details.h" 38 #include "content/public/browser/navigation_entry.h" 39 #include "content/public/browser/navigation_type.h" 40 #include "content/public/browser/render_process_host.h" 41 #include "content/public/browser/user_metrics.h" 42 #include "content/public/browser/web_contents.h" 43 #include "content/public/browser/web_contents_view.h" 44 #include "content/public/common/page_transition_types.h" 45 #include "content/public/common/referrer.h" 46 #include "grit/generated_resources.h" 47 #include "net/base/net_errors.h" 48 #include "ui/base/l10n/l10n_util.h" 49 #include "url/gurl.h" 50 51 DEFINE_WEB_CONTENTS_USER_DATA_KEY(SearchTabHelper); 52 53 namespace { 54 55 // For reporting Cacheable NTP navigations. 56 enum CacheableNTPLoad { 57 CACHEABLE_NTP_LOAD_FAILED = 0, 58 CACHEABLE_NTP_LOAD_SUCCEEDED = 1, 59 CACHEABLE_NTP_LOAD_MAX = 2 60 }; 61 62 void RecordCacheableNTPLoadHistogram(bool succeeded) { 63 UMA_HISTOGRAM_ENUMERATION("InstantExtended.CacheableNTPLoad", 64 succeeded ? CACHEABLE_NTP_LOAD_SUCCEEDED : 65 CACHEABLE_NTP_LOAD_FAILED, 66 CACHEABLE_NTP_LOAD_MAX); 67 } 68 69 bool IsCacheableNTP(const content::WebContents* contents) { 70 const content::NavigationEntry* entry = 71 contents->GetController().GetLastCommittedEntry(); 72 return chrome::ShouldUseCacheableNTP() && 73 chrome::NavEntryIsInstantNTP(contents, entry) && 74 entry->GetURL() != GURL(chrome::kChromeSearchLocalNtpUrl); 75 } 76 77 bool IsNTP(const content::WebContents* contents) { 78 // We can't use WebContents::GetURL() because that uses the active entry, 79 // whereas we want the visible entry. 80 const content::NavigationEntry* entry = 81 contents->GetController().GetVisibleEntry(); 82 if (entry && entry->GetVirtualURL() == GURL(chrome::kChromeUINewTabURL)) 83 return true; 84 85 return chrome::IsInstantNTP(contents); 86 } 87 88 bool IsSearchResults(const content::WebContents* contents) { 89 return !chrome::GetSearchTerms(contents).empty(); 90 } 91 92 bool IsLocal(const content::WebContents* contents) { 93 if (!contents) 94 return false; 95 const content::NavigationEntry* entry = 96 contents->GetController().GetVisibleEntry(); 97 return entry && entry->GetURL() == GURL(chrome::kChromeSearchLocalNtpUrl); 98 } 99 100 // Returns true if |contents| are rendered inside an Instant process. 101 bool InInstantProcess(Profile* profile, 102 const content::WebContents* contents) { 103 if (!profile || !contents) 104 return false; 105 106 InstantService* instant_service = 107 InstantServiceFactory::GetForProfile(profile); 108 return instant_service && 109 instant_service->IsInstantProcess( 110 contents->GetRenderProcessHost()->GetID()); 111 } 112 113 // Updates the location bar to reflect |contents| Instant support state. 114 void UpdateLocationBar(content::WebContents* contents) { 115 // iOS and Android don't use the Instant framework. 116 #if !defined(OS_IOS) && !defined(OS_ANDROID) 117 if (!contents) 118 return; 119 120 Browser* browser = chrome::FindBrowserWithWebContents(contents); 121 if (!browser) 122 return; 123 browser->OnWebContentsInstantSupportDisabled(contents); 124 #endif 125 } 126 127 } // namespace 128 129 SearchTabHelper::SearchTabHelper(content::WebContents* web_contents) 130 : WebContentsObserver(web_contents), 131 is_search_enabled_(chrome::IsInstantExtendedAPIEnabled()), 132 user_input_in_progress_(false), 133 web_contents_(web_contents), 134 ipc_router_(web_contents, this, 135 make_scoped_ptr(new SearchIPCRouterPolicyImpl(web_contents)) 136 .PassAs<SearchIPCRouter::Policy>()), 137 instant_service_(NULL) { 138 if (!is_search_enabled_) 139 return; 140 141 instant_service_ = 142 InstantServiceFactory::GetForProfile( 143 Profile::FromBrowserContext(web_contents_->GetBrowserContext())); 144 if (instant_service_) 145 instant_service_->AddObserver(this); 146 } 147 148 SearchTabHelper::~SearchTabHelper() { 149 if (instant_service_) 150 instant_service_->RemoveObserver(this); 151 } 152 153 void SearchTabHelper::InitForPreloadedNTP() { 154 UpdateMode(true, true); 155 } 156 157 void SearchTabHelper::OmniboxEditModelChanged(bool user_input_in_progress, 158 bool cancelling) { 159 if (!is_search_enabled_) 160 return; 161 162 user_input_in_progress_ = user_input_in_progress; 163 if (!user_input_in_progress && !cancelling) 164 return; 165 166 UpdateMode(false, false); 167 } 168 169 void SearchTabHelper::NavigationEntryUpdated() { 170 if (!is_search_enabled_) 171 return; 172 173 UpdateMode(false, false); 174 } 175 176 void SearchTabHelper::InstantSupportChanged(bool instant_support) { 177 if (!is_search_enabled_) 178 return; 179 180 InstantSupportState new_state = instant_support ? INSTANT_SUPPORT_YES : 181 INSTANT_SUPPORT_NO; 182 183 model_.SetInstantSupportState(new_state); 184 185 content::NavigationEntry* entry = 186 web_contents_->GetController().GetVisibleEntry(); 187 if (entry) { 188 chrome::SetInstantSupportStateInNavigationEntry(new_state, entry); 189 if (!instant_support) 190 UpdateLocationBar(web_contents_); 191 } 192 } 193 194 bool SearchTabHelper::SupportsInstant() const { 195 return model_.instant_support() == INSTANT_SUPPORT_YES; 196 } 197 198 void SearchTabHelper::SetSuggestionToPrefetch( 199 const InstantSuggestion& suggestion) { 200 ipc_router_.SetSuggestionToPrefetch(suggestion); 201 } 202 203 void SearchTabHelper::Submit(const base::string16& text) { 204 ipc_router_.Submit(text); 205 } 206 207 void SearchTabHelper::OnTabActivated() { 208 ipc_router_.OnTabActivated(); 209 } 210 211 void SearchTabHelper::OnTabDeactivated() { 212 ipc_router_.OnTabDeactivated(); 213 } 214 215 void SearchTabHelper::ToggleVoiceSearch() { 216 ipc_router_.ToggleVoiceSearch(); 217 } 218 219 bool SearchTabHelper::IsSearchResultsPage() { 220 return model_.mode().is_origin_search(); 221 } 222 223 void SearchTabHelper::RenderViewCreated( 224 content::RenderViewHost* render_view_host) { 225 ipc_router_.SetPromoInformation(IsAppLauncherEnabled()); 226 } 227 228 void SearchTabHelper::DidStartNavigationToPendingEntry( 229 const GURL& url, 230 content::NavigationController::ReloadType /* reload_type */) { 231 if (chrome::IsNTPURL(url, profile())) { 232 // Set the title on any pending entry corresponding to the NTP. This 233 // prevents any flickering of the tab title. 234 content::NavigationEntry* entry = 235 web_contents_->GetController().GetPendingEntry(); 236 if (entry) 237 entry->SetTitle(l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE)); 238 } 239 } 240 241 void SearchTabHelper::DidNavigateMainFrame( 242 const content::LoadCommittedDetails& details, 243 const content::FrameNavigateParams& params) { 244 if (IsCacheableNTP(web_contents_)) { 245 if (details.http_status_code == 204 || details.http_status_code >= 400) { 246 RedirectToLocalNTP(); 247 RecordCacheableNTPLoadHistogram(false); 248 return; 249 } 250 RecordCacheableNTPLoadHistogram(true); 251 } 252 253 // Always set the title on the new tab page to be the one from our UI 254 // resources. Normally, we set the title when we begin a NTP load, but it can 255 // get reset in several places (like when you press Reload). This check 256 // ensures that the title is properly set to the string defined by the Chrome 257 // UI language (rather than the server language) in all cases. 258 // 259 // We only override the title when it's nonempty to allow the page to set the 260 // title if it really wants. An empty title means to use the default. There's 261 // also a race condition between this code and the page's SetTitle call which 262 // this rule avoids. 263 content::NavigationEntry* entry = 264 web_contents_->GetController().GetLastCommittedEntry(); 265 if (entry && entry->GetTitle().empty() && 266 (entry->GetVirtualURL() == GURL(chrome::kChromeUINewTabURL) || 267 chrome::NavEntryIsInstantNTP(web_contents_, entry))) { 268 entry->SetTitle(l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE)); 269 } 270 } 271 272 void SearchTabHelper::DidFailProvisionalLoad( 273 int64 /* frame_id */, 274 const base::string16& /* frame_unique_name */, 275 bool is_main_frame, 276 const GURL& validated_url, 277 int error_code, 278 const base::string16& /* error_description */, 279 content::RenderViewHost* /* render_view_host */) { 280 // If error_code is ERR_ABORTED means that the user has canceled this 281 // navigation so it shouldn't be redirected. 282 if (is_main_frame && 283 error_code != net::ERR_ABORTED && 284 chrome::ShouldUseCacheableNTP() && 285 validated_url != GURL(chrome::kChromeSearchLocalNtpUrl) && 286 chrome::IsNTPURL(validated_url, profile())) { 287 RedirectToLocalNTP(); 288 RecordCacheableNTPLoadHistogram(false); 289 } 290 } 291 292 void SearchTabHelper::DidFinishLoad( 293 int64 /* frame_id */, 294 const GURL& /* validated_url */, 295 bool is_main_frame, 296 content::RenderViewHost* /* render_view_host */) { 297 if (is_main_frame) 298 DetermineIfPageSupportsInstant(); 299 } 300 301 void SearchTabHelper::NavigationEntryCommitted( 302 const content::LoadCommittedDetails& load_details) { 303 if (!is_search_enabled_) 304 return; 305 306 if (!load_details.is_main_frame) 307 return; 308 309 // TODO(kmadhusu): Set the page initial states (such as omnibox margin, etc) 310 // from here. Please refer to crbug.com/247517 for more details. 311 if (chrome::ShouldAssignURLToInstantRenderer(web_contents_->GetURL(), 312 profile())) { 313 ipc_router_.SetDisplayInstantResults(); 314 } 315 316 UpdateMode(true, false); 317 318 content::NavigationEntry* entry = 319 web_contents_->GetController().GetVisibleEntry(); 320 DCHECK(entry); 321 322 // Already determined the instant support state for this page, do not reset 323 // the instant support state. 324 // 325 // When we get a navigation entry committed event, there seem to be two ways 326 // to tell whether the navigation was "in-page". Ideally, when 327 // LoadCommittedDetails::is_in_page is true, we should have 328 // LoadCommittedDetails::type to be NAVIGATION_TYPE_IN_PAGE. Unfortunately, 329 // they are different in some cases. To workaround this bug, we are checking 330 // (is_in_page || type == NAVIGATION_TYPE_IN_PAGE). Please refer to 331 // crbug.com/251330 for more details. 332 if (load_details.is_in_page || 333 load_details.type == content::NAVIGATION_TYPE_IN_PAGE) { 334 // When an "in-page" navigation happens, we will not receive a 335 // DidFinishLoad() event. Therefore, we will not determine the Instant 336 // support for the navigated page. So, copy over the Instant support from 337 // the previous entry. If the page does not support Instant, update the 338 // location bar from here to turn off search terms replacement. 339 chrome::SetInstantSupportStateInNavigationEntry(model_.instant_support(), 340 entry); 341 if (model_.instant_support() == INSTANT_SUPPORT_NO) 342 UpdateLocationBar(web_contents_); 343 return; 344 } 345 346 model_.SetInstantSupportState(INSTANT_SUPPORT_UNKNOWN); 347 model_.SetVoiceSearchSupported(false); 348 chrome::SetInstantSupportStateInNavigationEntry(model_.instant_support(), 349 entry); 350 } 351 352 void SearchTabHelper::OnInstantSupportDetermined(bool supports_instant) { 353 InstantSupportChanged(supports_instant); 354 } 355 356 void SearchTabHelper::OnSetVoiceSearchSupport(bool supports_voice_search) { 357 model_.SetVoiceSearchSupported(supports_voice_search); 358 } 359 360 void SearchTabHelper::ThemeInfoChanged(const ThemeBackgroundInfo& theme_info) { 361 ipc_router_.SendThemeBackgroundInfo(theme_info); 362 } 363 364 void SearchTabHelper::MostVisitedItemsChanged( 365 const std::vector<InstantMostVisitedItem>& items) { 366 std::vector<InstantMostVisitedItem> items_copy(items); 367 MaybeRemoveMostVisitedItems(&items_copy); 368 ipc_router_.SendMostVisitedItems(items_copy); 369 } 370 371 void SearchTabHelper::MaybeRemoveMostVisitedItems( 372 std::vector<InstantMostVisitedItem>* items) { 373 // The code below uses APIs not available on Android and the experiment should 374 // not run there. 375 #if !defined(OS_ANDROID) 376 if (!history::MostVisitedTilesExperiment::IsDontShowOpenURLsEnabled()) 377 return; 378 379 Profile* profile = 380 Profile::FromBrowserContext(web_contents_->GetBrowserContext()); 381 if (!profile) 382 return; 383 384 Browser* browser = chrome::FindBrowserWithProfile(profile, 385 chrome::GetActiveDesktop()); 386 if (!browser) 387 return; 388 389 TabStripModel* tab_strip_model = browser->tab_strip_model(); 390 history::TopSites* top_sites = profile->GetTopSites(); 391 if (!tab_strip_model || !top_sites) { 392 NOTREACHED(); 393 return; 394 } 395 396 std::set<std::string> open_urls; 397 chrome::GetOpenUrls(*tab_strip_model, *top_sites, &open_urls); 398 history::MostVisitedTilesExperiment::RemoveItemsMatchingOpenTabs( 399 open_urls, items); 400 #endif 401 } 402 403 void SearchTabHelper::FocusOmnibox(OmniboxFocusState state) { 404 // iOS and Android don't use the Instant framework. 405 #if !defined(OS_IOS) && !defined(OS_ANDROID) 406 Browser* browser = chrome::FindBrowserWithWebContents(web_contents()); 407 if (!browser) 408 return; 409 410 OmniboxView* omnibox = browser->window()->GetLocationBar()->GetOmniboxView(); 411 // Do not add a default case in the switch block for the following reasons: 412 // (1) Explicitly handle the new states. If new states are added in the 413 // OmniboxFocusState, the compiler will warn the developer to handle the new 414 // states. 415 // (2) An attacker may control the renderer and sends the browser process a 416 // malformed IPC. This function responds to the invalid |state| values by 417 // doing nothing instead of crashing the browser process (intentional no-op). 418 switch (state) { 419 case OMNIBOX_FOCUS_VISIBLE: 420 omnibox->SetFocus(); 421 omnibox->model()->SetCaretVisibility(true); 422 break; 423 case OMNIBOX_FOCUS_INVISIBLE: 424 omnibox->SetFocus(); 425 omnibox->model()->SetCaretVisibility(false); 426 // If the user clicked on the fakebox, any text already in the omnibox 427 // should get cleared when they start typing. Selecting all the existing 428 // text is a convenient way to accomplish this. It also gives a slight 429 // visual cue to users who really understand selection state about what 430 // will happen if they start typing. 431 omnibox->SelectAll(false); 432 break; 433 case OMNIBOX_FOCUS_NONE: 434 // Remove focus only if the popup is closed. This will prevent someone 435 // from changing the omnibox value and closing the popup without user 436 // interaction. 437 if (!omnibox->model()->popup_model()->IsOpen()) 438 web_contents()->GetView()->Focus(); 439 break; 440 } 441 #endif 442 } 443 444 void SearchTabHelper::NavigateToURL(const GURL& url, 445 WindowOpenDisposition disposition, 446 bool is_most_visited_item_url) { 447 // iOS and Android don't use the Instant framework. 448 #if !defined(OS_IOS) && !defined(OS_ANDROID) 449 // TODO(kmadhusu): Remove chrome::FindBrowser...() function call from here. 450 // Create a SearchTabHelperDelegate interface and have the Browser object 451 // implement that interface to provide the necessary functionality. 452 Browser* browser = chrome::FindBrowserWithWebContents(web_contents_); 453 Profile* profile = 454 Profile::FromBrowserContext(web_contents_->GetBrowserContext()); 455 if (!browser || !profile) 456 return; 457 458 if (is_most_visited_item_url) { 459 content::RecordAction( 460 content::UserMetricsAction("InstantExtended.MostVisitedClicked")); 461 } 462 463 chrome::NavigateParams params(browser, url, 464 content::PAGE_TRANSITION_AUTO_BOOKMARK); 465 params.referrer = content::Referrer(); 466 params.source_contents = web_contents_; 467 params.disposition = disposition; 468 params.is_renderer_initiated = false; 469 params.initiating_profile = profile; 470 chrome::Navigate(¶ms); 471 #endif 472 } 473 474 void SearchTabHelper::OnDeleteMostVisitedItem(const GURL& url) { 475 DCHECK(!url.is_empty()); 476 if (instant_service_) 477 instant_service_->DeleteMostVisitedItem(url); 478 } 479 480 void SearchTabHelper::OnUndoMostVisitedDeletion(const GURL& url) { 481 DCHECK(!url.is_empty()); 482 if (instant_service_) 483 instant_service_->UndoMostVisitedDeletion(url); 484 } 485 486 void SearchTabHelper::OnUndoAllMostVisitedDeletions() { 487 if (instant_service_) 488 instant_service_->UndoAllMostVisitedDeletions(); 489 } 490 491 void SearchTabHelper::OnLogEvent(NTPLoggingEventType event) { 492 NTPUserDataLogger::GetOrCreateFromWebContents( 493 web_contents())->LogEvent(event); 494 } 495 496 void SearchTabHelper::OnLogImpression(int position, 497 const base::string16& provider) { 498 NTPUserDataLogger::GetOrCreateFromWebContents( 499 web_contents())->LogImpression(position, provider); 500 } 501 502 void SearchTabHelper::PasteIntoOmnibox(const base::string16& text) { 503 // iOS and Android don't use the Instant framework. 504 #if !defined(OS_IOS) && !defined(OS_ANDROID) 505 Browser* browser = chrome::FindBrowserWithWebContents(web_contents()); 506 if (!browser) 507 return; 508 509 OmniboxView* omnibox = browser->window()->GetLocationBar()->GetOmniboxView(); 510 // The first case is for right click to paste, where the text is retrieved 511 // from the clipboard already sanitized. The second case is needed to handle 512 // drag-and-drop value and it has to be sanitazed before setting it into the 513 // omnibox. 514 base::string16 text_to_paste = text.empty() ? omnibox->GetClipboardText() : 515 omnibox->SanitizeTextForPaste(text); 516 517 if (text_to_paste.empty()) 518 return; 519 520 if (!omnibox->model()->has_focus()) 521 omnibox->SetFocus(); 522 523 omnibox->OnBeforePossibleChange(); 524 omnibox->model()->OnPaste(); 525 omnibox->SetUserText(text_to_paste); 526 omnibox->OnAfterPossibleChange(); 527 #endif 528 } 529 530 void SearchTabHelper::OnChromeIdentityCheck(const base::string16& identity) { 531 SigninManagerBase* manager = SigninManagerFactory::GetForProfile(profile()); 532 if (manager) { 533 const base::string16 username = 534 UTF8ToUTF16(manager->GetAuthenticatedUsername()); 535 ipc_router_.SendChromeIdentityCheckResult(identity, 536 identity == username); 537 } 538 } 539 540 void SearchTabHelper::UpdateMode(bool update_origin, bool is_preloaded_ntp) { 541 SearchMode::Type type = SearchMode::MODE_DEFAULT; 542 SearchMode::Origin origin = SearchMode::ORIGIN_DEFAULT; 543 if (IsNTP(web_contents_) || is_preloaded_ntp) { 544 type = SearchMode::MODE_NTP; 545 origin = SearchMode::ORIGIN_NTP; 546 } else if (IsSearchResults(web_contents_)) { 547 type = SearchMode::MODE_SEARCH_RESULTS; 548 origin = SearchMode::ORIGIN_SEARCH; 549 } 550 if (!update_origin) 551 origin = model_.mode().origin; 552 if (user_input_in_progress_) 553 type = SearchMode::MODE_SEARCH_SUGGESTIONS; 554 model_.SetMode(SearchMode(type, origin)); 555 } 556 557 void SearchTabHelper::DetermineIfPageSupportsInstant() { 558 if (!InInstantProcess(profile(), web_contents_)) { 559 // The page is not in the Instant process. This page does not support 560 // instant. If we send an IPC message to a page that is not in the Instant 561 // process, it will never receive it and will never respond. Therefore, 562 // return immediately. 563 InstantSupportChanged(false); 564 } else if (IsLocal(web_contents_)) { 565 // Local pages always support Instant. 566 InstantSupportChanged(true); 567 } else { 568 ipc_router_.DetermineIfPageSupportsInstant(); 569 } 570 } 571 572 Profile* SearchTabHelper::profile() const { 573 return Profile::FromBrowserContext(web_contents_->GetBrowserContext()); 574 } 575 576 void SearchTabHelper::RedirectToLocalNTP() { 577 // Extra parentheses to declare a variable. 578 content::NavigationController::LoadURLParams load_params( 579 (GURL(chrome::kChromeSearchLocalNtpUrl))); 580 load_params.referrer = content::Referrer(); 581 load_params.transition_type = content::PAGE_TRANSITION_SERVER_REDIRECT; 582 // Don't push a history entry. 583 load_params.should_replace_current_entry = true; 584 web_contents_->GetController().LoadURLWithParams(load_params); 585 } 586