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 31 using content::BrowserThread; 32 33 namespace { 34 35 // Template for optional authorization header when using an OAuth access token. 36 const char kAuthorizationHeader[] = 37 "Authorization: Bearer %s"; 38 39 // URL requesting user info. 40 const 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. 45 const char kAPIScope[] = "https://www.googleapis.com/auth/userinfo.profile"; 46 47 // Path in JSON dictionary to user's photo thumbnail URL. 48 const char kPhotoThumbnailURLPath[] = "picture"; 49 50 // From the user info API, this field corresponds to the full name of the user. 51 const char kFullNamePath[] = "name"; 52 53 const char kGivenNamePath[] = "given_name"; 54 55 // Path in JSON dictionary to user's preferred locale. 56 const char kLocalePath[] = "locale"; 57 58 // Path format for specifying thumbnail's size. 59 const char kThumbnailSizeFormat[] = "s%d-c"; 60 // Default thumbnail size. 61 const int kDefaultThumbnailSize = 64; 62 63 // Separator of URL path components. 64 const char kURLPathSeparator = '/'; 65 66 // Photo ID of the Picasa Web Albums profile picture (base64 of 0). 67 const char kPicasaPhotoId[] = "AAAAAAAAAAA"; 68 69 // Photo version of the default PWA profile picture (base64 of 1). 70 const char kDefaultPicasaPhotoVersion[] = "AAAAAAAAAAE"; 71 72 // The minimum number of path components in profile picture URL. 73 const size_t kProfileImageURLPathComponentsCount = 6; 74 75 // Index of path component with photo ID. 76 const int kPhotoIdPathComponentIndex = 2; 77 78 // Index of path component with photo version. 79 const 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 88 bool 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. 134 bool 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 186 bool 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 210 ProfileDownloader::ProfileDownloader(ProfileDownloaderDelegate* delegate) 211 : delegate_(delegate), 212 picture_status_(PICTURE_FAILED) { 213 DCHECK(delegate_); 214 } 215 216 void ProfileDownloader::Start() { 217 StartForAccount(std::string()); 218 } 219 220 void 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 244 base::string16 ProfileDownloader::GetProfileFullName() const { 245 return profile_full_name_; 246 } 247 248 base::string16 ProfileDownloader::GetProfileGivenName() const { 249 return profile_given_name_; 250 } 251 252 std::string ProfileDownloader::GetProfileLocale() const { 253 return profile_locale_; 254 } 255 256 SkBitmap ProfileDownloader::GetProfilePicture() const { 257 return profile_picture_; 258 } 259 260 ProfileDownloader::PictureStatus ProfileDownloader::GetProfilePictureStatus() 261 const { 262 return picture_status_; 263 } 264 265 std::string ProfileDownloader::GetProfilePictureURL() const { 266 return picture_url_; 267 } 268 269 void 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 284 void 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 294 ProfileDownloader::~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 304 void 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 374 void 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 387 void ProfileDownloader::OnDecodeImageFailed(const ImageDecoder* decoder) { 388 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 389 delegate_->OnProfileDownloadFailure( 390 this, ProfileDownloaderDelegate::IMAGE_DECODE_FAILED); 391 } 392 393 void 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. 406 void 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. 417 void 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