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