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