普通文本  |  428行  |  17.57 KB

// Copyright (c) 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/google/google_url_tracker.h"

#include "base/bind.h"
#include "base/command_line.h"
#include "base/prefs/pref_service.h"
#include "base/strings/string_util.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/google/google_url_tracker_factory.h"
#include "chrome/browser/google/google_url_tracker_infobar_delegate.h"
#include "chrome/browser/google/google_url_tracker_navigation_helper.h"
#include "chrome/browser/google/google_util.h"
#include "chrome/browser/infobars/infobar.h"
#include "chrome/browser/infobars/infobar_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/notification_service.h"
#include "net/base/load_flags.h"
#include "net/base/net_util.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_request_status.h"


const char GoogleURLTracker::kDefaultGoogleHomepage[] =
    "http://www.google.com/";
const char GoogleURLTracker::kSearchDomainCheckURL[] =
    "https://www.google.com/searchdomaincheck?format=url&type=chrome";

GoogleURLTracker::GoogleURLTracker(
    Profile* profile,
    scoped_ptr<GoogleURLTrackerNavigationHelper> nav_helper,
    Mode mode)
    : profile_(profile),
      nav_helper_(nav_helper.Pass()),
      infobar_creator_(base::Bind(&GoogleURLTrackerInfoBarDelegate::Create)),
      google_url_(mode == UNIT_TEST_MODE ? kDefaultGoogleHomepage :
          profile->GetPrefs()->GetString(prefs::kLastKnownGoogleURL)),
      weak_ptr_factory_(this),
      fetcher_id_(0),
      in_startup_sleep_(true),
      already_fetched_(false),
      need_to_fetch_(false),
      need_to_prompt_(false),
      search_committed_(false) {
  net::NetworkChangeNotifier::AddIPAddressObserver(this);
  nav_helper_->SetGoogleURLTracker(this);

  // Because this function can be called during startup, when kicking off a URL
  // fetch can eat up 20 ms of time, we delay five seconds, which is hopefully
  // long enough to be after startup, but still get results back quickly.
  // Ideally, instead of this timer, we'd do something like "check if the
  // browser is starting up, and if so, come back later", but there is currently
  // no function to do this.
  //
  // In UNIT_TEST mode, where we want to explicitly control when the tracker
  // "wakes up", we do nothing at all.
  if (mode == NORMAL_MODE) {
    static const int kStartFetchDelayMS = 5000;
    base::MessageLoop::current()->PostDelayedTask(FROM_HERE,
        base::Bind(&GoogleURLTracker::FinishSleep,
                   weak_ptr_factory_.GetWeakPtr()),
        base::TimeDelta::FromMilliseconds(kStartFetchDelayMS));
  }
}

GoogleURLTracker::~GoogleURLTracker() {
  // We should only reach here after any tabs and their infobars have been torn
  // down.
  DCHECK(entry_map_.empty());
}

// static
GURL GoogleURLTracker::GoogleURL(Profile* profile) {
  const GoogleURLTracker* tracker =
      GoogleURLTrackerFactory::GetForProfile(profile);
  return tracker ? tracker->google_url_ : GURL(kDefaultGoogleHomepage);
}

// static
void GoogleURLTracker::RequestServerCheck(Profile* profile, bool force) {
  GoogleURLTracker* tracker = GoogleURLTrackerFactory::GetForProfile(profile);
  // If the tracker already has a fetcher, SetNeedToFetch() is unnecessary, and
  // changing |already_fetched_| is wrong.
  if (tracker && !tracker->fetcher_) {
    if (force)
      tracker->already_fetched_ = false;
    tracker->SetNeedToFetch();
  }
}

// static
void GoogleURLTracker::GoogleURLSearchCommitted(Profile* profile) {
  GoogleURLTracker* tracker = GoogleURLTrackerFactory::GetForProfile(profile);
  if (tracker)
    tracker->SearchCommitted();
}

void GoogleURLTracker::AcceptGoogleURL(bool redo_searches) {
  UpdatedDetails urls(google_url_, fetched_google_url_);
  google_url_ = fetched_google_url_;
  PrefService* prefs = profile_->GetPrefs();
  prefs->SetString(prefs::kLastKnownGoogleURL, google_url_.spec());
  prefs->SetString(prefs::kLastPromptedGoogleURL, google_url_.spec());
  content::NotificationService::current()->Notify(
      chrome::NOTIFICATION_GOOGLE_URL_UPDATED,
      content::Source<Profile>(profile_),
      content::Details<UpdatedDetails>(&urls));
  need_to_prompt_ = false;
  CloseAllEntries(redo_searches);
}

