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