1// Copyright (c) 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/profiles/profile_downloader.h"
6
7#include <string>
8#include <vector>
9
10#include "base/json/json_reader.h"
11#include "base/logging.h"
12#include "base/message_loop/message_loop.h"
13#include "base/strings/string_split.h"
14#include "base/strings/string_util.h"
15#include "base/strings/stringprintf.h"
16#include "base/values.h"
17#include "chrome/browser/profiles/profile.h"
18#include "chrome/browser/profiles/profile_downloader_delegate.h"
19#include "chrome/browser/profiles/profile_manager.h"
20#include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
21#include "chrome/browser/signin/signin_manager_factory.h"
22#include "components/signin/core/browser/profile_oauth2_token_service.h"
23#include "components/signin/core/browser/signin_manager.h"
24#include "content/public/browser/browser_thread.h"
25#include "google_apis/gaia/gaia_constants.h"
26#include "google_apis/gaia/gaia_urls.h"
27#include "net/base/load_flags.h"
28#include "net/url_request/url_fetcher.h"
29#include "net/url_request/url_request_status.h"
30#include "skia/ext/image_operations.h"
31#include "url/gurl.h"
32
33using content::BrowserThread;
34
35namespace {
36
37// Template for optional authorization header when using an OAuth access token.
38const char kAuthorizationHeader[] =
39    "Authorization: Bearer %s";
40
41// URL requesting user info.
42const char kUserEntryURL[] =
43    "https://www.googleapis.com/oauth2/v1/userinfo?alt=json";
44
45// OAuth scope for the user info API.
46// For more info, see https://developers.google.com/accounts/docs/OAuth2LoginV1.
47const char kAPIScope[] = "https://www.googleapis.com/auth/userinfo.profile";
48
49// Path in JSON dictionary to user's photo thumbnail URL.
50const char kPhotoThumbnailURLPath[] = "picture";
51
52// From the user info API, this field corresponds to the full name of the user.
53const char kFullNamePath[] = "name";
54
55const char kGivenNamePath[] = "given_name";
56
57// Path in JSON dictionary to user's preferred locale.
58const char kLocalePath[] = "locale";
59
60// Path format for specifying thumbnail's size.
61const char kThumbnailSizeFormat[] = "s%d-c";
62// Default thumbnail size.
63const int kDefaultThumbnailSize = 64;
64
65// Separator of URL path components.
66const char kURLPathSeparator = '/';
67
68// Photo ID of the Picasa Web Albums profile picture (base64 of 0).
69const char kPicasaPhotoId[] = "AAAAAAAAAAA";
70
71// Photo version of the default PWA profile picture (base64 of 1).
72const char kDefaultPicasaPhotoVersion[] = "AAAAAAAAAAE";
73
74// The minimum number of path components in profile picture URL.
75const size_t kProfileImageURLPathComponentsCount = 6;
76
77// Index of path component with photo ID.
78const int kPhotoIdPathComponentIndex = 2;
79
80// Index of path component with photo version.
81const int kPhotoVersionPathComponentIndex = 3;
82
83// Given an image URL this function builds a new URL set to |size|.
84// For example, if |size| was set to 256 and |old_url| was either:
85//   https://example.com/--Abc/AAAAAAAAAAI/AAAAAAAAACQ/Efg/photo.jpg
86//   or
87//   https://example.com/--Abc/AAAAAAAAAAI/AAAAAAAAACQ/Efg/s64-c/photo.jpg
88// then return value in |new_url| would be:
89//   https://example.com/--Abc/AAAAAAAAAAI/AAAAAAAAACQ/Efg/s256-c/photo.jpg
90bool GetImageURLWithSize(const GURL& old_url, int size, GURL* new_url) {
91  DCHECK(new_url);
92  std::vector<std::string> components;
93  base::SplitString(old_url.path(), kURLPathSeparator, &components);
94  if (components.size() == 0)
95    return false;
96
97  const std::string& old_spec = old_url.spec();
98  std::string default_size_component(
99      base::StringPrintf(kThumbnailSizeFormat, kDefaultThumbnailSize));
100  std::string new_size_component(
101      base::StringPrintf(kThumbnailSizeFormat, size));
102
103  size_t pos = old_spec.find(default_size_component);
104  size_t end = std::string::npos;
105  if (pos != std::string::npos) {
106    // The default size is already specified in the URL so it needs to be
107    // replaced with the new size.
108    end = pos + default_size_component.size();
109  } else {
110    // The default size is not in the URL so try to insert it before the last
111    // component.
112    const std::string& file_name = old_url.ExtractFileName();
113    if (!file_name.empty()) {
114      pos = old_spec.find(file_name);
115      end = pos - 1;
116    }
117  }
118
119  if (pos != std::string::npos) {
120    std::string new_spec = old_spec.substr(0, pos) + new_size_component +
121                           old_spec.substr(end);
122    *new_url = GURL(new_spec);
123    return new_url->is_valid();
124  }
125
126  // We can't set the image size, just use the default size.
127  *new_url = old_url;
128  return true;
129}
130
131}  // namespace
132
133// Parses the entry response and gets the name and profile image URL.
134// |data| should be the JSON formatted data return by the response.
135// Returns false to indicate a parsing error.
136bool ProfileDownloader::ParseProfileJSON(const std::string& data,
137                                         base::string16* full_name,
138                                         base::string16* given_name,
139                                         std::string* url,
140                                         int image_size,
141                                         std::string* profile_locale) {
142  DCHECK(full_name);
143  DCHECK(given_name);
144  DCHECK(url);
145  DCHECK(profile_locale);
146
147  *full_name = base::string16();
148  *given_name = base::string16();
149  *url = std::string();
150  *profile_locale = std::string();
151
152  int error_code = -1;
153  std::string error_message;
154  scoped_ptr<base::Value> root_value(base::JSONReader::ReadAndReturnError(
155      data, base::JSON_PARSE_RFC, &error_code, &error_message));
156  if (!root_value) {
157    LOG(ERROR) << "Error while parsing user entry response: "
158               << error_message;
159    return false;
160  }
161  if (!root_value->IsType(base::Value::TYPE_DICTIONARY)) {
162    LOG(ERROR) << "JSON root is not a dictionary: "
163               << root_value->GetType();
164    return false;
165  }
166  base::DictionaryValue* root_dictionary =
167      static_cast<base::DictionaryValue*>(root_value.get());
168
169  root_dictionary->GetString(kFullNamePath, full_name);
170  root_dictionary->GetString(kGivenNamePath, given_name);
171  root_dictionary->GetString(kLocalePath, profile_locale);
172
173  std::string url_string;
174  if (root_dictionary->GetString(kPhotoThumbnailURLPath, &url_string)) {
175    GURL new_url;
176    if (!GetImageURLWithSize(GURL(url_string), image_size, &new_url)) {
177      LOG(ERROR) << "GetImageURLWithSize failed for url: " << url_string;
178      return false;
179    }
180    *url = new_url.spec();
181  }
182
183  // The profile data is considered valid as long as it has a name or a picture.
184  return !full_name->empty() || !url->empty();
185}
186
187// static
188bool ProfileDownloader::IsDefaultProfileImageURL(const std::string& url) {
189  if (url.empty())
190    return true;
191
192  GURL image_url_object(url);
193  DCHECK(image_url_object.is_valid());
194  VLOG(1) << "URL to check for default image: " << image_url_object.spec();
195  std::vector<std::string> path_components;
196  base::SplitString(image_url_object.path(),
197                    kURLPathSeparator,
198                    &path_components);
199
200  if (path_components.size() < kProfileImageURLPathComponentsCount)
201    return false;
202
203  const std::string& photo_id = path_components[kPhotoIdPathComponentIndex];
204  const std::string& photo_version =
205      path_components[kPhotoVersionPathComponentIndex];
206
207  // Check that the ID and version match the default Picasa profile photo.
208  return photo_id == kPicasaPhotoId &&
209         photo_version == kDefaultPicasaPhotoVersion;
210}
211
212ProfileDownloader::ProfileDownloader(ProfileDownloaderDelegate* delegate)
213    : OAuth2TokenService::Consumer("profile_downloader"),
214      delegate_(delegate),
215      picture_status_(PICTURE_FAILED) {
216  DCHECK(delegate_);
217}
218
219void ProfileDownloader::Start() {
220  StartForAccount(std::string());
221}
222
223void ProfileDownloader::StartForAccount(const std::string& account_id) {
224  VLOG(1) << "Starting profile downloader...";
225  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
226
227  ProfileOAuth2TokenService* service =
228      ProfileOAuth2TokenServiceFactory::GetForProfile(
229          delegate_->GetBrowserProfile());
230  if (!service) {
231    // This can happen in some test paths.
232    LOG(WARNING) << "User has no token service";
233    delegate_->OnProfileDownloadFailure(
234        this, ProfileDownloaderDelegate::TOKEN_ERROR);
235    return;
236  }
237
238  SigninManagerBase* signin_manager =
239      SigninManagerFactory::GetForProfile(delegate_->GetBrowserProfile());
240  account_id_ =
241      account_id.empty() ?
242          signin_manager->GetAuthenticatedAccountId() : account_id;
243  if (service->RefreshTokenIsAvailable(account_id_)) {
244    StartFetchingOAuth2AccessToken();
245  } else {
246    service->AddObserver(this);
247  }
248}
249
250base::string16 ProfileDownloader::GetProfileFullName() const {
251  return profile_full_name_;
252}
253
254base::string16 ProfileDownloader::GetProfileGivenName() const {
255  return profile_given_name_;
256}
257
258std::string ProfileDownloader::GetProfileLocale() const {
259  return profile_locale_;
260}
261
262SkBitmap ProfileDownloader::GetProfilePicture() const {
263  return profile_picture_;
264}
265
266ProfileDownloader::PictureStatus ProfileDownloader::GetProfilePictureStatus()
267    const {
268  return picture_status_;
269}
270
271std::string ProfileDownloader::GetProfilePictureURL() const {
272  return picture_url_;
273}
274
275void ProfileDownloader::StartFetchingImage() {
276  VLOG(1) << "Fetching user entry with token: " << auth_token_;
277  user_entry_fetcher_.reset(net::URLFetcher::Create(
278      GURL(kUserEntryURL), net::URLFetcher::GET, this));
279  user_entry_fetcher_->SetRequestContext(
280      delegate_->GetBrowserProfile()->GetRequestContext());
281  user_entry_fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
282                                    net::LOAD_DO_NOT_SAVE_COOKIES);
283  if (!auth_token_.empty()) {
284    user_entry_fetcher_->SetExtraRequestHeaders(
285        base::StringPrintf(kAuthorizationHeader, auth_token_.c_str()));
286  }
287  user_entry_fetcher_->Start();
288}
289
290void ProfileDownloader::StartFetchingOAuth2AccessToken() {
291  Profile* profile = delegate_->GetBrowserProfile();
292  OAuth2TokenService::ScopeSet scopes;
293  scopes.insert(kAPIScope);
294  ProfileOAuth2TokenService* token_service =
295      ProfileOAuth2TokenServiceFactory::GetForProfile(profile);
296  oauth2_access_token_request_ = token_service->StartRequest(
297      account_id_, scopes, this);
298}
299
300ProfileDownloader::~ProfileDownloader() {
301  // Ensures PO2TS observation is cleared when ProfileDownloader is destructed
302  // before refresh token is available.
303  ProfileOAuth2TokenService* service =
304      ProfileOAuth2TokenServiceFactory::GetForProfile(
305          delegate_->GetBrowserProfile());
306  if (service)
307    service->RemoveObserver(this);
308}
309
310void ProfileDownloader::OnURLFetchComplete(const net::URLFetcher* source) {
311  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
312  std::string data;
313  source->GetResponseAsString(&data);
314  bool network_error =
315      source->GetStatus().status() != net::URLRequestStatus::SUCCESS;
316  if (network_error || source->GetResponseCode() != 200) {
317    LOG(WARNING) << "Fetching profile data failed";
318    DVLOG(1) << "  Status: " << source->GetStatus().status();
319    DVLOG(1) << "  Error: " << source->GetStatus().error();
320    DVLOG(1) << "  Response code: " << source->GetResponseCode();
321    DVLOG(1) << "  Url: " << source->GetURL().spec();
322    delegate_->OnProfileDownloadFailure(this, network_error ?
323        ProfileDownloaderDelegate::NETWORK_ERROR :
324        ProfileDownloaderDelegate::SERVICE_ERROR);
325    return;
326  }
327
328  if (source == user_entry_fetcher_.get()) {
329    std::string image_url;
330    if (!ParseProfileJSON(data,
331                          &profile_full_name_,
332                          &profile_given_name_,
333                          &image_url,
334                          delegate_->GetDesiredImageSideLength(),
335                          &profile_locale_)) {
336      delegate_->OnProfileDownloadFailure(
337          this, ProfileDownloaderDelegate::SERVICE_ERROR);
338      return;
339    }
340    if (!delegate_->NeedsProfilePicture()) {
341      VLOG(1) << "Skipping profile picture download";
342      delegate_->OnProfileDownloadSuccess(this);
343      return;
344    }
345    if (IsDefaultProfileImageURL(image_url)) {
346      VLOG(1) << "User has default profile picture";
347      picture_status_ = PICTURE_DEFAULT;
348      delegate_->OnProfileDownloadSuccess(this);
349      return;
350    }
351    if (!image_url.empty() && image_url == delegate_->GetCachedPictureURL()) {
352      VLOG(1) << "Picture URL matches cached picture URL";
353      picture_status_ = PICTURE_CACHED;
354      delegate_->OnProfileDownloadSuccess(this);
355      return;
356    }
357    VLOG(1) << "Fetching profile image from " << image_url;
358    picture_url_ = image_url;
359    profile_image_fetcher_.reset(net::URLFetcher::Create(
360        GURL(image_url), net::URLFetcher::GET, this));
361    profile_image_fetcher_->SetRequestContext(
362        delegate_->GetBrowserProfile()->GetRequestContext());
363    profile_image_fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
364                                         net::LOAD_DO_NOT_SAVE_COOKIES);
365    if (!auth_token_.empty()) {
366      profile_image_fetcher_->SetExtraRequestHeaders(
367          base::StringPrintf(kAuthorizationHeader, auth_token_.c_str()));
368    }
369    profile_image_fetcher_->Start();
370  } else if (source == profile_image_fetcher_.get()) {
371    VLOG(1) << "Decoding the image...";
372    scoped_refptr<ImageDecoder> image_decoder = new ImageDecoder(
373        this, data, ImageDecoder::DEFAULT_CODEC);
374    scoped_refptr<base::MessageLoopProxy> task_runner =
375        BrowserThread::GetMessageLoopProxyForThread(BrowserThread::UI);
376    image_decoder->Start(task_runner);
377  }
378}
379
380void ProfileDownloader::OnImageDecoded(const ImageDecoder* decoder,
381                                       const SkBitmap& decoded_image) {
382  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
383  int image_size = delegate_->GetDesiredImageSideLength();
384  profile_picture_ = skia::ImageOperations::Resize(
385      decoded_image,
386      skia::ImageOperations::RESIZE_BEST,
387      image_size,
388      image_size);
389  picture_status_ = PICTURE_SUCCESS;
390  delegate_->OnProfileDownloadSuccess(this);
391}
392
393void ProfileDownloader::OnDecodeImageFailed(const ImageDecoder* decoder) {
394  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
395  delegate_->OnProfileDownloadFailure(
396      this, ProfileDownloaderDelegate::IMAGE_DECODE_FAILED);
397}
398
399void ProfileDownloader::OnRefreshTokenAvailable(const std::string& account_id) {
400  ProfileOAuth2TokenService* service =
401      ProfileOAuth2TokenServiceFactory::GetForProfile(
402          delegate_->GetBrowserProfile());
403  if (account_id != account_id_)
404    return;
405
406  service->RemoveObserver(this);
407  StartFetchingOAuth2AccessToken();
408}
409
410// Callback for OAuth2TokenService::Request on success. |access_token| is the
411// token used to start fetching user data.
412void ProfileDownloader::OnGetTokenSuccess(
413    const OAuth2TokenService::Request* request,
414    const std::string& access_token,
415    const base::Time& expiration_time) {
416  DCHECK_EQ(request, oauth2_access_token_request_.get());
417  oauth2_access_token_request_.reset();
418  auth_token_ = access_token;
419  StartFetchingImage();
420}
421
422// Callback for OAuth2TokenService::Request on failure.
423void ProfileDownloader::OnGetTokenFailure(
424    const OAuth2TokenService::Request* request,
425    const GoogleServiceAuthError& error) {
426  DCHECK_EQ(request, oauth2_access_token_request_.get());
427  oauth2_access_token_request_.reset();
428  LOG(WARNING) << "ProfileDownloader: token request using refresh token failed:"
429               << error.ToString();
430  delegate_->OnProfileDownloadFailure(
431      this, ProfileDownloaderDelegate::TOKEN_ERROR);
432}
433