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