1// Copyright 2013 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.
4
5#include "chrome/browser/signin/signin_header_helper.h"
6
7#include "base/strings/string_number_conversions.h"
8#include "base/strings/string_util.h"
9#include "base/strings/stringprintf.h"
10#include "chrome/browser/prefs/incognito_mode_prefs.h"
11#include "chrome/browser/profiles/profile_io_data.h"
12#include "chrome/browser/tab_contents/tab_util.h"
13#include "chrome/browser/ui/browser_window.h"
14#include "chrome/common/url_constants.h"
15#include "components/google/core/browser/google_util.h"
16#include "components/signin/core/common/profile_management_switches.h"
17#include "content/public/browser/browser_thread.h"
18#include "content/public/browser/resource_request_info.h"
19#include "content/public/browser/web_contents.h"
20#include "google_apis/gaia/gaia_auth_util.h"
21#include "net/http/http_response_headers.h"
22#include "net/url_request/url_request.h"
23
24#if defined(OS_ANDROID)
25#include "chrome/browser/android/signin/account_management_screen_helper.h"
26#else
27#include "chrome/browser/ui/browser_commands.h"
28#include "chrome/browser/ui/browser_finder.h"
29#endif  // defined(OS_ANDROID)
30
31namespace {
32
33// Dictionary of fields in a mirror response header.
34typedef std::map<std::string, std::string> MirrorResponseHeaderDictionary;
35
36const char kChromeConnectedHeader[] = "X-Chrome-Connected";
37const char kChromeManageAccountsHeader[] = "X-Chrome-Manage-Accounts";
38const char kGaiaIdAttrName[] = "id";
39const char kProfileModeAttrName[] = "mode";
40const char kEnableAccountConsistencyAttrName[] = "enable_account_consistency";
41
42const char kServiceTypeAttrName[] = "action";
43const char kEmailAttrName[] = "email";
44const char kIsSamlAttrName[] = "is_saml";
45const char kContinueUrlAttrName[] = "continue_url";
46const char kIsSameTabAttrName[] = "is_same_tab";
47
48// Determines the service type that has been passed from GAIA in the header.
49signin::GAIAServiceType GetGAIAServiceTypeFromHeader(
50    const std::string& header_value) {
51  if (header_value == "SIGNOUT")
52    return signin::GAIA_SERVICE_TYPE_SIGNOUT;
53  else if (header_value == "INCOGNITO")
54    return signin::GAIA_SERVICE_TYPE_INCOGNITO;
55  else if (header_value == "ADDSESSION")
56    return signin::GAIA_SERVICE_TYPE_ADDSESSION;
57  else if (header_value == "REAUTH")
58    return signin::GAIA_SERVICE_TYPE_REAUTH;
59  else if (header_value == "SIGNUP")
60    return signin::GAIA_SERVICE_TYPE_SIGNUP;
61  else if (header_value == "DEFAULT")
62    return signin::GAIA_SERVICE_TYPE_DEFAULT;
63  else
64    return signin::GAIA_SERVICE_TYPE_NONE;
65}
66
67// Parses the mirror response header. Its expected format is
68// "key1=value1,key2=value2,...".
69MirrorResponseHeaderDictionary ParseMirrorResponseHeader(
70    const std::string& header_value) {
71  std::vector<std::string> fields;
72  if (!Tokenize(header_value, std::string(","), &fields))
73    return MirrorResponseHeaderDictionary();
74
75  MirrorResponseHeaderDictionary dictionary;
76  for (std::vector<std::string>::iterator i = fields.begin();
77       i != fields.end(); ++i) {
78    std::string field(*i);
79    std::vector<std::string> tokens;
80    size_t delim = field.find_first_of('=');
81    if (delim == std::string::npos) {
82      DLOG(WARNING) << "Unexpected GAIA header field '" << field << "'.";
83      continue;
84    }
85    dictionary[field.substr(0, delim)] = net::UnescapeURLComponent(
86        field.substr(delim + 1), net::UnescapeRule::URL_SPECIAL_CHARS);
87  }
88  return dictionary;
89}
90
91// Returns the parameters contained in the X-Chrome-Manage-Accounts response
92// header.
93signin::ManageAccountsParams BuildManageAccountsParams(
94    const std::string& header_value) {
95  signin::ManageAccountsParams params;
96  MirrorResponseHeaderDictionary header_dictionary =
97      ParseMirrorResponseHeader(header_value);
98  MirrorResponseHeaderDictionary::const_iterator it = header_dictionary.begin();
99  for (; it != header_dictionary.end(); ++it) {
100    const std::string key_name(it->first);
101    if (key_name == kServiceTypeAttrName) {
102      params.service_type =
103          GetGAIAServiceTypeFromHeader(header_dictionary[kServiceTypeAttrName]);
104    } else if (key_name == kEmailAttrName) {
105      params.email = header_dictionary[kEmailAttrName];
106    } else if (key_name == kIsSamlAttrName) {
107      params.is_saml = header_dictionary[kIsSamlAttrName] == "true";
108    } else if (key_name == kContinueUrlAttrName) {
109      params.continue_url = header_dictionary[kContinueUrlAttrName];
110    } else if (key_name == kIsSameTabAttrName) {
111      params.is_same_tab = header_dictionary[kIsSameTabAttrName] == "true";
112    } else {
113      DLOG(WARNING) << "Unexpected GAIA header attribute '" << key_name << "'.";
114    }
115  }
116  return params;
117}
118
119#if !defined(OS_IOS)
120// Processes the mirror response header on the UI thread. Currently depending
121// on the value of |header_value|, it either shows the profile avatar menu, or
122// opens an incognito window/tab.
123void ProcessMirrorHeaderUIThread(
124    int child_id, int route_id,
125    signin::ManageAccountsParams manage_accounts_params) {
126  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
127
128  signin::GAIAServiceType service_type = manage_accounts_params.service_type;
129  DCHECK_NE(signin::GAIA_SERVICE_TYPE_NONE, service_type);
130
131  content::WebContents* web_contents =
132      tab_util::GetWebContentsByID(child_id, route_id);
133  if (!web_contents)
134    return;
135
136#if !defined(OS_ANDROID)
137  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
138  if (browser) {
139    BrowserWindow::AvatarBubbleMode bubble_mode;
140    switch (service_type) {
141      case signin::GAIA_SERVICE_TYPE_INCOGNITO:
142        chrome::NewIncognitoWindow(browser);
143        return;
144      case signin::GAIA_SERVICE_TYPE_ADDSESSION:
145        bubble_mode = BrowserWindow::AVATAR_BUBBLE_MODE_ADD_ACCOUNT;
146        break;
147      case signin::GAIA_SERVICE_TYPE_REAUTH:
148        bubble_mode = BrowserWindow::AVATAR_BUBBLE_MODE_REAUTH;
149        break;
150      default:
151        bubble_mode = BrowserWindow::AVATAR_BUBBLE_MODE_ACCOUNT_MANAGEMENT;
152    }
153    browser->window()->ShowAvatarBubbleFromAvatarButton(
154        bubble_mode, manage_accounts_params);
155  }
156#else  // defined(OS_ANDROID)
157  if (service_type == signin::GAIA_SERVICE_TYPE_INCOGNITO) {
158    GURL url(manage_accounts_params.continue_url.empty() ?
159        chrome::kChromeUINativeNewTabURL :
160        manage_accounts_params.continue_url);
161    web_contents->OpenURL(content::OpenURLParams(
162        url, content::Referrer(), OFF_THE_RECORD,
163        ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false));
164  } else {
165    AccountManagementScreenHelper::OpenAccountManagementScreen(
166        Profile::FromBrowserContext(web_contents->GetBrowserContext()),
167        service_type);
168  }
169#endif // OS_ANDROID
170}
171#endif // !defined(OS_IOS)
172
173bool IsDriveOrigin(const GURL& url) {
174  if (!url.SchemeIsSecure())
175    return false;
176
177  const GURL kGoogleDriveURL("https://drive.google.com");
178  const GURL kGoogleDocsURL("https://docs.google.com");
179  return url == kGoogleDriveURL || url == kGoogleDocsURL;
180}
181
182} // empty namespace
183
184namespace signin {
185
186ManageAccountsParams::ManageAccountsParams() :
187    service_type(GAIA_SERVICE_TYPE_NONE),
188    email(""),
189    is_saml(false),
190    continue_url(""),
191    is_same_tab(false),
192    child_id(0),
193    route_id(0) {}
194
195bool AppendMirrorRequestHeaderIfPossible(
196    net::URLRequest* request,
197    const GURL& redirect_url,
198    ProfileIOData* io_data) {
199  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
200
201  if (io_data->IsOffTheRecord() ||
202      io_data->google_services_username()->GetValue().empty()) {
203    return false;
204  }
205
206  // Only set the header for Drive always, and other Google properties if
207  // new-profile-management is enabled.
208  // Vasquette, which is integrated with most Google properties, needs the
209  // header to redirect certain user actions to Chrome native UI. Drive needs
210  // the header to tell if the current user is connected. The drive path is a
211  // temporary workaround until the more generic chrome.principals API is
212  // available.
213  const GURL& url = redirect_url.is_empty() ? request->url() : redirect_url;
214  GURL origin(url.GetOrigin());
215  bool is_enable_account_consistency = switches::IsEnableAccountConsistency();
216  bool is_google_url =
217      !switches::IsEnableWebBasedSignin() &&
218      is_enable_account_consistency &&
219      (google_util::IsGoogleDomainUrl(
220           url,
221           google_util::ALLOW_SUBDOMAIN,
222           google_util::DISALLOW_NON_STANDARD_PORTS) ||
223       google_util::IsYoutubeDomainUrl(
224           url,
225           google_util::ALLOW_SUBDOMAIN,
226           google_util::DISALLOW_NON_STANDARD_PORTS));
227  if (!is_google_url && !IsDriveOrigin(origin))
228    return false;
229
230  std::string account_id(io_data->google_services_account_id()->GetValue());
231
232  int profile_mode_mask = PROFILE_MODE_DEFAULT;
233  if (io_data->incognito_availibility()->GetValue() ==
234          IncognitoModePrefs::DISABLED ||
235      IncognitoModePrefs::ArePlatformParentalControlsEnabledCached()) {
236    profile_mode_mask |= PROFILE_MODE_INCOGNITO_DISABLED;
237  }
238
239  // TODO(guohui): needs to make a new flag for enabling account consistency.
240  std::string header_value(base::StringPrintf("%s=%s,%s=%s,%s=%s",
241      kGaiaIdAttrName, account_id.c_str(),
242      kProfileModeAttrName, base::IntToString(profile_mode_mask).c_str(),
243      kEnableAccountConsistencyAttrName,
244      is_enable_account_consistency ? "true" : "false"));
245  request->SetExtraRequestHeaderByName(
246      kChromeConnectedHeader, header_value, false);
247  return true;
248}
249
250void ProcessMirrorResponseHeaderIfExists(
251    net::URLRequest* request,
252    ProfileIOData* io_data,
253    int child_id,
254    int route_id) {
255#if defined(OS_IOS)
256  NOTREACHED();
257#else
258  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
259  if (!gaia::IsGaiaSignonRealm(request->url().GetOrigin()))
260    return;
261
262  const content::ResourceRequestInfo* info =
263      content::ResourceRequestInfo::ForRequest(request);
264  if (!(info && info->GetResourceType() == content::RESOURCE_TYPE_MAIN_FRAME))
265    return;
266
267  std::string header_value;
268  if (!request->response_headers()->GetNormalizedHeader(
269          kChromeManageAccountsHeader, &header_value)) {
270    return;
271  }
272
273  DCHECK(switches::IsEnableAccountConsistency() && !io_data->IsOffTheRecord());
274  ManageAccountsParams params(BuildManageAccountsParams(header_value));
275  if (params.service_type == GAIA_SERVICE_TYPE_NONE)
276    return;
277
278  params.child_id = child_id;
279  params.route_id = route_id;
280  content::BrowserThread::PostTask(
281      content::BrowserThread::UI, FROM_HERE,
282      base::Bind(ProcessMirrorHeaderUIThread, child_id, route_id, params));
283#endif  // defined(OS_IOS)
284}
285
286} // namespace signin
287