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