blob: ee238b67518273915af0c956133ac5fb4e03d53b [file] [log] [blame]
// 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/intents/cws_intents_registry.h"
#include "base/callback.h"
#include "base/json/json_string_value_serializer.h"
#include "base/memory/scoped_ptr.h"
#include "base/stl_util.h"
#include "base/string16.h"
#include "base/utf_string_conversions.h"
#include "chrome/common/extensions/extension_l10n_util.h"
#include "chrome/common/extensions/message_bundle.h"
#include "chrome/browser/net/chrome_url_request_context.h"
#include "chrome/browser/webdata/web_data_service.h"
#include "chrome/common/net/url_util.h"
#include "google_apis/google_api_keys.h"
#include "net/base/load_flags.h"
#include "net/base/mime_util.h"
#include "net/url_request/url_fetcher.h"
namespace {
// Limit for the number of suggestions we fix from CWS. Ideally, the registry
// simply get all of them, but there is a) chunking on the CWS side, and b)
// there is a cost with suggestions fetched. (Network overhead for favicons,
// roundtrips to registry to check if installed).
//
// Since the picker limits the number of suggestions displayed to 5, 15 means
// the suggestion list only has the potential to be shorter than that once the
// user has at least 10 installed handlers for the particular action/type.
//
// TODO(groby): Adopt number of suggestions dynamically so the picker can
// always display 5 suggestions unless there are less than 5 viable extensions
// in the CWS.
const char kMaxSuggestions[] = "15";
// URL for CWS intents API.
const char kCWSIntentServiceURL[] =
"https://www.googleapis.com/chromewebstore/v1.1b/items/intent";
// Determines if a string is a candidate for localization.
bool ShouldLocalize(const std::string& value) {
std::string::size_type index = 0;
index = value.find(extensions::MessageBundle::kMessageBegin);
if (index == std::string::npos)
return false;
index = value.find(extensions::MessageBundle::kMessageEnd, index);
return (index != std::string::npos);
}
// Parses a JSON |response| from the CWS into a list of suggested extensions,
// stored in |intents|. |intents| must not be NULL.
void ParseResponse(const std::string& response,
CWSIntentsRegistry::IntentExtensionList* intents) {
std::string error;
scoped_ptr<Value> parsed_response;
JSONStringValueSerializer serializer(response);
parsed_response.reset(serializer.Deserialize(NULL, &error));
if (parsed_response.get() == NULL)
return;
DictionaryValue* response_dict = NULL;
if (!parsed_response->GetAsDictionary(&response_dict) || !response_dict)
return;
ListValue* items;
if (!response_dict->GetList("items", &items))
return;
for (ListValue::const_iterator iter(items->begin());
iter != items->end(); ++iter) {
DictionaryValue* item = static_cast<DictionaryValue*>(*iter);
// All fields are mandatory - skip this result if any field isn't found.
CWSIntentsRegistry::IntentExtensionInfo info;
if (!item->GetString("id", &info.id))
continue;
if (!item->GetInteger("num_ratings", &info.num_ratings))
continue;
if (!item->GetDouble("average_rating", &info.average_rating))
continue;
if (!item->GetString("manifest", &info.manifest))
continue;
std::string manifest_utf8 = UTF16ToUTF8(info.manifest);
JSONStringValueSerializer manifest_serializer(manifest_utf8);
scoped_ptr<Value> manifest_value;
manifest_value.reset(manifest_serializer.Deserialize(NULL, &error));
if (manifest_value.get() == NULL)
continue;
DictionaryValue* manifest_dict;
if (!manifest_value->GetAsDictionary(&manifest_dict) ||
!manifest_dict->GetString("name", &info.name))
continue;
string16 url_string;
if (!item->GetString("icon_url", &url_string))
continue;
info.icon_url = GURL(url_string);
// Need to parse CWS reply, since it is not pre-l10n'd.
ListValue* all_locales = NULL;
if (ShouldLocalize(UTF16ToUTF8(info.name)) &&
item->GetList("locale_data", &all_locales)) {
std::map<std::string, std::string> localized_title;
for (ListValue::const_iterator locale_iter(all_locales->begin());
locale_iter != all_locales->end(); ++locale_iter) {
DictionaryValue* locale = static_cast<DictionaryValue*>(*locale_iter);
std::string locale_id, title;
if (!locale->GetString("locale_string", &locale_id) ||
!locale->GetString("title", &title))
continue;
localized_title[locale_id] = title;
}
std::vector<std::string> valid_locales;
extension_l10n_util::GetAllFallbackLocales(
extension_l10n_util::CurrentLocaleOrDefault(),
"all",
&valid_locales);
for (std::vector<std::string>::iterator iter = valid_locales.begin();
iter != valid_locales.end(); ++iter) {
if (localized_title.count(*iter)) {
info.name = UTF8ToUTF16(localized_title[*iter]);
break;
}
}
}
intents->push_back(info);
}
}
} // namespace
// Internal object representing all data associated with a single query.
struct CWSIntentsRegistry::IntentsQuery {
IntentsQuery();
~IntentsQuery();
// Underlying URL request query.
scoped_ptr<net::URLFetcher> url_fetcher;
// The callback - invoked on completed retrieval.
ResultsCallback callback;
};
CWSIntentsRegistry::IntentsQuery::IntentsQuery() {
}
CWSIntentsRegistry::IntentsQuery::~IntentsQuery() {
}
CWSIntentsRegistry::IntentExtensionInfo::IntentExtensionInfo()
: num_ratings(0),
average_rating(0) {
}
CWSIntentsRegistry::IntentExtensionInfo::~IntentExtensionInfo() {
}
CWSIntentsRegistry::CWSIntentsRegistry(net::URLRequestContextGetter* context)
: request_context_(context) {
}
CWSIntentsRegistry::~CWSIntentsRegistry() {
// Cancel all pending queries, since we can't handle them any more.
STLDeleteValues(&queries_);
}
void CWSIntentsRegistry::OnURLFetchComplete(const net::URLFetcher* source) {
DCHECK(source);
URLFetcherHandle handle = reinterpret_cast<URLFetcherHandle>(source);
QueryMap::iterator it = queries_.find(handle);
DCHECK(it != queries_.end());
scoped_ptr<IntentsQuery> query(it->second);
DCHECK(query.get() != NULL);
queries_.erase(it);
std::string response;
source->GetResponseAsString(&response);
// TODO(groby): Do we really only accept 200, or any 2xx codes?
IntentExtensionList intents;
if (source->GetResponseCode() == 200)
ParseResponse(response, &intents);
if (!query->callback.is_null())
query->callback.Run(intents);
}
void CWSIntentsRegistry::GetIntentServices(const string16& action,
const string16& mimetype,
const ResultsCallback& cb) {
scoped_ptr<IntentsQuery> query(new IntentsQuery);
query->callback = cb;
query->url_fetcher.reset(net::URLFetcher::Create(
0, BuildQueryURL(action,mimetype), net::URLFetcher::GET, this));
if (query->url_fetcher.get() == NULL)
return;
query->url_fetcher->SetRequestContext(request_context_);
query->url_fetcher->SetLoadFlags(
net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES);
URLFetcherHandle handle = reinterpret_cast<URLFetcherHandle>(
query->url_fetcher.get());
queries_[handle] = query.release();
queries_[handle]->url_fetcher->Start();
}
// static
GURL CWSIntentsRegistry::BuildQueryURL(const string16& action,
const string16& type) {
GURL request(kCWSIntentServiceURL);
request = chrome_common_net::AppendQueryParameter(request, "intent",
UTF16ToUTF8(action));
request = chrome_common_net::AppendQueryParameter(request, "mime_types",
UTF16ToUTF8(type));
request = chrome_common_net::AppendQueryParameter(request, "start_index",
"0");
request = chrome_common_net::AppendQueryParameter(request, "num_results",
kMaxSuggestions);
request = chrome_common_net::AppendQueryParameter(request, "key",
google_apis::GetAPIKey());
return request;
}