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