1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/signin/ios/browser/account_consistency_service.h"
7 #include <WebKit/WebKit.h>
9 #import "base/ios/weak_nsobject.h"
10 #include "base/logging.h"
11 #import "base/mac/foundation_util.h"
12 #include "base/prefs/scoped_user_pref_update.h"
13 #include "base/strings/sys_string_conversions.h"
14 #include "base/time/time.h"
15 #include "components/google/core/browser/google_util.h"
16 #include "components/pref_registry/pref_registry_syncable.h"
17 #include "components/signin/core/browser/signin_client.h"
18 #include "components/signin/core/browser/signin_header_helper.h"
19 #include "ios/web/public/browser_state.h"
20 #include "ios/web/public/web_state/web_state_policy_decider.h"
21 #include "ios/web/public/web_view_creation_util.h"
22 #include "net/base/mac/url_conversions.h"
23 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
27 // JavaScript template used to set (or delete) the X-CHROME-CONNECTED cookie.
28 // It takes 3 arguments: the value of the cookie, its domain and its expiration
30 NSString* const kXChromeConnectedCookieTemplate =
31 @"<html><script>document.cookie=\"X-CHROME-CONNECTED=%@; path=/; domain=%@;"
32 " expires=\" + new Date(%f).toGMTString();</script></html>";
34 // WebStatePolicyDecider that monitors the HTTP headers on Gaia responses,
35 // reacting on the X-Chrome-Manage-Accounts header and notifying its delegate.
36 // It also notifies the AccountConsistencyService of domains it should add the
37 // X-CHROME-CONNECTED cookie to.
38 class AccountConsistencyHandler : public web::WebStatePolicyDecider {
40 AccountConsistencyHandler(web::WebState* web_state,
41 AccountConsistencyService* service,
42 id<ManageAccountsDelegate> delegate);
45 // web::WebStatePolicyDecider override
46 bool ShouldAllowResponse(NSURLResponse* response) override;
47 void WebStateDestroyed() override;
49 AccountConsistencyService* account_consistency_service_; // Weak.
50 base::WeakNSProtocol<id<ManageAccountsDelegate>> delegate_;
54 AccountConsistencyHandler::AccountConsistencyHandler(
55 web::WebState* web_state,
56 AccountConsistencyService* service,
57 id<ManageAccountsDelegate> delegate)
58 : web::WebStatePolicyDecider(web_state),
59 account_consistency_service_(service),
60 delegate_(delegate) {}
62 bool AccountConsistencyHandler::ShouldAllowResponse(NSURLResponse* response) {
63 NSHTTPURLResponse* http_response =
64 base::mac::ObjCCast<NSHTTPURLResponse>(response);
68 GURL url = net::GURLWithNSURL(http_response.URL);
69 if (google_util::IsGoogleDomainUrl(
70 url, google_util::ALLOW_SUBDOMAIN,
71 google_util::DISALLOW_NON_STANDARD_PORTS)) {
72 // User is showing intent to navigate to a Google domain. Add the
73 // X-CHROME-CONNECTED cookie to the domain if necessary.
74 std::string domain = net::registry_controlled_domains::GetDomainAndRegistry(
75 url, net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES);
76 account_consistency_service_->AddXChromeConnectedCookieToDomain(domain);
79 if (!gaia::IsGaiaSignonRealm(url.GetOrigin()))
81 NSString* manage_accounts_header = [[http_response allHeaderFields]
82 objectForKey:@"X-Chrome-Manage-Accounts"];
83 if (!manage_accounts_header)
86 signin::ManageAccountsParams params = signin::BuildManageAccountsParams(
87 base::SysNSStringToUTF8(manage_accounts_header));
89 switch (params.service_type) {
90 case signin::GAIA_SERVICE_TYPE_INCOGNITO: {
91 GURL continue_url = GURL(params.continue_url);
92 DLOG_IF(ERROR, !params.continue_url.empty() && !continue_url.is_valid())
93 << "Invalid continuation URL: \"" << continue_url << "\"";
94 [delegate_ onGoIncognito:continue_url];
97 case signin::GAIA_SERVICE_TYPE_SIGNOUT:
98 case signin::GAIA_SERVICE_TYPE_ADDSESSION:
99 case signin::GAIA_SERVICE_TYPE_REAUTH:
100 case signin::GAIA_SERVICE_TYPE_SIGNUP:
101 case signin::GAIA_SERVICE_TYPE_DEFAULT:
102 [delegate_ onManageAccounts];
104 case signin::GAIA_SERVICE_TYPE_NONE:
109 // WKWebView loads a blank page even if the response code is 204
110 // ("No Content"). http://crbug.com/368717
112 // Manage accounts responses are handled via native UI. Abort this request
113 // for the following reasons:
114 // * Avoid loading a blank page in WKWebView.
115 // * Avoid adding this request to history.
119 void AccountConsistencyHandler::WebStateDestroyed() {
120 account_consistency_service_->RemoveWebStateHandler(web_state());
123 // WKWebView navigation delegate that calls its callback every time a navigation
125 @interface AccountConsistencyNavigationDelegate : NSObject<WKNavigationDelegate>
127 // Designated initializer. |callback| will be called every time a navigation has
128 // finished. |callback| must not be empty.
129 - (instancetype)initWithCallback:(const base::Closure&)callback
130 NS_DESIGNATED_INITIALIZER;
132 - (instancetype)init NS_UNAVAILABLE;
135 @implementation AccountConsistencyNavigationDelegate {
136 // Callback that will be called every time a navigation has finished.
137 base::Closure _callback;
140 - (instancetype)initWithCallback:(const base::Closure&)callback {
143 DCHECK(!callback.is_null());
144 _callback = callback;
149 - (instancetype)init {
154 #pragma mark - WKNavigationDelegate
156 - (void)webView:(WKWebView*)webView
157 didFinishNavigation:(WKNavigation*)navigation {
163 const char AccountConsistencyService::kDomainsWithCookiePref[] =
164 "signin.domains_with_cookie";
166 AccountConsistencyService::CookieRequest
167 AccountConsistencyService::CookieRequest::CreateAddCookieRequest(
168 const std::string& domain) {
169 AccountConsistencyService::CookieRequest cookie_request;
170 cookie_request.request_type = ADD_X_CHROME_CONNECTED_COOKIE;
171 cookie_request.domain = domain;
172 return cookie_request;
175 AccountConsistencyService::CookieRequest
176 AccountConsistencyService::CookieRequest::CreateRemoveCookieRequest(
177 const std::string& domain) {
178 AccountConsistencyService::CookieRequest cookie_request;
179 cookie_request.request_type = REMOVE_X_CHROME_CONNECTED_COOKIE;
180 cookie_request.domain = domain;
181 return cookie_request;
184 AccountConsistencyService::AccountConsistencyService(
185 web::BrowserState* browser_state,
186 scoped_refptr<content_settings::CookieSettings> cookie_settings,
187 GaiaCookieManagerService* gaia_cookie_manager_service,
188 SigninClient* signin_client,
189 SigninManager* signin_manager)
190 : browser_state_(browser_state),
191 cookie_settings_(cookie_settings),
192 gaia_cookie_manager_service_(gaia_cookie_manager_service),
193 signin_client_(signin_client),
194 signin_manager_(signin_manager),
195 applying_cookie_requests_(false) {
196 gaia_cookie_manager_service_->AddObserver(this);
197 signin_manager_->AddObserver(this);
198 web::BrowserState::GetActiveStateManager(browser_state_)->AddObserver(this);
202 AccountConsistencyService::~AccountConsistencyService() {
204 DCHECK(!navigation_delegate_);
208 void AccountConsistencyService::RegisterPrefs(
209 user_prefs::PrefRegistrySyncable* registry) {
210 registry->RegisterDictionaryPref(
211 AccountConsistencyService::kDomainsWithCookiePref);
214 void AccountConsistencyService::SetWebStateHandler(
215 web::WebState* web_state,
216 id<ManageAccountsDelegate> delegate) {
217 DCHECK_EQ(0u, web_state_handlers_.count(web_state));
218 web_state_handlers_[web_state].reset(
219 new AccountConsistencyHandler(web_state, this, delegate));
222 void AccountConsistencyService::RemoveWebStateHandler(
223 web::WebState* web_state) {
224 DCHECK_LT(0u, web_state_handlers_.count(web_state));
225 web_state_handlers_.erase(web_state);
228 void AccountConsistencyService::AddXChromeConnectedCookieToDomain(
229 const std::string& domain) {
230 if (domains_with_cookies_.count(domain) > 0) {
231 // Cookie has recently been added. Nothing to do.
234 domains_with_cookies_.insert(domain);
235 cookie_requests_.push_back(CookieRequest::CreateAddCookieRequest(domain));
236 ApplyCookieRequests();
239 void AccountConsistencyService::RemoveXChromeConnectedCookieFromDomain(
240 const std::string& domain) {
241 if (domains_with_cookies_.count(domain) == 0) {
242 // Cookie is not on the domain. Nothing to do.
245 domains_with_cookies_.erase(domain);
246 cookie_requests_.push_back(CookieRequest::CreateRemoveCookieRequest(domain));
247 ApplyCookieRequests();
250 void AccountConsistencyService::LoadFromPrefs() {
251 const base::DictionaryValue* dict =
252 signin_client_->GetPrefs()->GetDictionary(kDomainsWithCookiePref);
253 for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd(); it.Advance()) {
254 domains_with_cookies_.insert(it.key());
258 void AccountConsistencyService::Shutdown() {
259 gaia_cookie_manager_service_->RemoveObserver(this);
260 signin_manager_->RemoveObserver(this);
261 web::BrowserState::GetActiveStateManager(browser_state_)
262 ->RemoveObserver(this);
264 web_state_handlers_.clear();
267 void AccountConsistencyService::ApplyCookieRequests() {
268 if (applying_cookie_requests_) {
269 // A cookie request is already being applied, the following ones will be
270 // handled as soon as the current one is done.
273 if (cookie_requests_.empty()) {
276 if (!web::BrowserState::GetActiveStateManager(browser_state_)->IsActive()) {
277 // Web view usage isn't active for now, ignore cookie requests for now and
278 // wait to be notified that it became active again.
281 applying_cookie_requests_ = true;
283 const GURL url("https://" + cookie_requests_.front().domain);
284 std::string cookie_value = "";
285 // Expiration date of the cookie in the JavaScript convention of time, a
286 // number of milliseconds since the epoch.
287 double expiration_date = 0;
288 switch (cookie_requests_.front().request_type) {
289 case ADD_X_CHROME_CONNECTED_COOKIE:
290 cookie_value = signin::BuildMirrorRequestCookieIfPossible(
291 url, signin_manager_->GetAuthenticatedAccountInfo().gaia,
292 cookie_settings_.get(), signin::PROFILE_MODE_DEFAULT);
293 if (cookie_value.empty()) {
294 // Don't add the cookie. Tentatively correct |domains_with_cookies_|.
295 domains_with_cookies_.erase(cookie_requests_.front().domain);
296 FinishedApplyingCookieRequest(false);
299 // Create expiration date of Now+2y to roughly follow the APISID cookie.
301 (base::Time::Now() + base::TimeDelta::FromDays(730)).ToJsTime();
303 case REMOVE_X_CHROME_CONNECTED_COOKIE:
304 // Nothing to do. Default values correspond to removing the cookie (no
305 // value, expiration date in the past).
308 NSString* html = [NSString
309 stringWithFormat:kXChromeConnectedCookieTemplate,
310 base::SysUTF8ToNSString(cookie_value),
311 base::SysUTF8ToNSString(url.host()), expiration_date];
312 // Load an HTML string with embedded JavaScript that will set or remove the
313 // cookie. By setting the base URL to |url|, this effectively allows to modify
314 // cookies on the correct domain without having to do a network request.
315 [GetWKWebView() loadHTMLString:html baseURL:net::NSURLWithGURL(url)];
318 void AccountConsistencyService::FinishedApplyingCookieRequest(bool success) {
319 DCHECK(!cookie_requests_.empty());
321 const CookieRequest& request = cookie_requests_.front();
322 DictionaryPrefUpdate update(
323 signin_client_->GetPrefs(),
324 AccountConsistencyService::kDomainsWithCookiePref);
325 switch (request.request_type) {
326 case ADD_X_CHROME_CONNECTED_COOKIE:
327 // Add request.domain to prefs, use |true| as a dummy value (that is
328 // never used), as the dictionary is used as a set.
329 update->SetBooleanWithoutPathExpansion(request.domain, true);
331 case REMOVE_X_CHROME_CONNECTED_COOKIE:
332 // Remove request.domain from prefs.
333 update->RemoveWithoutPathExpansion(request.domain, nullptr);
337 cookie_requests_.pop_front();
338 applying_cookie_requests_ = false;
339 ApplyCookieRequests();
342 WKWebView* AccountConsistencyService::GetWKWebView() {
343 if (!web::BrowserState::GetActiveStateManager(browser_state_)->IsActive()) {
344 // |browser_state_| is not active, WKWebView linked to this browser state
345 // should not exist or be created.
349 web_view_.reset(CreateWKWebView());
350 navigation_delegate_.reset([[AccountConsistencyNavigationDelegate alloc]
351 initWithCallback:base::Bind(&AccountConsistencyService::
352 FinishedApplyingCookieRequest,
353 base::Unretained(this), true)]);
354 [web_view_ setNavigationDelegate:navigation_delegate_];
356 return web_view_.get();
359 WKWebView* AccountConsistencyService::CreateWKWebView() {
360 return web::CreateWKWebView(CGRectZero, browser_state_);
363 void AccountConsistencyService::ResetWKWebView() {
364 [web_view_ setNavigationDelegate:nil];
365 [web_view_ stopLoading];
367 navigation_delegate_.reset();
368 applying_cookie_requests_ = false;
371 void AccountConsistencyService::AddXChromeConnectedCookies() {
372 DCHECK(!browser_state_->IsOffTheRecord());
373 AddXChromeConnectedCookieToDomain("google.com");
374 AddXChromeConnectedCookieToDomain("youtube.com");
377 void AccountConsistencyService::RemoveXChromeConnectedCookies() {
378 DCHECK(!browser_state_->IsOffTheRecord());
379 std::set<std::string> domains_with_cookies = domains_with_cookies_;
380 for (const std::string& domain : domains_with_cookies) {
381 RemoveXChromeConnectedCookieFromDomain(domain);
385 void AccountConsistencyService::OnAddAccountToCookieCompleted(
386 const std::string& account_id,
387 const GoogleServiceAuthError& error) {
388 AddXChromeConnectedCookies();
391 void AccountConsistencyService::OnGaiaAccountsInCookieUpdated(
392 const std::vector<gaia::ListedAccount>& accounts,
393 const GoogleServiceAuthError& error) {
394 AddXChromeConnectedCookies();
397 void AccountConsistencyService::GoogleSigninSucceeded(
398 const std::string& account_id,
399 const std::string& username,
400 const std::string& password) {
401 AddXChromeConnectedCookies();
404 void AccountConsistencyService::GoogleSignedOut(const std::string& account_id,
405 const std::string& username) {
406 RemoveXChromeConnectedCookies();
409 void AccountConsistencyService::OnActive() {
410 // |browser_state_| is now active. There might be some pending cookie requests
412 ApplyCookieRequests();
415 void AccountConsistencyService::OnInactive() {
416 // |browser_state_| is now inactive. Stop using |web_view_| and don't create
417 // a new one until it is active.