15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Copyright (c) 2012 The Chromium Authors. All rights reserved. 25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be 35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// found in the LICENSE file. 45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/api/push_messaging/push_messaging_api.h" 65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include <set> 85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/bind.h" 102a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "base/lazy_instance.h" 115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/logging.h" 122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "base/strings/string_number_conversions.h" 135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "base/values.h" 145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/api/push_messaging/push_messaging_invalidation_handler.h" 155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/extensions/extension_service.h" 162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "chrome/browser/extensions/token_cache/token_cache_service.h" 172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)#include "chrome/browser/extensions/token_cache/token_cache_service_factory.h" 18f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include "chrome/browser/invalidation/profile_invalidation_provider_factory.h" 195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/browser/profiles/profile.h" 207dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch#include "chrome/browser/signin/profile_oauth2_token_service_factory.h" 215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "chrome/browser/signin/signin_manager_factory.h" 225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "chrome/common/extensions/api/push_messaging.h" 230de6073388f4e2780db8536178b129cd8f6ab386Torne (Richard Coles)#include "components/invalidation/invalidation_service.h" 24f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include "components/invalidation/profile_invalidation_provider.h" 25effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch#include "components/signin/core/browser/profile_oauth2_token_service.h" 26e5d81f57cb97b3b6b7fccc9c5610d21eb81db09dBen Murdoch#include "components/signin/core/browser/signin_manager.h" 275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "content/public/browser/browser_thread.h" 28f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)#include "extensions/browser/event_router.h" 29cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "extensions/browser/extension_registry.h" 30f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include "extensions/browser/extension_registry_factory.h" 315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "extensions/browser/extension_system_provider.h" 325d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "extensions/browser/extensions_browser_client.h" 33f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)#include "extensions/common/extension.h" 344e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)#include "extensions/common/permissions/api_permission.h" 3546d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)#include "extensions/common/permissions/permissions_data.h" 365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#include "google_apis/gaia/gaia_constants.h" 370529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch#include "google_apis/gaia/identity_provider.h" 385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)using content::BrowserThread; 405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 41f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)namespace extensions { 42f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) 43f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)namespace { 445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const char kChannelIdSeparator[] = "/"; 455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)const char kUserNotSignedIn[] = "The user is not signed in."; 467dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdochconst char kUserAccessTokenFailure[] = 477dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch "Cannot obtain access token for the user."; 48c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdochconst char kAPINotAvailableForUser[] = 49c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch "The API is not available for this user."; 502a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)const int kObfuscatedGaiaIdTimeoutInDays = 30; 511320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucciconst char kDeprecationMessage[] = 521320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci "The chrome.pushMessaging API is deprecated. Use chrome.gcm API instead."; 531320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci} // namespace 545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)namespace glue = api::push_messaging; 565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 57f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)PushMessagingEventRouter::PushMessagingEventRouter( 58f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) content::BrowserContext* context) 59f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) : browser_context_(context) { 605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} 615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)PushMessagingEventRouter::~PushMessagingEventRouter() {} 635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void PushMessagingEventRouter::TriggerMessageForTest( 655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) const std::string& extension_id, 665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) int subchannel, 675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) const std::string& payload) { 685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) OnMessage(extension_id, subchannel, payload); 695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} 705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void PushMessagingEventRouter::OnMessage(const std::string& extension_id, 725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) int subchannel, 735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) const std::string& payload) { 745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) glue::Message message; 755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) message.subchannel_id = subchannel; 765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) message.payload = payload; 775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 782a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) DVLOG(2) << "PushMessagingEventRouter::OnMessage" 792a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) << " payload = '" << payload 802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) << "' subchannel = '" << subchannel 812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) << "' extension = '" << extension_id << "'"; 822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) scoped_ptr<base::ListValue> args(glue::OnMessage::Create(message)); 84cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) scoped_ptr<Event> event(new Event(glue::OnMessage::kEventName, args.Pass())); 85f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) event->restrict_to_browser_context = browser_context_; 86f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) EventRouter::Get(browser_context_) 87f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) ->DispatchEventToExtension(extension_id, event.Pass()); 885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} 895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// GetChannelId class functions 915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)PushMessagingGetChannelIdFunction::PushMessagingGetChannelIdFunction() 935d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) : OAuth2TokenService::Consumer("push_messaging"), 945d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) interactive_(false) {} 955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)PushMessagingGetChannelIdFunction::~PushMessagingGetChannelIdFunction() {} 975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 98010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles)bool PushMessagingGetChannelIdFunction::RunAsync() { 991320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci // Issue a deprecation message. 1001320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci WriteToConsole(content::CONSOLE_MESSAGE_LEVEL_WARNING, kDeprecationMessage); 1011320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci 1025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // Fetch the function arguments. 1035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) scoped_ptr<glue::GetChannelId::Params> params( 1045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) glue::GetChannelId::Params::Create(*args_)); 1055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) EXTENSION_FUNCTION_VALIDATE(params.get()); 1065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (params && params->interactive) { 1085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) interactive_ = *params->interactive; 1095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 1105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // Balanced in ReportResult() 1125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) AddRef(); 1135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 114f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation::ProfileInvalidationProvider* invalidation_provider = 115f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation::ProfileInvalidationProviderFactory::GetForProfile( 116f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) GetProfile()); 117f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) if (!invalidation_provider) { 118c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch error_ = kAPINotAvailableForUser; 119c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch ReportResult(std::string(), error_); 120c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch return false; 121c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch } 122c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch 1230529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch IdentityProvider* identity_provider = 124f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation_provider->GetInvalidationService()->GetIdentityProvider(); 1250529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch if (!identity_provider->GetTokenService()->RefreshTokenIsAvailable( 1260529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch identity_provider->GetActiveAccountId())) { 1270529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch if (interactive_ && identity_provider->RequestLogin()) { 1280529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch identity_provider->AddActiveAccountRefreshTokenObserver(this); 1295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return true; 1305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } else { 1315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) error_ = kUserNotSignedIn; 1325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) ReportResult(std::string(), error_); 1335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return false; 1345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 1355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 1365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1371e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) DVLOG(2) << "Logged in profile name: " << GetProfile()->GetProfileName(); 1383551c9c881056c480085172ff9840cab31610854Torne (Richard Coles) 1397dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch StartAccessTokenFetch(); 1407dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch return true; 1415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} 1425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 1437dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdochvoid PushMessagingGetChannelIdFunction::StartAccessTokenFetch() { 144f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation::ProfileInvalidationProvider* invalidation_provider = 145f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation::ProfileInvalidationProviderFactory::GetForProfile( 146f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) GetProfile()); 147f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) CHECK(invalidation_provider); 1480529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch IdentityProvider* identity_provider = 149f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation_provider->GetInvalidationService()->GetIdentityProvider(); 150c5cede9ae108bb15f6b7a8aea21c7e1fefa2834cBen Murdoch 151f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) std::vector<std::string> scope_vector = ObfuscatedGaiaIdFetcher::GetScopes(); 1527dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch OAuth2TokenService::ScopeSet scopes(scope_vector.begin(), scope_vector.end()); 15323730a6e56a168d1879203e4b3819bb36e3d8f1fTorne (Richard Coles) fetcher_access_token_request_ = 1540529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch identity_provider->GetTokenService()->StartRequest( 1550529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch identity_provider->GetActiveAccountId(), scopes, this); 1567dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch} 1577dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch 1587dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdochvoid PushMessagingGetChannelIdFunction::OnRefreshTokenAvailable( 1597dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch const std::string& account_id) { 160f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation::ProfileInvalidationProvider* invalidation_provider = 161f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation::ProfileInvalidationProviderFactory::GetForProfile( 162f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) GetProfile()); 163f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) CHECK(invalidation_provider); 164f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation_provider->GetInvalidationService()->GetIdentityProvider()-> 1650529e5d033099cbfc42635f6f6183833b09dff6eBen Murdoch RemoveActiveAccountRefreshTokenObserver(this); 1661e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) DVLOG(2) << "Newly logged in: " << GetProfile()->GetProfileName(); 1677dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch StartAccessTokenFetch(); 1687dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch} 1697dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch 1707dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdochvoid PushMessagingGetChannelIdFunction::OnGetTokenSuccess( 1717dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch const OAuth2TokenService::Request* request, 1727dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch const std::string& access_token, 1737dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch const base::Time& expiration_time) { 1747dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch DCHECK_EQ(fetcher_access_token_request_.get(), request); 1757dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch fetcher_access_token_request_.reset(); 1767dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch 1777dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch StartGaiaIdFetch(access_token); 1787dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch} 1797dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch 1807dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdochvoid PushMessagingGetChannelIdFunction::OnGetTokenFailure( 1817dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch const OAuth2TokenService::Request* request, 1827dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch const GoogleServiceAuthError& error) { 1837dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch DCHECK_EQ(fetcher_access_token_request_.get(), request); 1847dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch fetcher_access_token_request_.reset(); 1857dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch 1867dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch // TODO(fgorski): We are currently ignoring the error passed in upon failure. 1877dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch // It should be revisited when we are working on improving general error 1887dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch // handling for the identity related code. 1893551c9c881056c480085172ff9840cab31610854Torne (Richard Coles) DVLOG(1) << "Cannot obtain access token for this user " 1903551c9c881056c480085172ff9840cab31610854Torne (Richard Coles) << error.error_message() << " " << error.state(); 1917dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch error_ = kUserAccessTokenFailure; 1927dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch ReportResult(std::string(), error_); 193c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles)} 194c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 1957dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdochvoid PushMessagingGetChannelIdFunction::StartGaiaIdFetch( 1967dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch const std::string& access_token) { 197c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // Start the async fetch of the Gaia Id. 198effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch DCHECK_CURRENTLY_ON(BrowserThread::UI); 1991e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) net::URLRequestContextGetter* context = GetProfile()->GetRequestContext(); 2007dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch fetcher_.reset(new ObfuscatedGaiaIdFetcher(context, this, access_token)); 2015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2022a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) // Get the token cache and see if we have already cached a Gaia Id. 2032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) TokenCacheService* token_cache = 2041e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) TokenCacheServiceFactory::GetForProfile(GetProfile()); 2052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 206c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // Check the cache, if we already have a Gaia ID, use it instead of 2075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // fetching the ID over the network. 2085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) const std::string& gaia_id = 2092a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) token_cache->RetrieveToken(GaiaConstants::kObfuscatedGaiaId); 2105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (!gaia_id.empty()) { 2115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) ReportResult(gaia_id, std::string()); 2127dbb3d5cf0c15f500944d211057644d6a2f37371Ben Murdoch return; 2135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 2145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) fetcher_->Start(); 2165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} 2175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void PushMessagingGetChannelIdFunction::ReportResult( 2195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) const std::string& gaia_id, const std::string& error_string) { 220effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch DCHECK_CURRENTLY_ON(BrowserThread::UI); 2215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) BuildAndSendResult(gaia_id, error_string); 2235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // Cache the obfuscated ID locally. It never changes for this user, 2255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // and if we call the web API too often, we get errors due to rate limiting. 2265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (!gaia_id.empty()) { 2272a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) base::TimeDelta timeout = 2282a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) base::TimeDelta::FromDays(kObfuscatedGaiaIdTimeoutInDays); 2292a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) TokenCacheService* token_cache = 2301e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles) TokenCacheServiceFactory::GetForProfile(GetProfile()); 2312a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) token_cache->StoreToken(GaiaConstants::kObfuscatedGaiaId, gaia_id, 2322a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) timeout); 2335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 2345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 235010d83a9304c5a91596085d917d248abff47903aTorne (Richard Coles) // Balanced in RunAsync. 2365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) Release(); 2375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} 2385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void PushMessagingGetChannelIdFunction::BuildAndSendResult( 2405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) const std::string& gaia_id, const std::string& error_message) { 2415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) std::string channel_id; 2425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (!gaia_id.empty()) { 2435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) channel_id = gaia_id; 2445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) channel_id += kChannelIdSeparator; 2455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) channel_id += extension_id(); 2465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 2475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // TODO(petewil): It may be a good idea to further 249c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // obfuscate the channel ID to prevent the user's obfuscated Gaia Id 2505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // from being readily obtained. Security review will tell us if we need to. 2515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // Create a ChannelId results object and set the fields. 2535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) glue::ChannelIdResult result; 2545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) result.channel_id = channel_id; 2555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) SetError(error_message); 2565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) results_ = glue::GetChannelId::Results::Create(result); 2575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) bool success = error_message.empty() && !gaia_id.empty(); 2595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) SendResponse(success); 2605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} 2615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void PushMessagingGetChannelIdFunction::OnObfuscatedGaiaIdFetchSuccess( 2635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) const std::string& gaia_id) { 2645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) ReportResult(gaia_id, std::string()); 2655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} 2665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)void PushMessagingGetChannelIdFunction::OnObfuscatedGaiaIdFetchFailure( 2685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) const GoogleServiceAuthError& error) { 2695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) std::string error_text = error.error_message(); 2705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) // If the error message is blank, see if we can set it from the state. 2715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if (error_text.empty() && 2725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) (0 != error.state())) { 2735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) error_text = base::IntToString(error.state()); 2745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) } 2755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 2762a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) DVLOG(1) << "GetChannelId status: '" << error_text << "'"; 277c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) 278c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // If we had bad credentials, try the logon again. 279c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) switch (error.state()) { 280c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) case GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS: 281c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) case GoogleServiceAuthError::ACCOUNT_DELETED: 282c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) case GoogleServiceAuthError::ACCOUNT_DISABLED: { 283f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation::ProfileInvalidationProvider* invalidation_provider = 284f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation::ProfileInvalidationProviderFactory::GetForProfile( 285f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) GetProfile()); 286f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) CHECK(invalidation_provider); 287f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) if (!interactive_ || !invalidation_provider->GetInvalidationService()-> 288f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) GetIdentityProvider()->RequestLogin()) { 289c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) ReportResult(std::string(), error_text); 290c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) } 291c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) return; 292c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) } 293c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) default: 294c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) // Return error to caller. 295c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) ReportResult(std::string(), error_text); 296c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) return; 297c2e0dbddbe15c98d52c4786dac06cb8952a8ae6dTorne (Richard Coles) } 2982a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 2992a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 300a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)PushMessagingAPI::PushMessagingAPI(content::BrowserContext* context) 301f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) : extension_registry_observer_(this), browser_context_(context) { 302f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) extension_registry_observer_.Add(ExtensionRegistry::Get(browser_context_)); 3032a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 3042a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3052a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)PushMessagingAPI::~PushMessagingAPI() { 3062a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 3072a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3082a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// static 309a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)PushMessagingAPI* PushMessagingAPI::Get(content::BrowserContext* context) { 310a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) return BrowserContextKeyedAPIFactory<PushMessagingAPI>::Get(context); 3112a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 3122a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3132a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)void PushMessagingAPI::Shutdown() { 3142a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) event_router_.reset(); 3152a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) handler_.reset(); 3162a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 3172a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 318a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)static base::LazyInstance<BrowserContextKeyedAPIFactory<PushMessagingAPI> > 319a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) g_factory = LAZY_INSTANCE_INITIALIZER; 3202a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3212a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)// static 322a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)BrowserContextKeyedAPIFactory<PushMessagingAPI>* 3232a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)PushMessagingAPI::GetFactoryInstance() { 3245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return g_factory.Pointer(); 3252a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 3262a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 327cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)bool PushMessagingAPI::InitEventRouterAndHandler() { 328f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation::ProfileInvalidationProvider* invalidation_provider = 329f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation::ProfileInvalidationProviderFactory::GetForProfile( 330f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) Profile::FromBrowserContext(browser_context_)); 331f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) if (!invalidation_provider) 332cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) return false; 3332a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3342a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (!event_router_) 335f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) event_router_.reset(new PushMessagingEventRouter(browser_context_)); 3362a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) if (!handler_) { 337f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) handler_.reset(new PushMessagingInvalidationHandler( 338f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) invalidation_provider->GetInvalidationService(), 339f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) event_router_.get())); 3402a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 341cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) 342cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) return true; 343cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)} 344cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) 345cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)void PushMessagingAPI::OnExtensionLoaded( 346cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) content::BrowserContext* browser_context, 347cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) const Extension* extension) { 348cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) if (!InitEventRouterAndHandler()) 349cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) return; 350cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) 35146d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles) if (extension->permissions_data()->HasAPIPermission( 35246d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles) APIPermission::kPushMessaging)) { 353cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) handler_->RegisterExtension(extension->id()); 354cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) } 355cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)} 356cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) 357cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)void PushMessagingAPI::OnExtensionUnloaded( 358cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) content::BrowserContext* browser_context, 359cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) const Extension* extension, 360cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) UnloadedExtensionInfo::Reason reason) { 361cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) if (!InitEventRouterAndHandler()) 362cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) return; 363cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) 36446d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles) if (extension->permissions_data()->HasAPIPermission( 36546d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles) APIPermission::kPushMessaging)) { 366cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) handler_->UnregisterExtension(extension->id()); 367cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) } 368cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)} 369cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) 370f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)void PushMessagingAPI::OnExtensionWillBeInstalled( 371f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) content::BrowserContext* browser_context, 372f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) const Extension* extension, 373f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) bool is_update, 374f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) bool from_ephemeral, 375f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) const std::string& old_name) { 376f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) if (InitEventRouterAndHandler() && 377f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) extension->permissions_data()->HasAPIPermission( 37846d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles) APIPermission::kPushMessaging)) { 379cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles) handler_->SuppressInitialInvalidationsForExtension(extension->id()); 3802a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) } 3812a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 3822a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3832a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)void PushMessagingAPI::SetMapperForTest( 3842a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) scoped_ptr<PushMessagingInvalidationMapper> mapper) { 3852a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) handler_ = mapper.Pass(); 3862a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)} 3872a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles) 3882a99a7e74a7f215066514fe81d2bfa6639d9edddTorne (Richard Coles)template <> 389a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)void 390a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)BrowserContextKeyedAPIFactory<PushMessagingAPI>::DeclareFactoryDependencies() { 391f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) DependsOn(ExtensionRegistryFactory::GetInstance()); 3925d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) DependsOn(ExtensionsBrowserClient::Get()->GetExtensionSystemFactory()); 393f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles) DependsOn(invalidation::ProfileInvalidationProviderFactory::GetInstance()); 3945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} 3955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 3965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)} // namespace extensions 397