firefox2_importer.cc revision ddb351dbec246cf1fab5ec20d2d5520909041de1
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/importer/firefox2_importer.h" 6 7#include <string> 8#include <vector> 9 10#include "base/file_path.h" 11#include "base/file_util.h" 12#include "base/i18n/icu_string_conversions.h" 13#include "base/message_loop.h" 14#include "base/path_service.h" 15#include "base/stl_util-inl.h" 16#include "base/string_number_conversions.h" 17#include "base/string_split.h" 18#include "base/string_util.h" 19#include "base/utf_string_conversions.h" 20#include "chrome/browser/history/history_types.h" 21#include "chrome/browser/importer/firefox_importer_utils.h" 22#include "chrome/browser/importer/importer_bridge.h" 23#include "chrome/browser/importer/mork_reader.h" 24#include "chrome/browser/importer/nss_decryptor.h" 25#include "chrome/browser/search_engines/template_url.h" 26#include "chrome/browser/search_engines/template_url_parser.h" 27#include "chrome/common/time_format.h" 28#include "chrome/common/url_constants.h" 29#include "googleurl/src/gurl.h" 30#include "grit/generated_resources.h" 31#include "net/base/data_url.h" 32#include "webkit/glue/password_form.h" 33 34namespace { 35const char kItemOpen[] = "<DT><A"; 36const char kItemClose[] = "</A>"; 37const char kFeedURLAttribute[] = "FEEDURL"; 38const char kHrefAttribute[] = "HREF"; 39const char kIconAttribute[] = "ICON"; 40const char kShortcutURLAttribute[] = "SHORTCUTURL"; 41const char kAddDateAttribute[] = "ADD_DATE"; 42const char kPostDataAttribute[] = "POST_DATA"; 43} 44 45Firefox2Importer::Firefox2Importer() : parsing_bookmarks_html_file_(false) {} 46 47Firefox2Importer::~Firefox2Importer() {} 48 49void Firefox2Importer::StartImport( 50 const importer::SourceProfile& source_profile, 51 uint16 items, 52 ImporterBridge* bridge) { 53 bridge_ = bridge; 54 source_path_ = source_profile.source_path; 55 app_path_ = source_profile.app_path; 56 57 parsing_bookmarks_html_file_ = 58 (source_profile.importer_type == importer::BOOKMARKS_HTML); 59 60 // The order here is important! 61 bridge_->NotifyStarted(); 62 if ((items & importer::HOME_PAGE) && !cancelled()) 63 ImportHomepage(); // Doesn't have a UI item. 64 65 // Note history should be imported before bookmarks because bookmark import 66 // will also import favicons and we store favicon for a URL only if the URL 67 // exist in history or bookmarks. 68 if ((items & importer::HISTORY) && !cancelled()) { 69 bridge_->NotifyItemStarted(importer::HISTORY); 70 ImportHistory(); 71 bridge_->NotifyItemEnded(importer::HISTORY); 72 } 73 74 if ((items & importer::FAVORITES) && !cancelled()) { 75 bridge_->NotifyItemStarted(importer::FAVORITES); 76 ImportBookmarks(); 77 bridge_->NotifyItemEnded(importer::FAVORITES); 78 } 79 if ((items & importer::SEARCH_ENGINES) && !cancelled()) { 80 bridge_->NotifyItemStarted(importer::SEARCH_ENGINES); 81 ImportSearchEngines(); 82 bridge_->NotifyItemEnded(importer::SEARCH_ENGINES); 83 } 84 if ((items & importer::PASSWORDS) && !cancelled()) { 85 bridge_->NotifyItemStarted(importer::PASSWORDS); 86 ImportPasswords(); 87 bridge_->NotifyItemEnded(importer::PASSWORDS); 88 } 89 bridge_->NotifyEnded(); 90} 91 92// static 93void Firefox2Importer::LoadDefaultBookmarks(const FilePath& app_path, 94 std::set<GURL> *urls) { 95 FilePath file = app_path.AppendASCII("defaults") 96 .AppendASCII("profile") 97 .AppendASCII("bookmarks.html"); 98 99 urls->clear(); 100 101 // Read the whole file. 102 std::string content; 103 file_util::ReadFileToString(file, &content); 104 std::vector<std::string> lines; 105 base::SplitString(content, '\n', &lines); 106 107 std::string charset; 108 for (size_t i = 0; i < lines.size(); ++i) { 109 std::string line; 110 TrimString(lines[i], " ", &line); 111 112 // Get the encoding of the bookmark file. 113 if (ParseCharsetFromLine(line, &charset)) 114 continue; 115 116 // Get the bookmark. 117 string16 title; 118 GURL url, favicon; 119 string16 shortcut; 120 base::Time add_date; 121 string16 post_data; 122 if (ParseBookmarkFromLine(line, charset, &title, &url, 123 &favicon, &shortcut, &add_date, 124 &post_data)) 125 urls->insert(url); 126 } 127} 128 129// static 130TemplateURL* Firefox2Importer::CreateTemplateURL(const string16& title, 131 const string16& keyword, 132 const GURL& url) { 133 // Skip if the keyword or url is invalid. 134 if (keyword.empty() && url.is_valid()) 135 return NULL; 136 137 TemplateURL* t_url = new TemplateURL(); 138 // We set short name by using the title if it exists. 139 // Otherwise, we use the shortcut. 140 t_url->set_short_name(!title.empty() ? title : keyword); 141 t_url->set_keyword(keyword); 142 t_url->SetURL(TemplateURLRef::DisplayURLToURLRef(UTF8ToUTF16(url.spec())), 143 0, 0); 144 return t_url; 145} 146 147// static 148void Firefox2Importer::ImportBookmarksFile( 149 const FilePath& file_path, 150 const std::set<GURL>& default_urls, 151 bool import_to_bookmark_bar, 152 const string16& first_folder_name, 153 Importer* importer, 154 std::vector<ProfileWriter::BookmarkEntry>* bookmarks, 155 std::vector<TemplateURL*>* template_urls, 156 std::vector<history::ImportedFaviconUsage>* favicons) { 157 std::string content; 158 file_util::ReadFileToString(file_path, &content); 159 std::vector<std::string> lines; 160 base::SplitString(content, '\n', &lines); 161 162 std::vector<ProfileWriter::BookmarkEntry> toolbar_bookmarks; 163 string16 last_folder = first_folder_name; 164 bool last_folder_on_toolbar = false; 165 bool last_folder_is_empty = true; 166 base::Time last_folder_add_date; 167 std::vector<string16> path; 168 size_t toolbar_folder = 0; 169 std::string charset; 170 for (size_t i = 0; i < lines.size() && (!importer || !importer->cancelled()); 171 ++i) { 172 std::string line; 173 TrimString(lines[i], " ", &line); 174 175 // Get the encoding of the bookmark file. 176 if (ParseCharsetFromLine(line, &charset)) 177 continue; 178 179 // Get the folder name. 180 if (ParseFolderNameFromLine(line, charset, &last_folder, 181 &last_folder_on_toolbar, 182 &last_folder_add_date)) 183 continue; 184 185 // Get the bookmark entry. 186 string16 title; 187 string16 shortcut; 188 GURL url, favicon; 189 base::Time add_date; 190 string16 post_data; 191 bool is_bookmark; 192 // TODO(jcampan): http://b/issue?id=1196285 we do not support POST based 193 // keywords yet. 194 is_bookmark = ParseBookmarkFromLine(line, charset, &title, 195 &url, &favicon, &shortcut, &add_date, 196 &post_data) || 197 ParseMinimumBookmarkFromLine(line, charset, &title, &url); 198 199 if (is_bookmark) 200 last_folder_is_empty = false; 201 202 if (is_bookmark && 203 post_data.empty() && 204 CanImportURL(GURL(url)) && 205 default_urls.find(url) == default_urls.end()) { 206 if (toolbar_folder > path.size() && !path.empty()) { 207 NOTREACHED(); // error in parsing. 208 break; 209 } 210 211 ProfileWriter::BookmarkEntry entry; 212 entry.creation_time = add_date; 213 entry.url = url; 214 entry.title = title; 215 216 if (import_to_bookmark_bar && toolbar_folder) { 217 // Flatten the items in toolbar. 218 entry.in_toolbar = true; 219 entry.path.assign(path.begin() + toolbar_folder, path.end()); 220 toolbar_bookmarks.push_back(entry); 221 } else { 222 // Insert the item into the "Imported from Firefox" folder. 223 entry.path.assign(path.begin(), path.end()); 224 if (import_to_bookmark_bar) 225 entry.path.erase(entry.path.begin()); 226 bookmarks->push_back(entry); 227 } 228 229 // Save the favicon. DataURLToFaviconUsage will handle the case where 230 // there is no favicon. 231 if (favicons) 232 DataURLToFaviconUsage(url, favicon, favicons); 233 234 if (template_urls) { 235 // If there is a SHORTCUT attribute for this bookmark, we 236 // add it as our keywords. 237 TemplateURL* t_url = CreateTemplateURL(title, shortcut, url); 238 if (t_url) 239 template_urls->push_back(t_url); 240 } 241 242 continue; 243 } 244 245 // Bookmarks in sub-folder are encapsulated with <DL> tag. 246 if (StartsWithASCII(line, "<DL>", false)) { 247 path.push_back(last_folder); 248 last_folder.clear(); 249 if (last_folder_on_toolbar && !toolbar_folder) 250 toolbar_folder = path.size(); 251 252 // Mark next folder empty as initial state. 253 last_folder_is_empty = true; 254 } else if (StartsWithASCII(line, "</DL>", false)) { 255 if (path.empty()) 256 break; // Mismatch <DL>. 257 258 string16 folder_title = path.back(); 259 path.pop_back(); 260 261 if (last_folder_is_empty) { 262 // Empty folder should be added explicitly. 263 ProfileWriter::BookmarkEntry entry; 264 entry.is_folder = true; 265 entry.creation_time = last_folder_add_date; 266 entry.title = folder_title; 267 if (import_to_bookmark_bar && toolbar_folder) { 268 // Flatten the folder in toolbar. 269 entry.in_toolbar = true; 270 entry.path.assign(path.begin() + toolbar_folder, path.end()); 271 toolbar_bookmarks.push_back(entry); 272 } else { 273 // Insert the folder into the "Imported from Firefox" folder. 274 entry.path.assign(path.begin(), path.end()); 275 if (import_to_bookmark_bar) 276 entry.path.erase(entry.path.begin()); 277 bookmarks->push_back(entry); 278 } 279 280 // Parent folder include current one, so it's not empty. 281 last_folder_is_empty = false; 282 } 283 284 if (toolbar_folder > path.size()) 285 toolbar_folder = 0; 286 } 287 } 288 289 bookmarks->insert(bookmarks->begin(), toolbar_bookmarks.begin(), 290 toolbar_bookmarks.end()); 291} 292 293void Firefox2Importer::ImportBookmarks() { 294 // Load the default bookmarks. 295 std::set<GURL> default_urls; 296 if (!parsing_bookmarks_html_file_) 297 LoadDefaultBookmarks(app_path_, &default_urls); 298 299 // Parse the bookmarks.html file. 300 std::vector<ProfileWriter::BookmarkEntry> bookmarks, toolbar_bookmarks; 301 std::vector<TemplateURL*> template_urls; 302 std::vector<history::ImportedFaviconUsage> favicons; 303 FilePath file = source_path_; 304 if (!parsing_bookmarks_html_file_) 305 file = file.AppendASCII("bookmarks.html"); 306 string16 first_folder_name = bridge_->GetLocalizedString( 307 parsing_bookmarks_html_file_ ? IDS_BOOKMARK_GROUP : 308 IDS_BOOKMARK_GROUP_FROM_FIREFOX); 309 310 ImportBookmarksFile(file, default_urls, import_to_bookmark_bar(), 311 first_folder_name, this, &bookmarks, &template_urls, 312 &favicons); 313 314 // Write data into profile. 315 if (!bookmarks.empty() && !cancelled()) { 316 int options = 0; 317 if (import_to_bookmark_bar()) 318 options |= ProfileWriter::IMPORT_TO_BOOKMARK_BAR; 319 if (bookmark_bar_disabled()) 320 options |= ProfileWriter::BOOKMARK_BAR_DISABLED; 321 bridge_->AddBookmarkEntries(bookmarks, first_folder_name, options); 322 } 323 if (!parsing_bookmarks_html_file_ && !template_urls.empty() && 324 !cancelled()) { 325 bridge_->SetKeywords(template_urls, -1, false); 326 } else { 327 STLDeleteContainerPointers(template_urls.begin(), template_urls.end()); 328 } 329 if (!favicons.empty()) { 330 bridge_->SetFavicons(favicons); 331 } 332} 333 334void Firefox2Importer::ImportPasswords() { 335 // Initializes NSS3. 336 NSSDecryptor decryptor; 337 if (!decryptor.Init(source_path_, source_path_) && 338 !decryptor.Init(app_path_, source_path_)) { 339 return; 340 } 341 342 // Firefox 2 uses signons2.txt to store the pssswords. If it doesn't 343 // exist, we try to find its older version. 344 FilePath file = source_path_.AppendASCII("signons2.txt"); 345 if (!file_util::PathExists(file)) { 346 file = source_path_.AppendASCII("signons.txt"); 347 } 348 349 std::string content; 350 file_util::ReadFileToString(file, &content); 351 std::vector<webkit_glue::PasswordForm> forms; 352 decryptor.ParseSignons(content, &forms); 353 354 if (!cancelled()) { 355 for (size_t i = 0; i < forms.size(); ++i) { 356 bridge_->SetPasswordForm(forms[i]); 357 } 358 } 359} 360 361void Firefox2Importer::ImportHistory() { 362 FilePath file = source_path_.AppendASCII("history.dat"); 363 ImportHistoryFromFirefox2(file, bridge_); 364} 365 366void Firefox2Importer::ImportSearchEngines() { 367 std::vector<FilePath> files; 368 GetSearchEnginesXMLFiles(&files); 369 370 std::vector<TemplateURL*> search_engines; 371 ParseSearchEnginesFromXMLFiles(files, &search_engines); 372 373 int default_index = 374 GetFirefoxDefaultSearchEngineIndex(search_engines, source_path_); 375 bridge_->SetKeywords(search_engines, default_index, true); 376} 377 378void Firefox2Importer::ImportHomepage() { 379 GURL home_page = GetHomepage(source_path_); 380 if (home_page.is_valid() && !IsDefaultHomepage(home_page, app_path_)) { 381 bridge_->AddHomePage(home_page); 382 } 383} 384 385void Firefox2Importer::GetSearchEnginesXMLFiles( 386 std::vector<FilePath>* files) { 387 // Search engines are contained in XML files in a searchplugins directory that 388 // can be found in 2 locations: 389 // - Firefox install dir (default search engines) 390 // - the profile dir (user added search engines) 391 FilePath dir = app_path_.AppendASCII("searchplugins"); 392 FindXMLFilesInDir(dir, files); 393 394 FilePath profile_dir = source_path_.AppendASCII("searchplugins"); 395 FindXMLFilesInDir(profile_dir, files); 396} 397 398// static 399bool Firefox2Importer::ParseCharsetFromLine(const std::string& line, 400 std::string* charset) { 401 const char kCharset[] = "charset="; 402 if (StartsWithASCII(line, "<META", false) && 403 (line.find("CONTENT=\"") != std::string::npos || 404 line.find("content=\"") != std::string::npos)) { 405 size_t begin = line.find(kCharset); 406 if (begin == std::string::npos) 407 return false; 408 begin += std::string(kCharset).size(); 409 size_t end = line.find_first_of('\"', begin); 410 *charset = line.substr(begin, end - begin); 411 return true; 412 } 413 return false; 414} 415 416// static 417bool Firefox2Importer::ParseFolderNameFromLine(const std::string& line, 418 const std::string& charset, 419 string16* folder_name, 420 bool* is_toolbar_folder, 421 base::Time* add_date) { 422 const char kFolderOpen[] = "<DT><H3"; 423 const char kFolderClose[] = "</H3>"; 424 const char kToolbarFolderAttribute[] = "PERSONAL_TOOLBAR_FOLDER"; 425 const char kAddDateAttribute[] = "ADD_DATE"; 426 427 if (!StartsWithASCII(line, kFolderOpen, true)) 428 return false; 429 430 size_t end = line.find(kFolderClose); 431 size_t tag_end = line.rfind('>', end) + 1; 432 // If no end tag or start tag is broken, we skip to find the folder name. 433 if (end == std::string::npos || tag_end < arraysize(kFolderOpen)) 434 return false; 435 436 base::CodepageToUTF16(line.substr(tag_end, end - tag_end), charset.c_str(), 437 base::OnStringConversionError::SKIP, folder_name); 438 HTMLUnescape(folder_name); 439 440 std::string attribute_list = line.substr(arraysize(kFolderOpen), 441 tag_end - arraysize(kFolderOpen) - 1); 442 std::string value; 443 444 // Add date 445 if (GetAttribute(attribute_list, kAddDateAttribute, &value)) { 446 int64 time; 447 base::StringToInt64(value, &time); 448 // Upper bound it at 32 bits. 449 if (0 < time && time < (1LL << 32)) 450 *add_date = base::Time::FromTimeT(time); 451 } 452 453 if (GetAttribute(attribute_list, kToolbarFolderAttribute, &value) && 454 LowerCaseEqualsASCII(value, "true")) 455 *is_toolbar_folder = true; 456 else 457 *is_toolbar_folder = false; 458 459 return true; 460} 461 462// static 463bool Firefox2Importer::ParseBookmarkFromLine(const std::string& line, 464 const std::string& charset, 465 string16* title, 466 GURL* url, 467 GURL* favicon, 468 string16* shortcut, 469 base::Time* add_date, 470 string16* post_data) { 471 title->clear(); 472 *url = GURL(); 473 *favicon = GURL(); 474 shortcut->clear(); 475 post_data->clear(); 476 *add_date = base::Time(); 477 478 if (!StartsWithASCII(line, kItemOpen, true)) 479 return false; 480 481 size_t end = line.find(kItemClose); 482 size_t tag_end = line.rfind('>', end) + 1; 483 if (end == std::string::npos || tag_end < arraysize(kItemOpen)) 484 return false; // No end tag or start tag is broken. 485 486 std::string attribute_list = line.substr(arraysize(kItemOpen), 487 tag_end - arraysize(kItemOpen) - 1); 488 489 // We don't import Live Bookmark folders, which is Firefox's RSS reading 490 // feature, since the user never necessarily bookmarked them and we don't 491 // have this feature to update their contents. 492 std::string value; 493 if (GetAttribute(attribute_list, kFeedURLAttribute, &value)) 494 return false; 495 496 // Title 497 base::CodepageToUTF16(line.substr(tag_end, end - tag_end), charset.c_str(), 498 base::OnStringConversionError::SKIP, title); 499 HTMLUnescape(title); 500 501 // URL 502 if (GetAttribute(attribute_list, kHrefAttribute, &value)) { 503 string16 url16; 504 base::CodepageToUTF16(value, charset.c_str(), 505 base::OnStringConversionError::SKIP, &url16); 506 HTMLUnescape(&url16); 507 508 *url = GURL(url16); 509 } 510 511 // Favicon 512 if (GetAttribute(attribute_list, kIconAttribute, &value)) 513 *favicon = GURL(value); 514 515 // Keyword 516 if (GetAttribute(attribute_list, kShortcutURLAttribute, &value)) { 517 base::CodepageToUTF16(value, charset.c_str(), 518 base::OnStringConversionError::SKIP, shortcut); 519 HTMLUnescape(shortcut); 520 } 521 522 // Add date 523 if (GetAttribute(attribute_list, kAddDateAttribute, &value)) { 524 int64 time; 525 base::StringToInt64(value, &time); 526 // Upper bound it at 32 bits. 527 if (0 < time && time < (1LL << 32)) 528 *add_date = base::Time::FromTimeT(time); 529 } 530 531 // Post data. 532 if (GetAttribute(attribute_list, kPostDataAttribute, &value)) { 533 base::CodepageToUTF16(value, charset.c_str(), 534 base::OnStringConversionError::SKIP, post_data); 535 HTMLUnescape(post_data); 536 } 537 538 return true; 539} 540 541// static 542bool Firefox2Importer::ParseMinimumBookmarkFromLine(const std::string& line, 543 const std::string& charset, 544 string16* title, 545 GURL* url) { 546 const char kItemOpen[] = "<DT><A"; 547 const char kItemClose[] = "</"; 548 const char kHrefAttributeUpper[] = "HREF"; 549 const char kHrefAttributeLower[] = "href"; 550 551 title->clear(); 552 *url = GURL(); 553 554 // Case-insensitive check of open tag. 555 if (!StartsWithASCII(line, kItemOpen, false)) 556 return false; 557 558 // Find any close tag. 559 size_t end = line.find(kItemClose); 560 size_t tag_end = line.rfind('>', end) + 1; 561 if (end == std::string::npos || tag_end < arraysize(kItemOpen)) 562 return false; // No end tag or start tag is broken. 563 564 std::string attribute_list = line.substr(arraysize(kItemOpen), 565 tag_end - arraysize(kItemOpen) - 1); 566 567 // Title 568 base::CodepageToUTF16(line.substr(tag_end, end - tag_end), charset.c_str(), 569 base::OnStringConversionError::SKIP, title); 570 HTMLUnescape(title); 571 572 // URL 573 std::string value; 574 if (GetAttribute(attribute_list, kHrefAttributeUpper, &value) || 575 GetAttribute(attribute_list, kHrefAttributeLower, &value)) { 576 if (charset.length() != 0) { 577 string16 url16; 578 base::CodepageToUTF16(value, charset.c_str(), 579 base::OnStringConversionError::SKIP, &url16); 580 HTMLUnescape(&url16); 581 582 *url = GURL(url16); 583 } else { 584 *url = GURL(value); 585 } 586 } 587 588 return true; 589} 590 591// static 592bool Firefox2Importer::GetAttribute(const std::string& attribute_list, 593 const std::string& attribute, 594 std::string* value) { 595 const char kQuote[] = "\""; 596 597 size_t begin = attribute_list.find(attribute + "=" + kQuote); 598 if (begin == std::string::npos) 599 return false; // Can't find the attribute. 600 601 begin = attribute_list.find(kQuote, begin) + 1; 602 603 size_t end = begin + 1; 604 while (end < attribute_list.size()) { 605 if (attribute_list[end] == '"' && 606 attribute_list[end - 1] != '\\') { 607 break; 608 } 609 end++; 610 } 611 612 if (end == attribute_list.size()) 613 return false; // The value is not quoted. 614 615 *value = attribute_list.substr(begin, end - begin); 616 return true; 617} 618 619// static 620void Firefox2Importer::HTMLUnescape(string16* text) { 621 string16 text16 = *text; 622 ReplaceSubstringsAfterOffset( 623 &text16, 0, ASCIIToUTF16("<"), ASCIIToUTF16("<")); 624 ReplaceSubstringsAfterOffset( 625 &text16, 0, ASCIIToUTF16(">"), ASCIIToUTF16(">")); 626 ReplaceSubstringsAfterOffset( 627 &text16, 0, ASCIIToUTF16("&"), ASCIIToUTF16("&")); 628 ReplaceSubstringsAfterOffset( 629 &text16, 0, ASCIIToUTF16("""), ASCIIToUTF16("\"")); 630 ReplaceSubstringsAfterOffset( 631 &text16, 0, ASCIIToUTF16("'"), ASCIIToUTF16("\'")); 632 text->assign(text16); 633} 634 635// static 636void Firefox2Importer::FindXMLFilesInDir( 637 const FilePath& dir, 638 std::vector<FilePath>* xml_files) { 639 file_util::FileEnumerator file_enum(dir, false, 640 file_util::FileEnumerator::FILES, 641 FILE_PATH_LITERAL("*.xml")); 642 FilePath file(file_enum.Next()); 643 while (!file.empty()) { 644 xml_files->push_back(file); 645 file = file_enum.Next(); 646 } 647} 648 649// static 650void Firefox2Importer::DataURLToFaviconUsage( 651 const GURL& link_url, 652 const GURL& favicon_data, 653 std::vector<history::ImportedFaviconUsage>* favicons) { 654 if (!link_url.is_valid() || !favicon_data.is_valid() || 655 !favicon_data.SchemeIs(chrome::kDataScheme)) 656 return; 657 658 // Parse the data URL. 659 std::string mime_type, char_set, data; 660 if (!net::DataURL::Parse(favicon_data, &mime_type, &char_set, &data) || 661 data.empty()) 662 return; 663 664 history::ImportedFaviconUsage usage; 665 if (!ReencodeFavicon(reinterpret_cast<const unsigned char*>(&data[0]), 666 data.size(), &usage.png_data)) 667 return; // Unable to decode. 668 669 // We need to make up a URL for the favicon. We use a version of the page's 670 // URL so that we can be sure it will not collide. 671 usage.favicon_url = GURL(std::string("made-up-favicon:") + link_url.spec()); 672 673 // We only have one URL per favicon for Firefox 2 bookmarks. 674 usage.urls.insert(link_url); 675 676 favicons->push_back(usage); 677} 678