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