void GoogleURLTracker::CancelGoogleURL() {
  profile_->GetPrefs()->SetString(prefs::kLastPromptedGoogleURL,
                                  fetched_google_url_.spec());
  need_to_prompt_ = false;
  CloseAllEntries(false);
}

void GoogleURLTracker::OnURLFetchComplete(const net::URLFetcher* source) {
  // Delete the fetcher on this function's exit.
  scoped_ptr<net::URLFetcher> clean_up_fetcher(fetcher_.release());

  // Don't update the URL if the request didn't succeed.
  if (!source->GetStatus().is_success() || (source->GetResponseCode() != 200)) {
    already_fetched_ = false;
    return;
  }

  // See if the response data was valid.  It should be
  // "<scheme>://[www.]google.<TLD>/".
  std::string url_str;
  source->GetResponseAsString(&url_str);
  TrimWhitespace(url_str, TRIM_ALL, &url_str);
  GURL url(url_str);
  if (!url.is_valid() || (url.path().length() > 1) || url.has_query() ||
      url.has_ref() ||
      !google_util::IsGoogleDomainUrl(url, google_util::DISALLOW_SUBDOMAIN,
                                      google_util::DISALLOW_NON_STANDARD_PORTS))
    return;

  std::swap(url, fetched_google_url_);
  GURL last_prompted_url(
      profile_->GetPrefs()->GetString(prefs::kLastPromptedGoogleURL));

  if (last_prompted_url.is_empty()) {
    // On the very first run of Chrome, when we've never looked up the URL at
    // all, we should just silently switch over to whatever we get immediately.
    AcceptGoogleURL(true);  // Arg is irrelevant.
    return;
  }

  base::string16 fetched_host(net::StripWWWFromHost(fetched_google_url_));
  if (fetched_google_url_ == google_url_) {
    // Either the user has continually been on this URL, or we prompted for a
    // different URL but have now changed back before they responded to any of
    // the prompts.  In this latter case we want to close any infobars and stop
    // prompting.
    CancelGoogleURL();
  } else if (fetched_host == net::StripWWWFromHost(google_url_)) {
    // Similar to the above case, but this time the new URL differs from the
    // existing one, probably due to switching between HTTP and HTTPS searching.
    // Like before we want to close any infobars and stop prompting; we also
    // want to silently accept the change in scheme.  We don't redo open
    // searches so as to avoid suddenly changing a page the user might be
    // interacting with; it's enough to simply get future searches right.
    AcceptGoogleURL(false);
  } else if (fetched_host == net::StripWWWFromHost(last_prompted_url)) {
    // We've re-fetched a TLD the user previously turned down.  Although the new
    // URL might have a different scheme than the old, we want to preserve the
    // user's decision.  Note that it's possible that, like in the above two
    // cases, we fetched yet another different URL in the meantime, which we
    // have infobars prompting about; in this case, as in those above, we want
    // to go ahead and close the infobars and stop prompting, since we've
    // switched back away from that URL.
    CancelGoogleURL();
  } else {
    // We've fetched a URL with a different TLD than the user is currently using
    // or was previously prompted about.  This means we need to prompt again.
    need_to_prompt_ = true;

    // As in all the above cases, there could be infobars prompting about some
    // URL.  If these URLs have the same TLD (e.g. for scheme changes), we can
    // simply leave the existing infobars open as their messages will still be
    // accurate.  Otherwise we go ahead and close them because we need to
    // display a new message.
    // Note: |url| is the previous |fetched_google_url_|.
    if (url.is_valid() && (fetched_host != net::StripWWWFromHost(url)))
      CloseAllEntries(false);
  }
}

void GoogleURLTracker::OnIPAddressChanged() {
  already_fetched_ = false;
  StartFetchIfDesirable();
}

void GoogleURLTracker::Shutdown() {
  nav_helper_.reset();
  weak_ptr_factory_.InvalidateWeakPtrs();
  fetcher_.reset();
  net::NetworkChangeNotifier::RemoveIPAddressObserver(this);
}

void GoogleURLTracker::DeleteMapEntryForService(
    const InfoBarService* infobar_service) {
  // WARNING: |infobar_service| may point to a deleted object.  Do not
  // dereference it!  See OnTabClosed().
  EntryMap::iterator i(entry_map_.find(infobar_service));
  DCHECK(i != entry_map_.end());
  GoogleURLTrackerMapEntry* map_entry = i->second;

  UnregisterForEntrySpecificNotifications(*map_entry, false);
  entry_map_.erase(i);
  delete map_entry;
}

