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