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
22const char Toolbar5Importer::kXmlApiReplyXmlTag[] = "xml_api_reply";
23const char Toolbar5Importer::kBookmarksXmlTag[] = "bookmarks";
24const char Toolbar5Importer::kBookmarkXmlTag[] = "bookmark";
25const char Toolbar5Importer::kTitleXmlTag[] = "title";
26const char Toolbar5Importer::kUrlXmlTag[] = "url";
27const char Toolbar5Importer::kTimestampXmlTag[] = "timestamp";
28const char Toolbar5Importer::kLabelsXmlTag[] = "labels";
29const char Toolbar5Importer::kLabelsXmlCloseTag[] = "/labels";
30const char Toolbar5Importer::kLabelXmlTag[] = "label";
31const char Toolbar5Importer::kAttributesXmlTag[] = "attributes";
32
33const char Toolbar5Importer::kRandomNumberToken[] = "{random_number}";
34const char Toolbar5Importer::kAuthorizationToken[] = "{auth_token}";
35const char Toolbar5Importer::kAuthorizationTokenPrefix[] = "/*";
36const char Toolbar5Importer::kAuthorizationTokenSuffix[] = "*/";
37const char Toolbar5Importer::kMaxNumToken[] = "{max_num}";
38const char Toolbar5Importer::kMaxTimestampToken[] = "{max_timestamp}";
39
40const char Toolbar5Importer::kT5AuthorizationTokenUrl[] =
41    "http://www.google.com/notebook/token?zx={random_number}";
42const 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
46Toolbar5Importer::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.
56Toolbar5Importer::~Toolbar5Importer() {
57  DCHECK(!token_fetcher_);
58  DCHECK(!data_fetcher_);
59}
60
61void 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.
79void 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
95void 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
128void 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
153void 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
174void Toolbar5Importer::BeginImportBookmarks() {
175  bridge_->NotifyItemStarted(importer::FAVORITES);
176  GetAuthenticationFromServer();
177}
178
179void Toolbar5Importer::EndImportBookmarks() {
180  bridge_->NotifyItemEnded(importer::FAVORITES);
181  ContinueImport();
182}
183
184
185// Notebook front-end connection manager implementation follows.
186void 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
213void 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
246void 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
266bool 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
286bool 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
330bool 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
340bool 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
352bool 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
370bool 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
430bool 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
443bool 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
459bool 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
475bool 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, &timestamp)) {
488    return false;
489  }
490  entry->creation_time = base::Time::FromTimeT(timestamp);
491  return true;
492}
493
494bool 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
551void  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