• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
29namespace {
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.
35bool 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
45SafariImporter::SafariImporter(const base::FilePath& library_dir)
46    : library_dir_(library_dir) {
47}
48
49SafariImporter::~SafariImporter() {
50}
51
52void 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
80void 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
108bool 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
121void 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
133void 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
168void 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
269void 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
294void 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
303void 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
312double 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
321void 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