blob: 5090e74ea8f5f1aef52426641a6b6403ab74d37a [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <Cocoa/Cocoa.h>
#include "chrome/utility/importer/safari_importer.h"
#include <map>
#include <vector>
#include "base/files/file_util.h"
#include "base/mac/foundation_util.h"
#include "base/strings/string16.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/common/importer/imported_bookmark_entry.h"
#include "chrome/common/importer/importer_bridge.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/utility/importer/favicon_reencode.h"
#include "components/strings/grit/components_strings.h"
#include "net/base/data_url.h"
#include "sql/statement.h"
#include "url/gurl.h"
namespace {
// A function like this is used by other importers in order to filter out
// URLS we don't want to import.
// For now it's pretty basic, but I've split it out so it's easy to slot
// in necessary logic for filtering URLS, should we need it.
bool CanImportSafariURL(const GURL& url) {
// The URL is not valid.
if (!url.is_valid())
return false;
return true;
}
} // namespace
SafariImporter::SafariImporter(const base::FilePath& library_dir)
: library_dir_(library_dir) {
}
SafariImporter::~SafariImporter() {
}
void SafariImporter::StartImport(const importer::SourceProfile& source_profile,
uint16_t items,
ImporterBridge* bridge) {
bridge_ = bridge;
// The order here is important!
bridge_->NotifyStarted();
// In keeping with import on other platforms (and for other browsers), we
// don't import the home page (since it may lead to a useless homepage); see
// crbug.com/25603.
if ((items & importer::HISTORY) && !cancelled()) {
bridge_->NotifyItemStarted(importer::HISTORY);
ImportHistory();
bridge_->NotifyItemEnded(importer::HISTORY);
}
if ((items & importer::FAVORITES) && !cancelled()) {
bridge_->NotifyItemStarted(importer::FAVORITES);
ImportBookmarks();
bridge_->NotifyItemEnded(importer::FAVORITES);
}
if ((items & importer::PASSWORDS) && !cancelled()) {
bridge_->NotifyItemStarted(importer::PASSWORDS);
ImportPasswords();
bridge_->NotifyItemEnded(importer::PASSWORDS);
}
bridge_->NotifyEnded();
}
void SafariImporter::ImportBookmarks() {
base::string16 toolbar_name =
bridge_->GetLocalizedString(IDS_BOOKMARK_BAR_FOLDER_NAME);
std::vector<ImportedBookmarkEntry> bookmarks;
ParseBookmarks(toolbar_name, &bookmarks);
// Write bookmarks into profile.
if (!bookmarks.empty() && !cancelled()) {
const base::string16& first_folder_name =
bridge_->GetLocalizedString(IDS_BOOKMARK_GROUP_FROM_SAFARI);
bridge_->AddBookmarks(bookmarks, first_folder_name);
}
// Import favicons.
sql::Database db;
if (!OpenDatabase(&db))
return;
FaviconMap favicon_map;
ImportFaviconURLs(&db, &favicon_map);
// Write favicons into profile.
if (!favicon_map.empty() && !cancelled()) {
favicon_base::FaviconUsageDataList favicons;
LoadFaviconData(&db, favicon_map, &favicons);
bridge_->SetFavicons(favicons);
}
}
bool SafariImporter::OpenDatabase(sql::Database* db) {
// Construct ~/Library/Safari/WebIcons.db path.
NSString* library_dir = [NSString
stringWithUTF8String:library_dir_.value().c_str()];
NSString* safari_dir = [library_dir
stringByAppendingPathComponent:@"Safari"];
NSString* favicons_db_path = [safari_dir
stringByAppendingPathComponent:@"WebpageIcons.db"];
const char* db_path = [favicons_db_path fileSystemRepresentation];
return db->Open(base::FilePath(db_path));
}
void SafariImporter::ImportFaviconURLs(sql::Database* db,
FaviconMap* favicon_map) {
const char query[] = "SELECT iconID, url FROM PageURL;";
sql::Statement s(db->GetUniqueStatement(query));
while (s.Step() && !cancelled()) {
int64_t icon_id = s.ColumnInt64(0);
GURL url = GURL(s.ColumnString(1));
(*favicon_map)[icon_id].insert(url);
}
}
void SafariImporter::LoadFaviconData(
sql::Database* db,
const FaviconMap& favicon_map,
favicon_base::FaviconUsageDataList* favicons) {
const char query[] = "SELECT i.url, d.data "
"FROM IconInfo i JOIN IconData d "
"ON i.iconID = d.iconID "
"WHERE i.iconID = ?;";
sql::Statement s(db->GetUniqueStatement(query));
for (FaviconMap::const_iterator i = favicon_map.begin();
i != favicon_map.end(); ++i) {
s.Reset(true);
s.BindInt64(0, i->first);
if (s.Step()) {
favicon_base::FaviconUsageData usage;
usage.favicon_url = GURL(s.ColumnString(0));
if (!usage.favicon_url.is_valid())
continue; // Don't bother importing favicons with invalid URLs.
std::vector<unsigned char> data;
s.ColumnBlobAsVector(1, &data);
if (data.empty())
continue; // Data definitely invalid.
if (!importer::ReencodeFavicon(&data[0], data.size(), &usage.png_data))
continue; // Unable to decode.
usage.urls = i->second;
favicons->push_back(usage);
}
}
}
void SafariImporter::RecursiveReadBookmarksFolder(
NSDictionary* bookmark_folder,
const std::vector<base::string16>& parent_path_elements,
bool is_in_toolbar,
const base::string16& toolbar_name,
std::vector<ImportedBookmarkEntry>* out_bookmarks) {
DCHECK(bookmark_folder);
NSString* type = [bookmark_folder objectForKey:@"WebBookmarkType"];
NSString* title = [bookmark_folder objectForKey:@"Title"];
// Are we the dictionary that contains all other bookmarks?
// We need to know this so we don't add it to the path.
bool is_top_level_bookmarks_container = [bookmark_folder
objectForKey:@"WebBookmarkFileVersion"] != nil;
// We're expecting a list of bookmarks here, if that isn't what we got, fail.
if (!is_top_level_bookmarks_container) {
// Top level containers sometimes don't have title attributes.
if (![type isEqualToString:@"WebBookmarkTypeList"] || !title) {
NOTREACHED() << "Type=("
<< (type ? base::SysNSStringToUTF8(type) : "Null type")
<< ") Title=("
<< (title ? base::SysNSStringToUTF8(title) : "Null title")
<< ")";
return;
}
}
NSArray* elements = [bookmark_folder objectForKey:@"Children"];
if (!elements &&
(!parent_path_elements.empty() || !is_in_toolbar) &&
![title isEqualToString:@"BookmarksMenu"]) {
// This is an empty folder, so add it explicitly. Note that the condition
// above prevents either the toolbar folder or the bookmarks menu from being
// added if either is empty. Note also that all non-empty folders are added
// implicitly when their children are added.
ImportedBookmarkEntry entry;
// Safari doesn't specify a creation time for the folder.
entry.creation_time = base::Time::Now();
entry.title = base::SysNSStringToUTF16(title);
entry.path = parent_path_elements;
entry.in_toolbar = is_in_toolbar;
entry.is_folder = true;
out_bookmarks->push_back(entry);
return;
}
std::vector<base::string16> path_elements(parent_path_elements);
// Create a folder for the toolbar, but not for the bookmarks menu.
if (path_elements.empty() && [title isEqualToString:@"BookmarksBar"]) {
is_in_toolbar = true;
path_elements.push_back(toolbar_name);
} else if (!is_top_level_bookmarks_container &&
!(path_elements.empty() &&
[title isEqualToString:@"BookmarksMenu"])) {
if (title)
path_elements.push_back(base::SysNSStringToUTF16(title));
}
// Iterate over individual bookmarks.
for (NSDictionary* bookmark in elements) {
NSString* type = [bookmark objectForKey:@"WebBookmarkType"];
if (!type)
continue;
// If this is a folder, recurse.
if ([type isEqualToString:@"WebBookmarkTypeList"]) {
RecursiveReadBookmarksFolder(bookmark,
path_elements,
is_in_toolbar,
toolbar_name,
out_bookmarks);
}
// If we didn't see a bookmark folder, then we're expecting a bookmark
// item. If that's not what we got then ignore it.
if (![type isEqualToString:@"WebBookmarkTypeLeaf"])
continue;
NSString* url = [bookmark objectForKey:@"URLString"];
NSString* title = [[bookmark objectForKey:@"URIDictionary"]
objectForKey:@"title"];
if (!url || !title)
continue;
// Output Bookmark.
ImportedBookmarkEntry entry;
// Safari doesn't specify a creation time for the bookmark.
entry.creation_time = base::Time::Now();
entry.title = base::SysNSStringToUTF16(title);
entry.url = GURL(base::SysNSStringToUTF8(url));
entry.path = path_elements;
entry.in_toolbar = is_in_toolbar;
out_bookmarks->push_back(entry);
}
}
void SafariImporter::ParseBookmarks(
const base::string16& toolbar_name,
std::vector<ImportedBookmarkEntry>* bookmarks) {
DCHECK(bookmarks);
// Construct ~/Library/Safari/Bookmarks.plist path
NSString* library_dir = [NSString
stringWithUTF8String:library_dir_.value().c_str()];
NSString* safari_dir = [library_dir
stringByAppendingPathComponent:@"Safari"];
NSString* bookmarks_plist = [safari_dir
stringByAppendingPathComponent:@"Bookmarks.plist"];
// Load the plist file.
NSDictionary* bookmarks_dict = [NSDictionary
dictionaryWithContentsOfFile:bookmarks_plist];
if (!bookmarks_dict)
return;
// Recursively read in bookmarks.
std::vector<base::string16> parent_path_elements;
RecursiveReadBookmarksFolder(bookmarks_dict, parent_path_elements, false,
toolbar_name, bookmarks);
}
void SafariImporter::ImportPasswords() {
// Safari stores it's passwords in the Keychain, same as us so we don't need
// to import them.
// Note: that we don't automatically pick them up, there is some logic around
// the user needing to explicitly input their username in a page and blurring
// the field before we pick it up, but the details of that are beyond the
// scope of this comment.
}
void SafariImporter::ImportHistory() {
std::vector<ImporterURLRow> rows;
ParseHistoryItems(&rows);
if (!rows.empty() && !cancelled()) {
bridge_->SetHistoryItems(rows, importer::VISIT_SOURCE_SAFARI_IMPORTED);
}
}
double SafariImporter::HistoryTimeToEpochTime(NSString* history_time) {
DCHECK(history_time);
// Add Difference between Unix epoch and CFAbsoluteTime epoch in seconds.
// Unix epoch is 1970-01-01 00:00:00.0 UTC,
// CF epoch is 2001-01-01 00:00:00.0 UTC.
return CFStringGetDoubleValue(base::mac::NSToCFCast(history_time)) +
kCFAbsoluteTimeIntervalSince1970;
}
void SafariImporter::ParseHistoryItems(
std::vector<ImporterURLRow>* history_items) {
DCHECK(history_items);
// Construct ~/Library/Safari/History.plist path
NSString* library_dir = [NSString
stringWithUTF8String:library_dir_.value().c_str()];
NSString* safari_dir = [library_dir
stringByAppendingPathComponent:@"Safari"];
NSString* history_plist = [safari_dir
stringByAppendingPathComponent:@"History.plist"];
// Load the plist file.
NSDictionary* history_dict = [NSDictionary
dictionaryWithContentsOfFile:history_plist];
if (!history_dict)
return;
NSArray* safari_history_items = [history_dict
objectForKey:@"WebHistoryDates"];
for (NSDictionary* history_item in safari_history_items) {
NSString* url_ns = [history_item objectForKey:@""];
if (!url_ns)
continue;
GURL url(base::SysNSStringToUTF8(url_ns));
if (!CanImportSafariURL(url))
continue;
ImporterURLRow row(url);
NSString* title_ns = [history_item objectForKey:@"title"];
// Sometimes items don't have a title, in which case we just substitue
// the url.
if (!title_ns)
title_ns = url_ns;
row.title = base::SysNSStringToUTF16(title_ns);
int visit_count = [[history_item objectForKey:@"visitCount"]
intValue];
row.visit_count = visit_count;
// Include imported URLs in autocompletion - don't hide them.
row.hidden = 0;
// Item was never typed before in the omnibox.
row.typed_count = 0;
NSString* last_visit_str = [history_item objectForKey:@"lastVisitedDate"];
// The last visit time should always be in the history item, but if not
/// just continue without this item.
DCHECK(last_visit_str);
if (!last_visit_str)
continue;
// Convert Safari's last visit time to Unix Epoch time.
double seconds_since_unix_epoch = HistoryTimeToEpochTime(last_visit_str);
row.last_visit = base::Time::FromDoubleT(seconds_since_unix_epoch);
history_items->push_back(row);
}
}