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