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