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/bookmarks/bookmark_html_writer.h"
6
7#include "base/base64.h"
8#include "base/callback.h"
9#include "base/file_path.h"
10#include "base/memory/scoped_ptr.h"
11#include "base/message_loop.h"
12#include "base/platform_file.h"
13#include "base/string_number_conversions.h"
14#include "base/time.h"
15#include "base/values.h"
16#include "chrome/browser/bookmarks/bookmark_codec.h"
17#include "chrome/browser/bookmarks/bookmark_model.h"
18#include "chrome/browser/history/history_types.h"
19#include "chrome/browser/profiles/profile.h"
20#include "content/browser/browser_thread.h"
21#include "content/common/notification_source.h"
22#include "grit/generated_resources.h"
23#include "net/base/escape.h"
24#include "net/base/file_stream.h"
25#include "net/base/net_errors.h"
26#include "ui/base/l10n/l10n_util.h"
27
28namespace {
29
30static BookmarkFaviconFetcher* fetcher = NULL;
31
32// File header.
33const char kHeader[] =
34    "<!DOCTYPE NETSCAPE-Bookmark-file-1>\r\n"
35    "<!-- This is an automatically generated file.\r\n"
36    "     It will be read and overwritten.\r\n"
37    "     DO NOT EDIT! -->\r\n"
38    "<META HTTP-EQUIV=\"Content-Type\""
39    " CONTENT=\"text/html; charset=UTF-8\">\r\n"
40    "<TITLE>Bookmarks</TITLE>\r\n"
41    "<H1>Bookmarks</H1>\r\n"
42    "<DL><p>\r\n";
43
44// Newline separator.
45const char kNewline[] = "\r\n";
46
47// The following are used for bookmarks.
48
49// Start of a bookmark.
50const char kBookmarkStart[] = "<DT><A HREF=\"";
51// After kBookmarkStart.
52const char kAddDate[] = "\" ADD_DATE=\"";
53// After kAddDate.
54const char kIcon[] = "\" ICON=\"";
55// After kIcon.
56const char kBookmarkAttributeEnd[] = "\">";
57// End of a bookmark.
58const char kBookmarkEnd[] = "</A>";
59
60// The following are used when writing folders.
61
62// Start of a folder.
63const char kFolderStart[] = "<DT><H3 ADD_DATE=\"";
64// After kFolderStart.
65const char kLastModified[] = "\" LAST_MODIFIED=\"";
66// After kLastModified when writing the bookmark bar.
67const char kBookmarkBar[] = "\" PERSONAL_TOOLBAR_FOLDER=\"true\">";
68// After kLastModified when writing a user created folder.
69const char kFolderAttributeEnd[] = "\">";
70// End of the folder.
71const char kFolderEnd[] = "</H3>";
72// Start of the children of a folder.
73const char kFolderChildren[] = "<DL><p>";
74// End of the children for a folder.
75const char kFolderChildrenEnd[] = "</DL><p>";
76
77// Number of characters to indent by.
78const size_t kIndentSize = 4;
79
80// Class responsible for the actual writing. Takes ownership of favicons_map.
81class Writer : public Task {
82 public:
83  Writer(Value* bookmarks,
84         const FilePath& path,
85         BookmarkFaviconFetcher::URLFaviconMap* favicons_map,
86         BookmarksExportObserver* observer)
87      : bookmarks_(bookmarks),
88        path_(path),
89        favicons_map_(favicons_map),
90        observer_(observer) {
91  }
92
93  virtual void Run() {
94    RunImpl();
95    NotifyOnFinish();
96  }
97
98  // Writing bookmarks and favicons data to file.
99  virtual void RunImpl() {
100    if (!OpenFile()) {
101      return;
102    }
103
104    Value* roots;
105    if (!Write(kHeader) ||
106        bookmarks_->GetType() != Value::TYPE_DICTIONARY ||
107        !static_cast<DictionaryValue*>(bookmarks_.get())->Get(
108            BookmarkCodec::kRootsKey, &roots) ||
109        roots->GetType() != Value::TYPE_DICTIONARY) {
110      NOTREACHED();
111      return;
112    }
113
114    DictionaryValue* roots_d_value = static_cast<DictionaryValue*>(roots);
115    Value* root_folder_value;
116    Value* other_folder_value;
117    if (!roots_d_value->Get(BookmarkCodec::kRootFolderNameKey,
118                            &root_folder_value) ||
119        root_folder_value->GetType() != Value::TYPE_DICTIONARY ||
120        !roots_d_value->Get(BookmarkCodec::kOtherBookmarkFolderNameKey,
121                            &other_folder_value) ||
122        other_folder_value->GetType() != Value::TYPE_DICTIONARY) {
123      NOTREACHED();
124      return;  // Invalid type for root folder and/or other folder.
125    }
126
127    IncrementIndent();
128
129    if (!WriteNode(*static_cast<DictionaryValue*>(root_folder_value),
130                   BookmarkNode::BOOKMARK_BAR) ||
131        !WriteNode(*static_cast<DictionaryValue*>(other_folder_value),
132                   BookmarkNode::OTHER_NODE)) {
133      return;
134    }
135
136    DecrementIndent();
137
138    Write(kFolderChildrenEnd);
139    Write(kNewline);
140    // File stream close is forced so that unit test could read it.
141    file_stream_.Close();
142  }
143
144 private:
145  // Types of text being written out. The type dictates how the text is
146  // escaped.
147  enum TextType {
148    // The text is the value of an html attribute, eg foo in
149    // <a href="foo">.
150    ATTRIBUTE_VALUE,
151
152    // Actual content, eg foo in <h1>foo</h2>.
153    CONTENT
154  };
155
156  // Opens the file, returning true on success.
157  bool OpenFile() {
158    int flags = base::PLATFORM_FILE_CREATE_ALWAYS | base::PLATFORM_FILE_WRITE;
159    return (file_stream_.Open(path_, flags) == net::OK);
160  }
161
162  // Increments the indent.
163  void IncrementIndent() {
164    indent_.resize(indent_.size() + kIndentSize, ' ');
165  }
166
167  // Decrements the indent.
168  void DecrementIndent() {
169    DCHECK(!indent_.empty());
170    indent_.resize(indent_.size() - kIndentSize, ' ');
171  }
172
173  // Called at the end of the export process.
174  void NotifyOnFinish() {
175    if (observer_ != NULL) {
176      observer_->OnExportFinished();
177    }
178  }
179
180  // Writes raw text out returning true on success. This does not escape
181  // the text in anyway.
182  bool Write(const std::string& text) {
183    size_t wrote = file_stream_.Write(text.c_str(), text.length(), NULL);
184    bool result = (wrote == text.length());
185    DCHECK(result);
186    return result;
187  }
188
189  // Writes out the text string (as UTF8). The text is escaped based on
190  // type.
191  bool Write(const std::string& text, TextType type) {
192    DCHECK(IsStringUTF8(text));
193    std::string utf8_string;
194
195    switch (type) {
196      case ATTRIBUTE_VALUE:
197        // Convert " to &quot;
198        utf8_string = text;
199        ReplaceSubstringsAfterOffset(&utf8_string, 0, "\"", "&quot;");
200        break;
201
202      case CONTENT:
203        utf8_string = EscapeForHTML(text);
204        break;
205
206      default:
207        NOTREACHED();
208    }
209
210    return Write(utf8_string);
211  }
212
213  // Indents the current line.
214  bool WriteIndent() {
215    return Write(indent_);
216  }
217
218  // Converts a time string written to the JSON codec into a time_t string
219  // (used by bookmarks.html) and writes it.
220  bool WriteTime(const std::string& time_string) {
221    int64 internal_value;
222    base::StringToInt64(time_string, &internal_value);
223    return Write(base::Int64ToString(
224        base::Time::FromInternalValue(internal_value).ToTimeT()));
225  }
226
227  // Writes the node and all its children, returning true on success.
228  bool WriteNode(const DictionaryValue& value,
229                BookmarkNode::Type folder_type) {
230    std::string title, date_added_string, type_string;
231    if (!value.GetString(BookmarkCodec::kNameKey, &title) ||
232        !value.GetString(BookmarkCodec::kDateAddedKey, &date_added_string) ||
233        !value.GetString(BookmarkCodec::kTypeKey, &type_string) ||
234        (type_string != BookmarkCodec::kTypeURL &&
235         type_string != BookmarkCodec::kTypeFolder))  {
236      NOTREACHED();
237      return false;
238    }
239
240    if (type_string == BookmarkCodec::kTypeURL) {
241      std::string url_string;
242      if (!value.GetString(BookmarkCodec::kURLKey, &url_string)) {
243        NOTREACHED();
244        return false;
245      }
246
247      std::string favicon_string;
248      BookmarkFaviconFetcher::URLFaviconMap::iterator itr =
249          favicons_map_->find(url_string);
250      if (itr != favicons_map_->end()) {
251        scoped_refptr<RefCountedMemory> data(itr->second.get());
252        std::string favicon_data;
253        favicon_data.assign(reinterpret_cast<const char*>(data->front()),
254                            data->size());
255        std::string favicon_base64_encoded;
256        if (base::Base64Encode(favicon_data, &favicon_base64_encoded)) {
257          GURL favicon_url("data:image/png;base64," + favicon_base64_encoded);
258          favicon_string = favicon_url.spec();
259        }
260      }
261
262      if (!WriteIndent() ||
263          !Write(kBookmarkStart) ||
264          !Write(url_string, ATTRIBUTE_VALUE) ||
265          !Write(kAddDate) ||
266          !WriteTime(date_added_string) ||
267          (!favicon_string.empty() &&
268              (!Write(kIcon) ||
269               !Write(favicon_string, ATTRIBUTE_VALUE))) ||
270          !Write(kBookmarkAttributeEnd) ||
271          !Write(title, CONTENT) ||
272          !Write(kBookmarkEnd) ||
273          !Write(kNewline)) {
274        return false;
275      }
276      return true;
277    }
278
279    // Folder.
280    std::string last_modified_date;
281    Value* child_values;
282    if (!value.GetString(BookmarkCodec::kDateModifiedKey,
283                         &last_modified_date) ||
284        !value.Get(BookmarkCodec::kChildrenKey, &child_values) ||
285        child_values->GetType() != Value::TYPE_LIST) {
286      NOTREACHED();
287      return false;
288    }
289    if (folder_type != BookmarkNode::OTHER_NODE) {
290      // The other folder name is not written out. This gives the effect of
291      // making the contents of the 'other folder' be a sibling to the bookmark
292      // bar folder.
293      if (!WriteIndent() ||
294          !Write(kFolderStart) ||
295          !WriteTime(date_added_string) ||
296          !Write(kLastModified) ||
297          !WriteTime(last_modified_date)) {
298        return false;
299      }
300      if (folder_type == BookmarkNode::BOOKMARK_BAR) {
301        if (!Write(kBookmarkBar))
302          return false;
303        title = l10n_util::GetStringUTF8(IDS_BOOMARK_BAR_FOLDER_NAME);
304      } else if (!Write(kFolderAttributeEnd)) {
305        return false;
306      }
307      if (!Write(title, CONTENT) ||
308          !Write(kFolderEnd) ||
309          !Write(kNewline) ||
310          !WriteIndent() ||
311          !Write(kFolderChildren) ||
312          !Write(kNewline)) {
313        return false;
314      }
315      IncrementIndent();
316    }
317
318    // Write the children.
319    ListValue* children = static_cast<ListValue*>(child_values);
320    for (size_t i = 0; i < children->GetSize(); ++i) {
321      Value* child_value;
322      if (!children->Get(i, &child_value) ||
323          child_value->GetType() != Value::TYPE_DICTIONARY) {
324        NOTREACHED();
325        return false;
326      }
327      if (!WriteNode(*static_cast<DictionaryValue*>(child_value),
328                     BookmarkNode::FOLDER)) {
329        return false;
330      }
331    }
332    if (folder_type != BookmarkNode::OTHER_NODE) {
333      // Close out the folder.
334      DecrementIndent();
335      if (!WriteIndent() ||
336          !Write(kFolderChildrenEnd) ||
337          !Write(kNewline)) {
338        return false;
339      }
340    }
341    return true;
342  }
343
344  // The BookmarkModel as a Value. This value was generated from the
345  // BookmarkCodec.
346  scoped_ptr<Value> bookmarks_;
347
348  // Path we're writing to.
349  FilePath path_;
350
351  // Map that stores favicon per URL.
352  scoped_ptr<BookmarkFaviconFetcher::URLFaviconMap> favicons_map_;
353
354  // Observer to be notified on finish.
355  BookmarksExportObserver* observer_;
356
357  // File we're writing to.
358  net::FileStream file_stream_;
359
360  // How much we indent when writing a bookmark/folder. This is modified
361  // via IncrementIndent and DecrementIndent.
362  std::string indent_;
363};
364
365}  // namespace
366
367BookmarkFaviconFetcher::BookmarkFaviconFetcher(
368    Profile* profile,
369    const FilePath& path,
370    BookmarksExportObserver* observer)
371    : profile_(profile),
372      path_(path),
373      observer_(observer) {
374  favicons_map_.reset(new URLFaviconMap());
375  registrar_.Add(this,
376                 NotificationType::PROFILE_DESTROYED,
377                 Source<Profile>(profile_));
378}
379
380BookmarkFaviconFetcher::~BookmarkFaviconFetcher() {
381}
382
383void BookmarkFaviconFetcher::ExportBookmarks() {
384  ExtractUrls(profile_->GetBookmarkModel()->GetBookmarkBarNode());
385  ExtractUrls(profile_->GetBookmarkModel()->other_node());
386  if (!bookmark_urls_.empty()) {
387    FetchNextFavicon();
388  } else {
389    ExecuteWriter();
390  }
391}
392
393void BookmarkFaviconFetcher::Observe(NotificationType type,
394                                     const NotificationSource& source,
395                                     const NotificationDetails& details) {
396  if (NotificationType::PROFILE_DESTROYED == type && fetcher != NULL) {
397    MessageLoop::current()->DeleteSoon(FROM_HERE, fetcher);
398    fetcher = NULL;
399  }
400}
401
402void BookmarkFaviconFetcher::ExtractUrls(const BookmarkNode* node) {
403  if (BookmarkNode::URL == node->type()) {
404    std::string url = node->GetURL().spec();
405    if (!url.empty()) {
406      bookmark_urls_.push_back(url);
407    }
408  } else {
409    for (int i = 0; i < node->child_count(); ++i) {
410      ExtractUrls(node->GetChild(i));
411    }
412  }
413}
414
415void BookmarkFaviconFetcher::ExecuteWriter() {
416  // BookmarkModel isn't thread safe (nor would we want to lock it down
417  // for the duration of the write), as such we make a copy of the
418  // BookmarkModel using BookmarkCodec then write from that.
419  BookmarkCodec codec;
420  BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
421      new Writer(codec.Encode(profile_->GetBookmarkModel()),
422                 path_,
423                 favicons_map_.release(),
424                 observer_));
425  if (fetcher != NULL) {
426    MessageLoop::current()->DeleteSoon(FROM_HERE, fetcher);
427    fetcher = NULL;
428  }
429}
430
431bool BookmarkFaviconFetcher::FetchNextFavicon() {
432  if (bookmark_urls_.empty()) {
433    return false;
434  }
435  do {
436    std::string url = bookmark_urls_.front();
437    // Filter out urls that we've already got favicon for.
438    URLFaviconMap::const_iterator iter = favicons_map_->find(url);
439    if (favicons_map_->end() == iter) {
440      FaviconService* favicon_service =
441          profile_->GetFaviconService(Profile::EXPLICIT_ACCESS);
442      favicon_service->GetFaviconForURL(GURL(url), history::FAVICON,
443          &favicon_consumer_,
444          NewCallback(this, &BookmarkFaviconFetcher::OnFaviconDataAvailable));
445      return true;
446    } else {
447      bookmark_urls_.pop_front();
448    }
449  } while (!bookmark_urls_.empty());
450  return false;
451}
452
453void BookmarkFaviconFetcher::OnFaviconDataAvailable(
454    FaviconService::Handle handle,
455    history::FaviconData favicon) {
456  GURL url;
457  if (!bookmark_urls_.empty()) {
458    url = GURL(bookmark_urls_.front());
459    bookmark_urls_.pop_front();
460  }
461  if (favicon.is_valid() && !url.is_empty()) {
462    favicons_map_->insert(make_pair(url.spec(), favicon.image_data));
463  }
464
465  if (FetchNextFavicon()) {
466    return;
467  }
468  ExecuteWriter();
469}
470
471namespace bookmark_html_writer {
472
473void WriteBookmarks(Profile* profile,
474                    const FilePath& path,
475                    BookmarksExportObserver* observer) {
476  // BookmarkModel isn't thread safe (nor would we want to lock it down
477  // for the duration of the write), as such we make a copy of the
478  // BookmarkModel using BookmarkCodec then write from that.
479  if (fetcher == NULL) {
480    fetcher = new BookmarkFaviconFetcher(profile, path, observer);
481    fetcher->ExportBookmarks();
482  }
483}
484
485}  // namespace bookmark_html_writer
486