Home | History | Annotate | Download | only in importer
      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 
     27 namespace {
     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.
     33 bool 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 
     43 SafariImporter::SafariImporter(const FilePath& library_dir)
     44     : library_dir_(library_dir) {
     45 }
     46 
     47 SafariImporter::~SafariImporter() {
     48 }
     49 
     50 // static
     51 bool 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 
     71 void 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 
     99 void 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 
    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(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   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 
    155 void 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 
    192 void 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 
    277 void 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 
    301 void 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 
    310 void 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 
    319 double 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 
    328 void 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