Home | History | Annotate | Download | only in profiles
      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 
     33 using content::BrowserThread;
     34 
     35 namespace {
     36 
     37 // Template for optional authorization header when using an OAuth access token.
     38 const char kAuthorizationHeader[] =
     39     "Authorization: Bearer %s";
     40 
     41 // URL requesting user info.
     42 const 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.
     47 const char kAPIScope[] = "https://www.googleapis.com/auth/userinfo.profile";
     48 
     49 // Path in JSON dictionary to user's photo thumbnail URL.
     50 const char kPhotoThumbnailURLPath[] = "picture";
     51 
     52 // From the user info API, this field corresponds to the full name of the user.
     53 const char kFullNamePath[] = "name";
     54 
     55 const char kGivenNamePath[] = "given_name";
     56 
     57 // Path in JSON dictionary to user's preferred locale.
     58 const char kLocalePath[] = "locale";
     59 
     60 // Path format for specifying thumbnail's size.
     61 const char kThumbnailSizeFormat[] = "s%d-c";
     62 // Default thumbnail size.
     63 const int kDefaultThumbnailSize = 64;
     64 
     65 // Separator of URL path components.
     66 const char kURLPathSeparator = '/';
     67 
     68 // Photo ID of the Picasa Web Albums profile picture (base64 of 0).
     69 const char kPicasaPhotoId[] = "AAAAAAAAAAA";
     70 
     71 // Photo version of the default PWA profile picture (base64 of 1).
     72 const char kDefaultPicasaPhotoVersion[] = "AAAAAAAAAAE";
     73 
     74 // The minimum number of path components in profile picture URL.
     75 const size_t kProfileImageURLPathComponentsCount = 6;
     76 
     77 // Index of path component with photo ID.
     78 const int kPhotoIdPathComponentIndex = 2;
     79 
     80 // Index of path component with photo version.
     81 const 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
     90 bool 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.
    136 bool 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
    188 bool 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 
    212 ProfileDownloader::ProfileDownloader(ProfileDownloaderDelegate* delegate)
    213     : OAuth2TokenService::Consumer("profile_downloader"),
    214       delegate_(delegate),
    215       picture_status_(PICTURE_FAILED) {
    216   DCHECK(delegate_);
    217 }
    218 
    219 void ProfileDownloader::Start() {
    220   StartForAccount(std::string());
    221 }
    222 
    223 void 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 
    250 base::string16 ProfileDownloader::GetProfileFullName() const {
    251   return profile_full_name_;
    252 }
    253 
    254 base::string16 ProfileDownloader::GetProfileGivenName() const {
    255   return profile_given_name_;
    256 }
    257 
    258 std::string ProfileDownloader::GetProfileLocale() const {
    259   return profile_locale_;
    260 }
    261 
    262 SkBitmap ProfileDownloader::GetProfilePicture() const {
    263   return profile_picture_;
    264 }
    265 
    266 ProfileDownloader::PictureStatus ProfileDownloader::GetProfilePictureStatus()
    267     const {
    268   return picture_status_;
    269 }
    270 
    271 std::string ProfileDownloader::GetProfilePictureURL() const {
    272   return picture_url_;
    273 }
    274 
    275 void 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 
    290 void 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 
    300 ProfileDownloader::~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 
    310 void 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 
    380 void 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 
    393 void ProfileDownloader::OnDecodeImageFailed(const ImageDecoder* decoder) {
    394   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    395   delegate_->OnProfileDownloadFailure(
    396       this, ProfileDownloaderDelegate::IMAGE_DECODE_FAILED);
    397 }
    398 
    399 void 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.
    412 void 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.
    423 void 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