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