Roll src/third_party/WebKit 9f7fb92:f103b33 (svn 202621:202622)
[chromium-blink-merge.git] / components / signin / ios / browser / account_consistency_service.mm
blob9f1269baaf445668c62f2de6b5681827fda885c2
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"
24 #include "url/gurl.h"
26 namespace {
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
29 // date.
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 {
39  public:
40   AccountConsistencyHandler(web::WebState* web_state,
41                             AccountConsistencyService* service,
42                             id<ManageAccountsDelegate> delegate);
44  private:
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);
65   if (!http_response)
66     return true;
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);
77   }
79   if (!gaia::IsGaiaSignonRealm(url.GetOrigin()))
80     return true;
81   NSString* manage_accounts_header = [[http_response allHeaderFields]
82       objectForKey:@"X-Chrome-Manage-Accounts"];
83   if (!manage_accounts_header)
84     return true;
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];
95       break;
96     }
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];
103       break;
104     case signin::GAIA_SERVICE_TYPE_NONE:
105       NOTREACHED();
106       break;
107   }
109   // WKWebView loads a blank page even if the response code is 204
110   // ("No Content"). http://crbug.com/368717
111   //
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.
116   return false;
119 void AccountConsistencyHandler::WebStateDestroyed() {
120   account_consistency_service_->RemoveWebStateHandler(web_state());
123 // WKWebView navigation delegate that calls its callback every time a navigation
124 // has finished.
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;
133 @end
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 {
141   self = [super init];
142   if (self) {
143     DCHECK(!callback.is_null());
144     _callback = callback;
145   }
146   return self;
149 - (instancetype)init {
150   NOTREACHED();
151   return nil;
154 #pragma mark - WKNavigationDelegate
156 - (void)webView:(WKWebView*)webView
157     didFinishNavigation:(WKNavigation*)navigation {
158   _callback.Run();
161 @end
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);
199   LoadFromPrefs();
202 AccountConsistencyService::~AccountConsistencyService() {
203   DCHECK(!web_view_);
204   DCHECK(!navigation_delegate_);
207 // static
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.
232     return;
233   }
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.
243     return;
244   }
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());
255   }
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);
263   ResetWKWebView();
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.
271     return;
272   }
273   if (cookie_requests_.empty()) {
274     return;
275   }
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.
279     return;
280   }
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);
297         return;
298       }
299       // Create expiration date of Now+2y to roughly follow the APISID cookie.
300       expiration_date =
301           (base::Time::Now() + base::TimeDelta::FromDays(730)).ToJsTime();
302       break;
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).
306       break;
307   }
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());
320   if (success) {
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);
330         break;
331       case REMOVE_X_CHROME_CONNECTED_COOKIE:
332         // Remove request.domain from prefs.
333         update->RemoveWithoutPathExpansion(request.domain, nullptr);
334         break;
335     }
336   }
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.
346     return nil;
347   }
348   if (!web_view_) {
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_];
355   }
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];
366   web_view_.reset();
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);
382   }
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
411   // to apply.
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.
418   ResetWKWebView();