void GoogleURLTracker::SetNeedToFetch() {
  need_to_fetch_ = true;
  StartFetchIfDesirable();
}

void GoogleURLTracker::FinishSleep() {
  in_startup_sleep_ = false;
  StartFetchIfDesirable();
}

void GoogleURLTracker::StartFetchIfDesirable() {
  // Bail if a fetch isn't appropriate right now.  This function will be called
  // again each time one of the preconditions changes, so we'll fetch
  // immediately once all of them are met.
  //
  // See comments in header on the class, on RequestServerCheck(), and on the
  // various members here for more detail on exactly what the conditions are.
  if (in_startup_sleep_ || already_fetched_ || !need_to_fetch_)
    return;

  // Some switches should disable the Google URL tracker entirely.  If we can't
  // do background networking, we can't do the necessary fetch, and if the user
  // specified a Google base URL manually, we shouldn't bother to look up any
  // alternatives or offer to switch to them.
  if (CommandLine::ForCurrentProcess()->HasSwitch(
      switches::kDisableBackgroundNetworking) ||
      CommandLine::ForCurrentProcess()->HasSwitch(switches::kGoogleBaseURL))
    return;

  std::string fetch_url = CommandLine::ForCurrentProcess()->
      GetSwitchValueASCII(switches::kGoogleSearchDomainCheckURL);
  if (fetch_url.empty())
    fetch_url = kSearchDomainCheckURL;

  already_fetched_ = true;
  fetcher_.reset(net::URLFetcher::Create(fetcher_id_, GURL(fetch_url),
                                         net::URLFetcher::GET, this));
  ++fetcher_id_;
  // We don't want this fetch to set new entries in the cache or cookies, lest
  // we alarm the user.
  fetcher_->SetLoadFlags(net::LOAD_DISABLE_CACHE |
                         net::LOAD_DO_NOT_SAVE_COOKIES);
  fetcher_->SetRequestContext(profile_->GetRequestContext());

  // Configure to max_retries at most kMaxRetries times for 5xx errors.
  static const int kMaxRetries = 5;
  fetcher_->SetMaxRetriesOn5xx(kMaxRetries);

  fetcher_->Start();
}

void GoogleURLTracker::SearchCommitted() {
  if (need_to_prompt_) {
    search_committed_ = true;
    // These notifications will fire a bit later in the same call chain we're
    // currently in.
    if (!nav_helper_->IsListeningForNavigationStart())
      nav_helper_->SetListeningForNavigationStart(true);
  }
}

void GoogleURLTracker::OnNavigationPending(
    content::NavigationController* navigation_controller,
    InfoBarService* infobar_service,
    int pending_id) {
  EntryMap::iterator i(entry_map_.find(infobar_service));

  if (search_committed_) {
    search_committed_ = false;
    // Whether there's an existing infobar or not, we need to listen for the
    // load to commit, so we can show and/or update the infobar when it does.
    // (We may already be registered for this if there is an existing infobar
    // that had a previous pending search that hasn't yet committed.)
    if (!nav_helper_->IsListeningForNavigationCommit(navigation_controller)) {
      nav_helper_->SetListeningForNavigationCommit(navigation_controller,
                                                   true);
    }
    if (i == entry_map_.end()) {
      // This is a search on a tab that doesn't have one of our infobars, so
      // prepare to add one.  Note that we only listen for the tab's destruction
      // on this path; if there was already a map entry, then either it doesn't
      // yet have an infobar and we're already registered for this, or it has an
      // infobar and the infobar's owner will handle tearing it down when the
      // tab is destroyed.
      nav_helper_->SetListeningForTabDestruction(navigation_controller, true);
      entry_map_.insert(std::make_pair(
          infobar_service,
          new GoogleURLTrackerMapEntry(this, infobar_service,
                                       navigation_controller)));
    } else if (i->second->has_infobar_delegate()) {
      // This is a new search on a tab where we already have an infobar.
      i->second->infobar_delegate()->set_pending_id(pending_id);
    }
  } else if (i != entry_map_.end()){
    if (i->second->has_infobar_delegate()) {
      // This is a non-search navigation on a tab with an infobar.  If there was
      // a previous pending search on this tab, this means it won't commit, so
      // undo anything we did in response to seeing that.  Note that if there
      // was no pending search on this tab, these statements are effectively a
      // no-op.
      //
      // If this navigation actually commits, that will trigger the infobar's
      // owner to expire the infobar if need be.  If it doesn't commit, then
      // simply leaving the infobar as-is will have been the right thing.
      UnregisterForEntrySpecificNotifications(*i->second, false);
      i->second->infobar_delegate()->set_pending_id(0);
    } else {
      // Non-search navigation on a tab with an entry that has not yet created
      // an infobar.  This means the original search won't commit, so delete the
      // entry.
      i->second->Close(false);
    }
  } else {
    // Non-search navigation on a tab without an infobars.  This is irrelevant
    // to us.
  }
}

