bookmark_html_writer.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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.h"
13#include "base/platform_file.h"
14#include "base/strings/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/bookmarks/bookmark_model_factory.h"
20#include "chrome/browser/favicon/favicon_service.h"
21#include "chrome/browser/favicon/favicon_service_factory.h"
22#include "chrome/browser/history/history_types.h"
23#include "chrome/common/chrome_notification_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    Value* roots = NULL;
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 = NULL;
118    Value* mobile_folder_value = NULL;
119    if (!roots_d_value->Get(BookmarkCodec::kRootFolderNameKey,
120                            &root_folder_value) ||
121        root_folder_value->GetType() != Value::TYPE_DICTIONARY ||
122        !roots_d_value->Get(BookmarkCodec::kOtherBookmarkFolderNameKey,
123                            &other_folder_value) ||
124        other_folder_value->GetType() != Value::TYPE_DICTIONARY ||
125        !roots_d_value->Get(BookmarkCodec::kMobileBookmarkFolderNameKey,
126                            &mobile_folder_value) ||
127        mobile_folder_value->GetType() != 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<DictionaryValue*>(root_folder_value),
135                   BookmarkNode::BOOKMARK_BAR) ||
136        !WriteNode(*static_cast<DictionaryValue*>(other_folder_value),
137                   BookmarkNode::OTHER_NODE) ||
138        !WriteNode(*static_cast<DictionaryValue*>(mobile_folder_value),
139                   BookmarkNode::MOBILE)) {
140      return;
141    }
142
143    DecrementIndent();
144
145    Write(kFolderChildrenEnd);
146    Write(kNewline);
147    // File stream close is forced so that unit test could read it.
148    file_stream_.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    file_stream_.reset(new net::FileStream(NULL));
172    int flags = base::PLATFORM_FILE_CREATE_ALWAYS | base::PLATFORM_FILE_WRITE;
173    return (file_stream_->OpenSync(path_, flags) == net::OK);
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    // net::FileStream does not allow 0-byte writes.
198    if (!text.length())
199      return true;
200    size_t wrote = file_stream_->WriteSync(text.c_str(), text.length());
201    bool result = (wrote == text.length());
202    DCHECK(result);
203    return result;
204  }
205
206  // Writes out the text string (as UTF8). The text is escaped based on
207  // type.
208  bool Write(const std::string& text, TextType type) {
209    DCHECK(IsStringUTF8(text));
210    std::string utf8_string;
211
212    switch (type) {
213      case ATTRIBUTE_VALUE:
214        // Convert " to &quot;
215        utf8_string = text;
216        ReplaceSubstringsAfterOffset(&utf8_string, 0, "\"", "&quot;");
217        break;
218
219      case CONTENT:
220        utf8_string = net::EscapeForHTML(text);
221        break;
222
223      default:
224        NOTREACHED();
225    }
226
227    return Write(utf8_string);
228  }
229
230  // Indents the current line.
231  bool WriteIndent() {
232    return Write(indent_);
233  }
234
235  // Converts a time string written to the JSON codec into a time_t string
236  // (used by bookmarks.html) and writes it.
237  bool WriteTime(const std::string& time_string) {
238    int64 internal_value;
239    base::StringToInt64(time_string, &internal_value);
240    return Write(base::Int64ToString(
241        base::Time::FromInternalValue(internal_value).ToTimeT()));
242  }
243
244  // Writes the node and all its children, returning true on success.
245  bool WriteNode(const DictionaryValue& value,
246                BookmarkNode::Type folder_type) {
247    std::string title, date_added_string, type_string;
248    if (!value.GetString(BookmarkCodec::kNameKey, &title) ||
249        !value.GetString(BookmarkCodec::kDateAddedKey, &date_added_string) ||
250        !value.GetString(BookmarkCodec::kTypeKey, &type_string) ||
251        (type_string != BookmarkCodec::kTypeURL &&
252         type_string != BookmarkCodec::kTypeFolder))  {
253      NOTREACHED();
254      return false;
255    }
256
257    if (type_string == BookmarkCodec::kTypeURL) {
258      std::string url_string;
259      if (!value.GetString(BookmarkCodec::kURLKey, &url_string)) {
260        NOTREACHED();
261        return false;
262      }
263
264      std::string favicon_string;
265      BookmarkFaviconFetcher::URLFaviconMap::iterator itr =
266          favicons_map_->find(url_string);
267      if (itr != favicons_map_->end()) {
268        scoped_refptr<base::RefCountedMemory> data(itr->second.get());
269        std::string favicon_data;
270        favicon_data.assign(reinterpret_cast<const char*>(data->front()),
271                            data->size());
272        std::string favicon_base64_encoded;
273        if (base::Base64Encode(favicon_data, &favicon_base64_encoded)) {
274          GURL favicon_url("data:image/png;base64," + favicon_base64_encoded);
275          favicon_string = favicon_url.spec();
276        }
277      }
278
279      if (!WriteIndent() ||
280          !Write(kBookmarkStart) ||
281          !Write(url_string, ATTRIBUTE_VALUE) ||
282          !Write(kAddDate) ||
283          !WriteTime(date_added_string) ||
284          (!favicon_string.empty() &&
285              (!Write(kIcon) ||
286               !Write(favicon_string, ATTRIBUTE_VALUE))) ||
287          !Write(kBookmarkAttributeEnd) ||
288          !Write(title, CONTENT) ||
289          !Write(kBookmarkEnd) ||
290          !Write(kNewline)) {
291        return false;
292      }
293      return true;
294    }
295
296    // Folder.
297    std::string last_modified_date;
298    const Value* child_values = NULL;
299    if (!value.GetString(BookmarkCodec::kDateModifiedKey,
300                         &last_modified_date) ||
301        !value.Get(BookmarkCodec::kChildrenKey, &child_values) ||
302        child_values->GetType() != Value::TYPE_LIST) {
303      NOTREACHED();
304      return false;
305    }
306    if (folder_type != BookmarkNode::OTHER_NODE &&
307        folder_type != BookmarkNode::MOBILE) {
308      // The other/mobile folder name are not written out. This gives the effect
309      // of making the contents of the 'other folder' be a sibling to the
310      // bookmark bar folder.
311      if (!WriteIndent() ||
312          !Write(kFolderStart) ||
313          !WriteTime(date_added_string) ||
314          !Write(kLastModified) ||
315          !WriteTime(last_modified_date)) {
316        return false;
317      }
318      if (folder_type == BookmarkNode::BOOKMARK_BAR) {
319        if (!Write(kBookmarkBar))
320          return false;
321        title = l10n_util::GetStringUTF8(IDS_BOOKMARK_BAR_FOLDER_NAME);
322      } else if (!Write(kFolderAttributeEnd)) {
323        return false;
324      }
325      if (!Write(title, CONTENT) ||
326          !Write(kFolderEnd) ||
327          !Write(kNewline) ||
328          !WriteIndent() ||
329          !Write(kFolderChildren) ||
330          !Write(kNewline)) {
331        return false;
332      }
333      IncrementIndent();
334    }
335
336    // Write the children.
337    const ListValue* children = static_cast<const ListValue*>(child_values);
338    for (size_t i = 0; i < children->GetSize(); ++i) {
339      const Value* child_value;
340      if (!children->Get(i, &child_value) ||
341          child_value->GetType() != Value::TYPE_DICTIONARY) {
342        NOTREACHED();
343        return false;
344      }
345      if (!WriteNode(*static_cast<const DictionaryValue*>(child_value),
346                     BookmarkNode::FOLDER)) {
347        return false;
348      }
349    }
350    if (folder_type != BookmarkNode::OTHER_NODE &&
351        folder_type != BookmarkNode::MOBILE) {
352      // Close out the folder.
353      DecrementIndent();
354      if (!WriteIndent() ||
355          !Write(kFolderChildrenEnd) ||
356          !Write(kNewline)) {
357        return false;
358      }
359    }
360    return true;
361  }
362
363  // The BookmarkModel as a Value. This value was generated from the
364  // BookmarkCodec.
365  scoped_ptr<Value> bookmarks_;
366
367  // Path we're writing to.
368  base::FilePath path_;
369
370  // Map that stores favicon per URL.
371  scoped_ptr<BookmarkFaviconFetcher::URLFaviconMap> favicons_map_;
372
373  // Observer to be notified on finish.
374  BookmarksExportObserver* observer_;
375
376  // File we're writing to.
377  scoped_ptr<net::FileStream> file_stream_;
378
379  // How much we indent when writing a bookmark/folder. This is modified
380  // via IncrementIndent and DecrementIndent.
381  std::string indent_;
382
383  DISALLOW_COPY_AND_ASSIGN(Writer);
384};
385
386}  // namespace
387
388BookmarkFaviconFetcher::BookmarkFaviconFetcher(
389    Profile* profile,
390    const base::FilePath& path,
391    BookmarksExportObserver* observer)
392    : profile_(profile),
393      path_(path),
394      observer_(observer) {
395  favicons_map_.reset(new URLFaviconMap());
396  registrar_.Add(this,
397                 chrome::NOTIFICATION_PROFILE_DESTROYED,
398                 content::Source<Profile>(profile_));
399}
400
401BookmarkFaviconFetcher::~BookmarkFaviconFetcher() {
402}
403
404void BookmarkFaviconFetcher::ExportBookmarks() {
405  ExtractUrls(BookmarkModelFactory::GetForProfile(
406      profile_)->bookmark_bar_node());
407  ExtractUrls(BookmarkModelFactory::GetForProfile(profile_)->other_node());
408  ExtractUrls(BookmarkModelFactory::GetForProfile(profile_)->mobile_node());
409  if (!bookmark_urls_.empty())
410    FetchNextFavicon();
411  else
412    ExecuteWriter();
413}
414
415void BookmarkFaviconFetcher::Observe(
416    int type,
417    const content::NotificationSource& source,
418    const content::NotificationDetails& details) {
419  if (chrome::NOTIFICATION_PROFILE_DESTROYED == type && fetcher != NULL) {
420    MessageLoop::current()->DeleteSoon(FROM_HERE, fetcher);
421    fetcher = NULL;
422  }
423}
424
425void BookmarkFaviconFetcher::ExtractUrls(const BookmarkNode* node) {
426  if (node->is_url()) {
427    std::string url = node->url().spec();
428    if (!url.empty())
429      bookmark_urls_.push_back(url);
430  } else {
431    for (int i = 0; i < node->child_count(); ++i)
432      ExtractUrls(node->GetChild(i));
433  }
434}
435
436void BookmarkFaviconFetcher::ExecuteWriter() {
437  // BookmarkModel isn't thread safe (nor would we want to lock it down
438  // for the duration of the write), as such we make a copy of the
439  // BookmarkModel using BookmarkCodec then write from that.
440  BookmarkCodec codec;
441  BrowserThread::PostTask(
442      BrowserThread::FILE, FROM_HERE,
443      base::Bind(&Writer::DoWrite,
444                 new Writer(codec.Encode(BookmarkModelFactory::GetForProfile(
445                                profile_)),
446                            path_, favicons_map_.release(), observer_)));
447  if (fetcher != NULL) {
448    MessageLoop::current()->DeleteSoon(FROM_HERE, fetcher);
449    fetcher = NULL;
450  }
451}
452
453bool BookmarkFaviconFetcher::FetchNextFavicon() {
454  if (bookmark_urls_.empty()) {
455    return false;
456  }
457  do {
458    std::string url = bookmark_urls_.front();
459    // Filter out urls that we've already got favicon for.
460    URLFaviconMap::const_iterator iter = favicons_map_->find(url);
461    if (favicons_map_->end() == iter) {
462      FaviconService* favicon_service = FaviconServiceFactory::GetForProfile(
463          profile_, Profile::EXPLICIT_ACCESS);
464      favicon_service->GetRawFaviconForURL(
465          FaviconService::FaviconForURLParams(
466              profile_, GURL(url), history::FAVICON, gfx::kFaviconSize),
467          ui::SCALE_FACTOR_100P,
468          base::Bind(&BookmarkFaviconFetcher::OnFaviconDataAvailable,
469                     base::Unretained(this)),
470          &cancelable_task_tracker_);
471      return true;
472    } else {
473      bookmark_urls_.pop_front();
474    }
475  } while (!bookmark_urls_.empty());
476  return false;
477}
478
479void BookmarkFaviconFetcher::OnFaviconDataAvailable(
480    const history::FaviconBitmapResult& bitmap_result) {
481  GURL url;
482  if (!bookmark_urls_.empty()) {
483    url = GURL(bookmark_urls_.front());
484    bookmark_urls_.pop_front();
485  }
486  if (bitmap_result.is_valid() && !url.is_empty()) {
487    favicons_map_->insert(
488        make_pair(url.spec(), bitmap_result.bitmap_data));
489  }
490
491  if (FetchNextFavicon()) {
492    return;
493  }
494  ExecuteWriter();
495}
496
497namespace bookmark_html_writer {
498
499void WriteBookmarks(Profile* profile,
500                    const base::FilePath& path,
501                    BookmarksExportObserver* observer) {
502  // BookmarkModel isn't thread safe (nor would we want to lock it down
503  // for the duration of the write), as such we make a copy of the
504  // BookmarkModel using BookmarkCodec then write from that.
505  if (fetcher == NULL) {
506    fetcher = new BookmarkFaviconFetcher(profile, path, observer);
507    fetcher->ExportBookmarks();
508  }
509}
510
511}  // namespace bookmark_html_writer
512