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.
5 #include <Cocoa/Cocoa.h>
7 #include "chrome/utility/importer/safari_importer.h"
12 #include "base/file_util.h"
13 #include "base/mac/mac_util.h"
14 #include "base/strings/string16.h"
15 #include "base/strings/sys_string_conversions.h"
16 #include "base/strings/utf_string_conversions.h"
17 #include "base/time/time.h"
18 #include "chrome/common/importer/imported_bookmark_entry.h"
19 #include "chrome/common/importer/imported_favicon_usage.h"
20 #include "chrome/common/importer/importer_bridge.h"
21 #include "chrome/common/url_constants.h"
22 #include "chrome/utility/importer/favicon_reencode.h"
23 #include "grit/components_strings.h"
24 #include "grit/generated_resources.h"
25 #include "net/base/data_url.h"
26 #include "sql/statement.h"
31 // A function like this is used by other importers in order to filter out
32 // URLS we don't want to import.
33 // For now it's pretty basic, but I've split it out so it's easy to slot
34 // in necessary logic for filtering URLS, should we need it.
35 bool CanImportSafariURL(const GURL& url) {
36 // The URL is not valid.
45 SafariImporter::SafariImporter(const base::FilePath& library_dir)
46 : library_dir_(library_dir) {
49 SafariImporter::~SafariImporter() {
52 void SafariImporter::StartImport(const importer::SourceProfile& source_profile,
54 ImporterBridge* bridge) {
56 // The order here is important!
57 bridge_->NotifyStarted();
59 // In keeping with import on other platforms (and for other browsers), we
60 // don't import the home page (since it may lead to a useless homepage); see
62 if ((items & importer::HISTORY) && !cancelled()) {
63 bridge_->NotifyItemStarted(importer::HISTORY);
65 bridge_->NotifyItemEnded(importer::HISTORY);
67 if ((items & importer::FAVORITES) && !cancelled()) {
68 bridge_->NotifyItemStarted(importer::FAVORITES);
70 bridge_->NotifyItemEnded(importer::FAVORITES);
72 if ((items & importer::PASSWORDS) && !cancelled()) {
73 bridge_->NotifyItemStarted(importer::PASSWORDS);
75 bridge_->NotifyItemEnded(importer::PASSWORDS);
77 bridge_->NotifyEnded();
80 void SafariImporter::ImportBookmarks() {
81 base::string16 toolbar_name =
82 bridge_->GetLocalizedString(IDS_BOOKMARK_BAR_FOLDER_NAME);
83 std::vector<ImportedBookmarkEntry> bookmarks;
84 ParseBookmarks(toolbar_name, &bookmarks);
86 // Write bookmarks into profile.
87 if (!bookmarks.empty() && !cancelled()) {
88 const base::string16& first_folder_name =
89 bridge_->GetLocalizedString(IDS_BOOKMARK_GROUP_FROM_SAFARI);
90 bridge_->AddBookmarks(bookmarks, first_folder_name);
95 if (!OpenDatabase(&db))
98 FaviconMap favicon_map;
99 ImportFaviconURLs(&db, &favicon_map);
100 // Write favicons into profile.
101 if (!favicon_map.empty() && !cancelled()) {
102 std::vector<ImportedFaviconUsage> favicons;
103 LoadFaviconData(&db, favicon_map, &favicons);
104 bridge_->SetFavicons(favicons);
108 bool SafariImporter::OpenDatabase(sql::Connection* db) {
109 // Construct ~/Library/Safari/WebIcons.db path.
110 NSString* library_dir = [NSString
111 stringWithUTF8String:library_dir_.value().c_str()];
112 NSString* safari_dir = [library_dir
113 stringByAppendingPathComponent:@"Safari"];
114 NSString* favicons_db_path = [safari_dir
115 stringByAppendingPathComponent:@"WebpageIcons.db"];
117 const char* db_path = [favicons_db_path fileSystemRepresentation];
118 return db->Open(base::FilePath(db_path));
121 void SafariImporter::ImportFaviconURLs(sql::Connection* db,
122 FaviconMap* favicon_map) {
123 const char* query = "SELECT iconID, url FROM PageURL;";
124 sql::Statement s(db->GetUniqueStatement(query));
126 while (s.Step() && !cancelled()) {
127 int64 icon_id = s.ColumnInt64(0);
128 GURL url = GURL(s.ColumnString(1));
129 (*favicon_map)[icon_id].insert(url);
133 void SafariImporter::LoadFaviconData(
135 const FaviconMap& favicon_map,
136 std::vector<ImportedFaviconUsage>* favicons) {
137 const char* query = "SELECT i.url, d.data "
138 "FROM IconInfo i JOIN IconData d "
139 "ON i.iconID = d.iconID "
140 "WHERE i.iconID = ?;";
141 sql::Statement s(db->GetUniqueStatement(query));
143 for (FaviconMap::const_iterator i = favicon_map.begin();
144 i != favicon_map.end(); ++i) {
146 s.BindInt64(0, i->first);
148 ImportedFaviconUsage usage;
150 usage.favicon_url = GURL(s.ColumnString(0));
151 if (!usage.favicon_url.is_valid())
152 continue; // Don't bother importing favicons with invalid URLs.
154 std::vector<unsigned char> data;
155 s.ColumnBlobAsVector(1, &data);
157 continue; // Data definitely invalid.
159 if (!importer::ReencodeFavicon(&data[0], data.size(), &usage.png_data))
160 continue; // Unable to decode.
162 usage.urls = i->second;
163 favicons->push_back(usage);
168 void SafariImporter::RecursiveReadBookmarksFolder(
169 NSDictionary* bookmark_folder,
170 const std::vector<base::string16>& parent_path_elements,
172 const base::string16& toolbar_name,
173 std::vector<ImportedBookmarkEntry>* out_bookmarks) {
174 DCHECK(bookmark_folder);
176 NSString* type = [bookmark_folder objectForKey:@"WebBookmarkType"];
177 NSString* title = [bookmark_folder objectForKey:@"Title"];
179 // Are we the dictionary that contains all other bookmarks?
180 // We need to know this so we don't add it to the path.
181 bool is_top_level_bookmarks_container = [bookmark_folder
182 objectForKey:@"WebBookmarkFileVersion"] != nil;
184 // We're expecting a list of bookmarks here, if that isn't what we got, fail.
185 if (!is_top_level_bookmarks_container) {
186 // Top level containers sometimes don't have title attributes.
187 if (![type isEqualToString:@"WebBookmarkTypeList"] || !title) {
188 NOTREACHED() << "Type=("
189 << (type ? base::SysNSStringToUTF8(type) : "Null type")
191 << (title ? base::SysNSStringToUTF8(title) : "Null title")
197 NSArray* elements = [bookmark_folder objectForKey:@"Children"];
199 (!parent_path_elements.empty() || !is_in_toolbar) &&
200 ![title isEqualToString:@"BookmarksMenu"]) {
201 // This is an empty folder, so add it explicitly. Note that the condition
202 // above prevents either the toolbar folder or the bookmarks menu from being
203 // added if either is empty. Note also that all non-empty folders are added
204 // implicitly when their children are added.
205 ImportedBookmarkEntry entry;
206 // Safari doesn't specify a creation time for the folder.
207 entry.creation_time = base::Time::Now();
208 entry.title = base::SysNSStringToUTF16(title);
209 entry.path = parent_path_elements;
210 entry.in_toolbar = is_in_toolbar;
211 entry.is_folder = true;
213 out_bookmarks->push_back(entry);
217 std::vector<base::string16> path_elements(parent_path_elements);
218 // Create a folder for the toolbar, but not for the bookmarks menu.
219 if (path_elements.empty() && [title isEqualToString:@"BookmarksBar"]) {
220 is_in_toolbar = true;
221 path_elements.push_back(toolbar_name);
222 } else if (!is_top_level_bookmarks_container &&
223 !(path_elements.empty() &&
224 [title isEqualToString:@"BookmarksMenu"])) {
226 path_elements.push_back(base::SysNSStringToUTF16(title));
229 // Iterate over individual bookmarks.
230 for (NSDictionary* bookmark in elements) {
231 NSString* type = [bookmark objectForKey:@"WebBookmarkType"];
235 // If this is a folder, recurse.
236 if ([type isEqualToString:@"WebBookmarkTypeList"]) {
237 RecursiveReadBookmarksFolder(bookmark,
244 // If we didn't see a bookmark folder, then we're expecting a bookmark
245 // item. If that's not what we got then ignore it.
246 if (![type isEqualToString:@"WebBookmarkTypeLeaf"])
249 NSString* url = [bookmark objectForKey:@"URLString"];
250 NSString* title = [[bookmark objectForKey:@"URIDictionary"]
251 objectForKey:@"title"];
257 ImportedBookmarkEntry entry;
258 // Safari doesn't specify a creation time for the bookmark.
259 entry.creation_time = base::Time::Now();
260 entry.title = base::SysNSStringToUTF16(title);
261 entry.url = GURL(base::SysNSStringToUTF8(url));
262 entry.path = path_elements;
263 entry.in_toolbar = is_in_toolbar;
265 out_bookmarks->push_back(entry);
269 void SafariImporter::ParseBookmarks(
270 const base::string16& toolbar_name,
271 std::vector<ImportedBookmarkEntry>* bookmarks) {
274 // Construct ~/Library/Safari/Bookmarks.plist path
275 NSString* library_dir = [NSString
276 stringWithUTF8String:library_dir_.value().c_str()];
277 NSString* safari_dir = [library_dir
278 stringByAppendingPathComponent:@"Safari"];
279 NSString* bookmarks_plist = [safari_dir
280 stringByAppendingPathComponent:@"Bookmarks.plist"];
282 // Load the plist file.
283 NSDictionary* bookmarks_dict = [NSDictionary
284 dictionaryWithContentsOfFile:bookmarks_plist];
288 // Recursively read in bookmarks.
289 std::vector<base::string16> parent_path_elements;
290 RecursiveReadBookmarksFolder(bookmarks_dict, parent_path_elements, false,
291 toolbar_name, bookmarks);
294 void SafariImporter::ImportPasswords() {
295 // Safari stores it's passwords in the Keychain, same as us so we don't need
297 // Note: that we don't automatically pick them up, there is some logic around
298 // the user needing to explicitly input his username in a page and blurring
299 // the field before we pick it up, but the details of that are beyond the
300 // scope of this comment.
303 void SafariImporter::ImportHistory() {
304 std::vector<ImporterURLRow> rows;
305 ParseHistoryItems(&rows);
307 if (!rows.empty() && !cancelled()) {
308 bridge_->SetHistoryItems(rows, importer::VISIT_SOURCE_SAFARI_IMPORTED);
312 double SafariImporter::HistoryTimeToEpochTime(NSString* history_time) {
313 DCHECK(history_time);
314 // Add Difference between Unix epoch and CFAbsoluteTime epoch in seconds.
315 // Unix epoch is 1970-01-01 00:00:00.0 UTC,
316 // CF epoch is 2001-01-01 00:00:00.0 UTC.
317 return CFStringGetDoubleValue(base::mac::NSToCFCast(history_time)) +
318 kCFAbsoluteTimeIntervalSince1970;
321 void SafariImporter::ParseHistoryItems(
322 std::vector<ImporterURLRow>* history_items) {
323 DCHECK(history_items);
325 // Construct ~/Library/Safari/History.plist path
326 NSString* library_dir = [NSString
327 stringWithUTF8String:library_dir_.value().c_str()];
328 NSString* safari_dir = [library_dir
329 stringByAppendingPathComponent:@"Safari"];
330 NSString* history_plist = [safari_dir
331 stringByAppendingPathComponent:@"History.plist"];
333 // Load the plist file.
334 NSDictionary* history_dict = [NSDictionary
335 dictionaryWithContentsOfFile:history_plist];
339 NSArray* safari_history_items = [history_dict
340 objectForKey:@"WebHistoryDates"];
342 for (NSDictionary* history_item in safari_history_items) {
343 NSString* url_ns = [history_item objectForKey:@""];
347 GURL url(base::SysNSStringToUTF8(url_ns));
349 if (!CanImportSafariURL(url))
352 ImporterURLRow row(url);
353 NSString* title_ns = [history_item objectForKey:@"title"];
355 // Sometimes items don't have a title, in which case we just substitue
360 row.title = base::SysNSStringToUTF16(title_ns);
361 int visit_count = [[history_item objectForKey:@"visitCount"]
363 row.visit_count = visit_count;
364 // Include imported URLs in autocompletion - don't hide them.
366 // Item was never typed before in the omnibox.
369 NSString* last_visit_str = [history_item objectForKey:@"lastVisitedDate"];
370 // The last visit time should always be in the history item, but if not
371 /// just continue without this item.
372 DCHECK(last_visit_str);
376 // Convert Safari's last visit time to Unix Epoch time.
377 double seconds_since_unix_epoch = HistoryTimeToEpochTime(last_visit_str);
378 row.last_visit = base::Time::FromDoubleT(seconds_since_unix_epoch);
380 history_items->push_back(row);