void GoogleURLTracker::OnNavigationCommitted(InfoBarService* infobar_service,
                                             const GURL& search_url) {
  EntryMap::iterator i(entry_map_.find(infobar_service));
  DCHECK(i != entry_map_.end());
  GoogleURLTrackerMapEntry* map_entry = i->second;
  DCHECK(search_url.is_valid());

  UnregisterForEntrySpecificNotifications(*map_entry, true);
  if (map_entry->has_infobar_delegate()) {
    map_entry->infobar_delegate()->Update(search_url);
  } else {
    InfoBar* infobar = infobar_creator_.Run(infobar_service, this, search_url);
    if (infobar) {
      map_entry->SetInfoBarDelegate(
          static_cast<GoogleURLTrackerInfoBarDelegate*>(infobar->delegate()));
    } else {
      map_entry->Close(false);
    }
  }
}

void GoogleURLTracker::OnTabClosed(
    content::NavigationController* navigation_controller) {
  // Because InfoBarService tears itself down on tab destruction, it's possible
  // to get a non-NULL InfoBarService pointer here, depending on which order
  // notifications fired in.  Likewise, the pointer in |entry_map_| (and in its
  // associated MapEntry) may point to deleted memory.  Therefore, if we were to
  // access the InfoBarService* we have for this tab, we'd need to ensure we
  // just looked at the raw pointer value, and never dereferenced it.  This
  // function doesn't need to do even that, but others in the call chain from
  // here might (and have comments pointing back here).
  for (EntryMap::iterator i(entry_map_.begin()); i != entry_map_.end(); ++i) {
    if (i->second->navigation_controller() == navigation_controller) {
      i->second->Close(false);
      return;
    }
  }
  NOTREACHED();
}

void GoogleURLTracker::CloseAllEntries(bool redo_searches) {
  // Delete all entries, whether they have infobars or not.
  while (!entry_map_.empty())
    entry_map_.begin()->second->Close(redo_searches);
}

void GoogleURLTracker::UnregisterForEntrySpecificNotifications(
    const GoogleURLTrackerMapEntry& map_entry,
    bool must_be_listening_for_commit) {
  // For tabs with map entries but no infobars, we should always be listening
  // for both these notifications.  For tabs with infobars, we may be listening
  // for navigation commits if the user has performed a new search on this tab.
  if (nav_helper_->IsListeningForNavigationCommit(
          map_entry.navigation_controller())) {
    nav_helper_->SetListeningForNavigationCommit(
        map_entry.navigation_controller(), false);
  } else {
    DCHECK(!must_be_listening_for_commit);
    DCHECK(map_entry.has_infobar_delegate());
  }
  const bool registered_for_tab_destruction =
      nav_helper_->IsListeningForTabDestruction(
          map_entry.navigation_controller());
  DCHECK_NE(registered_for_tab_destruction, map_entry.has_infobar_delegate());
  if (registered_for_tab_destruction) {
    nav_helper_->SetListeningForTabDestruction(
        map_entry.navigation_controller(), false);
  }

  // Our global listeners for these other notifications should be in place iff
  // we have any tabs still listening for commits.  These tabs either have no
  // infobars or have received new pending searches atop existing infobars; in
  // either case we want to catch subsequent pending non-search navigations.
  // See the various cases inside OnNavigationPending().
  for (EntryMap::const_iterator i(entry_map_.begin()); i != entry_map_.end();
       ++i) {
    if (nav_helper_->IsListeningForNavigationCommit(
            i->second->navigation_controller())) {
      DCHECK(nav_helper_->IsListeningForNavigationStart());
      return;
    }
  }
  if (nav_helper_->IsListeningForNavigationStart()) {
    DCHECK(!search_committed_);
    nav_helper_->SetListeningForNavigationStart(false);
  }
}