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