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