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 <Cocoa/Cocoa.h> 6 7#include "chrome/browser/importer/safari_importer.h" 8 9#include <map> 10#include <vector> 11 12#include "app/sql/statement.h" 13#include "base/file_util.h" 14#include "base/mac/mac_util.h" 15#include "base/memory/scoped_nsobject.h" 16#include "base/string16.h" 17#include "base/sys_string_conversions.h" 18#include "base/time.h" 19#include "base/utf_string_conversions.h" 20#include "chrome/browser/history/history_types.h" 21#include "chrome/browser/importer/importer_bridge.h" 22#include "chrome/common/url_constants.h" 23#include "googleurl/src/gurl.h" 24#include "grit/generated_resources.h" 25#include "net/base/data_url.h" 26 27namespace { 28 29// A function like this is used by other importers in order to filter out 30// URLS we don't want to import. 31// For now it's pretty basic, but I've split it out so it's easy to slot 32// in necessary logic for filtering URLS, should we need it. 33bool CanImportSafariURL(const GURL& url) { 34 // The URL is not valid. 35 if (!url.is_valid()) 36 return false; 37 38 return true; 39} 40 41} // namespace 42 43SafariImporter::SafariImporter(const FilePath& library_dir) 44 : library_dir_(library_dir) { 45} 46 47SafariImporter::~SafariImporter() { 48} 49 50// static 51bool SafariImporter::CanImport(const FilePath& library_dir, 52 uint16* services_supported) { 53 DCHECK(services_supported); 54 *services_supported = importer::NONE; 55 56 // Import features are toggled by the following: 57 // bookmarks import: existence of ~/Library/Safari/Bookmarks.plist file. 58 // history import: existence of ~/Library/Safari/History.plist file. 59 FilePath safari_dir = library_dir.Append("Safari"); 60 FilePath bookmarks_path = safari_dir.Append("Bookmarks.plist"); 61 FilePath history_path = safari_dir.Append("History.plist"); 62 63 if (file_util::PathExists(bookmarks_path)) 64 *services_supported |= importer::FAVORITES; 65 if (file_util::PathExists(history_path)) 66 *services_supported |= importer::HISTORY; 67 68 return *services_supported != importer::NONE; 69} 70 71void SafariImporter::StartImport(const importer::SourceProfile& source_profile, 72 uint16 items, 73 ImporterBridge* bridge) { 74 bridge_ = bridge; 75 // The order here is important! 76 bridge_->NotifyStarted(); 77 78 // In keeping with import on other platforms (and for other browsers), we 79 // don't import the home page (since it may lead to a useless homepage); see 80 // crbug.com/25603. 81 if ((items & importer::HISTORY) && !cancelled()) { 82 bridge_->NotifyItemStarted(importer::HISTORY); 83 ImportHistory(); 84 bridge_->NotifyItemEnded(importer::HISTORY); 85 } 86 if ((items & importer::FAVORITES) && !cancelled()) { 87 bridge_->NotifyItemStarted(importer::FAVORITES); 88 ImportBookmarks(); 89 bridge_->NotifyItemEnded(importer::FAVORITES); 90 } 91 if ((items & importer::PASSWORDS) && !cancelled()) { 92 bridge_->NotifyItemStarted(importer::PASSWORDS); 93 ImportPasswords(); 94 bridge_->NotifyItemEnded(importer::PASSWORDS); 95 } 96 bridge_->NotifyEnded(); 97} 98 99void SafariImporter::ImportBookmarks() { 100 std::vector<ProfileWriter::BookmarkEntry> bookmarks; 101 ParseBookmarks(&bookmarks); 102 103 // Write bookmarks into profile. 104 if (!bookmarks.empty() && !cancelled()) { 105 const string16& first_folder_name = 106 bridge_->GetLocalizedString(IDS_BOOKMARK_GROUP_FROM_SAFARI); 107 int options = 0; 108 if (import_to_bookmark_bar()) 109 options = ProfileWriter::IMPORT_TO_BOOKMARK_BAR; 110 bridge_->AddBookmarkEntries(bookmarks, first_folder_name, options); 111 } 112 113 // Import favicons. 114 sql::Connection db; 115 if (!OpenDatabase(&db)) 116 return; 117 118 FaviconMap favicon_map; 119 ImportFaviconURLs(&db, &favicon_map); 120 // Write favicons into profile. 121 if (!favicon_map.empty() && !cancelled()) { 122 std::vector<history::ImportedFaviconUsage> favicons; 123 LoadFaviconData(&db, favicon_map, &favicons); 124 bridge_->SetFavicons(favicons); 125 } 126} 127 128bool SafariImporter::OpenDatabase(sql::Connection* db) { 129 // Construct ~/Library/Safari/WebIcons.db path. 130 NSString* library_dir = [NSString 131 stringWithUTF8String:library_dir_.value().c_str()]; 132 NSString* safari_dir = [library_dir 133 stringByAppendingPathComponent:@"Safari"]; 134 NSString* favicons_db_path = [safari_dir 135 stringByAppendingPathComponent:@"WebpageIcons.db"]; 136 137 const char* db_path = [favicons_db_path fileSystemRepresentation]; 138 return db->Open(FilePath(db_path)); 139} 140 141void SafariImporter::ImportFaviconURLs(sql::Connection* db, 142 FaviconMap* favicon_map) { 143 const char* query = "SELECT iconID, url FROM PageURL;"; 144 sql::Statement s(db->GetUniqueStatement(query)); 145 if (!s) 146 return; 147 148 while (s.Step() && !cancelled()) { 149 int64 icon_id = s.ColumnInt64(0); 150 GURL url = GURL(s.ColumnString(1)); 151 (*favicon_map)[icon_id].insert(url); 152 } 153} 154 155void SafariImporter::LoadFaviconData( 156 sql::Connection* db, 157 const FaviconMap& favicon_map, 158 std::vector<history::ImportedFaviconUsage>* favicons) { 159 const char* query = "SELECT i.url, d.data " 160 "FROM IconInfo i JOIN IconData d " 161 "ON i.iconID = d.iconID " 162 "WHERE i.iconID = ?;"; 163 sql::Statement s(db->GetUniqueStatement(query)); 164 if (!s) 165 return; 166 167 for (FaviconMap::const_iterator i = favicon_map.begin(); 168 i != favicon_map.end(); ++i) { 169 s.Reset(); 170 s.BindInt64(0, i->first); 171 if (s.Step()) { 172 history::ImportedFaviconUsage usage; 173 174 usage.favicon_url = GURL(s.ColumnString(0)); 175 if (!usage.favicon_url.is_valid()) 176 continue; // Don't bother importing favicons with invalid URLs. 177 178 std::vector<unsigned char> data; 179 s.ColumnBlobAsVector(1, &data); 180 if (data.empty()) 181 continue; // Data definitely invalid. 182 183 if (!ReencodeFavicon(&data[0], data.size(), &usage.png_data)) 184 continue; // Unable to decode. 185 186 usage.urls = i->second; 187 favicons->push_back(usage); 188 } 189 } 190} 191 192void SafariImporter::RecursiveReadBookmarksFolder( 193 NSDictionary* bookmark_folder, 194 const std::vector<string16>& parent_path_elements, 195 bool is_in_toolbar, 196 std::vector<ProfileWriter::BookmarkEntry>* out_bookmarks) { 197 DCHECK(bookmark_folder); 198 199 NSString* type = [bookmark_folder objectForKey:@"WebBookmarkType"]; 200 NSString* title = [bookmark_folder objectForKey:@"Title"]; 201 202 // Are we the dictionary that contains all other bookmarks? 203 // We need to know this so we don't add it to the path. 204 bool is_top_level_bookmarks_container = [bookmark_folder 205 objectForKey:@"WebBookmarkFileVersion"] != nil; 206 207 // We're expecting a list of bookmarks here, if that isn't what we got, fail. 208 if (!is_top_level_bookmarks_container) { 209 // Top level containers sometimes don't have title attributes. 210 if (![type isEqualToString:@"WebBookmarkTypeList"] || !title) { 211 DCHECK(false) << "Type =(" 212 << (type ? base::SysNSStringToUTF8(type) : "Null Type") 213 << ") Title=(" << (title ? base::SysNSStringToUTF8(title) : "Null title") 214 << ")"; 215 return; 216 } 217 } 218 219 std::vector<string16> path_elements(parent_path_elements); 220 // Is this the toolbar folder? 221 if ([title isEqualToString:@"BookmarksBar"]) { 222 // Be defensive, the toolbar items shouldn't have a prepended path. 223 path_elements.clear(); 224 is_in_toolbar = true; 225 } else if ([title isEqualToString:@"BookmarksMenu"]) { 226 // top level container for normal bookmarks. 227 path_elements.clear(); 228 } else if (!is_top_level_bookmarks_container) { 229 if (title) 230 path_elements.push_back(base::SysNSStringToUTF16(title)); 231 } 232 233 NSArray* elements = [bookmark_folder objectForKey:@"Children"]; 234 // TODO(jeremy) Does Chrome support importing empty folders? 235 if (!elements) 236 return; 237 238 // Iterate over individual bookmarks. 239 for (NSDictionary* bookmark in elements) { 240 NSString* type = [bookmark objectForKey:@"WebBookmarkType"]; 241 if (!type) 242 continue; 243 244 // If this is a folder, recurse. 245 if ([type isEqualToString:@"WebBookmarkTypeList"]) { 246 RecursiveReadBookmarksFolder(bookmark, 247 path_elements, 248 is_in_toolbar, 249 out_bookmarks); 250 } 251 252 // If we didn't see a bookmark folder, then we're expecting a bookmark 253 // item, if that's not what we got then ignore it. 254 if (![type isEqualToString:@"WebBookmarkTypeLeaf"]) 255 continue; 256 257 NSString* url = [bookmark objectForKey:@"URLString"]; 258 NSString* title = [[bookmark objectForKey:@"URIDictionary"] 259 objectForKey:@"title"]; 260 261 if (!url || !title) 262 continue; 263 264 // Output Bookmark. 265 ProfileWriter::BookmarkEntry entry; 266 // Safari doesn't specify a creation time for the bookmark. 267 entry.creation_time = base::Time::Now(); 268 entry.title = base::SysNSStringToUTF16(title); 269 entry.url = GURL(base::SysNSStringToUTF8(url)); 270 entry.path = path_elements; 271 entry.in_toolbar = is_in_toolbar; 272 273 out_bookmarks->push_back(entry); 274 } 275} 276 277void SafariImporter::ParseBookmarks( 278 std::vector<ProfileWriter::BookmarkEntry>* bookmarks) { 279 DCHECK(bookmarks); 280 281 // Construct ~/Library/Safari/Bookmarks.plist path 282 NSString* library_dir = [NSString 283 stringWithUTF8String:library_dir_.value().c_str()]; 284 NSString* safari_dir = [library_dir 285 stringByAppendingPathComponent:@"Safari"]; 286 NSString* bookmarks_plist = [safari_dir 287 stringByAppendingPathComponent:@"Bookmarks.plist"]; 288 289 // Load the plist file. 290 NSDictionary* bookmarks_dict = [NSDictionary 291 dictionaryWithContentsOfFile:bookmarks_plist]; 292 if (!bookmarks_dict) 293 return; 294 295 // Recursively read in bookmarks. 296 std::vector<string16> parent_path_elements; 297 RecursiveReadBookmarksFolder(bookmarks_dict, parent_path_elements, false, 298 bookmarks); 299} 300 301void SafariImporter::ImportPasswords() { 302 // Safari stores it's passwords in the Keychain, same as us so we don't need 303 // to import them. 304 // Note: that we don't automatically pick them up, there is some logic around 305 // the user needing to explicitly input his username in a page and blurring 306 // the field before we pick it up, but the details of that are beyond the 307 // scope of this comment. 308} 309 310void SafariImporter::ImportHistory() { 311 std::vector<history::URLRow> rows; 312 ParseHistoryItems(&rows); 313 314 if (!rows.empty() && !cancelled()) { 315 bridge_->SetHistoryItems(rows, history::SOURCE_SAFARI_IMPORTED); 316 } 317} 318 319double SafariImporter::HistoryTimeToEpochTime(NSString* history_time) { 320 DCHECK(history_time); 321 // Add Difference between Unix epoch and CFAbsoluteTime epoch in seconds. 322 // Unix epoch is 1970-01-01 00:00:00.0 UTC, 323 // CF epoch is 2001-01-01 00:00:00.0 UTC. 324 return CFStringGetDoubleValue(base::mac::NSToCFCast(history_time)) + 325 kCFAbsoluteTimeIntervalSince1970; 326} 327 328void SafariImporter::ParseHistoryItems( 329 std::vector<history::URLRow>* history_items) { 330 DCHECK(history_items); 331 332 // Construct ~/Library/Safari/History.plist path 333 NSString* library_dir = [NSString 334 stringWithUTF8String:library_dir_.value().c_str()]; 335 NSString* safari_dir = [library_dir 336 stringByAppendingPathComponent:@"Safari"]; 337 NSString* history_plist = [safari_dir 338 stringByAppendingPathComponent:@"History.plist"]; 339 340 // Load the plist file. 341 NSDictionary* history_dict = [NSDictionary 342 dictionaryWithContentsOfFile:history_plist]; 343 if (!history_dict) 344 return; 345 346 NSArray* safari_history_items = [history_dict 347 objectForKey:@"WebHistoryDates"]; 348 349 for (NSDictionary* history_item in safari_history_items) { 350 NSString* url_ns = [history_item objectForKey:@""]; 351 if (!url_ns) 352 continue; 353 354 GURL url(base::SysNSStringToUTF8(url_ns)); 355 356 if (!CanImportSafariURL(url)) 357 continue; 358 359 history::URLRow row(url); 360 NSString* title_ns = [history_item objectForKey:@"title"]; 361 362 // Sometimes items don't have a title, in which case we just substitue 363 // the url. 364 if (!title_ns) 365 title_ns = url_ns; 366 367 row.set_title(base::SysNSStringToUTF16(title_ns)); 368 int visit_count = [[history_item objectForKey:@"visitCount"] 369 intValue]; 370 row.set_visit_count(visit_count); 371 // Include imported URLs in autocompletion - don't hide them. 372 row.set_hidden(0); 373 // Item was never typed before in the omnibox. 374 row.set_typed_count(0); 375 376 NSString* last_visit_str = [history_item objectForKey:@"lastVisitedDate"]; 377 // The last visit time should always be in the history item, but if not 378 /// just continue without this item. 379 DCHECK(last_visit_str); 380 if (!last_visit_str) 381 continue; 382 383 // Convert Safari's last visit time to Unix Epoch time. 384 double seconds_since_unix_epoch = HistoryTimeToEpochTime(last_visit_str); 385 row.set_last_visit(base::Time::FromDoubleT(seconds_since_unix_epoch)); 386 387 history_items->push_back(row); 388 } 389} 390