// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/importer/toolbar_importer.h" #include <limits> #include "base/rand_util.h" #include "base/string_number_conversions.h" #include "base/string_split.h" #include "base/utf_string_conversions.h" #include "chrome/browser/first_run/first_run.h" #include "chrome/browser/importer/importer_bridge.h" #include "chrome/browser/importer/importer_data_types.h" #include "chrome/browser/profiles/profile.h" #include "chrome/common/libxml_utils.h" #include "content/browser/browser_thread.h" #include "grit/generated_resources.h" // Toolbar5Importer const char Toolbar5Importer::kXmlApiReplyXmlTag[] = "xml_api_reply"; const char Toolbar5Importer::kBookmarksXmlTag[] = "bookmarks"; const char Toolbar5Importer::kBookmarkXmlTag[] = "bookmark"; const char Toolbar5Importer::kTitleXmlTag[] = "title"; const char Toolbar5Importer::kUrlXmlTag[] = "url"; const char Toolbar5Importer::kTimestampXmlTag[] = "timestamp"; const char Toolbar5Importer::kLabelsXmlTag[] = "labels"; const char Toolbar5Importer::kLabelsXmlCloseTag[] = "/labels"; const char Toolbar5Importer::kLabelXmlTag[] = "label"; const char Toolbar5Importer::kAttributesXmlTag[] = "attributes"; const char Toolbar5Importer::kRandomNumberToken[] = "{random_number}"; const char Toolbar5Importer::kAuthorizationToken[] = "{auth_token}"; const char Toolbar5Importer::kAuthorizationTokenPrefix[] = "/*"; const char Toolbar5Importer::kAuthorizationTokenSuffix[] = "*/"; const char Toolbar5Importer::kMaxNumToken[] = "{max_num}"; const char Toolbar5Importer::kMaxTimestampToken[] = "{max_timestamp}"; const char Toolbar5Importer::kT5AuthorizationTokenUrl[] = "http://www.google.com/notebook/token?zx={random_number}"; const char Toolbar5Importer::kT5FrontEndUrlTemplate[] = "http://www.google.com/notebook/toolbar?cmd=list&tok={auth_token}&" "num={max_num}&min={max_timestamp}&all=0&zx={random_number}"; Toolbar5Importer::Toolbar5Importer() : state_(NOT_USED), items_to_import_(importer::NONE), token_fetcher_(NULL), data_fetcher_(NULL) { } // The destructor insures that the fetchers are currently not being used, as // their thread-safe implementation requires that they are cancelled from the // thread in which they were constructed. Toolbar5Importer::~Toolbar5Importer() { DCHECK(!token_fetcher_); DCHECK(!data_fetcher_); } void Toolbar5Importer::StartImport( const importer::SourceProfile& source_profile, uint16 items, ImporterBridge* bridge) { DCHECK(bridge); bridge_ = bridge; items_to_import_ = items; state_ = INITIALIZED; bridge_->NotifyStarted(); ContinueImport(); } // The public cancel method serves two functions, as a callback from the UI as // well as an internal callback in case of cancel. An internal callback is // required since the URLFetcher must be destroyed from the thread it was // created. void Toolbar5Importer::Cancel() { // In the case when the thread is not importing messages we are to cancel as // soon as possible. Importer::Cancel(); // If we are conducting network operations, post a message to the importer // thread for synchronization. if (BrowserThread::CurrentlyOn(BrowserThread::UI)) { EndImport(); } else { BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, NewRunnableMethod(this, &Toolbar5Importer::Cancel)); } } void Toolbar5Importer::OnURLFetchComplete( const URLFetcher* source, const GURL& url, const net::URLRequestStatus& status, int response_code, const ResponseCookies& cookies, const std::string& data) { if (cancelled()) { EndImport(); return; } if (200 != response_code) { // HTTP/Ok // Cancelling here will update the UI and bypass the rest of bookmark // import. EndImportBookmarks(); return; } switch (state_) { case GET_AUTHORIZATION_TOKEN: GetBookmarkDataFromServer(data); break; case GET_BOOKMARKS: GetBookmarksFromServerDataResponse(data); break; default: NOTREACHED() << "Invalid state."; EndImportBookmarks(); break; } } void Toolbar5Importer::ContinueImport() { DCHECK((items_to_import_ == importer::FAVORITES) || (items_to_import_ == importer::NONE)) << "The items requested are not supported"; // The order here is important. Each Begin... will clear the flag // of its item before its task finishes and re-enters this method. if (importer::NONE == items_to_import_) { EndImport(); return; } if ((items_to_import_ & importer::FAVORITES) && !cancelled()) { items_to_import_ &= ~importer::FAVORITES; BeginImportBookmarks(); return; } // TODO(brg): Import history, autocomplete, other toolbar information // in a future release. // This code should not be reached, but gracefully handles the possibility // that StartImport was called with unsupported items_to_import. if (!cancelled()) EndImport(); } void Toolbar5Importer::EndImport() { if (state_ != DONE) { state_ = DONE; // By spec the fetchers must be destroyed within the same // thread they are created. The importer is destroyed in the ui_thread // so when we complete in the file_thread we destroy them first. if (NULL != token_fetcher_) { delete token_fetcher_; token_fetcher_ = NULL; } if (NULL != data_fetcher_) { delete data_fetcher_; data_fetcher_ = NULL; } if (bridge_) bridge_->NotifyEnded(); } } void Toolbar5Importer::BeginImportBookmarks() { bridge_->NotifyItemStarted(importer::FAVORITES); GetAuthenticationFromServer(); } void Toolbar5Importer::EndImportBookmarks() { bridge_->NotifyItemEnded(importer::FAVORITES); ContinueImport(); } // Notebook front-end connection manager implementation follows. void Toolbar5Importer::GetAuthenticationFromServer() { if (cancelled()) { EndImport(); return; } // Authentication is a token string retrieved from the authentication server // To access it we call the url below with a random number replacing the // value in the string. state_ = GET_AUTHORIZATION_TOKEN; // Random number construction. int random = base::RandInt(0, std::numeric_limits<int>::max()); std::string random_string = base::UintToString(random); // Retrieve authorization token from the network. std::string url_string(kT5AuthorizationTokenUrl); url_string.replace(url_string.find(kRandomNumberToken), arraysize(kRandomNumberToken) - 1, random_string); GURL url(url_string); token_fetcher_ = new URLFetcher(url, URLFetcher::GET, this); token_fetcher_->set_request_context(Profile::GetDefaultRequestContext()); token_fetcher_->Start(); } void Toolbar5Importer::GetBookmarkDataFromServer(const std::string& response) { if (cancelled()) { EndImport(); return; } state_ = GET_BOOKMARKS; // Parse and verify the authorization token from the response. std::string token; if (!ParseAuthenticationTokenResponse(response, &token)) { EndImportBookmarks(); return; } // Build the Toolbar FE connection string, and call the server for // the xml blob. We must tag the connection string with a random number. std::string conn_string = kT5FrontEndUrlTemplate; int random = base::RandInt(0, std::numeric_limits<int>::max()); std::string random_string = base::UintToString(random); conn_string.replace(conn_string.find(kRandomNumberToken), arraysize(kRandomNumberToken) - 1, random_string); conn_string.replace(conn_string.find(kAuthorizationToken), arraysize(kAuthorizationToken) - 1, token); GURL url(conn_string); data_fetcher_ = new URLFetcher(url, URLFetcher::GET, this); data_fetcher_->set_request_context(Profile::GetDefaultRequestContext()); data_fetcher_->Start(); } void Toolbar5Importer::GetBookmarksFromServerDataResponse( const std::string& response) { if (cancelled()) { EndImport(); return; } state_ = PARSE_BOOKMARKS; XmlReader reader; if (reader.Load(response) && !cancelled()) { // Construct Bookmarks std::vector<ProfileWriter::BookmarkEntry> bookmarks; if (ParseBookmarksFromReader(&reader, &bookmarks, bridge_->GetLocalizedString(IDS_BOOKMARK_GROUP_FROM_GOOGLE_TOOLBAR))) AddBookmarksToChrome(bookmarks); } EndImportBookmarks(); } bool Toolbar5Importer::ParseAuthenticationTokenResponse( const std::string& response, std::string* token) { DCHECK(token); *token = response; size_t position = token->find(kAuthorizationTokenPrefix); if (0 != position) return false; token->replace(position, arraysize(kAuthorizationTokenPrefix) - 1, ""); position = token->find(kAuthorizationTokenSuffix); if (token->size() != (position + (arraysize(kAuthorizationTokenSuffix) - 1))) return false; token->replace(position, arraysize(kAuthorizationTokenSuffix) - 1, ""); return true; } // Parsing bool Toolbar5Importer::ParseBookmarksFromReader( XmlReader* reader, std::vector<ProfileWriter::BookmarkEntry>* bookmarks, const string16& bookmark_group_string) { DCHECK(reader); DCHECK(bookmarks); // The XML blob returned from the server is described in the // Toolbar-Notebook/Bookmarks Protocol document located at // https://docs.google.com/a/google.com/Doc?docid=cgt3m7dr_24djt62m&hl=en // We are searching for the section with structure // <bookmarks><bookmark>...</bookmark><bookmark>...</bookmark></bookmarks> // Locate the |bookmarks| blob. if (!reader->SkipToElement()) return false; if (!LocateNextTagByName(reader, kBookmarksXmlTag)) return false; // Parse each |bookmark| blob while (LocateNextTagWithStopByName(reader, kBookmarkXmlTag, kBookmarksXmlTag)) { ProfileWriter::BookmarkEntry bookmark_entry; std::vector<BookmarkFolderType> folders; if (ExtractBookmarkInformation(reader, &bookmark_entry, &folders, bookmark_group_string)) { // For each folder we create a new bookmark entry. Duplicates will // be detected when we attempt to create the bookmark in the profile. for (std::vector<BookmarkFolderType>::iterator folder = folders.begin(); folder != folders.end(); ++folder) { bookmark_entry.path = *folder; bookmarks->push_back(bookmark_entry); } } } if (0 == bookmarks->size()) return false; return true; } bool Toolbar5Importer::LocateNextOpenTag(XmlReader* reader) { DCHECK(reader); while (!reader->SkipToElement()) { if (!reader->Read()) return false; } return true; } bool Toolbar5Importer::LocateNextTagByName(XmlReader* reader, const std::string& tag) { DCHECK(reader); // Locate the |tag| blob. while (tag != reader->NodeName()) { if (!reader->Read() || !LocateNextOpenTag(reader)) return false; } return true; } bool Toolbar5Importer::LocateNextTagWithStopByName(XmlReader* reader, const std::string& tag, const std::string& stop) { DCHECK(reader); DCHECK_NE(tag, stop); // Locate the |tag| blob. while (tag != reader->NodeName()) { // Move to the next open tag. if (!reader->Read() || !LocateNextOpenTag(reader)) return false; // If we encounter the stop word return false. if (stop == reader->NodeName()) return false; } return true; } bool Toolbar5Importer::ExtractBookmarkInformation( XmlReader* reader, ProfileWriter::BookmarkEntry* bookmark_entry, std::vector<BookmarkFolderType>* bookmark_folders, const string16& bookmark_group_string) { DCHECK(reader); DCHECK(bookmark_entry); DCHECK(bookmark_folders); // The following is a typical bookmark entry. // The reader should be pointing to the <title> tag at the moment. // // <bookmark> // <title>MyTitle</title> // <url>http://www.sohu.com/</url> // <timestamp>1153328691085181</timestamp> // <id>N123nasdf239</id> // <notebook_id>Bxxxxxxx</notebook_id> (for bookmarks, a special id is used) // <section_id>Sxxxxxx</section_id> // <has_highlight>0</has_highlight> // <labels> // <label>China</label> // <label>^k</label> (if this special label is present, the note is deleted) // </labels> // <attributes> // <attribute> // <name>favicon_url</name> // <value>http://www.sohu.com/favicon.ico</value> // </attribute> // <attribute> // <name>favicon_timestamp</name> // <value>1153328653</value> // </attribute> // <attribute> // <name>notebook_name</name> // <value>My notebook 0</value> // </attribute> // <attribute> // <name>section_name</name> // <value>My section 0</value> // </attribute> // </attributes> // </bookmark> // // We parse the blob in order, title->url->timestamp etc. Any failure // causes us to skip this bookmark. if (!ExtractTitleFromXmlReader(reader, bookmark_entry)) return false; if (!ExtractUrlFromXmlReader(reader, bookmark_entry)) return false; if (!ExtractTimeFromXmlReader(reader, bookmark_entry)) return false; if (!ExtractFoldersFromXmlReader(reader, bookmark_folders, bookmark_group_string)) return false; return true; } bool Toolbar5Importer::ExtractNamedValueFromXmlReader(XmlReader* reader, const std::string& name, std::string* buffer) { DCHECK(reader); DCHECK(buffer); if (name != reader->NodeName()) return false; if (!reader->ReadElementContent(buffer)) return false; return true; } bool Toolbar5Importer::ExtractTitleFromXmlReader( XmlReader* reader, ProfileWriter::BookmarkEntry* entry) { DCHECK(reader); DCHECK(entry); if (!LocateNextTagWithStopByName(reader, kTitleXmlTag, kUrlXmlTag)) return false; std::string buffer; if (!ExtractNamedValueFromXmlReader(reader, kTitleXmlTag, &buffer)) { return false; } entry->title = UTF8ToUTF16(buffer); return true; } bool Toolbar5Importer::ExtractUrlFromXmlReader( XmlReader* reader, ProfileWriter::BookmarkEntry* entry) { DCHECK(reader); DCHECK(entry); if (!LocateNextTagWithStopByName(reader, kUrlXmlTag, kTimestampXmlTag)) return false; std::string buffer; if (!ExtractNamedValueFromXmlReader(reader, kUrlXmlTag, &buffer)) { return false; } entry->url = GURL(buffer); return true; } bool Toolbar5Importer::ExtractTimeFromXmlReader( XmlReader* reader, ProfileWriter::BookmarkEntry* entry) { DCHECK(reader); DCHECK(entry); if (!LocateNextTagWithStopByName(reader, kTimestampXmlTag, kLabelsXmlTag)) return false; std::string buffer; if (!ExtractNamedValueFromXmlReader(reader, kTimestampXmlTag, &buffer)) { return false; } int64 timestamp; if (!base::StringToInt64(buffer, ×tamp)) { return false; } entry->creation_time = base::Time::FromTimeT(timestamp); return true; } bool Toolbar5Importer::ExtractFoldersFromXmlReader( XmlReader* reader, std::vector<BookmarkFolderType>* bookmark_folders, const string16& bookmark_group_string) { DCHECK(reader); DCHECK(bookmark_folders); // Read in the labels for this bookmark from the xml. There may be many // labels for any one bookmark. if (!LocateNextTagWithStopByName(reader, kLabelsXmlTag, kAttributesXmlTag)) return false; // It is within scope to have an empty labels section, so we do not // return false if the labels are empty. if (!reader->Read() || !LocateNextOpenTag(reader)) return false; std::vector<string16> label_vector; while (kLabelXmlTag == reader->NodeName()) { std::string label_buffer; if (!reader->ReadElementContent(&label_buffer)) { label_buffer = ""; } label_vector.push_back(UTF8ToUTF16(label_buffer)); LocateNextOpenTag(reader); } if (0 == label_vector.size()) { if (!FirstRun::IsChromeFirstRun()) { bookmark_folders->resize(1); (*bookmark_folders)[0].push_back(bookmark_group_string); } return true; } // We will be making one bookmark folder for each label. bookmark_folders->resize(label_vector.size()); for (size_t index = 0; index < label_vector.size(); ++index) { // If this is the first run then we place favorites with no labels // in the title bar. Else they are placed in the "Google Toolbar" folder. if (!FirstRun::IsChromeFirstRun() || !label_vector[index].empty()) { (*bookmark_folders)[index].push_back(bookmark_group_string); } // If the label and is in the form "xxx:yyy:zzz" this was created from an // IE or Firefox folder. We undo the label creation and recreate the // correct folder. std::vector<string16> folder_names; base::SplitString(label_vector[index], ':', &folder_names); (*bookmark_folders)[index].insert((*bookmark_folders)[index].end(), folder_names.begin(), folder_names.end()); } return true; } void Toolbar5Importer::AddBookmarksToChrome( const std::vector<ProfileWriter::BookmarkEntry>& bookmarks) { if (!bookmarks.empty() && !cancelled()) { const string16& first_folder_name = bridge_->GetLocalizedString(IDS_BOOKMARK_GROUP_FROM_GOOGLE_TOOLBAR); int options = ProfileWriter::ADD_IF_UNIQUE | (import_to_bookmark_bar() ? ProfileWriter::IMPORT_TO_BOOKMARK_BAR : 0); bridge_->AddBookmarkEntries(bookmarks, first_folder_name, options); } }