Home | History | Annotate | Download | only in importer
      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/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 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();
     57 
     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();
     77 }
     78 
     79 void SafariImporter::ImportBookmarks() {
     80   string16 toolbar_name =
     81       bridge_->GetLocalizedString(IDS_BOOKMARK_BAR_FOLDER_NAME);
     82   std::vector<ImportedBookmarkEntry> bookmarks;
     83   ParseBookmarks(toolbar_name, &bookmarks);
     84 
     85   // Write bookmarks into profile.
     86   if (!bookmarks.empty() && !cancelled()) {
     87     const string16& first_folder_name =
     88         bridge_->GetLocalizedString(IDS_BOOKMARK_GROUP_FROM_SAFARI);
     89     bridge_->AddBookmarks(bookmarks, first_folder_name);
     90   }
     91 
     92   // Import favicons.
     93   sql::Connection db;
     94   if (!OpenDatabase(&db))
     95     return;
     96 
     97   FaviconMap favicon_map;
     98   ImportFaviconURLs(&db, &favicon_map);
     99   // Write favicons into profile.
    100   if (!favicon_map.empty() && !cancelled()) {
    101     std::vector<ImportedFaviconUsage> favicons;
    102     LoadFaviconData(&db, favicon_map, &favicons);
    103     bridge_->SetFavicons(favicons);
    104   }
    105 }
    106 
    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"];
    115 
    116   const char* db_path = [favicons_db_path fileSystemRepresentation];
    117   return db->Open(base::FilePath(db_path));
    118 }
    119 
    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));
    124 
    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   }
    130 }
    131 
    132 void SafariImporter::LoadFaviconData(
    133     sql::Connection* db,
    134     const FaviconMap& favicon_map,
    135     std::vector<ImportedFaviconUsage>* 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));
    141 
    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       ImportedFaviconUsage usage;
    148 
    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.
    152 
    153       std::vector<unsigned char> data;
    154       s.ColumnBlobAsVector(1, &data);
    155       if (data.empty())
    156         continue;  // Data definitely invalid.
    157 
    158       if (!importer::ReencodeFavicon(&data[0], data.size(), &usage.png_data))
    159         continue;  // Unable to decode.
    160 
    161       usage.urls = i->second;
    162       favicons->push_back(usage);
    163     }
    164   }
    165 }
    166 
    167 void SafariImporter::RecursiveReadBookmarksFolder(
    168     NSDictionary* bookmark_folder,
    169     const std::vector<string16>& parent_path_elements,
    170     bool is_in_toolbar,
    171     const string16& toolbar_name,
    172     std::vector<ImportedBookmarkEntry>* out_bookmarks) {
    173   DCHECK(bookmark_folder);
    174 
    175   NSString* type = [bookmark_folder objectForKey:@"WebBookmarkType"];
    176   NSString* title = [bookmark_folder objectForKey:@"Title"];
    177 
    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;
    182 
    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   }
    195 
    196   NSArray* elements = [bookmark_folder objectForKey:@"Children"];
    197   if (!elements && (!parent_path_elements.empty() || !is_in_toolbar)) {
    198     // This is an empty folder, so add it explicitly; but don't add the toolbar
    199     // folder if it is empty.  Note that all non-empty folders are added
    200     // implicitly when their children are added.
    201     ImportedBookmarkEntry entry;
    202     // Safari doesn't specify a creation time for the folder.
    203     entry.creation_time = base::Time::Now();
    204     entry.title = base::SysNSStringToUTF16(title);
    205     entry.path = parent_path_elements;
    206     entry.in_toolbar = is_in_toolbar;
    207     entry.is_folder = true;
    208 
    209     out_bookmarks->push_back(entry);
    210     return;
    211   }
    212 
    213   std::vector<string16> path_elements(parent_path_elements);
    214   // Create a folder for the toolbar, but not for the bookmarks menu.
    215   if (path_elements.empty() && [title isEqualToString:@"BookmarksBar"]) {
    216     is_in_toolbar = true;
    217     path_elements.push_back(toolbar_name);
    218   } else if (!is_top_level_bookmarks_container &&
    219              !(path_elements.empty() &&
    220                [title isEqualToString:@"BookmarksMenu"])) {
    221     if (title)
    222       path_elements.push_back(base::SysNSStringToUTF16(title));
    223   }
    224 
    225   // Iterate over individual bookmarks.
    226   for (NSDictionary* bookmark in elements) {
    227     NSString* type = [bookmark objectForKey:@"WebBookmarkType"];
    228     if (!type)
    229       continue;
    230 
    231     // If this is a folder, recurse.
    232     if ([type isEqualToString:@"WebBookmarkTypeList"]) {
    233       RecursiveReadBookmarksFolder(bookmark,
    234                                    path_elements,
    235                                    is_in_toolbar,
    236                                    toolbar_name,
    237                                    out_bookmarks);
    238     }
    239 
    240     // If we didn't see a bookmark folder, then we're expecting a bookmark
    241     // item.  If that's not what we got then ignore it.
    242     if (![type isEqualToString:@"WebBookmarkTypeLeaf"])
    243       continue;
    244 
    245     NSString* url = [bookmark objectForKey:@"URLString"];
    246     NSString* title = [[bookmark objectForKey:@"URIDictionary"]
    247         objectForKey:@"title"];
    248 
    249     if (!url || !title)
    250       continue;
    251 
    252     // Output Bookmark.
    253     ImportedBookmarkEntry entry;
    254     // Safari doesn't specify a creation time for the bookmark.
    255     entry.creation_time = base::Time::Now();
    256     entry.title = base::SysNSStringToUTF16(title);
    257     entry.url = GURL(base::SysNSStringToUTF8(url));
    258     entry.path = path_elements;
    259     entry.in_toolbar = is_in_toolbar;
    260 
    261     out_bookmarks->push_back(entry);
    262   }
    263 }
    264 
    265 void SafariImporter::ParseBookmarks(
    266     const string16& toolbar_name,
    267     std::vector<ImportedBookmarkEntry>* bookmarks) {
    268   DCHECK(bookmarks);
    269 
    270   // Construct ~/Library/Safari/Bookmarks.plist path
    271   NSString* library_dir = [NSString
    272       stringWithUTF8String:library_dir_.value().c_str()];
    273   NSString* safari_dir = [library_dir
    274       stringByAppendingPathComponent:@"Safari"];
    275   NSString* bookmarks_plist = [safari_dir
    276     stringByAppendingPathComponent:@"Bookmarks.plist"];
    277 
    278   // Load the plist file.
    279   NSDictionary* bookmarks_dict = [NSDictionary
    280       dictionaryWithContentsOfFile:bookmarks_plist];
    281   if (!bookmarks_dict)
    282     return;
    283 
    284   // Recursively read in bookmarks.
    285   std::vector<string16> parent_path_elements;
    286   RecursiveReadBookmarksFolder(bookmarks_dict, parent_path_elements, false,
    287                                toolbar_name, bookmarks);
    288 }
    289 
    290 void SafariImporter::ImportPasswords() {
    291   // Safari stores it's passwords in the Keychain, same as us so we don't need
    292   // to import them.
    293   // Note: that we don't automatically pick them up, there is some logic around
    294   // the user needing to explicitly input his username in a page and blurring
    295   // the field before we pick it up, but the details of that are beyond the
    296   // scope of this comment.
    297 }
    298 
    299 void SafariImporter::ImportHistory() {
    300   std::vector<ImporterURLRow> rows;
    301   ParseHistoryItems(&rows);
    302 
    303   if (!rows.empty() && !cancelled()) {
    304     bridge_->SetHistoryItems(rows, importer::VISIT_SOURCE_SAFARI_IMPORTED);
    305   }
    306 }
    307 
    308 double SafariImporter::HistoryTimeToEpochTime(NSString* history_time) {
    309   DCHECK(history_time);
    310   // Add Difference between Unix epoch and CFAbsoluteTime epoch in seconds.
    311   // Unix epoch is 1970-01-01 00:00:00.0 UTC,
    312   // CF epoch is 2001-01-01 00:00:00.0 UTC.
    313   return CFStringGetDoubleValue(base::mac::NSToCFCast(history_time)) +
    314       kCFAbsoluteTimeIntervalSince1970;
    315 }
    316 
    317 void SafariImporter::ParseHistoryItems(
    318     std::vector<ImporterURLRow>* history_items) {
    319   DCHECK(history_items);
    320 
    321   // Construct ~/Library/Safari/History.plist path
    322   NSString* library_dir = [NSString
    323       stringWithUTF8String:library_dir_.value().c_str()];
    324   NSString* safari_dir = [library_dir
    325       stringByAppendingPathComponent:@"Safari"];
    326   NSString* history_plist = [safari_dir
    327       stringByAppendingPathComponent:@"History.plist"];
    328 
    329   // Load the plist file.
    330   NSDictionary* history_dict = [NSDictionary
    331       dictionaryWithContentsOfFile:history_plist];
    332   if (!history_dict)
    333     return;
    334 
    335   NSArray* safari_history_items = [history_dict
    336       objectForKey:@"WebHistoryDates"];
    337 
    338   for (NSDictionary* history_item in safari_history_items) {
    339     NSString* url_ns = [history_item objectForKey:@""];
    340     if (!url_ns)
    341       continue;
    342 
    343     GURL url(base::SysNSStringToUTF8(url_ns));
    344 
    345     if (!CanImportSafariURL(url))
    346       continue;
    347 
    348     ImporterURLRow row(url);
    349     NSString* title_ns = [history_item objectForKey:@"title"];
    350 
    351     // Sometimes items don't have a title, in which case we just substitue
    352     // the url.
    353     if (!title_ns)
    354       title_ns = url_ns;
    355 
    356     row.title = base::SysNSStringToUTF16(title_ns);
    357     int visit_count = [[history_item objectForKey:@"visitCount"]
    358                           intValue];
    359     row.visit_count = visit_count;
    360     // Include imported URLs in autocompletion - don't hide them.
    361     row.hidden = 0;
    362     // Item was never typed before in the omnibox.
    363     row.typed_count = 0;
    364 
    365     NSString* last_visit_str = [history_item objectForKey:@"lastVisitedDate"];
    366     // The last visit time should always be in the history item, but if not
    367     /// just continue without this item.
    368     DCHECK(last_visit_str);
    369     if (!last_visit_str)
    370       continue;
    371 
    372     // Convert Safari's last visit time to Unix Epoch time.
    373     double seconds_since_unix_epoch = HistoryTimeToEpochTime(last_visit_str);
    374     row.last_visit = base::Time::FromDoubleT(seconds_since_unix_epoch);
    375 
    376     history_items->push_back(row);
    377   }
    378 }
    379