| // 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/web_intents_registry.h" |
| |
| #include <algorithm> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/callback.h" |
| #include "base/string_util.h" |
| #include "base/string16.h" |
| #include "base/utf_string_conversions.h" |
| #include "chrome/browser/intents/default_web_intent_service.h" |
| #include "chrome/browser/intents/web_intents_util.h" |
| #include "chrome/browser/webdata/web_data_service.h" |
| #include "chrome/common/extensions/extension.h" |
| #include "chrome/common/extensions/extension_set.h" |
| #include "googleurl/src/gurl.h" |
| #include "net/base/mime_util.h" |
| |
| using extensions::Extension; |
| using net::IsMimeType; |
| |
| namespace { |
| |
| // TODO(hshi): Temporary workaround for http://crbug.com/134197. |
| // If no user-set default service is found, use built-in QuickOffice Viewer as |
| // default for MS office files. Remove this once full defaults is in place. |
| const char kViewActionURL[] = "http://webintents.org/view"; |
| |
| const char kQuickOfficeViewerServiceURL[] = |
| "chrome-extension://gbkeegbaiigmenfmjfclcdgdpimamgkj/views/appViewer.html"; |
| |
| const char* kQuickOfficeViewerMimeType[] = { |
| "application/msword", |
| "application/vnd.ms-powerpoint", |
| "application/vnd.ms-excel", |
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document", |
| "application/vnd.openxmlformats-officedocument.presentationml.presentation", |
| "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" |
| }; |
| |
| typedef base::Callback<void(const WDTypedResult* result)> ResultsHandler; |
| typedef WebIntentsRegistry::IntentServiceList IntentServiceList; |
| |
| // Compares two web intents type specifiers to see if there is a match. |
| // First checks if both are MIME types. If so, uses MatchesMimeType. |
| // If not, uses exact string equality. |
| bool WebIntentsTypesMatch(const string16& type1, const string16& type2) { |
| return (IsMimeType(UTF16ToUTF8(type1)) && IsMimeType(UTF16ToUTF8(type2))) |
| ? web_intents::MimeTypesMatch(type1, type2) |
| : type1 == type2; |
| } |
| |
| // Adds any intent services of |extension| that match |action| to |
| // |matching_services|. |
| void AddMatchingServicesForExtension(const Extension& extension, |
| const string16& action, |
| IntentServiceList* matching_services) { |
| const IntentServiceList& services = extension.intents_services(); |
| for (IntentServiceList::const_iterator i = services.begin(); |
| i != services.end(); ++i) { |
| if (action.empty() || action == i->action) |
| matching_services->push_back(*i); |
| } |
| } |
| |
| // Removes all services from |matching_services| that do not match |type|. |
| // Wildcards are supported, of the form '<type>/*' or '*'. |
| void FilterServicesByType(const string16& type, |
| IntentServiceList* matching_services) { |
| // Filter out all services not matching the query type. |
| IntentServiceList::iterator iter(matching_services->begin()); |
| while (iter != matching_services->end()) { |
| if (WebIntentsTypesMatch(iter->type, type)) |
| ++iter; |
| else |
| iter = matching_services->erase(iter); |
| } |
| } |
| |
| // Callback for existence checks. Converts a callback for a list of services |
| // into a callback that returns true if the list contains a specific service. |
| void ExistenceCallback(const webkit_glue::WebIntentServiceData& service, |
| const base::Callback<void(bool)>& callback, |
| const WDTypedResult* result) { |
| |
| WebIntentsRegistry::IntentServiceList list = static_cast< |
| const WDResult<IntentServiceList>*>(result)->GetValue(); |
| |
| for (WebIntentsRegistry::IntentServiceList::const_iterator i = list.begin(); |
| i != list.end(); ++i) { |
| if (*i == service) { |
| callback.Run(true); |
| return; |
| } |
| } |
| |
| callback.Run(false); |
| } |
| |
| // Functor object for intent ordering. |
| struct IntentOrdering { |
| // Implements StrictWeakOrdering for intents, based on intent-equivalence. |
| // Order by |service_url|, |action|, |title|, and |disposition| in order. |
| bool operator()(const webkit_glue::WebIntentServiceData& lhs, |
| const webkit_glue::WebIntentServiceData& rhs) { |
| if (lhs.service_url != rhs.service_url) |
| return lhs.service_url < rhs.service_url; |
| |
| if (lhs.action != rhs.action) |
| return lhs.action < rhs.action; |
| |
| if (lhs.title != rhs.title) |
| return lhs.title < rhs.title; |
| |
| if (lhs.disposition != rhs.disposition) |
| return lhs.disposition < rhs.disposition; |
| |
| // At this point, we consider intents to be equal, even if |type| differs. |
| return false; |
| } |
| }; |
| |
| // Two intents are equivalent iff all fields except |type| are equal. |
| bool IntentsAreEquivalent(const webkit_glue::WebIntentServiceData& lhs, |
| const webkit_glue::WebIntentServiceData& rhs) { |
| return !((lhs.service_url != rhs.service_url) || |
| (lhs.action != rhs.action) || |
| (lhs.title != rhs.title) || |
| (lhs.disposition != rhs.disposition)); |
| } |
| |
| } // namespace |
| |
| using webkit_glue::WebIntentServiceData; |
| |
| // Internal object containing arguments to be used in post processing |
| // WDS results. |
| struct WebIntentsRegistry::QueryParams { |
| |
| // The particular action to filter for while searching through extensions. |
| // If |action_| is empty, return all extension-provided services. |
| string16 action_; |
| |
| // The MIME type that was requested for this service query. |
| // Suppports wild cards. |
| string16 type_; |
| |
| // The url of the invoking page. |
| GURL url_; |
| |
| // Create a new QueryParams for all intent services or for existence checks. |
| QueryParams() : type_(ASCIIToUTF16("*")) {} |
| |
| QueryParams(const string16& action, const string16& type) |
| : action_(action), type_(type) {} |
| |
| // Create a new QueryParams for default services. |
| QueryParams(const string16& action, const string16& type, const GURL& url) |
| : action_(action), type_(type), url_(url) {} |
| }; |
| |
| // Internal object adapting the WDS consumer interface to base::Bind |
| // callback way of doing business. |
| class WebIntentsRegistry::QueryAdapter : public WebDataServiceConsumer { |
| |
| public: |
| // Underlying data query. |
| WebDataService::Handle query_handle_; |
| |
| // Create a new QueryAdapter that delegates results to |handler|. |
| QueryAdapter(WebIntentsRegistry* registry, const ResultsHandler& handler) |
| : registry_(registry), handler_(handler) { |
| registry_->TrackQuery(this); |
| } |
| |
| void OnWebDataServiceRequestDone( |
| WebDataService::Handle h, |
| const WDTypedResult* result) OVERRIDE { |
| |
| handler_.Run(result); |
| registry_->ReleaseQuery(this); |
| } |
| |
| private: |
| // Handle so we can call back into the WebIntentsRegistry when |
| // processing query results. The registry is guaranteed to be |
| // valid for the life of this object. We do not own this object. |
| WebIntentsRegistry* registry_; |
| |
| // The callback for this particular query. |
| ResultsHandler handler_; |
| }; |
| |
| WebIntentsRegistry::WebIntentsRegistry() {} |
| |
| WebIntentsRegistry::~WebIntentsRegistry() { |
| |
| // Cancel all pending queries, since we can't handle them any more. |
| for (QueryVector::iterator it = pending_queries_.begin(); |
| it != pending_queries_.end(); ++it) { |
| QueryAdapter* query = *it; |
| wds_->CancelRequest(query->query_handle_); |
| delete query; |
| } |
| } |
| |
| void WebIntentsRegistry::Initialize( |
| scoped_refptr<WebDataService> wds, |
| ExtensionServiceInterface* extension_service) { |
| wds_ = wds; |
| extension_service_ = extension_service; |
| } |
| |
| void WebIntentsRegistry::OnWebIntentsResultReceived( |
| const QueryParams& params, |
| const QueryCallback& callback, |
| const WDTypedResult* result) { |
| DCHECK(result); |
| DCHECK(result->GetType() == WEB_INTENTS_RESULT); |
| |
| IntentServiceList matching_services = static_cast< |
| const WDResult<IntentServiceList>*>(result)->GetValue(); |
| |
| // Loop over all services in all extensions, collect ones |
| // matching the query. |
| if (extension_service_) { |
| const ExtensionSet* extensions = extension_service_->extensions(); |
| if (extensions) { |
| for (ExtensionSet::const_iterator i(extensions->begin()); |
| i != extensions->end(); ++i) { |
| AddMatchingServicesForExtension(**i, params.action_, |
| &matching_services); |
| } |
| } |
| } |
| |
| // Filter out all services not matching the query type. |
| FilterServicesByType(params.type_, &matching_services); |
| |
| // Collapse intents that are equivalent for all but |type|. |
| CollapseIntents(&matching_services); |
| |
| callback.Run(matching_services); |
| } |
| |
| void WebIntentsRegistry::OnAllDefaultIntentServicesReceived( |
| const DefaultIntentServicesCallback& callback, |
| const WDTypedResult* result) { |
| DCHECK(result); |
| DCHECK(result->GetType() == WEB_INTENTS_DEFAULTS_RESULT); |
| |
| const std::vector<DefaultWebIntentService> services = static_cast< |
| const WDResult<std::vector<DefaultWebIntentService> >*>(result)-> |
| GetValue(); |
| |
| callback.Run(services); |
| } |
| |
| void WebIntentsRegistry::OnWebIntentsDefaultsResultReceived( |
| const QueryParams& params, |
| const DefaultQueryCallback& callback, |
| const WDTypedResult* result) { |
| DCHECK(result); |
| DCHECK(result->GetType() == WEB_INTENTS_DEFAULTS_RESULT); |
| |
| std::vector<DefaultWebIntentService> services = static_cast< |
| const WDResult<std::vector<DefaultWebIntentService> >*>(result)-> |
| GetValue(); |
| |
| DefaultWebIntentService default_service; |
| std::vector<DefaultWebIntentService>::iterator iter(services.begin()); |
| for (; iter != services.end(); ++iter) { |
| if (!WebIntentsTypesMatch(iter->type, params.type_)) { |
| continue; |
| } |
| if (!iter->url_pattern.MatchesURL(params.url_)) { |
| continue; |
| } |
| const Extension* extension = ExtensionForURL(iter->service_url); |
| if (extension != NULL && |
| !extension_service_->IsExtensionEnabled(extension->id())) { |
| continue; |
| } |
| |
| // Found a match. If it is better than default_service, use it. |
| // Currently the metric is that if the new value is user-set, |
| // prefer it. If the new value has a more specific pattern, prefer it. |
| // If the new value has a more recent date, prefer it. |
| if (default_service.user_date <= 0 && iter->user_date >= 0) { |
| default_service = *iter; |
| } else if (default_service.url_pattern.match_all_urls() && |
| !iter->url_pattern.match_all_urls()) { |
| default_service = *iter; |
| } else if (iter->url_pattern < default_service.url_pattern) { |
| default_service = *iter; |
| } else if (default_service.user_date < iter->user_date) { |
| default_service = *iter; |
| } |
| } |
| |
| // TODO(hshi): Temporary workaround for http://crbug.com/134197. |
| // If no user-set default service is found, use built-in QuickOffice Viewer as |
| // default for MS office files. Remove this once full defaults is in place. |
| if (default_service.user_date <= 0) { |
| for (size_t i = 0; i < sizeof(kQuickOfficeViewerMimeType) / sizeof(char*); |
| ++i) { |
| DefaultWebIntentService qoviewer_service; |
| qoviewer_service.action = ASCIIToUTF16(kViewActionURL); |
| qoviewer_service.type = ASCIIToUTF16(kQuickOfficeViewerMimeType[i]); |
| qoviewer_service.service_url = kQuickOfficeViewerServiceURL; |
| if (WebIntentsTypesMatch(qoviewer_service.type, params.type_)) { |
| default_service = qoviewer_service; |
| break; |
| } |
| } |
| } |
| |
| callback.Run(default_service); |
| } |
| |
| void WebIntentsRegistry::GetIntentServices( |
| const string16& action, const string16& type, |
| const QueryCallback& callback) { |
| DCHECK(wds_.get()); |
| DCHECK(!callback.is_null()); |
| |
| const QueryParams params(action, type); |
| const ResultsHandler handler = base::Bind( |
| &WebIntentsRegistry::OnWebIntentsResultReceived, |
| base::Unretained(this), |
| params, |
| callback); |
| |
| QueryAdapter* query = new QueryAdapter(this, handler); |
| query->query_handle_ = wds_->GetWebIntentServices(action, query); |
| } |
| |
| void WebIntentsRegistry::GetAllIntentServices( |
| const QueryCallback& callback) { |
| DCHECK(wds_.get()); |
| DCHECK(!callback.is_null()); |
| |
| const QueryParams params; |
| const ResultsHandler handler = base::Bind( |
| &WebIntentsRegistry::OnWebIntentsResultReceived, |
| base::Unretained(this), |
| params, |
| callback); |
| |
| QueryAdapter* query = new QueryAdapter(this, handler); |
| query->query_handle_ = wds_->GetAllWebIntentServices(query); |
| } |
| |
| void WebIntentsRegistry::GetAllDefaultIntentServices( |
| const DefaultIntentServicesCallback& callback) { |
| DCHECK(!callback.is_null()); |
| |
| ResultsHandler handler = base::Bind( |
| &WebIntentsRegistry::OnAllDefaultIntentServicesReceived, |
| base::Unretained(this), |
| callback); |
| |
| QueryAdapter* query = new QueryAdapter(this, handler); |
| query->query_handle_ = |
| wds_->GetAllDefaultWebIntentServices(query); |
| } |
| |
| void WebIntentsRegistry::IntentServiceExists( |
| const WebIntentServiceData& service, |
| const base::Callback<void(bool)>& callback) { |
| DCHECK(!callback.is_null()); |
| |
| ResultsHandler handler = base::Bind( |
| &ExistenceCallback, |
| service, |
| callback); |
| |
| QueryAdapter* query = new QueryAdapter(this, handler); |
| query->query_handle_ = wds_->GetWebIntentServicesForURL( |
| UTF8ToUTF16(service.service_url.spec()), query); |
| } |
| |
| void WebIntentsRegistry::GetIntentServicesForExtensionFilter( |
| const string16& action, |
| const string16& type, |
| const std::string& extension_id, |
| IntentServiceList* services) { |
| if (extension_service_) { |
| const Extension* extension = |
| extension_service_->GetExtensionById(extension_id, false); |
| AddMatchingServicesForExtension(*extension, |
| action, |
| services); |
| FilterServicesByType(type, services); |
| } |
| } |
| |
| void WebIntentsRegistry::RegisterDefaultIntentService( |
| const DefaultWebIntentService& default_service) { |
| DCHECK(wds_.get()); |
| wds_->AddDefaultWebIntentService(default_service); |
| } |
| |
| void WebIntentsRegistry::UnregisterDefaultIntentService( |
| const DefaultWebIntentService& default_service) { |
| DCHECK(wds_.get()); |
| wds_->RemoveDefaultWebIntentService(default_service); |
| } |
| |
| void WebIntentsRegistry::UnregisterServiceDefaults(const GURL& service_url) { |
| DCHECK(wds_.get()); |
| wds_->RemoveWebIntentServiceDefaults(service_url); |
| } |
| |
| void WebIntentsRegistry::GetDefaultIntentService( |
| const string16& action, |
| const string16& type, |
| const GURL& invoking_url, |
| const DefaultQueryCallback& callback) { |
| DCHECK(!callback.is_null()); |
| |
| const QueryParams params(action, type); |
| |
| ResultsHandler handler = base::Bind( |
| &WebIntentsRegistry::OnWebIntentsDefaultsResultReceived, |
| base::Unretained(this), |
| params, |
| callback); |
| |
| QueryAdapter* query = new QueryAdapter(this, handler); |
| query->query_handle_ = |
| wds_->GetDefaultWebIntentServicesForAction(action, query); |
| } |
| |
| void WebIntentsRegistry::RegisterIntentService( |
| const WebIntentServiceData& service) { |
| DCHECK(wds_.get()); |
| wds_->AddWebIntentService(service); |
| } |
| |
| void WebIntentsRegistry::UnregisterIntentService( |
| const WebIntentServiceData& service) { |
| DCHECK(wds_.get()); |
| wds_->RemoveWebIntentService(service); |
| } |
| |
| void WebIntentsRegistry::CollapseIntents(IntentServiceList* services) { |
| DCHECK(services); |
| |
| // No need to do anything for no services/single service. |
| if (services->size() < 2) |
| return; |
| |
| // Sort so that intents that can be collapsed must be adjacent. |
| std::sort(services->begin(), services->end(), IntentOrdering()); |
| |
| // Combine adjacent services if they are equivalent. |
| IntentServiceList::iterator write_iter = services->begin(); |
| IntentServiceList::iterator read_iter = write_iter + 1; |
| while (read_iter != services->end()) { |
| if (IntentsAreEquivalent(*write_iter, *read_iter)) { |
| // If the two intents are equivalent, join types and collapse. |
| write_iter->type += ASCIIToUTF16(",") + read_iter->type; |
| } else { |
| // Otherwise, keep both intents. |
| ++write_iter; |
| if (write_iter != read_iter) |
| *write_iter = *read_iter; |
| } |
| ++read_iter; |
| } |
| |
| // Cut off everything after the last intent copied to the list. |
| if (++write_iter != services->end()) |
| services->erase(write_iter, services->end()); |
| } |
| |
| const Extension* WebIntentsRegistry::ExtensionForURL(const std::string& url) { |
| const ExtensionSet* extensions = extension_service_->extensions(); |
| if (!extensions) |
| return NULL; |
| |
| // Use the unsafe ExtensionURLInfo constructor: we don't care if the extension |
| // is running or not. |
| GURL gurl(url); |
| ExtensionURLInfo info(gurl); |
| return extensions->GetExtensionOrAppByURL(info); |
| } |
| |
| void WebIntentsRegistry::TrackQuery(QueryAdapter* query) { |
| DCHECK(query); |
| pending_queries_.push_back(query); |
| } |
| |
| void WebIntentsRegistry::ReleaseQuery(QueryAdapter* query) { |
| QueryVector::iterator it = std::find( |
| pending_queries_.begin(), pending_queries_.end(), query); |
| if (it != pending_queries_.end()) { |
| pending_queries_.erase(it); |
| delete query; |
| } else { |
| NOTREACHED(); |
| } |
| } |