Add ICU message format support
[chromium-blink-merge.git] / chrome / utility / importer / safari_importer.mm
blob7a121f17befb8bf2a0437f1208820c63673e1ed9
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"
9 #include <map>
10 #include <vector>
12 #include "base/files/file_util.h"
13 #include "base/mac/foundation_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/importer_bridge.h"
20 #include "chrome/common/url_constants.h"
21 #include "chrome/grit/generated_resources.h"
22 #include "chrome/utility/importer/favicon_reencode.h"
23 #include "components/strings/grit/components_strings.h"
24 #include "net/base/data_url.h"
25 #include "sql/statement.h"
26 #include "url/gurl.h"
28 namespace {
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;
39   return true;
42 }  // namespace
44 SafariImporter::SafariImporter(const base::FilePath& library_dir)
45     : library_dir_(library_dir) {
48 SafariImporter::~SafariImporter() {
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();
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();
79 void SafariImporter::ImportBookmarks() {
80   base::string16 toolbar_name =
81       bridge_->GetLocalizedString(IDS_BOOKMARK_BAR_FOLDER_NAME);
82   std::vector<ImportedBookmarkEntry> bookmarks;
83   ParseBookmarks(toolbar_name, &bookmarks);
85   // Write bookmarks into profile.
86   if (!bookmarks.empty() && !cancelled()) {
87     const base::string16& first_folder_name =
88         bridge_->GetLocalizedString(IDS_BOOKMARK_GROUP_FROM_SAFARI);
89     bridge_->AddBookmarks(bookmarks, first_folder_name);
90   }
92   // Import favicons.
93   sql::Connection db;
94   if (!OpenDatabase(&db))
95     return;
97   FaviconMap favicon_map;
98   ImportFaviconURLs(&db, &favicon_map);
99   // Write favicons into profile.
100   if (!favicon_map.empty() && !cancelled()) {
101     favicon_base::FaviconUsageDataList favicons;
102     LoadFaviconData(&db, favicon_map, &favicons);
103     bridge_->SetFavicons(favicons);
104   }
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"];
116   const char* db_path = [favicons_db_path fileSystemRepresentation];
117   return db->Open(base::FilePath(db_path));
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));
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   }
132 void SafariImporter::LoadFaviconData(
133     sql::Connection* db,
134     const FaviconMap& favicon_map,
135     favicon_base::FaviconUsageDataList* 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));
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       favicon_base::FaviconUsageData usage;
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.
153       std::vector<unsigned char> data;
154       s.ColumnBlobAsVector(1, &data);
155       if (data.empty())
156         continue;  // Data definitely invalid.
158       if (!importer::ReencodeFavicon(&data[0], data.size(), &usage.png_data))
159         continue;  // Unable to decode.
161       usage.urls = i->second;
162       favicons->push_back(usage);
163     }
164   }
167 void SafariImporter::RecursiveReadBookmarksFolder(
168     NSDictionary* bookmark_folder,
169     const std::vector<base::string16>& parent_path_elements,
170     bool is_in_toolbar,
171     const base::string16& toolbar_name,
172     std::vector<ImportedBookmarkEntry>* out_bookmarks) {
173   DCHECK(bookmark_folder);
175   NSString* type = [bookmark_folder objectForKey:@"WebBookmarkType"];
176   NSString* title = [bookmark_folder objectForKey:@"Title"];
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;
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   }
196   NSArray* elements = [bookmark_folder objectForKey:@"Children"];
197   if (!elements &&
198       (!parent_path_elements.empty() || !is_in_toolbar) &&
199       ![title isEqualToString:@"BookmarksMenu"]) {
200     // This is an empty folder, so add it explicitly.  Note that the condition
201     // above prevents either the toolbar folder or the bookmarks menu from being
202     // added if either is empty.  Note also that all non-empty folders are added
203     // implicitly when their children are added.
204     ImportedBookmarkEntry entry;
205     // Safari doesn't specify a creation time for the folder.
206     entry.creation_time = base::Time::Now();
207     entry.title = base::SysNSStringToUTF16(title);
208     entry.path = parent_path_elements;
209     entry.in_toolbar = is_in_toolbar;
210     entry.is_folder = true;
212     out_bookmarks->push_back(entry);
213     return;
214   }
216   std::vector<base::string16> path_elements(parent_path_elements);
217   // Create a folder for the toolbar, but not for the bookmarks menu.
218   if (path_elements.empty() && [title isEqualToString:@"BookmarksBar"]) {
219     is_in_toolbar = true;
220     path_elements.push_back(toolbar_name);
221   } else if (!is_top_level_bookmarks_container &&
222              !(path_elements.empty() &&
223                [title isEqualToString:@"BookmarksMenu"])) {
224     if (title)
225       path_elements.push_back(base::SysNSStringToUTF16(title));
226   }
228   // Iterate over individual bookmarks.
229   for (NSDictionary* bookmark in elements) {
230     NSString* type = [bookmark objectForKey:@"WebBookmarkType"];
231     if (!type)
232       continue;
234     // If this is a folder, recurse.
235     if ([type isEqualToString:@"WebBookmarkTypeList"]) {
236       RecursiveReadBookmarksFolder(bookmark,
237                                    path_elements,
238                                    is_in_toolbar,
239                                    toolbar_name,
240                                    out_bookmarks);
241     }
243     // If we didn't see a bookmark folder, then we're expecting a bookmark
244     // item.  If that's not what we got then ignore it.
245     if (![type isEqualToString:@"WebBookmarkTypeLeaf"])
246       continue;
248     NSString* url = [bookmark objectForKey:@"URLString"];
249     NSString* title = [[bookmark objectForKey:@"URIDictionary"]
250         objectForKey:@"title"];
252     if (!url || !title)
253       continue;
255     // Output Bookmark.
256     ImportedBookmarkEntry entry;
257     // Safari doesn't specify a creation time for the bookmark.
258     entry.creation_time = base::Time::Now();
259     entry.title = base::SysNSStringToUTF16(title);
260     entry.url = GURL(base::SysNSStringToUTF8(url));
261     entry.path = path_elements;
262     entry.in_toolbar = is_in_toolbar;
264     out_bookmarks->push_back(entry);
265   }
268 void SafariImporter::ParseBookmarks(
269     const base::string16& toolbar_name,
270     std::vector<ImportedBookmarkEntry>* bookmarks) {
271   DCHECK(bookmarks);
273   // Construct ~/Library/Safari/Bookmarks.plist path
274   NSString* library_dir = [NSString
275       stringWithUTF8String:library_dir_.value().c_str()];
276   NSString* safari_dir = [library_dir
277       stringByAppendingPathComponent:@"Safari"];
278   NSString* bookmarks_plist = [safari_dir
279     stringByAppendingPathComponent:@"Bookmarks.plist"];
281   // Load the plist file.
282   NSDictionary* bookmarks_dict = [NSDictionary
283       dictionaryWithContentsOfFile:bookmarks_plist];
284   if (!bookmarks_dict)
285     return;
287   // Recursively read in bookmarks.
288   std::vector<base::string16> parent_path_elements;
289   RecursiveReadBookmarksFolder(bookmarks_dict, parent_path_elements, false,
290                                toolbar_name, bookmarks);
293 void SafariImporter::ImportPasswords() {
294   // Safari stores it's passwords in the Keychain, same as us so we don't need
295   // to import them.
296   // Note: that we don't automatically pick them up, there is some logic around
297   // the user needing to explicitly input his username in a page and blurring
298   // the field before we pick it up, but the details of that are beyond the
299   // scope of this comment.
302 void SafariImporter::ImportHistory() {
303   std::vector<ImporterURLRow> rows;
304   ParseHistoryItems(&rows);
306   if (!rows.empty() && !cancelled()) {
307     bridge_->SetHistoryItems(rows, importer::VISIT_SOURCE_SAFARI_IMPORTED);
308   }
311 double SafariImporter::HistoryTimeToEpochTime(NSString* history_time) {
312   DCHECK(history_time);
313   // Add Difference between Unix epoch and CFAbsoluteTime epoch in seconds.
314   // Unix epoch is 1970-01-01 00:00:00.0 UTC,
315   // CF epoch is 2001-01-01 00:00:00.0 UTC.
316   return CFStringGetDoubleValue(base::mac::NSToCFCast(history_time)) +
317       kCFAbsoluteTimeIntervalSince1970;
320 void SafariImporter::ParseHistoryItems(
321     std::vector<ImporterURLRow>* history_items) {
322   DCHECK(history_items);
324   // Construct ~/Library/Safari/History.plist path
325   NSString* library_dir = [NSString
326       stringWithUTF8String:library_dir_.value().c_str()];
327   NSString* safari_dir = [library_dir
328       stringByAppendingPathComponent:@"Safari"];
329   NSString* history_plist = [safari_dir
330       stringByAppendingPathComponent:@"History.plist"];
332   // Load the plist file.
333   NSDictionary* history_dict = [NSDictionary
334       dictionaryWithContentsOfFile:history_plist];
335   if (!history_dict)
336     return;
338   NSArray* safari_history_items = [history_dict
339       objectForKey:@"WebHistoryDates"];
341   for (NSDictionary* history_item in safari_history_items) {
342     NSString* url_ns = [history_item objectForKey:@""];
343     if (!url_ns)
344       continue;
346     GURL url(base::SysNSStringToUTF8(url_ns));
348     if (!CanImportSafariURL(url))
349       continue;
351     ImporterURLRow row(url);
352     NSString* title_ns = [history_item objectForKey:@"title"];
354     // Sometimes items don't have a title, in which case we just substitue
355     // the url.
356     if (!title_ns)
357       title_ns = url_ns;
359     row.title = base::SysNSStringToUTF16(title_ns);
360     int visit_count = [[history_item objectForKey:@"visitCount"]
361                           intValue];
362     row.visit_count = visit_count;
363     // Include imported URLs in autocompletion - don't hide them.
364     row.hidden = 0;
365     // Item was never typed before in the omnibox.
366     row.typed_count = 0;
368     NSString* last_visit_str = [history_item objectForKey:@"lastVisitedDate"];
369     // The last visit time should always be in the history item, but if not
370     /// just continue without this item.
371     DCHECK(last_visit_str);
372     if (!last_visit_str)
373       continue;
375     // Convert Safari's last visit time to Unix Epoch time.
376     double seconds_since_unix_epoch = HistoryTimeToEpochTime(last_visit_str);
377     row.last_visit = base::Time::FromDoubleT(seconds_since_unix_epoch);
379     history_items->push_back(row);
380   }