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 <Cocoa/Cocoa.h> 6 7 #include "chrome/utility/importer/safari_importer.h" 8 9 #include <map> 10 #include <vector> 11 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" 27 #include "url/gurl.h" 28 29 namespace { 30 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. 37 if (!url.is_valid()) 38 return false; 39 40 return true; 41 } 42 43 } // namespace 44 45 SafariImporter::SafariImporter(const base::FilePath& library_dir) 46 : library_dir_(library_dir) { 47 } 48 49 SafariImporter::~SafariImporter() { 50 } 51 52 void SafariImporter::StartImport(const importer::SourceProfile& source_profile, 53 uint16 items, 54 ImporterBridge* bridge) { 55 bridge_ = bridge; 56 // The order here is important! 57 bridge_->NotifyStarted(); 58 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 61 // crbug.com/25603. 62 if ((items & importer::HISTORY) && !cancelled()) { 63 bridge_->NotifyItemStarted(importer::HISTORY); 64 ImportHistory(); 65 bridge_->NotifyItemEnded(importer::HISTORY); 66 } 67 if ((items & importer::FAVORITES) && !cancelled()) { 68 bridge_->NotifyItemStarted(importer::FAVORITES); 69 ImportBookmarks(); 70 bridge_->NotifyItemEnded(importer::FAVORITES); 71 } 72 if ((items & importer::PASSWORDS) && !cancelled()) { 73 bridge_->NotifyItemStarted(importer::PASSWORDS); 74 ImportPasswords(); 75 bridge_->NotifyItemEnded(importer::PASSWORDS); 76 } 77 bridge_->NotifyEnded(); 78 } 79 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); 85 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); 91 } 92 93 // Import favicons. 94 sql::Connection db; 95 if (!OpenDatabase(&db)) 96 return; 97 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); 105 } 106 } 107 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"]; 116 117 const char* db_path = [favicons_db_path fileSystemRepresentation]; 118 return db->Open(base::FilePath(db_path)); 119 } 120 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)); 125 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); 130 } 131 } 132 133 void SafariImporter::LoadFaviconData( 134 sql::Connection* db, 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)); 142 143 for (FaviconMap::const_iterator i = favicon_map.begin(); 144 i != favicon_map.end(); ++i) { 145 s.Reset(true); 146 s.BindInt64(0, i->first); 147 if (s.Step()) { 148 ImportedFaviconUsage usage; 149 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. 153 154 std::vector<unsigned char> data; 155 s.ColumnBlobAsVector(1, &data); 156 if (data.empty()) 157 continue; // Data definitely invalid. 158 159 if (!importer::ReencodeFavicon(&data[0], data.size(), &usage.png_data)) 160 continue; // Unable to decode. 161 162 usage.urls = i->second; 163 favicons->push_back(usage); 164 } 165 } 166 } 167 168 void SafariImporter::RecursiveReadBookmarksFolder( 169 NSDictionary* bookmark_folder, 170 const std::vector<base::string16>& parent_path_elements, 171 bool is_in_toolbar, 172 const base::string16& toolbar_name, 173 std::vector<ImportedBookmarkEntry>* out_bookmarks) { 174 DCHECK(bookmark_folder); 175 176 NSString* type = [bookmark_folder objectForKey:@"WebBookmarkType"]; 177 NSString* title = [bookmark_folder objectForKey:@"Title"]; 178 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; 183 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") 190 << ") Title=(" 191 << (title ? base::SysNSStringToUTF8(title) : "Null title") 192 << ")"; 193 return; 194 } 195 } 196 197 NSArray* elements = [bookmark_folder objectForKey:@"Children"]; 198 if (!elements && 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; 212 213 out_bookmarks->push_back(entry); 214 return; 215 } 216 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"])) { 225 if (title) 226 path_elements.push_back(base::SysNSStringToUTF16(title)); 227 } 228 229 // Iterate over individual bookmarks. 230 for (NSDictionary* bookmark in elements) { 231 NSString* type = [bookmark objectForKey:@"WebBookmarkType"]; 232 if (!type) 233 continue; 234 235 // If this is a folder, recurse. 236 if ([type isEqualToString:@"WebBookmarkTypeList"]) { 237 RecursiveReadBookmarksFolder(bookmark, 238 path_elements, 239 is_in_toolbar, 240 toolbar_name, 241 out_bookmarks); 242 } 243 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"]) 247 continue; 248 249 NSString* url = [bookmark objectForKey:@"URLString"]; 250 NSString* title = [[bookmark objectForKey:@"URIDictionary"] 251 objectForKey:@"title"]; 252 253 if (!url || !title) 254 continue; 255 256 // Output Bookmark. 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; 264 265 out_bookmarks->push_back(entry); 266 } 267 } 268 269 void SafariImporter::ParseBookmarks( 270 const base::string16& toolbar_name, 271 std::vector<ImportedBookmarkEntry>* bookmarks) { 272 DCHECK(bookmarks); 273 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"]; 281 282 // Load the plist file. 283 NSDictionary* bookmarks_dict = [NSDictionary 284 dictionaryWithContentsOfFile:bookmarks_plist]; 285 if (!bookmarks_dict) 286 return; 287 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); 292 } 293 294 void SafariImporter::ImportPasswords() { 295 // Safari stores it's passwords in the Keychain, same as us so we don't need 296 // to import them. 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. 301 } 302 303 void SafariImporter::ImportHistory() { 304 std::vector<ImporterURLRow> rows; 305 ParseHistoryItems(&rows); 306 307 if (!rows.empty() && !cancelled()) { 308 bridge_->SetHistoryItems(rows, importer::VISIT_SOURCE_SAFARI_IMPORTED); 309 } 310 } 311 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; 319 } 320 321 void SafariImporter::ParseHistoryItems( 322 std::vector<ImporterURLRow>* history_items) { 323 DCHECK(history_items); 324 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"]; 332 333 // Load the plist file. 334 NSDictionary* history_dict = [NSDictionary 335 dictionaryWithContentsOfFile:history_plist]; 336 if (!history_dict) 337 return; 338 339 NSArray* safari_history_items = [history_dict 340 objectForKey:@"WebHistoryDates"]; 341 342 for (NSDictionary* history_item in safari_history_items) { 343 NSString* url_ns = [history_item objectForKey:@""]; 344 if (!url_ns) 345 continue; 346 347 GURL url(base::SysNSStringToUTF8(url_ns)); 348 349 if (!CanImportSafariURL(url)) 350 continue; 351 352 ImporterURLRow row(url); 353 NSString* title_ns = [history_item objectForKey:@"title"]; 354 355 // Sometimes items don't have a title, in which case we just substitue 356 // the url. 357 if (!title_ns) 358 title_ns = url_ns; 359 360 row.title = base::SysNSStringToUTF16(title_ns); 361 int visit_count = [[history_item objectForKey:@"visitCount"] 362 intValue]; 363 row.visit_count = visit_count; 364 // Include imported URLs in autocompletion - don't hide them. 365 row.hidden = 0; 366 // Item was never typed before in the omnibox. 367 row.typed_count = 0; 368 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); 373 if (!last_visit_str) 374 continue; 375 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); 379 380 history_items->push_back(row); 381 } 382 } 383