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 " 214 utf8_string = text; 215 ReplaceSubstringsAfterOffset(&utf8_string, 0, "\"", """); 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