Home | History | Annotate | Download | only in bookmarks
      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 
     32 using bookmarks::BookmarkCodec;
     33 using content::BrowserThread;
     34 
     35 namespace {
     36 
     37 static BookmarkFaviconFetcher* fetcher = NULL;
     38 
     39 // File header.
     40 const 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.
     52 const char kNewline[] = "\r\n";
     53 
     54 // The following are used for bookmarks.
     55 
     56 // Start of a bookmark.
     57 const char kBookmarkStart[] = "<DT><A HREF=\"";
     58 // After kBookmarkStart.
     59 const char kAddDate[] = "\" ADD_DATE=\"";
     60 // After kAddDate.
     61 const char kIcon[] = "\" ICON=\"";
     62 // After kIcon.
     63 const char kBookmarkAttributeEnd[] = "\">";
     64 // End of a bookmark.
     65 const char kBookmarkEnd[] = "</A>";
     66 
     67 // The following are used when writing folders.
     68 
     69 // Start of a folder.
     70 const char kFolderStart[] = "<DT><H3 ADD_DATE=\"";
     71 // After kFolderStart.
     72 const char kLastModified[] = "\" LAST_MODIFIED=\"";
     73 // After kLastModified when writing the bookmark bar.
     74 const char kBookmarkBar[] = "\" PERSONAL_TOOLBAR_FOLDER=\"true\">";
     75 // After kLastModified when writing a user created folder.
     76 const char kFolderAttributeEnd[] = "\">";
     77 // End of the folder.
     78 const char kFolderEnd[] = "</H3>";
     79 // Start of the children of a folder.
     80 const char kFolderChildren[] = "<DL><p>";
     81 // End of the children for a folder.
     82 const char kFolderChildrenEnd[] = "</DL><p>";
     83 
     84 // Number of characters to indent by.
     85 const size_t kIndentSize = 4;
     86 
     87 // Class responsible for the actual writing. Takes ownership of favicons_map.
     88 class 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 
    386 BookmarkFaviconFetcher::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 
    399 BookmarkFaviconFetcher::~BookmarkFaviconFetcher() {
    400 }
    401 
    402 void 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 
    413 void 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 
    423 void 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 
    434 void 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 
    451 bool 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 
    477 void 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 
    495 namespace bookmark_html_writer {
    496 
    497 void 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