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