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/strings/string_util.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/browser/bookmark_codec.h"
23#include "components/bookmarks/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/components_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 bookmarks::BookmarkCodec;
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 close is forced so that unit test could read it.
149    file_.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    int flags = base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE;
173    file_.reset(new base::File(path_, flags));
174    return file_->IsValid();
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    if (!text.length())
199      return true;
200    size_t wrote = file_->WriteAtCurrentPos(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(base::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 base::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_base64_encoded;
270        base::Base64Encode(std::string(data->front_as<char>(), data->size()),
271                           &favicon_base64_encoded);
272        GURL favicon_url("data:image/png;base64," + favicon_base64_encoded);
273        favicon_string = favicon_url.spec();
274      }
275
276      if (!WriteIndent() ||
277          !Write(kBookmarkStart) ||
278          !Write(url_string, ATTRIBUTE_VALUE) ||
279          !Write(kAddDate) ||
280          !WriteTime(date_added_string) ||
281          (!favicon_string.empty() &&
282              (!Write(kIcon) ||
283               !Write(favicon_string, ATTRIBUTE_VALUE))) ||
284          !Write(kBookmarkAttributeEnd) ||
285          !Write(title, CONTENT) ||
286          !Write(kBookmarkEnd) ||
287          !Write(kNewline)) {
288        return false;
289      }
290      return true;
291    }
292
293    // Folder.
294    std::string last_modified_date;
295    const base::Value* child_values = NULL;
296    if (!value.GetString(BookmarkCodec::kDateModifiedKey,
297                         &last_modified_date) ||
298        !value.Get(BookmarkCodec::kChildrenKey, &child_values) ||
299        child_values->GetType() != base::Value::TYPE_LIST) {
300      NOTREACHED();
301      return false;
302    }
303    if (folder_type != BookmarkNode::OTHER_NODE &&
304        folder_type != BookmarkNode::MOBILE) {
305      // The other/mobile folder name are not written out. This gives the effect
306      // of making the contents of the 'other folder' be a sibling to the
307      // bookmark bar folder.
308      if (!WriteIndent() ||
309          !Write(kFolderStart) ||
310          !WriteTime(date_added_string) ||
311          !Write(kLastModified) ||
312          !WriteTime(last_modified_date)) {
313        return false;
314      }
315      if (folder_type == BookmarkNode::BOOKMARK_BAR) {
316        if (!Write(kBookmarkBar))
317          return false;
318        title = l10n_util::GetStringUTF8(IDS_BOOKMARK_BAR_FOLDER_NAME);
319      } else if (!Write(kFolderAttributeEnd)) {
320        return false;
321      }
322      if (!Write(title, CONTENT) ||
323          !Write(kFolderEnd) ||
324          !Write(kNewline) ||
325          !WriteIndent() ||
326          !Write(kFolderChildren) ||
327          !Write(kNewline)) {
328        return false;
329      }
330      IncrementIndent();
331    }
332
333    // Write the children.
334    const base::ListValue* children =
335        static_cast<const base::ListValue*>(child_values);
336    for (size_t i = 0; i < children->GetSize(); ++i) {
337      const base::Value* child_value;
338      if (!children->Get(i, &child_value) ||
339          child_value->GetType() != base::Value::TYPE_DICTIONARY) {
340        NOTREACHED();
341        return false;
342      }
343      if (!WriteNode(*static_cast<const base::DictionaryValue*>(child_value),
344                     BookmarkNode::FOLDER)) {
345        return false;
346      }
347    }
348    if (folder_type != BookmarkNode::OTHER_NODE &&
349        folder_type != BookmarkNode::MOBILE) {
350      // Close out the folder.
351      DecrementIndent();
352      if (!WriteIndent() ||
353          !Write(kFolderChildrenEnd) ||
354          !Write(kNewline)) {
355        return false;
356      }
357    }
358    return true;
359  }
360
361  // The BookmarkModel as a base::Value. This value was generated from the
362  // BookmarkCodec.
363  scoped_ptr<base::Value> bookmarks_;
364
365  // Path we're writing to.
366  base::FilePath path_;
367
368  // Map that stores favicon per URL.
369  scoped_ptr<BookmarkFaviconFetcher::URLFaviconMap> favicons_map_;
370
371  // Observer to be notified on finish.
372  BookmarksExportObserver* observer_;
373
374  // File we're writing to.
375  scoped_ptr<base::File> file_;
376
377  // How much we indent when writing a bookmark/folder. This is modified
378  // via IncrementIndent and DecrementIndent.
379  std::string indent_;
380
381  DISALLOW_COPY_AND_ASSIGN(Writer);
382};
383
384}  // namespace
385
386BookmarkFaviconFetcher::BookmarkFaviconFetcher(
387    Profile* profile,
388    const base::FilePath& path,
389    BookmarksExportObserver* observer)
390    : profile_(profile),
391      path_(path),
392      observer_(observer) {
393  favicons_map_.reset(new URLFaviconMap());
394  registrar_.Add(this,
395                 chrome::NOTIFICATION_PROFILE_DESTROYED,
396                 content::Source<Profile>(profile_));
397}
398
399BookmarkFaviconFetcher::~BookmarkFaviconFetcher() {
400}
401
402void BookmarkFaviconFetcher::ExportBookmarks() {
403  ExtractUrls(BookmarkModelFactory::GetForProfile(
404      profile_)->bookmark_bar_node());
405  ExtractUrls(BookmarkModelFactory::GetForProfile(profile_)->other_node());
406  ExtractUrls(BookmarkModelFactory::GetForProfile(profile_)->mobile_node());
407  if (!bookmark_urls_.empty())
408    FetchNextFavicon();
409  else
410    ExecuteWriter();
411}
412
413void BookmarkFaviconFetcher::Observe(
414    int type,
415    const content::NotificationSource& source,
416    const content::NotificationDetails& details) {
417  if (chrome::NOTIFICATION_PROFILE_DESTROYED == type && fetcher != NULL) {
418    base::MessageLoop::current()->DeleteSoon(FROM_HERE, fetcher);
419    fetcher = NULL;
420  }
421}
422
423void BookmarkFaviconFetcher::ExtractUrls(const BookmarkNode* node) {
424  if (node->is_url()) {
425    std::string url = node->url().spec();
426    if (!url.empty())
427      bookmark_urls_.push_back(url);
428  } else {
429    for (int i = 0; i < node->child_count(); ++i)
430      ExtractUrls(node->GetChild(i));
431  }
432}
433
434void BookmarkFaviconFetcher::ExecuteWriter() {
435  // BookmarkModel isn't thread safe (nor would we want to lock it down
436  // for the duration of the write), as such we make a copy of the
437  // BookmarkModel using BookmarkCodec then write from that.
438  BookmarkCodec codec;
439  BrowserThread::PostTask(
440      BrowserThread::FILE, FROM_HERE,
441      base::Bind(&Writer::DoWrite,
442                 new Writer(codec.Encode(BookmarkModelFactory::GetForProfile(
443                                profile_)),
444                            path_, favicons_map_.release(), observer_)));
445  if (fetcher != NULL) {
446    base::MessageLoop::current()->DeleteSoon(FROM_HERE, fetcher);
447    fetcher = NULL;
448  }
449}
450
451bool BookmarkFaviconFetcher::FetchNextFavicon() {
452  if (bookmark_urls_.empty()) {
453    return false;
454  }
455  do {
456    std::string url = bookmark_urls_.front();
457    // Filter out urls that we've already got favicon for.
458    URLFaviconMap::const_iterator iter = favicons_map_->find(url);
459    if (favicons_map_->end() == iter) {
460      FaviconService* favicon_service = FaviconServiceFactory::GetForProfile(
461          profile_, Profile::EXPLICIT_ACCESS);
462      favicon_service->GetRawFaviconForPageURL(
463          GURL(url),
464          favicon_base::FAVICON,
465          gfx::kFaviconSize,
466          base::Bind(&BookmarkFaviconFetcher::OnFaviconDataAvailable,
467                     base::Unretained(this)),
468          &cancelable_task_tracker_);
469      return true;
470    } else {
471      bookmark_urls_.pop_front();
472    }
473  } while (!bookmark_urls_.empty());
474  return false;
475}
476
477void BookmarkFaviconFetcher::OnFaviconDataAvailable(
478    const favicon_base::FaviconRawBitmapResult& bitmap_result) {
479  GURL url;
480  if (!bookmark_urls_.empty()) {
481    url = GURL(bookmark_urls_.front());
482    bookmark_urls_.pop_front();
483  }
484  if (bitmap_result.is_valid() && !url.is_empty()) {
485    favicons_map_->insert(
486        make_pair(url.spec(), bitmap_result.bitmap_data));
487  }
488
489  if (FetchNextFavicon()) {
490    return;
491  }
492  ExecuteWriter();
493}
494
495namespace bookmark_html_writer {
496
497void WriteBookmarks(Profile* profile,
498                    const base::FilePath& path,
499                    BookmarksExportObserver* observer) {
500  // BookmarkModel isn't thread safe (nor would we want to lock it down
501  // for the duration of the write), as such we make a copy of the
502  // BookmarkModel using BookmarkCodec then write from that.
503  if (fetcher == NULL) {
504    fetcher = new BookmarkFaviconFetcher(profile, path, observer);
505    fetcher->ExportBookmarks();
506  }
507}
508
509}  // namespace bookmark_html_writer
510