| Index: chrome/browser/autocomplete/base_search_provider.cc
|
| diff --git a/chrome/browser/autocomplete/base_search_provider.cc b/chrome/browser/autocomplete/base_search_provider.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..bba3786a4264dc8065ee13d0b772f430ad7cd133
|
| --- /dev/null
|
| +++ b/chrome/browser/autocomplete/base_search_provider.cc
|
| @@ -0,0 +1,869 @@
|
| +// Copyright (c) 2014 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/autocomplete/base_search_provider.h"
|
| +
|
| +#include <algorithm>
|
| +
|
| +#include "base/callback.h"
|
| +#include "base/i18n/case_conversion.h"
|
| +#include "base/i18n/icu_string_conversions.h"
|
| +#include "base/json/json_string_value_serializer.h"
|
| +#include "base/prefs/pref_service.h"
|
| +#include "base/strings/string16.h"
|
| +#include "base/strings/string_util.h"
|
| +#include "base/strings/utf_string_conversions.h"
|
| +#include "chrome/browser/autocomplete/autocomplete_input.h"
|
| +#include "chrome/browser/autocomplete/autocomplete_match.h"
|
| +#include "chrome/browser/autocomplete/autocomplete_provider.h"
|
| +#include "chrome/browser/autocomplete/url_prefix.h"
|
| +#include "chrome/browser/history/history_service.h"
|
| +#include "chrome/browser/history/history_service_factory.h"
|
| +#include "chrome/browser/omnibox/omnibox_field_trial.h"
|
| +#include "chrome/browser/profiles/profile.h"
|
| +#include "chrome/browser/search/search.h"
|
| +#include "chrome/browser/search_engines/template_url.h"
|
| +#include "chrome/browser/search_engines/template_url_prepopulate_data.h"
|
| +#include "chrome/browser/sync/profile_sync_service.h"
|
| +#include "chrome/browser/sync/profile_sync_service_factory.h"
|
| +#include "chrome/browser/sync/sync_prefs.h"
|
| +#include "chrome/browser/ui/browser.h"
|
| +#include "chrome/browser/ui/browser_finder.h"
|
| +#include "chrome/browser/ui/browser_instant_controller.h"
|
| +#include "chrome/browser/ui/host_desktop.h"
|
| +#include "chrome/common/autocomplete_match_type.h"
|
| +#include "chrome/common/net/url_fixer_upper.h"
|
| +#include "chrome/common/pref_names.h"
|
| +#include "chrome/common/url_constants.h"
|
| +#include "net/base/net_util.h"
|
| +#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
|
| +#include "net/http/http_response_headers.h"
|
| +#include "net/url_request/url_fetcher.h"
|
| +#include "net/url_request/url_fetcher_delegate.h"
|
| +#include "net/url_request/url_request_status.h"
|
| +
|
| +namespace {
|
| +AutocompleteMatchType::Type GetAutocompleteMatchType(const std::string& type) {
|
| + if (type == "ENTITY")
|
| + return AutocompleteMatchType::SEARCH_SUGGEST_ENTITY;
|
| + if (type == "INFINITE")
|
| + return AutocompleteMatchType::SEARCH_SUGGEST_INFINITE;
|
| + if (type == "PERSONALIZED")
|
| + return AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED;
|
| + if (type == "PROFILE")
|
| + return AutocompleteMatchType::SEARCH_SUGGEST_PROFILE;
|
| + return AutocompleteMatchType::SEARCH_SUGGEST;
|
| +}
|
| +} // namespace
|
| +
|
| +// SuggestionDeletionHandler --------------------------------------------------
|
| +
|
| +// This class handles making requests to the server in order to delete
|
| +// personalized suggestions.
|
| +class SuggestionDeletionHandler : public net::URLFetcherDelegate {
|
| + public:
|
| + typedef base::Callback<void(bool, SuggestionDeletionHandler*)>
|
| + DeletionCompletedCallback;
|
| +
|
| + SuggestionDeletionHandler(const std::string& deletion_url,
|
| + Profile* profile,
|
| + const DeletionCompletedCallback& callback);
|
| +
|
| + virtual ~SuggestionDeletionHandler();
|
| +
|
| + private:
|
| + // net::URLFetcherDelegate:
|
| + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE;
|
| +
|
| + scoped_ptr<net::URLFetcher> deletion_fetcher_;
|
| + DeletionCompletedCallback callback_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(SuggestionDeletionHandler);
|
| +};
|
| +
|
| +SuggestionDeletionHandler::SuggestionDeletionHandler(
|
| + const std::string& deletion_url,
|
| + Profile* profile,
|
| + const DeletionCompletedCallback& callback)
|
| + : callback_(callback) {
|
| + GURL url(deletion_url);
|
| + DCHECK(url.is_valid());
|
| +
|
| + deletion_fetcher_.reset(
|
| + net::URLFetcher::Create(BaseSearchProvider::kDeletionURLFetcherID,
|
| + url,
|
| + net::URLFetcher::GET,
|
| + this));
|
| + deletion_fetcher_->SetRequestContext(profile->GetRequestContext());
|
| + deletion_fetcher_->Start();
|
| +};
|
| +
|
| +SuggestionDeletionHandler::~SuggestionDeletionHandler() {}
|
| +
|
| +void SuggestionDeletionHandler::OnURLFetchComplete(
|
| + const net::URLFetcher* source) {
|
| + DCHECK(source == deletion_fetcher_.get());
|
| + callback_.Run(
|
| + source->GetStatus().is_success() && (source->GetResponseCode() == 200),
|
| + this);
|
| +};
|
| +
|
| +// BaseSearchProvider ---------------------------------------------------------
|
| +
|
| +// Arbitrary ID needed for testing, set to 7 as to not conflict with any
|
| +// additions to SearchProvider.
|
| +const int BaseSearchProvider::kDeletionURLFetcherID = 7;
|
| +
|
| +BaseSearchProvider::BaseSearchProvider(AutocompleteProviderListener* listener,
|
| + Profile* profile,
|
| + AutocompleteProvider::Type type)
|
| + : AutocompleteProvider(listener, profile, type),
|
| + field_trial_triggered_(false),
|
| + field_trial_triggered_in_session_(false),
|
| + suggest_results_pending_(0) {}
|
| +
|
| +void BaseSearchProvider::Stop(bool clear_cached_results) {
|
| + StopSuggest();
|
| + done_ = true;
|
| +
|
| + if (clear_cached_results)
|
| + ClearAllResults();
|
| +}
|
| +
|
| +void BaseSearchProvider::AddProviderInfo(ProvidersInfo* provider_info) const {
|
| + provider_info->push_back(metrics::OmniboxEventProto_ProviderInfo());
|
| + metrics::OmniboxEventProto_ProviderInfo& new_entry = provider_info->back();
|
| + new_entry.set_provider(AsOmniboxEventProviderType());
|
| + new_entry.set_provider_done(done_);
|
| + std::vector<uint32> field_trial_hashes;
|
| + OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(&field_trial_hashes);
|
| + for (size_t i = 0; i < field_trial_hashes.size(); ++i) {
|
| + if (field_trial_triggered_)
|
| + new_entry.mutable_field_trial_triggered()->Add(field_trial_hashes[i]);
|
| + if (field_trial_triggered_in_session_) {
|
| + new_entry.mutable_field_trial_triggered_in_session()->Add(
|
| + field_trial_hashes[i]);
|
| + }
|
| + }
|
| +}
|
| +
|
| +void BaseSearchProvider::DeleteMatch(const AutocompleteMatch& match) {
|
| + DCHECK(match.deletable);
|
| +
|
| + deletion_handlers_.push_back(new SuggestionDeletionHandler(
|
| + match.GetAdditionalInfo(BaseSearchProvider::kDeletionUrlKey),
|
| + profile_,
|
| + base::Bind(&BaseSearchProvider::OnDeletionComplete,
|
| + base::Unretained(this))));
|
| +
|
| + HistoryService* const history_service =
|
| + HistoryServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS);
|
| + TemplateURL* template_url = match.GetTemplateURL(profile_, false);
|
| + // This may be NULL if the template corresponding to the keyword has been
|
| + // deleted or there is no keyword set.
|
| + if (template_url != NULL) {
|
| + history_service->DeleteMatchingURLsForKeyword(template_url->id(),
|
| + match.contents);
|
| + }
|
| +
|
| + // Immediately update the list of matches to show the match was deleted,
|
| + // regardless of whether the server request actually succeeds.
|
| + DeleteMatchFromMatches(match);
|
| +}
|
| +
|
| +void BaseSearchProvider::ResetSession() {}
|
| +
|
| +// static
|
| +bool BaseSearchProvider::ShouldPrefetch(const AutocompleteMatch& match) {
|
| + return match.GetAdditionalInfo(kShouldPrefetchKey) == kTrue;
|
| +}
|
| +
|
| +// BaseSearchProvider::Result -------------------------------------------------
|
| +
|
| +BaseSearchProvider::Result::Result(bool from_keyword_provider,
|
| + int relevance,
|
| + bool relevance_from_server)
|
| + : from_keyword_provider_(from_keyword_provider),
|
| + relevance_(relevance),
|
| + relevance_from_server_(relevance_from_server) {}
|
| +
|
| +BaseSearchProvider::Result::~Result() {}
|
| +
|
| +// BaseSearchProvider::SuggestResult ------------------------------------------
|
| +
|
| +BaseSearchProvider::SuggestResult::SuggestResult(
|
| + const base::string16& suggestion,
|
| + AutocompleteMatchType::Type type,
|
| + const base::string16& match_contents,
|
| + const base::string16& annotation,
|
| + const std::string& suggest_query_params,
|
| + const std::string& deletion_url,
|
| + bool from_keyword_provider,
|
| + int relevance,
|
| + bool relevance_from_server,
|
| + bool should_prefetch,
|
| + const base::string16& input_text)
|
| + : Result(from_keyword_provider, relevance, relevance_from_server),
|
| + suggestion_(suggestion),
|
| + type_(type),
|
| + annotation_(annotation),
|
| + suggest_query_params_(suggest_query_params),
|
| + deletion_url_(deletion_url),
|
| + should_prefetch_(should_prefetch) {
|
| + match_contents_ = match_contents;
|
| + DCHECK(!match_contents_.empty());
|
| + ClassifyMatchContents(true, input_text);
|
| +}
|
| +
|
| +BaseSearchProvider::SuggestResult::~SuggestResult() {}
|
| +
|
| +void BaseSearchProvider::SuggestResult::ClassifyMatchContents(
|
| + const bool allow_bolding_all,
|
| + const base::string16& input_text) {
|
| + size_t input_position = match_contents_.find(input_text);
|
| + if (!allow_bolding_all && (input_position == base::string16::npos)) {
|
| + // Bail if the code below to update the bolding would bold the whole
|
| + // string. Note that the string may already be entirely bolded; if
|
| + // so, leave it as is.
|
| + return;
|
| + }
|
| + match_contents_class_.clear();
|
| + // We do intra-string highlighting for suggestions - the suggested segment
|
| + // will be highlighted, e.g. for input_text = "you" the suggestion may be
|
| + // "youtube", so we'll bold the "tube" section: you*tube*.
|
| + if (input_text != match_contents_) {
|
| + if (input_position == base::string16::npos) {
|
| + // The input text is not a substring of the query string, e.g. input
|
| + // text is "slasdot" and the query string is "slashdot", so we bold the
|
| + // whole thing.
|
| + match_contents_class_.push_back(
|
| + ACMatchClassification(0, ACMatchClassification::MATCH));
|
| + } else {
|
| + // We don't iterate over the string here annotating all matches because
|
| + // it looks odd to have every occurrence of a substring that may be as
|
| + // short as a single character highlighted in a query suggestion result,
|
| + // e.g. for input text "s" and query string "southwest airlines", it
|
| + // looks odd if both the first and last s are highlighted.
|
| + if (input_position != 0) {
|
| + match_contents_class_.push_back(
|
| + ACMatchClassification(0, ACMatchClassification::MATCH));
|
| + }
|
| + match_contents_class_.push_back(
|
| + ACMatchClassification(input_position, ACMatchClassification::NONE));
|
| + size_t next_fragment_position = input_position + input_text.length();
|
| + if (next_fragment_position < match_contents_.length()) {
|
| + match_contents_class_.push_back(ACMatchClassification(
|
| + next_fragment_position, ACMatchClassification::MATCH));
|
| + }
|
| + }
|
| + } else {
|
| + // Otherwise, match_contents_ is a verbatim (what-you-typed) match, either
|
| + // for the default provider or a keyword search provider.
|
| + match_contents_class_.push_back(
|
| + ACMatchClassification(0, ACMatchClassification::NONE));
|
| + }
|
| +}
|
| +
|
| +bool BaseSearchProvider::SuggestResult::IsInlineable(
|
| + const base::string16& input) const {
|
| + return StartsWith(suggestion_, input, false);
|
| +}
|
| +
|
| +int BaseSearchProvider::SuggestResult::CalculateRelevance(
|
| + const AutocompleteInput& input,
|
| + bool keyword_provider_requested) const {
|
| + if (!from_keyword_provider_ && keyword_provider_requested)
|
| + return 100;
|
| + return ((input.type() == AutocompleteInput::URL) ? 300 : 600);
|
| +}
|
| +
|
| +// BaseSearchProvider::NavigationResult ---------------------------------------
|
| +
|
| +BaseSearchProvider::NavigationResult::NavigationResult(
|
| + const AutocompleteProvider& provider,
|
| + const GURL& url,
|
| + const base::string16& description,
|
| + bool from_keyword_provider,
|
| + int relevance,
|
| + bool relevance_from_server,
|
| + const base::string16& input_text,
|
| + const std::string& languages)
|
| + : Result(from_keyword_provider, relevance, relevance_from_server),
|
| + url_(url),
|
| + formatted_url_(AutocompleteInput::FormattedStringWithEquivalentMeaning(
|
| + url,
|
| + provider.StringForURLDisplay(url, true, false))),
|
| + description_(description) {
|
| + DCHECK(url_.is_valid());
|
| + CalculateAndClassifyMatchContents(true, input_text, languages);
|
| +}
|
| +
|
| +BaseSearchProvider::NavigationResult::~NavigationResult() {}
|
| +
|
| +void BaseSearchProvider::NavigationResult::CalculateAndClassifyMatchContents(
|
| + const bool allow_bolding_nothing,
|
| + const base::string16& input_text,
|
| + const std::string& languages) {
|
| + // First look for the user's input inside the formatted url as it would be
|
| + // without trimming the scheme, so we can find matches at the beginning of
|
| + // the scheme.
|
| + const URLPrefix* prefix =
|
| + URLPrefix::BestURLPrefix(formatted_url_, input_text);
|
| + size_t match_start = (prefix == NULL) ? formatted_url_.find(input_text)
|
| + : prefix->prefix.length();
|
| + bool trim_http = !AutocompleteInput::HasHTTPScheme(input_text) &&
|
| + (!prefix || (match_start != 0));
|
| + const net::FormatUrlTypes format_types =
|
| + net::kFormatUrlOmitAll & ~(trim_http ? 0 : net::kFormatUrlOmitHTTP);
|
| +
|
| + base::string16 match_contents = net::FormatUrl(url_,
|
| + languages,
|
| + format_types,
|
| + net::UnescapeRule::SPACES,
|
| + NULL,
|
| + NULL,
|
| + &match_start);
|
| + // If the first match in the untrimmed string was inside a scheme that we
|
| + // trimmed, look for a subsequent match.
|
| + if (match_start == base::string16::npos)
|
| + match_start = match_contents.find(input_text);
|
| + // Update |match_contents_| and |match_contents_class_| if it's allowed.
|
| + if (allow_bolding_nothing || (match_start != base::string16::npos)) {
|
| + match_contents_ = match_contents;
|
| + // Safe if |match_start| is npos; also safe if the input is longer than the
|
| + // remaining contents after |match_start|.
|
| + AutocompleteMatch::ClassifyLocationInString(match_start,
|
| + input_text.length(),
|
| + match_contents_.length(),
|
| + ACMatchClassification::URL,
|
| + &match_contents_class_);
|
| + }
|
| +}
|
| +
|
| +bool BaseSearchProvider::NavigationResult::IsInlineable(
|
| + const base::string16& input) const {
|
| + return URLPrefix::BestURLPrefix(formatted_url_, input) != NULL;
|
| +}
|
| +
|
| +int BaseSearchProvider::NavigationResult::CalculateRelevance(
|
| + const AutocompleteInput& input,
|
| + bool keyword_provider_requested) const {
|
| + return (from_keyword_provider_ || !keyword_provider_requested) ? 800 : 150;
|
| +}
|
| +
|
| +// BaseSearchProvider::Results ------------------------------------------------
|
| +
|
| +BaseSearchProvider::Results::Results() : verbatim_relevance(-1) {}
|
| +
|
| +BaseSearchProvider::Results::~Results() {}
|
| +
|
| +void BaseSearchProvider::Results::Clear() {
|
| + suggest_results.clear();
|
| + navigation_results.clear();
|
| + verbatim_relevance = -1;
|
| + metadata.clear();
|
| +}
|
| +
|
| +bool BaseSearchProvider::Results::HasServerProvidedScores() const {
|
| + if (verbatim_relevance >= 0)
|
| + return true;
|
| +
|
| + // Right now either all results of one type will be server-scored or they
|
| + // will all be locally scored, but in case we change this later, we'll just
|
| + // check them all.
|
| + for (SuggestResults::const_iterator i(suggest_results.begin());
|
| + i != suggest_results.end();
|
| + ++i) {
|
| + if (i->relevance_from_server())
|
| + return true;
|
| + }
|
| + for (NavigationResults::const_iterator i(navigation_results.begin());
|
| + i != navigation_results.end();
|
| + ++i) {
|
| + if (i->relevance_from_server())
|
| + return true;
|
| + }
|
| +
|
| + return false;
|
| +}
|
| +
|
| +// BaseSearchProvider ---------------------------------------------------------
|
| +
|
| +const char BaseSearchProvider::kDeletionUrlKey[] = "deletion_url";
|
| +const char BaseSearchProvider::kRelevanceFromServerKey[] =
|
| + "relevance_from_server";
|
| +const char BaseSearchProvider::kShouldPrefetchKey[] = "should_prefetch";
|
| +const char BaseSearchProvider::kSuggestMetadataKey[] = "suggest_metadata";
|
| +const char BaseSearchProvider::kTrue[] = "true";
|
| +const char BaseSearchProvider::kFalse[] = "false";
|
| +
|
| +BaseSearchProvider::~BaseSearchProvider() {}
|
| +
|
| +// static
|
| +AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion(
|
| + AutocompleteProvider* autocomplete_provider,
|
| + const AutocompleteInput& input,
|
| + const base::string16& input_text,
|
| + const SuggestResult& suggestion,
|
| + const TemplateURL* template_url,
|
| + int accepted_suggestion,
|
| + int omnibox_start_margin,
|
| + bool append_extra_query_params) {
|
| + AutocompleteMatch match(
|
| + autocomplete_provider, suggestion.relevance(), false, suggestion.type());
|
| +
|
| + if (!template_url)
|
| + return match;
|
| + match.keyword = template_url->keyword();
|
| + match.contents = suggestion.match_contents();
|
| + match.contents_class = suggestion.match_contents_class();
|
| +
|
| + if (!suggestion.annotation().empty())
|
| + match.description = suggestion.annotation();
|
| +
|
| + match.allowed_to_be_default_match =
|
| + (input_text == suggestion.match_contents());
|
| +
|
| + // When the user forced a query, we need to make sure all the fill_into_edit
|
| + // values preserve that property. Otherwise, if the user starts editing a
|
| + // suggestion, non-Search results will suddenly appear.
|
| + if (input.type() == AutocompleteInput::FORCED_QUERY)
|
| + match.fill_into_edit.assign(base::ASCIIToUTF16("?"));
|
| + if (suggestion.from_keyword_provider())
|
| + match.fill_into_edit.append(match.keyword + base::char16(' '));
|
| + if (!input.prevent_inline_autocomplete() &&
|
| + StartsWith(suggestion.suggestion(), input_text, false)) {
|
| + match.inline_autocompletion =
|
| + suggestion.suggestion().substr(input_text.length());
|
| + match.allowed_to_be_default_match = true;
|
| + }
|
| + match.fill_into_edit.append(suggestion.suggestion());
|
| +
|
| + const TemplateURLRef& search_url = template_url->url_ref();
|
| + DCHECK(search_url.SupportsReplacement());
|
| + match.search_terms_args.reset(
|
| + new TemplateURLRef::SearchTermsArgs(suggestion.suggestion()));
|
| + match.search_terms_args->original_query = input_text;
|
| + match.search_terms_args->accepted_suggestion = accepted_suggestion;
|
| + match.search_terms_args->omnibox_start_margin = omnibox_start_margin;
|
| + match.search_terms_args->suggest_query_params =
|
| + suggestion.suggest_query_params();
|
| + match.search_terms_args->append_extra_query_params =
|
| + append_extra_query_params;
|
| + // This is the destination URL sans assisted query stats. This must be set
|
| + // so the AutocompleteController can properly de-dupe; the controller will
|
| + // eventually overwrite it before it reaches the user.
|
| + match.destination_url =
|
| + GURL(search_url.ReplaceSearchTerms(*match.search_terms_args.get()));
|
| +
|
| + // Search results don't look like URLs.
|
| + match.transition = suggestion.from_keyword_provider()
|
| + ? content::PAGE_TRANSITION_KEYWORD
|
| + : content::PAGE_TRANSITION_GENERATED;
|
| +
|
| + return match;
|
| +}
|
| +
|
| +// static
|
| +bool BaseSearchProvider::CanSendURL(
|
| + const GURL& current_page_url,
|
| + const GURL& suggest_url,
|
| + const TemplateURL* template_url,
|
| + AutocompleteInput::PageClassification page_classification,
|
| + Profile* profile) {
|
| + if (!current_page_url.is_valid())
|
| + return false;
|
| +
|
| + // TODO(hfung): Show Most Visited on NTP with appropriate verbatim
|
| + // description when the user actively focuses on the omnibox as discussed in
|
| + // crbug/305366 if Most Visited (or something similar) will launch.
|
| + if ((page_classification ==
|
| + AutocompleteInput::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS) ||
|
| + (page_classification ==
|
| + AutocompleteInput::INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS))
|
| + return false;
|
| +
|
| + // Only allow HTTP URLs or HTTPS URLs for the same domain as the search
|
| + // provider.
|
| + if ((current_page_url.scheme() != content::kHttpScheme) &&
|
| + ((current_page_url.scheme() != content::kHttpsScheme) ||
|
| + !net::registry_controlled_domains::SameDomainOrHost(
|
| + current_page_url,
|
| + suggest_url,
|
| + net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES)))
|
| + return false;
|
| +
|
| + // Make sure we are sending the suggest request through HTTPS to prevent
|
| + // exposing the current page URL to networks before the search provider.
|
| + if (!suggest_url.SchemeIs(content::kHttpsScheme))
|
| + return false;
|
| +
|
| + // Don't run if there's no profile or in incognito mode.
|
| + if (profile == NULL || profile->IsOffTheRecord())
|
| + return false;
|
| +
|
| + // Don't run if we can't get preferences or search suggest is not enabled.
|
| + PrefService* prefs = profile->GetPrefs();
|
| + if (!prefs->GetBoolean(prefs::kSearchSuggestEnabled))
|
| + return false;
|
| +
|
| + // Only make the request if we know that the provider supports zero suggest
|
| + // (currently only the prepopulated Google provider).
|
| + if (template_url == NULL || !template_url->SupportsReplacement() ||
|
| + TemplateURLPrepopulateData::GetEngineType(*template_url) !=
|
| + SEARCH_ENGINE_GOOGLE)
|
| + return false;
|
| +
|
| + // Check field trials and settings allow sending the URL on suggest requests.
|
| + ProfileSyncService* service =
|
| + ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile);
|
| + browser_sync::SyncPrefs sync_prefs(prefs);
|
| + if (!OmniboxFieldTrial::InZeroSuggestFieldTrial() || service == NULL ||
|
| + !service->IsSyncEnabledAndLoggedIn() ||
|
| + !sync_prefs.GetPreferredDataTypes(syncer::UserTypes())
|
| + .Has(syncer::PROXY_TABS) ||
|
| + service->GetEncryptedDataTypes().Has(syncer::SESSIONS))
|
| + return false;
|
| +
|
| + return true;
|
| +}
|
| +
|
| +void BaseSearchProvider::Start(const AutocompleteInput& input,
|
| + bool minimal_changes) {}
|
| +
|
| +void BaseSearchProvider::OnURLFetchComplete(const net::URLFetcher* source) {
|
| + DCHECK(!done_);
|
| + suggest_results_pending_--;
|
| + DCHECK_GE(suggest_results_pending_, 0); // Should never go negative
|
| +
|
| + LogFetchComplete(source);
|
| +
|
| + bool results_updated = false;
|
| + if (IsRequestSuccessful(source)) {
|
| + const net::HttpResponseHeaders* const response_headers =
|
| + source->GetResponseHeaders();
|
| + std::string json_data;
|
| + source->GetResponseAsString(&json_data);
|
| +
|
| + // JSON is supposed to be UTF-8, but some suggest service providers send
|
| + // JSON files in non-UTF-8 encodings. The actual encoding is usually
|
| + // specified in the Content-Type header field.
|
| + if (response_headers) {
|
| + std::string charset;
|
| + if (response_headers->GetCharset(&charset)) {
|
| + base::string16 data_16;
|
| + // TODO(jungshik): Switch to CodePageToUTF8 after it's added.
|
| + if (base::CodepageToUTF16(json_data,
|
| + charset.c_str(),
|
| + base::OnStringConversionError::FAIL,
|
| + &data_16))
|
| + json_data = base::UTF16ToUTF8(data_16);
|
| + }
|
| + }
|
| +
|
| + scoped_ptr<base::Value> data(DeserializeJsonData(json_data));
|
| + if (data.get())
|
| + results_updated = ParseSuggestResults(
|
| + *data.get(), source, GetResultsObjectToFill(source));
|
| + }
|
| +
|
| + UpdateMatches();
|
| +
|
| + if (ShouldSendProviderUpdate(results_updated))
|
| + listener_->OnProviderUpdate(results_updated);
|
| +}
|
| +
|
| +bool BaseSearchProvider::IsRequestSuccessful(const net::URLFetcher* source) {
|
| + return source->GetStatus().is_success() && (source->GetResponseCode() == 200);
|
| +}
|
| +
|
| +bool BaseSearchProvider::IsKeywordRequest(const net::URLFetcher* source) {
|
| + return false;
|
| +}
|
| +
|
| +bool BaseSearchProvider::ParseSuggestResults(const base::Value& root_val,
|
| + const net::URLFetcher* source,
|
| + Results* results) {
|
| + base::string16 query;
|
| + const base::ListValue* root_list = NULL;
|
| + const base::ListValue* results_list = NULL;
|
| + if (!root_val.GetAsList(&root_list) || !root_list->GetString(0, &query) ||
|
| + !IsValidQuery(query, source) || !root_list->GetList(1, &results_list))
|
| + return false;
|
| +
|
| + // 3rd element: Description list.
|
| + const base::ListValue* descriptions = NULL;
|
| + root_list->GetList(2, &descriptions);
|
| +
|
| + // 4th element: Disregard the query URL list for now.
|
| +
|
| + // Reset suggested relevance information from the default provider.
|
| + results->verbatim_relevance = GetDefaultRelevance();
|
| +
|
| + // 5th element: Optional key-value pairs from the Suggest server.
|
| + const base::ListValue* types = NULL;
|
| + const base::ListValue* relevances = NULL;
|
| + const base::ListValue* suggestion_details = NULL;
|
| + const base::DictionaryValue* extras = NULL;
|
| + int prefetch_index = -1;
|
| + if (root_list->GetDictionary(4, &extras)) {
|
| + extras->GetList("google:suggesttype", &types);
|
| +
|
| + // Discard this list if its size does not match that of the suggestions.
|
| + if (extras->GetList("google:suggestrelevance", &relevances) &&
|
| + (relevances->GetSize() != results_list->GetSize()))
|
| + relevances = NULL;
|
| + extras->GetInteger("google:verbatimrelevance",
|
| + &results->verbatim_relevance);
|
| +
|
| + // Check if the active suggest field trial (if any) has triggered.
|
| + bool triggered = false;
|
| + extras->GetBoolean("google:fieldtrialtriggered", &triggered);
|
| + field_trial_triggered_ |= triggered;
|
| + field_trial_triggered_in_session_ |= triggered;
|
| +
|
| + const base::DictionaryValue* client_data = NULL;
|
| + if (extras->GetDictionary("google:clientdata", &client_data) && client_data)
|
| + client_data->GetInteger("phi", &prefetch_index);
|
| +
|
| + if (extras->GetList("google:suggestdetail", &suggestion_details) &&
|
| + suggestion_details->GetSize() != results_list->GetSize())
|
| + suggestion_details = NULL;
|
| +
|
| + // Store the metadata that came with the response in case we need to pass
|
| + // it along with the prefetch query to Instant.
|
| + JSONStringValueSerializer json_serializer(&results->metadata);
|
| + json_serializer.Serialize(*extras);
|
| + }
|
| +
|
| + // Clear the previous results now that new results are available.
|
| + results->suggest_results.clear();
|
| + results->navigation_results.clear();
|
| +
|
| + base::string16 suggestion;
|
| + std::string type;
|
| + int relevance = GetDefaultRelevance();
|
| + const bool allow_navsuggest = ShouldAllowNavSuggest(source);
|
| + const std::string languages(
|
| + profile_->GetPrefs()->GetString(prefs::kAcceptLanguages));
|
| + for (size_t index = 0; results_list->GetString(index, &suggestion); ++index) {
|
| + // Google search may return empty suggestions for weird input characters,
|
| + // they make no sense at all and can cause problems in our code.
|
| + if (suggestion.empty())
|
| + continue;
|
| +
|
| + // Apply valid suggested relevance scores; discard invalid lists.
|
| + if (relevances != NULL && !relevances->GetInteger(index, &relevance))
|
| + relevances = NULL;
|
| + if (types && types->GetString(index, &type) && (type == "NAVIGATION")) {
|
| + // Do not blindly trust the URL coming from the server to be valid.
|
| + GURL url(URLFixerUpper::FixupURL(base::UTF16ToUTF8(suggestion),
|
| + std::string()));
|
| + if (url.is_valid() && allow_navsuggest) {
|
| + base::string16 title;
|
| + if (descriptions != NULL)
|
| + descriptions->GetString(index, &title);
|
| + results->navigation_results.push_back(
|
| + NavigationResult(*this,
|
| + url,
|
| + title,
|
| + IsKeywordRequest(source),
|
| + relevance,
|
| + relevances != NULL,
|
| + GetInputText(source),
|
| + languages));
|
| + }
|
| + } else {
|
| + AutocompleteMatchType::Type match_type = GetAutocompleteMatchType(type);
|
| + bool should_prefetch = static_cast<int>(index) == prefetch_index;
|
| + const base::DictionaryValue* suggestion_detail = NULL;
|
| + base::string16 match_contents = suggestion;
|
| + base::string16 annotation;
|
| + std::string suggest_query_params;
|
| + std::string deletion_url;
|
| +
|
| + if (suggestion_details) {
|
| + suggestion_details->GetDictionary(index, &suggestion_detail);
|
| + if (suggestion_detail) {
|
| + suggestion_detail->GetString("du", &deletion_url);
|
| + suggestion_detail->GetString("title", &match_contents) ||
|
| + suggestion_detail->GetString("t", &match_contents);
|
| + // Error correction for bad data from server.
|
| + if (match_contents.empty())
|
| + match_contents = suggestion;
|
| + suggestion_detail->GetString("annotation", &annotation) ||
|
| + suggestion_detail->GetString("a", &annotation);
|
| + suggestion_detail->GetString("query_params", &suggest_query_params) ||
|
| + suggestion_detail->GetString("q", &suggest_query_params);
|
| + }
|
| + }
|
| +
|
| + // TODO(kochi): Improve calculator suggestion presentation.
|
| + results->suggest_results.push_back(SuggestResult(suggestion,
|
| + match_type,
|
| + match_contents,
|
| + annotation,
|
| + suggest_query_params,
|
| + deletion_url,
|
| + IsKeywordRequest(source),
|
| + relevance,
|
| + relevances != NULL,
|
| + should_prefetch,
|
| + GetInputText(source)));
|
| + }
|
| + }
|
| +
|
| + SortResults(source, relevances, results);
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool BaseSearchProvider::ShouldAllowNavSuggest(const net::URLFetcher* source) {
|
| + return true;
|
| +}
|
| +
|
| +void BaseSearchProvider::SortResults(const net::URLFetcher* source,
|
| + const base::ListValue* relevances,
|
| + Results* results) {}
|
| +
|
| +void BaseSearchProvider::AddMatchToMap(const SuggestResult& result,
|
| + const AutocompleteInput input,
|
| + const base::string16& input_text,
|
| + const TemplateURL* template_url,
|
| + const std::string& metadata,
|
| + int accepted_suggestion,
|
| + bool append_extra_query_params,
|
| + MatchMap* map) {
|
| + // On non-mobile, ask the instant controller for the appropriate start margin.
|
| + // On mobile the start margin is unused, so leave the value as default there.
|
| + int omnibox_start_margin = chrome::kDisableStartMargin;
|
| +#if !defined(OS_ANDROID) && !defined(IOS)
|
| + if (chrome::IsInstantExtendedAPIEnabled()) {
|
| + Browser* browser =
|
| + chrome::FindBrowserWithProfile(profile_, chrome::GetActiveDesktop());
|
| + if (browser && browser->instant_controller() &&
|
| + browser->instant_controller()->instant()) {
|
| + omnibox_start_margin =
|
| + browser->instant_controller()->instant()->omnibox_bounds().x();
|
| + }
|
| + }
|
| +#endif // !defined(OS_ANDROID) && !defined(IOS)
|
| +
|
| + AutocompleteMatch match = CreateSearchSuggestion(this,
|
| + input,
|
| + input_text,
|
| + result,
|
| + template_url,
|
| + accepted_suggestion,
|
| + omnibox_start_margin,
|
| + append_extra_query_params);
|
| + if (!match.destination_url.is_valid())
|
| + return;
|
| + match.search_terms_args->bookmark_bar_pinned =
|
| + profile_->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar);
|
| + match.RecordAdditionalInfo(kRelevanceFromServerKey,
|
| + result.relevance_from_server() ? kTrue : kFalse);
|
| + match.RecordAdditionalInfo(kShouldPrefetchKey,
|
| + result.should_prefetch() ? kTrue : kFalse);
|
| +
|
| + if (!result.deletion_url().empty()) {
|
| + GURL url(match.destination_url.GetOrigin().Resolve(result.deletion_url()));
|
| + if (url.is_valid()) {
|
| + match.RecordAdditionalInfo(kDeletionUrlKey, url.spec());
|
| + match.deletable = true;
|
| + }
|
| + }
|
| +
|
| + // Metadata is needed only for prefetching queries.
|
| + if (result.should_prefetch())
|
| + match.RecordAdditionalInfo(kSuggestMetadataKey, metadata);
|
| +
|
| + // Try to add |match| to |map|. If a match for |query_string| is already in
|
| + // |map|, replace it if |match| is more relevant.
|
| + // NOTE: Keep this ToLower() call in sync with url_database.cc.
|
| + MatchKey match_key(
|
| + std::make_pair(base::i18n::ToLower(result.suggestion()),
|
| + match.search_terms_args->suggest_query_params));
|
| + const std::pair<MatchMap::iterator, bool> i(
|
| + map->insert(std::make_pair(match_key, match)));
|
| +
|
| + bool should_prefetch = result.should_prefetch();
|
| + if (!i.second) {
|
| + // NOTE: We purposefully do a direct relevance comparison here instead of
|
| + // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items
|
| + // added first" rather than "items alphabetically first" when the scores
|
| + // are equal. The only case this matters is when a user has results with
|
| + // the same score that differ only by capitalization; because the history
|
| + // system returns results sorted by recency, this means we'll pick the most
|
| + // recent such result even if the precision of our relevance score is too
|
| + // low to distinguish the two.
|
| + if (match.relevance > i.first->second.relevance) {
|
| + i.first->second = match;
|
| + } else if (match.keyword == i.first->second.keyword) {
|
| + // Old and new matches are from the same search provider. It is okay to
|
| + // record one match's prefetch data onto a different match (for the same
|
| + // query string) for the following reasons:
|
| + // 1. Because the suggest server only sends down a query string from
|
| + // which we construct a URL, rather than sending a full URL, and because
|
| + // we construct URLs from query strings in the same way every time, the
|
| + // URLs for the two matches will be the same. Therefore, we won't end up
|
| + // prefetching something the server didn't intend.
|
| + // 2. Presumably the server sets the prefetch bit on a match it things is
|
| + // sufficiently relevant that the user is likely to choose it. Surely
|
| + // setting the prefetch bit on a match of even higher relevance won't
|
| + // violate this assumption.
|
| + should_prefetch |= ShouldPrefetch(i.first->second);
|
| + i.first->second.RecordAdditionalInfo(kShouldPrefetchKey,
|
| + should_prefetch ? kTrue : kFalse);
|
| + if (should_prefetch)
|
| + i.first->second.RecordAdditionalInfo(kSuggestMetadataKey, metadata);
|
| + }
|
| + }
|
| +}
|
| +
|
| +scoped_ptr<base::Value> BaseSearchProvider::DeserializeJsonData(
|
| + std::string json_data) {
|
| + // The JSON response should be an array.
|
| + for (size_t response_start_index = json_data.find("["), i = 0;
|
| + response_start_index != std::string::npos && i < 5;
|
| + response_start_index = json_data.find("[", 1), ++i) {
|
| + // Remove any XSSI guards to allow for JSON parsing.
|
| + if (response_start_index > 0)
|
| + json_data.erase(0, response_start_index);
|
| +
|
| + JSONStringValueSerializer deserializer(json_data);
|
| + deserializer.set_allow_trailing_comma(true);
|
| + int error_code = 0;
|
| + scoped_ptr<base::Value> data(deserializer.Deserialize(&error_code, NULL));
|
| + if (error_code == 0)
|
| + return data.Pass();
|
| + }
|
| + return scoped_ptr<base::Value>();
|
| +}
|
| +
|
| +void BaseSearchProvider::OnDeletionComplete(
|
| + bool success,
|
| + SuggestionDeletionHandler* handler) {
|
| + RecordDeletionResult(success);
|
| + SuggestionDeletionHandlers::iterator it =
|
| + std::find(deletion_handlers_.begin(), deletion_handlers_.end(), handler);
|
| + DCHECK(it != deletion_handlers_.end());
|
| + deletion_handlers_.erase(it);
|
| +}
|
| +
|
| +void BaseSearchProvider::DeleteMatchFromMatches(
|
| + const AutocompleteMatch& match) {
|
| + for (ACMatches::iterator i(matches_.begin()); i != matches_.end(); ++i) {
|
| + // Find the desired match to delete by checking the type and contents.
|
| + // We can't check the destination URL, because the autocomplete controller
|
| + // may have reformulated that. Not that while checking for matching
|
| + // contents works for personalized suggestions, if more match types gain
|
| + // deletion support, this algorithm may need to be re-examined.
|
| + if (i->contents == match.contents && i->type == match.type) {
|
| + matches_.erase(i);
|
| + break;
|
| + }
|
| + }
|
| + listener_->OnProviderUpdate(true);
|
| +}
|
|
|