// 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 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(*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 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 localized_title; for (ListValue::const_iterator locale_iter(all_locales->begin()); locale_iter != all_locales->end(); ++locale_iter) { DictionaryValue* locale = static_cast(*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 valid_locales; extension_l10n_util::GetAllFallbackLocales( extension_l10n_util::CurrentLocaleOrDefault(), "all", &valid_locales); for (std::vector::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 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(source); QueryMap::iterator it = queries_.find(handle); DCHECK(it != queries_.end()); scoped_ptr 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 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( 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; }