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