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