// Copyright 2012 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. #include "chrome/browser/ui/search/search_tab_helper.h" #include #include "base/macros.h" #include "base/memory/scoped_ptr.h" #include "base/metrics/histogram.h" #include "base/strings/string16.h" #include "base/strings/string_util.h" #include "build/build_config.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/search/instant_service.h" #include "chrome/browser/search/instant_service_factory.h" #include "chrome/browser/search/search.h" #include "chrome/browser/signin/signin_manager_factory.h" #include "chrome/browser/sync/profile_sync_service_factory.h" #include "chrome/browser/ui/app_list/app_list_util.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/location_bar/location_bar.h" #include "chrome/browser/ui/omnibox/clipboard_utils.h" #include "chrome/browser/ui/search/instant_search_prerenderer.h" #include "chrome/browser/ui/search/instant_tab.h" #include "chrome/browser/ui/search/search_ipc_router_policy_impl.h" #include "chrome/browser/ui/search/search_tab_helper_delegate.h" #include "chrome/browser/ui/tab_contents/core_tab_helper.h" #include "chrome/browser/ui/webui/ntp/ntp_user_data_logger.h" #include "chrome/common/url_constants.h" #include "chrome/grit/generated_resources.h" #include "components/browser_sync/browser/profile_sync_service.h" #include "components/google/core/browser/google_util.h" #include "components/omnibox/browser/omnibox_edit_model.h" #include "components/omnibox/browser/omnibox_popup_model.h" #include "components/omnibox/browser/omnibox_view.h" #include "components/search/search.h" #include "components/signin/core/browser/signin_manager.h" #include "components/strings/grit/components_strings.h" #include "content/public/browser/navigation_controller.h" #include "content/public/browser/navigation_details.h" #include "content/public/browser/navigation_entry.h" #include "content/public/browser/navigation_type.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/user_metrics.h" #include "content/public/browser/web_contents.h" #include "content/public/common/referrer.h" #include "google_apis/gaia/gaia_auth_util.h" #include "net/base/net_errors.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/page_transition_types.h" #include "url/gurl.h" DEFINE_WEB_CONTENTS_USER_DATA_KEY(SearchTabHelper); namespace { bool IsCacheableNTP(const content::WebContents* contents) { const content::NavigationEntry* entry = contents->GetController().GetLastCommittedEntry(); return search::NavEntryIsInstantNTP(contents, entry) && entry->GetURL() != GURL(chrome::kChromeSearchLocalNtpUrl); } bool IsNTP(const content::WebContents* contents) { // We can't use WebContents::GetURL() because that uses the active entry, // whereas we want the visible entry. const content::NavigationEntry* entry = contents->GetController().GetVisibleEntry(); if (entry && entry->GetVirtualURL() == GURL(chrome::kChromeUINewTabURL)) return true; return search::IsInstantNTP(contents); } bool IsSearchResults(const content::WebContents* contents) { return !search::GetSearchTerms(contents).empty(); } bool IsLocal(const content::WebContents* contents) { if (!contents) return false; const content::NavigationEntry* entry = contents->GetController().GetVisibleEntry(); return entry && entry->GetURL() == GURL(chrome::kChromeSearchLocalNtpUrl); } // Returns true if |contents| are rendered inside an Instant process. bool InInstantProcess(Profile* profile, const content::WebContents* contents) { if (!profile || !contents) return false; InstantService* instant_service = InstantServiceFactory::GetForProfile(profile); return instant_service && instant_service->IsInstantProcess( contents->GetRenderProcessHost()->GetID()); } // Called when an NTP finishes loading. If the load start time was noted, // calculates and logs the total load time. void RecordNewTabLoadTime(content::WebContents* contents) { CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents); if (core_tab_helper->new_tab_start_time().is_null()) return; base::TimeDelta duration = base::TimeTicks::Now() - core_tab_helper->new_tab_start_time(); if (IsCacheableNTP(contents)) { if (google_util::IsGoogleDomainUrl( contents->GetController().GetLastCommittedEntry()->GetURL(), google_util::ALLOW_SUBDOMAIN, google_util::DISALLOW_NON_STANDARD_PORTS)) { UMA_HISTOGRAM_TIMES("Tab.NewTabOnload.Google", duration); } else { UMA_HISTOGRAM_TIMES("Tab.NewTabOnload.Other", duration); } } else { UMA_HISTOGRAM_TIMES("Tab.NewTabOnload.Local", duration); } core_tab_helper->set_new_tab_start_time(base::TimeTicks()); } // Returns true if the user wants to sync history. This function returning true // is not a guarantee that history is being synced, but it can be used to // disable a feature that should not be shown to users who prefer not to sync // their history. bool IsHistorySyncEnabled(Profile* profile) { ProfileSyncService* sync = ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile); return sync && sync->GetPreferredDataTypes().Has(syncer::HISTORY_DELETE_DIRECTIVES); } bool OmniboxHasFocus(OmniboxView* omnibox) { return omnibox && omnibox->model()->has_focus(); } } // namespace SearchTabHelper::SearchTabHelper(content::WebContents* web_contents) : WebContentsObserver(web_contents), is_search_enabled_(search::IsInstantExtendedAPIEnabled()), web_contents_(web_contents), ipc_router_(web_contents, this, make_scoped_ptr(new SearchIPCRouterPolicyImpl(web_contents))), instant_service_(NULL), delegate_(NULL), omnibox_has_focus_fn_(&OmniboxHasFocus) { if (!is_search_enabled_) return; instant_service_ = InstantServiceFactory::GetForProfile( Profile::FromBrowserContext(web_contents_->GetBrowserContext())); if (instant_service_) instant_service_->AddObserver(this); } SearchTabHelper::~SearchTabHelper() { if (instant_service_) instant_service_->RemoveObserver(this); } void SearchTabHelper::InitForPreloadedNTP() { UpdateMode(true, true); } void SearchTabHelper::OmniboxInputStateChanged() { if (!is_search_enabled_) return; UpdateMode(false, false); } void SearchTabHelper::OmniboxFocusChanged(OmniboxFocusState state, OmniboxFocusChangeReason reason) { content::NotificationService::current()->Notify( chrome::NOTIFICATION_OMNIBOX_FOCUS_CHANGED, content::Source(this), content::NotificationService::NoDetails()); ipc_router_.OmniboxFocusChanged(state, reason); // Don't send oninputstart/oninputend updates in response to focus changes // if there's a navigation in progress. This prevents Chrome from sending // a spurious oninputend when the user accepts a match in the omnibox. if (web_contents_->GetController().GetPendingEntry() == NULL) { ipc_router_.SetInputInProgress(IsInputInProgress()); InstantSearchPrerenderer* prerenderer = InstantSearchPrerenderer::GetForProfile(profile()); if (!prerenderer || !search::ShouldPrerenderInstantUrlOnOmniboxFocus()) return; if (state == OMNIBOX_FOCUS_NONE) { prerenderer->Cancel(); return; } if (!IsSearchResultsPage()) { prerenderer->Init( web_contents_->GetController().GetDefaultSessionStorageNamespace(), web_contents_->GetContainerBounds().size()); } } } void SearchTabHelper::NavigationEntryUpdated() { if (!is_search_enabled_) return; UpdateMode(false, false); } void SearchTabHelper::InstantSupportChanged(bool instant_support) { if (!is_search_enabled_) return; InstantSupportState new_state = instant_support ? INSTANT_SUPPORT_YES : INSTANT_SUPPORT_NO; model_.SetInstantSupportState(new_state); content::NavigationEntry* entry = web_contents_->GetController().GetLastCommittedEntry(); if (entry) { search::SetInstantSupportStateInNavigationEntry(new_state, entry); if (delegate_ && !instant_support) delegate_->OnWebContentsInstantSupportDisabled(web_contents_); } } bool SearchTabHelper::SupportsInstant() const { return model_.instant_support() == INSTANT_SUPPORT_YES; } void SearchTabHelper::SetSuggestionToPrefetch( const InstantSuggestion& suggestion) { ipc_router_.SetSuggestionToPrefetch(suggestion); } void SearchTabHelper::Submit(const base::string16& text, const EmbeddedSearchRequestParams& params) { ipc_router_.Submit(text, params); } void SearchTabHelper::OnTabActivated() { ipc_router_.OnTabActivated(); OmniboxView* omnibox_view = GetOmniboxView(); if (search::ShouldPrerenderInstantUrlOnOmniboxFocus() && omnibox_has_focus_fn_(omnibox_view)) { InstantSearchPrerenderer* prerenderer = InstantSearchPrerenderer::GetForProfile(profile()); if (prerenderer && !IsSearchResultsPage()) { prerenderer->Init( web_contents_->GetController().GetDefaultSessionStorageNamespace(), web_contents_->GetContainerBounds().size()); } } } void SearchTabHelper::OnTabDeactivated() { ipc_router_.OnTabDeactivated(); } bool SearchTabHelper::IsSearchResultsPage() { return model_.mode().is_origin_search(); } void SearchTabHelper::RenderViewCreated( content::RenderViewHost* render_view_host) { ipc_router_.SetPromoInformation(IsAppLauncherEnabled()); } void SearchTabHelper::DidStartNavigationToPendingEntry( const GURL& url, content::NavigationController::ReloadType /* reload_type */) { if (search::IsNTPURL(url, profile())) { // Set the title on any pending entry corresponding to the NTP. This // prevents any flickering of the tab title. content::NavigationEntry* entry = web_contents_->GetController().GetPendingEntry(); if (entry) entry->SetTitle(l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE)); } } void SearchTabHelper::DidNavigateMainFrame( const content::LoadCommittedDetails& details, const content::FrameNavigateParams& params) { if (IsCacheableNTP(web_contents_)) { UMA_HISTOGRAM_ENUMERATION("InstantExtended.CacheableNTPLoad", search::CACHEABLE_NTP_LOAD_SUCCEEDED, search::CACHEABLE_NTP_LOAD_MAX); } // Always set the title on the new tab page to be the one from our UI // resources. Normally, we set the title when we begin a NTP load, but it can // get reset in several places (like when you press Reload). This check // ensures that the title is properly set to the string defined by the Chrome // UI language (rather than the server language) in all cases. // // We only override the title when it's nonempty to allow the page to set the // title if it really wants. An empty title means to use the default. There's // also a race condition between this code and the page's SetTitle call which // this rule avoids. content::NavigationEntry* entry = web_contents_->GetController().GetLastCommittedEntry(); if (entry && entry->GetTitle().empty() && (entry->GetVirtualURL() == GURL(chrome::kChromeUINewTabURL) || search::NavEntryIsInstantNTP(web_contents_, entry))) { entry->SetTitle(l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE)); } } void SearchTabHelper::DidFinishLoad(content::RenderFrameHost* render_frame_host, const GURL& /* validated_url */) { if (!render_frame_host->GetParent()) { if (search::IsInstantNTP(web_contents_)) RecordNewTabLoadTime(web_contents_); DetermineIfPageSupportsInstant(); } } void SearchTabHelper::NavigationEntryCommitted( const content::LoadCommittedDetails& load_details) { if (!is_search_enabled_) return; if (!load_details.is_main_frame) return; if (search::ShouldAssignURLToInstantRenderer(web_contents_->GetURL(), profile())) ipc_router_.SetDisplayInstantResults(); UpdateMode(true, false); content::NavigationEntry* entry = web_contents_->GetController().GetVisibleEntry(); DCHECK(entry); // Already determined the instant support state for this page, do not reset // the instant support state. if (load_details.is_in_page) { // When an "in-page" navigation happens, we will not receive a // DidFinishLoad() event. Therefore, we will not determine the Instant // support for the navigated page. So, copy over the Instant support from // the previous entry. If the page does not support Instant, update the // location bar from here to turn off search terms replacement. search::SetInstantSupportStateInNavigationEntry(model_.instant_support(), entry); if (delegate_ && model_.instant_support() == INSTANT_SUPPORT_NO) delegate_->OnWebContentsInstantSupportDisabled(web_contents_); return; } model_.SetInstantSupportState(INSTANT_SUPPORT_UNKNOWN); search::SetInstantSupportStateInNavigationEntry(model_.instant_support(), entry); if (InInstantProcess(profile(), web_contents_)) ipc_router_.OnNavigationEntryCommitted(); } void SearchTabHelper::OnInstantSupportDetermined(bool supports_instant) { InstantSupportChanged(supports_instant); } void SearchTabHelper::ThemeInfoChanged(const ThemeBackgroundInfo& theme_info) { ipc_router_.SendThemeBackgroundInfo(theme_info); } void SearchTabHelper::MostVisitedItemsChanged( const std::vector& items) { // When most visited change, the NTP usually reloads the tiles. This means // our metrics get inconsistent. So we'd rather emit stats now. InstantTab::EmitNtpStatistics(web_contents_); ipc_router_.SendMostVisitedItems(items); LogMostVisitedItemsSource(items); } void SearchTabHelper::LogMostVisitedItemsSource( const std::vector& items) { for (auto item : items) { NTPLoggingEventType event; if (item.is_server_side_suggestion) { event = NTP_SERVER_SIDE_SUGGESTION; } else { event = NTP_CLIENT_SIDE_SUGGESTION; } // The metrics are emitted for each suggestion as the design requirement // even the ntp_user_data_logger.cc now only supports the scenario: // all suggestions are provided by server OR // all suggestions are provided by client. this->OnLogEvent(event, base::TimeDelta()); } } void SearchTabHelper::FocusOmnibox(OmniboxFocusState state) { // TODO(kmadhusu): Move platform specific code from here and get rid of #ifdef. #if !defined(OS_ANDROID) OmniboxView* omnibox = GetOmniboxView(); if (!omnibox) return; // Do not add a default case in the switch block for the following reasons: // (1) Explicitly handle the new states. If new states are added in the // OmniboxFocusState, the compiler will warn the developer to handle the new // states. // (2) An attacker may control the renderer and sends the browser process a // malformed IPC. This function responds to the invalid |state| values by // doing nothing instead of crashing the browser process (intentional no-op). switch (state) { case OMNIBOX_FOCUS_VISIBLE: omnibox->SetFocus(); omnibox->model()->SetCaretVisibility(true); break; case OMNIBOX_FOCUS_INVISIBLE: omnibox->SetFocus(); omnibox->model()->SetCaretVisibility(false); // If the user clicked on the fakebox, any text already in the omnibox // should get cleared when they start typing. Selecting all the existing // text is a convenient way to accomplish this. It also gives a slight // visual cue to users who really understand selection state about what // will happen if they start typing. omnibox->SelectAll(false); omnibox->ShowImeIfNeeded(); break; case OMNIBOX_FOCUS_NONE: // Remove focus only if the popup is closed. This will prevent someone // from changing the omnibox value and closing the popup without user // interaction. if (!omnibox->model()->popup_model()->IsOpen()) web_contents()->Focus(); break; } #endif } void SearchTabHelper::NavigateToURL(const GURL& url, WindowOpenDisposition disposition) { // Make sure the specified URL is actually on the most visited or suggested // items list. if (!instant_service_ || !instant_service_->IsValidURLForNavigation(url)) return; if (delegate_) delegate_->NavigateOnThumbnailClick(url, disposition, web_contents_); } void SearchTabHelper::OnDeleteMostVisitedItem(const GURL& url) { DCHECK(!url.is_empty()); if (instant_service_) instant_service_->DeleteMostVisitedItem(url); } void SearchTabHelper::OnUndoMostVisitedDeletion(const GURL& url) { DCHECK(!url.is_empty()); if (instant_service_) instant_service_->UndoMostVisitedDeletion(url); } void SearchTabHelper::OnUndoAllMostVisitedDeletions() { if (instant_service_) instant_service_->UndoAllMostVisitedDeletions(); } void SearchTabHelper::OnLogEvent(NTPLoggingEventType event, base::TimeDelta time) { // TODO(kmadhusu): Move platform specific code from here and get rid of #ifdef. #if !defined(OS_ANDROID) NTPUserDataLogger::GetOrCreateFromWebContents(web_contents()) ->LogEvent(event, time); #endif } void SearchTabHelper::OnLogMostVisitedImpression( int position, const base::string16& provider) { // TODO(kmadhusu): Move platform specific code from here and get rid of #ifdef. #if !defined(OS_ANDROID) NTPUserDataLogger::GetOrCreateFromWebContents( web_contents())->LogMostVisitedImpression(position, provider); #endif } void SearchTabHelper::OnLogMostVisitedNavigation( int position, const base::string16& provider) { // TODO(kmadhusu): Move platform specific code from here and get rid of #ifdef. #if !defined(OS_ANDROID) NTPUserDataLogger::GetOrCreateFromWebContents( web_contents())->LogMostVisitedNavigation(position, provider); #endif } void SearchTabHelper::PasteIntoOmnibox(const base::string16& text) { // TODO(kmadhusu): Move platform specific code from here and get rid of #ifdef. #if !defined(OS_ANDROID) OmniboxView* omnibox = GetOmniboxView(); if (!omnibox) return; // The first case is for right click to paste, where the text is retrieved // from the clipboard already sanitized. The second case is needed to handle // drag-and-drop value and it has to be sanitazed before setting it into the // omnibox. base::string16 text_to_paste = text.empty() ? GetClipboardText() : omnibox->SanitizeTextForPaste(text); if (text_to_paste.empty()) return; if (!omnibox->model()->has_focus()) omnibox->SetFocus(); omnibox->OnBeforePossibleChange(); omnibox->model()->OnPaste(); omnibox->SetUserText(text_to_paste); omnibox->OnAfterPossibleChange(true); #endif } void SearchTabHelper::OnChromeIdentityCheck(const base::string16& identity) { SigninManagerBase* manager = SigninManagerFactory::GetForProfile(profile()); if (manager) { ipc_router_.SendChromeIdentityCheckResult( identity, gaia::AreEmailsSame(base::UTF16ToUTF8(identity), manager->GetAuthenticatedAccountInfo().email)); } else { ipc_router_.SendChromeIdentityCheckResult(identity, false); } } void SearchTabHelper::OnHistorySyncCheck() { ipc_router_.SendHistorySyncCheckResult(IsHistorySyncEnabled(profile())); } void SearchTabHelper::UpdateMode(bool update_origin, bool is_preloaded_ntp) { SearchMode::Type type = SearchMode::MODE_DEFAULT; SearchMode::Origin origin = SearchMode::ORIGIN_DEFAULT; if (IsNTP(web_contents_) || is_preloaded_ntp) { type = SearchMode::MODE_NTP; origin = SearchMode::ORIGIN_NTP; } else if (IsSearchResults(web_contents_)) { type = SearchMode::MODE_SEARCH_RESULTS; origin = SearchMode::ORIGIN_SEARCH; } if (!update_origin) origin = model_.mode().origin; OmniboxView* omnibox = GetOmniboxView(); if (omnibox && omnibox->model()->user_input_in_progress()) type = SearchMode::MODE_SEARCH_SUGGESTIONS; SearchMode old_mode(model_.mode()); model_.SetMode(SearchMode(type, origin)); if (old_mode.is_ntp() != model_.mode().is_ntp()) { ipc_router_.SetInputInProgress(IsInputInProgress()); } } void SearchTabHelper::DetermineIfPageSupportsInstant() { if (!InInstantProcess(profile(), web_contents_)) { // The page is not in the Instant process. This page does not support // instant. If we send an IPC message to a page that is not in the Instant // process, it will never receive it and will never respond. Therefore, // return immediately. InstantSupportChanged(false); } else if (IsLocal(web_contents_)) { // Local pages always support Instant. InstantSupportChanged(true); } else { ipc_router_.DetermineIfPageSupportsInstant(); } } Profile* SearchTabHelper::profile() const { return Profile::FromBrowserContext(web_contents_->GetBrowserContext()); } bool SearchTabHelper::IsInputInProgress() const { OmniboxView* omnibox = GetOmniboxView(); return !model_.mode().is_ntp() && omnibox && omnibox->model()->focus_state() == OMNIBOX_FOCUS_VISIBLE; } OmniboxView* SearchTabHelper::GetOmniboxView() const { return delegate_ ? delegate_->GetOmniboxView() : NULL; }