1 // Copyright (c) 2011 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/importer/toolbar_importer.h" 6 7 #include <limits> 8 9 #include "base/rand_util.h" 10 #include "base/string_number_conversions.h" 11 #include "base/string_split.h" 12 #include "base/utf_string_conversions.h" 13 #include "chrome/browser/first_run/first_run.h" 14 #include "chrome/browser/importer/importer_bridge.h" 15 #include "chrome/browser/importer/importer_data_types.h" 16 #include "chrome/browser/profiles/profile.h" 17 #include "chrome/common/libxml_utils.h" 18 #include "content/browser/browser_thread.h" 19 #include "grit/generated_resources.h" 20 21 // Toolbar5Importer 22 const char Toolbar5Importer::kXmlApiReplyXmlTag[] = "xml_api_reply"; 23 const char Toolbar5Importer::kBookmarksXmlTag[] = "bookmarks"; 24 const char Toolbar5Importer::kBookmarkXmlTag[] = "bookmark"; 25 const char Toolbar5Importer::kTitleXmlTag[] = "title"; 26 const char Toolbar5Importer::kUrlXmlTag[] = "url"; 27 const char Toolbar5Importer::kTimestampXmlTag[] = "timestamp"; 28 const char Toolbar5Importer::kLabelsXmlTag[] = "labels"; 29 const char Toolbar5Importer::kLabelsXmlCloseTag[] = "/labels"; 30 const char Toolbar5Importer::kLabelXmlTag[] = "label"; 31 const char Toolbar5Importer::kAttributesXmlTag[] = "attributes"; 32 33 const char Toolbar5Importer::kRandomNumberToken[] = "{random_number}"; 34 const char Toolbar5Importer::kAuthorizationToken[] = "{auth_token}"; 35 const char Toolbar5Importer::kAuthorizationTokenPrefix[] = "/*"; 36 const char Toolbar5Importer::kAuthorizationTokenSuffix[] = "*/"; 37 const char Toolbar5Importer::kMaxNumToken[] = "{max_num}"; 38 const char Toolbar5Importer::kMaxTimestampToken[] = "{max_timestamp}"; 39 40 const char Toolbar5Importer::kT5AuthorizationTokenUrl[] = 41 "http://www.google.com/notebook/token?zx={random_number}"; 42 const char Toolbar5Importer::kT5FrontEndUrlTemplate[] = 43 "http://www.google.com/notebook/toolbar?cmd=list&tok={auth_token}&" 44 "num={max_num}&min={max_timestamp}&all=0&zx={random_number}"; 45 46 Toolbar5Importer::Toolbar5Importer() 47 : state_(NOT_USED), 48 items_to_import_(importer::NONE), 49 token_fetcher_(NULL), 50 data_fetcher_(NULL) { 51 } 52 53 // The destructor insures that the fetchers are currently not being used, as 54 // their thread-safe implementation requires that they are cancelled from the 55 // thread in which they were constructed. 56 Toolbar5Importer::~Toolbar5Importer() { 57 DCHECK(!token_fetcher_); 58 DCHECK(!data_fetcher_); 59 } 60 61 void Toolbar5Importer::StartImport( 62 const importer::SourceProfile& source_profile, 63 uint16 items, 64 ImporterBridge* bridge) { 65 DCHECK(bridge); 66 67 bridge_ = bridge; 68 items_to_import_ = items; 69 state_ = INITIALIZED; 70 71 bridge_->NotifyStarted(); 72 ContinueImport(); 73 } 74 75 // The public cancel method serves two functions, as a callback from the UI as 76 // well as an internal callback in case of cancel. An internal callback is 77 // required since the URLFetcher must be destroyed from the thread it was 78 // created. 79 void Toolbar5Importer::Cancel() { 80 // In the case when the thread is not importing messages we are to cancel as 81 // soon as possible. 82 Importer::Cancel(); 83 84 // If we are conducting network operations, post a message to the importer 85 // thread for synchronization. 86 if (BrowserThread::CurrentlyOn(BrowserThread::UI)) { 87 EndImport(); 88 } else { 89 BrowserThread::PostTask( 90 BrowserThread::UI, FROM_HERE, 91 NewRunnableMethod(this, &Toolbar5Importer::Cancel)); 92 } 93 } 94 95 void Toolbar5Importer::OnURLFetchComplete( 96 const URLFetcher* source, 97 const GURL& url, 98 const net::URLRequestStatus& status, 99 int response_code, 100 const ResponseCookies& cookies, 101 const std::string& data) { 102 if (cancelled()) { 103 EndImport(); 104 return; 105 } 106 107 if (200 != response_code) { // HTTP/Ok 108 // Cancelling here will update the UI and bypass the rest of bookmark 109 // import. 110 EndImportBookmarks(); 111 return; 112 } 113 114 switch (state_) { 115 case GET_AUTHORIZATION_TOKEN: 116 GetBookmarkDataFromServer(data); 117 break; 118 case GET_BOOKMARKS: 119 GetBookmarksFromServerDataResponse(data); 120 break; 121 default: 122 NOTREACHED() << "Invalid state."; 123 EndImportBookmarks(); 124 break; 125 } 126 } 127 128 void Toolbar5Importer::ContinueImport() { 129 DCHECK((items_to_import_ == importer::FAVORITES) || 130 (items_to_import_ == importer::NONE)) << 131 "The items requested are not supported"; 132 133 // The order here is important. Each Begin... will clear the flag 134 // of its item before its task finishes and re-enters this method. 135 if (importer::NONE == items_to_import_) { 136 EndImport(); 137 return; 138 } 139 if ((items_to_import_ & importer::FAVORITES) && !cancelled()) { 140 items_to_import_ &= ~importer::FAVORITES; 141 BeginImportBookmarks(); 142 return; 143 } 144 // TODO(brg): Import history, autocomplete, other toolbar information 145 // in a future release. 146 147 // This code should not be reached, but gracefully handles the possibility 148 // that StartImport was called with unsupported items_to_import. 149 if (!cancelled()) 150 EndImport(); 151 } 152 153 void Toolbar5Importer::EndImport() { 154 if (state_ != DONE) { 155 state_ = DONE; 156 // By spec the fetchers must be destroyed within the same 157 // thread they are created. The importer is destroyed in the ui_thread 158 // so when we complete in the file_thread we destroy them first. 159 if (NULL != token_fetcher_) { 160 delete token_fetcher_; 161 token_fetcher_ = NULL; 162 } 163 164 if (NULL != data_fetcher_) { 165 delete data_fetcher_; 166 data_fetcher_ = NULL; 167 } 168 169 if (bridge_) 170 bridge_->NotifyEnded(); 171 } 172 } 173 174 void Toolbar5Importer::BeginImportBookmarks() { 175 bridge_->NotifyItemStarted(importer::FAVORITES); 176 GetAuthenticationFromServer(); 177 } 178 179 void Toolbar5Importer::EndImportBookmarks() { 180 bridge_->NotifyItemEnded(importer::FAVORITES); 181 ContinueImport(); 182 } 183 184 185 // Notebook front-end connection manager implementation follows. 186 void Toolbar5Importer::GetAuthenticationFromServer() { 187 if (cancelled()) { 188 EndImport(); 189 return; 190 } 191 192 // Authentication is a token string retrieved from the authentication server 193 // To access it we call the url below with a random number replacing the 194 // value in the string. 195 state_ = GET_AUTHORIZATION_TOKEN; 196 197 // Random number construction. 198 int random = base::RandInt(0, std::numeric_limits<int>::max()); 199 std::string random_string = base::UintToString(random); 200 201 // Retrieve authorization token from the network. 202 std::string url_string(kT5AuthorizationTokenUrl); 203 url_string.replace(url_string.find(kRandomNumberToken), 204 arraysize(kRandomNumberToken) - 1, 205 random_string); 206 GURL url(url_string); 207 208 token_fetcher_ = new URLFetcher(url, URLFetcher::GET, this); 209 token_fetcher_->set_request_context(Profile::GetDefaultRequestContext()); 210 token_fetcher_->Start(); 211 } 212 213 void Toolbar5Importer::GetBookmarkDataFromServer(const std::string& response) { 214 if (cancelled()) { 215 EndImport(); 216 return; 217 } 218 219 state_ = GET_BOOKMARKS; 220 221 // Parse and verify the authorization token from the response. 222 std::string token; 223 if (!ParseAuthenticationTokenResponse(response, &token)) { 224 EndImportBookmarks(); 225 return; 226 } 227 228 // Build the Toolbar FE connection string, and call the server for 229 // the xml blob. We must tag the connection string with a random number. 230 std::string conn_string = kT5FrontEndUrlTemplate; 231 int random = base::RandInt(0, std::numeric_limits<int>::max()); 232 std::string random_string = base::UintToString(random); 233 conn_string.replace(conn_string.find(kRandomNumberToken), 234 arraysize(kRandomNumberToken) - 1, 235 random_string); 236 conn_string.replace(conn_string.find(kAuthorizationToken), 237 arraysize(kAuthorizationToken) - 1, 238 token); 239 GURL url(conn_string); 240 241 data_fetcher_ = new URLFetcher(url, URLFetcher::GET, this); 242 data_fetcher_->set_request_context(Profile::GetDefaultRequestContext()); 243 data_fetcher_->Start(); 244 } 245 246 void Toolbar5Importer::GetBookmarksFromServerDataResponse( 247 const std::string& response) { 248 if (cancelled()) { 249 EndImport(); 250 return; 251 } 252 253 state_ = PARSE_BOOKMARKS; 254 255 XmlReader reader; 256 if (reader.Load(response) && !cancelled()) { 257 // Construct Bookmarks 258 std::vector<ProfileWriter::BookmarkEntry> bookmarks; 259 if (ParseBookmarksFromReader(&reader, &bookmarks, 260 bridge_->GetLocalizedString(IDS_BOOKMARK_GROUP_FROM_GOOGLE_TOOLBAR))) 261 AddBookmarksToChrome(bookmarks); 262 } 263 EndImportBookmarks(); 264 } 265 266 bool Toolbar5Importer::ParseAuthenticationTokenResponse( 267 const std::string& response, 268 std::string* token) { 269 DCHECK(token); 270 271 *token = response; 272 size_t position = token->find(kAuthorizationTokenPrefix); 273 if (0 != position) 274 return false; 275 token->replace(position, arraysize(kAuthorizationTokenPrefix) - 1, ""); 276 277 position = token->find(kAuthorizationTokenSuffix); 278 if (token->size() != (position + (arraysize(kAuthorizationTokenSuffix) - 1))) 279 return false; 280 token->replace(position, arraysize(kAuthorizationTokenSuffix) - 1, ""); 281 282 return true; 283 } 284 285 // Parsing 286 bool Toolbar5Importer::ParseBookmarksFromReader( 287 XmlReader* reader, 288 std::vector<ProfileWriter::BookmarkEntry>* bookmarks, 289 const string16& bookmark_group_string) { 290 DCHECK(reader); 291 DCHECK(bookmarks); 292 293 // The XML blob returned from the server is described in the 294 // Toolbar-Notebook/Bookmarks Protocol document located at 295 // https://docs.google.com/a/google.com/Doc?docid=cgt3m7dr_24djt62m&hl=en 296 // We are searching for the section with structure 297 // <bookmarks><bookmark>...</bookmark><bookmark>...</bookmark></bookmarks> 298 299 // Locate the |bookmarks| blob. 300 if (!reader->SkipToElement()) 301 return false; 302 303 if (!LocateNextTagByName(reader, kBookmarksXmlTag)) 304 return false; 305 306 // Parse each |bookmark| blob 307 while (LocateNextTagWithStopByName(reader, kBookmarkXmlTag, 308 kBookmarksXmlTag)) { 309 ProfileWriter::BookmarkEntry bookmark_entry; 310 std::vector<BookmarkFolderType> folders; 311 if (ExtractBookmarkInformation(reader, &bookmark_entry, &folders, 312 bookmark_group_string)) { 313 // For each folder we create a new bookmark entry. Duplicates will 314 // be detected when we attempt to create the bookmark in the profile. 315 for (std::vector<BookmarkFolderType>::iterator folder = folders.begin(); 316 folder != folders.end(); 317 ++folder) { 318 bookmark_entry.path = *folder; 319 bookmarks->push_back(bookmark_entry); 320 } 321 } 322 } 323 324 if (0 == bookmarks->size()) 325 return false; 326 327 return true; 328 } 329 330 bool Toolbar5Importer::LocateNextOpenTag(XmlReader* reader) { 331 DCHECK(reader); 332 333 while (!reader->SkipToElement()) { 334 if (!reader->Read()) 335 return false; 336 } 337 return true; 338 } 339 340 bool Toolbar5Importer::LocateNextTagByName(XmlReader* reader, 341 const std::string& tag) { 342 DCHECK(reader); 343 344 // Locate the |tag| blob. 345 while (tag != reader->NodeName()) { 346 if (!reader->Read() || !LocateNextOpenTag(reader)) 347 return false; 348 } 349 return true; 350 } 351 352 bool Toolbar5Importer::LocateNextTagWithStopByName(XmlReader* reader, 353 const std::string& tag, 354 const std::string& stop) { 355 DCHECK(reader); 356 357 DCHECK_NE(tag, stop); 358 // Locate the |tag| blob. 359 while (tag != reader->NodeName()) { 360 // Move to the next open tag. 361 if (!reader->Read() || !LocateNextOpenTag(reader)) 362 return false; 363 // If we encounter the stop word return false. 364 if (stop == reader->NodeName()) 365 return false; 366 } 367 return true; 368 } 369 370 bool Toolbar5Importer::ExtractBookmarkInformation( 371 XmlReader* reader, 372 ProfileWriter::BookmarkEntry* bookmark_entry, 373 std::vector<BookmarkFolderType>* bookmark_folders, 374 const string16& bookmark_group_string) { 375 DCHECK(reader); 376 DCHECK(bookmark_entry); 377 DCHECK(bookmark_folders); 378 379 // The following is a typical bookmark entry. 380 // The reader should be pointing to the <title> tag at the moment. 381 // 382 // <bookmark> 383 // <title>MyTitle</title> 384 // <url>http://www.sohu.com/</url> 385 // <timestamp>1153328691085181</timestamp> 386 // <id>N123nasdf239</id> 387 // <notebook_id>Bxxxxxxx</notebook_id> (for bookmarks, a special id is used) 388 // <section_id>Sxxxxxx</section_id> 389 // <has_highlight>0</has_highlight> 390 // <labels> 391 // <label>China</label> 392 // <label>^k</label> (if this special label is present, the note is deleted) 393 // </labels> 394 // <attributes> 395 // <attribute> 396 // <name>favicon_url</name> 397 // <value>http://www.sohu.com/favicon.ico</value> 398 // </attribute> 399 // <attribute> 400 // <name>favicon_timestamp</name> 401 // <value>1153328653</value> 402 // </attribute> 403 // <attribute> 404 // <name>notebook_name</name> 405 // <value>My notebook 0</value> 406 // </attribute> 407 // <attribute> 408 // <name>section_name</name> 409 // <value>My section 0</value> 410 // </attribute> 411 // </attributes> 412 // </bookmark> 413 // 414 // We parse the blob in order, title->url->timestamp etc. Any failure 415 // causes us to skip this bookmark. 416 417 if (!ExtractTitleFromXmlReader(reader, bookmark_entry)) 418 return false; 419 if (!ExtractUrlFromXmlReader(reader, bookmark_entry)) 420 return false; 421 if (!ExtractTimeFromXmlReader(reader, bookmark_entry)) 422 return false; 423 if (!ExtractFoldersFromXmlReader(reader, bookmark_folders, 424 bookmark_group_string)) 425 return false; 426 427 return true; 428 } 429 430 bool Toolbar5Importer::ExtractNamedValueFromXmlReader(XmlReader* reader, 431 const std::string& name, 432 std::string* buffer) { 433 DCHECK(reader); 434 DCHECK(buffer); 435 436 if (name != reader->NodeName()) 437 return false; 438 if (!reader->ReadElementContent(buffer)) 439 return false; 440 return true; 441 } 442 443 bool Toolbar5Importer::ExtractTitleFromXmlReader( 444 XmlReader* reader, 445 ProfileWriter::BookmarkEntry* entry) { 446 DCHECK(reader); 447 DCHECK(entry); 448 449 if (!LocateNextTagWithStopByName(reader, kTitleXmlTag, kUrlXmlTag)) 450 return false; 451 std::string buffer; 452 if (!ExtractNamedValueFromXmlReader(reader, kTitleXmlTag, &buffer)) { 453 return false; 454 } 455 entry->title = UTF8ToUTF16(buffer); 456 return true; 457 } 458 459 bool Toolbar5Importer::ExtractUrlFromXmlReader( 460 XmlReader* reader, 461 ProfileWriter::BookmarkEntry* entry) { 462 DCHECK(reader); 463 DCHECK(entry); 464 465 if (!LocateNextTagWithStopByName(reader, kUrlXmlTag, kTimestampXmlTag)) 466 return false; 467 std::string buffer; 468 if (!ExtractNamedValueFromXmlReader(reader, kUrlXmlTag, &buffer)) { 469 return false; 470 } 471 entry->url = GURL(buffer); 472 return true; 473 } 474 475 bool Toolbar5Importer::ExtractTimeFromXmlReader( 476 XmlReader* reader, 477 ProfileWriter::BookmarkEntry* entry) { 478 DCHECK(reader); 479 DCHECK(entry); 480 if (!LocateNextTagWithStopByName(reader, kTimestampXmlTag, kLabelsXmlTag)) 481 return false; 482 std::string buffer; 483 if (!ExtractNamedValueFromXmlReader(reader, kTimestampXmlTag, &buffer)) { 484 return false; 485 } 486 int64 timestamp; 487 if (!base::StringToInt64(buffer, ×tamp)) { 488 return false; 489 } 490 entry->creation_time = base::Time::FromTimeT(timestamp); 491 return true; 492 } 493 494 bool Toolbar5Importer::ExtractFoldersFromXmlReader( 495 XmlReader* reader, 496 std::vector<BookmarkFolderType>* bookmark_folders, 497 const string16& bookmark_group_string) { 498 DCHECK(reader); 499 DCHECK(bookmark_folders); 500 501 // Read in the labels for this bookmark from the xml. There may be many 502 // labels for any one bookmark. 503 if (!LocateNextTagWithStopByName(reader, kLabelsXmlTag, kAttributesXmlTag)) 504 return false; 505 506 // It is within scope to have an empty labels section, so we do not 507 // return false if the labels are empty. 508 if (!reader->Read() || !LocateNextOpenTag(reader)) 509 return false; 510 511 std::vector<string16> label_vector; 512 while (kLabelXmlTag == reader->NodeName()) { 513 std::string label_buffer; 514 if (!reader->ReadElementContent(&label_buffer)) { 515 label_buffer = ""; 516 } 517 label_vector.push_back(UTF8ToUTF16(label_buffer)); 518 LocateNextOpenTag(reader); 519 } 520 521 if (0 == label_vector.size()) { 522 if (!FirstRun::IsChromeFirstRun()) { 523 bookmark_folders->resize(1); 524 (*bookmark_folders)[0].push_back(bookmark_group_string); 525 } 526 return true; 527 } 528 529 // We will be making one bookmark folder for each label. 530 bookmark_folders->resize(label_vector.size()); 531 532 for (size_t index = 0; index < label_vector.size(); ++index) { 533 // If this is the first run then we place favorites with no labels 534 // in the title bar. Else they are placed in the "Google Toolbar" folder. 535 if (!FirstRun::IsChromeFirstRun() || !label_vector[index].empty()) { 536 (*bookmark_folders)[index].push_back(bookmark_group_string); 537 } 538 539 // If the label and is in the form "xxx:yyy:zzz" this was created from an 540 // IE or Firefox folder. We undo the label creation and recreate the 541 // correct folder. 542 std::vector<string16> folder_names; 543 base::SplitString(label_vector[index], ':', &folder_names); 544 (*bookmark_folders)[index].insert((*bookmark_folders)[index].end(), 545 folder_names.begin(), folder_names.end()); 546 } 547 548 return true; 549 } 550 551 void Toolbar5Importer::AddBookmarksToChrome( 552 const std::vector<ProfileWriter::BookmarkEntry>& bookmarks) { 553 if (!bookmarks.empty() && !cancelled()) { 554 const string16& first_folder_name = 555 bridge_->GetLocalizedString(IDS_BOOKMARK_GROUP_FROM_GOOGLE_TOOLBAR); 556 int options = ProfileWriter::ADD_IF_UNIQUE | 557 (import_to_bookmark_bar() ? ProfileWriter::IMPORT_TO_BOOKMARK_BAR : 0); 558 bridge_->AddBookmarkEntries(bookmarks, first_folder_name, options); 559 } 560 } 561