| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/bookmarks/browser/bookmark_pasteboard_helper_mac.h" |
| |
| #import <Cocoa/Cocoa.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <memory> |
| |
| #include "base/apple/foundation_util.h" |
| #include "base/files/file_path.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/uuid.h" |
| #include "components/bookmarks/browser/bookmark_node.h" |
| #include "ui/base/clipboard/clipboard.h" |
| #include "ui/base/clipboard/clipboard_constants.h" |
| #include "ui/base/clipboard/clipboard_util_mac.h" |
| |
| namespace bookmarks { |
| |
| NSString* const kUTTypeChromiumBookmarkDictionaryList = |
| @"org.chromium.bookmark-dictionary-list"; |
| |
| namespace { |
| |
| // UTI used to store profile path to determine which profile a set of bookmarks |
| // came from. |
| NSString* const kUTTypeChromiumProfilePath = @"org.chromium.profile-path"; |
| |
| // Internal bookmark ID for a bookmark node. Used only when moving inside of |
| // one profile. |
| NSString* const kChromiumBookmarkIdKey = @"ChromiumBookmarkId"; |
| |
| // Internal bookmark meta info dictionary for a bookmark node. |
| NSString* const kChromiumBookmarkMetaInfoKey = @"ChromiumBookmarkMetaInfo"; |
| |
| // Keys for the type of node in kUTTypeChromiumBookmarkDictionaryList. |
| NSString* const kWebBookmarkTypeKey = @"WebBookmarkType"; |
| NSString* const kWebBookmarkTypeList = @"WebBookmarkTypeList"; |
| NSString* const kWebBookmarkTypeLeaf = @"WebBookmarkTypeLeaf"; |
| |
| // Property keys. |
| NSString* const kTitleKey = @"Title"; |
| NSString* const kURLStringKey = @"URLString"; |
| NSString* const kChildrenKey = @"Children"; |
| |
| BookmarkNode::MetaInfoMap MetaInfoMapFromDictionary(NSDictionary* dictionary) { |
| __block BookmarkNode::MetaInfoMap meta_info_map; |
| |
| [dictionary |
| enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL* stop) { |
| NSString* key_ns = base::apple::ObjCCast<NSString>(key); |
| NSString* value_ns = base::apple::ObjCCast<NSString>(value); |
| if (key_ns && value_ns) { |
| meta_info_map[base::SysNSStringToUTF8(key_ns)] = |
| base::SysNSStringToUTF8(value_ns); |
| } |
| }]; |
| |
| return meta_info_map; |
| } |
| |
| NSDictionary* DictionaryFromBookmarkMetaInfo( |
| const BookmarkNode::MetaInfoMap& meta_info_map) { |
| NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; |
| |
| for (const auto& item : meta_info_map) { |
| dictionary[base::SysUTF8ToNSString(item.first)] = |
| base::SysUTF8ToNSString(item.second); |
| } |
| |
| return dictionary; |
| } |
| |
| void ConvertNSArrayToElements( |
| NSArray* input, |
| std::vector<BookmarkNodeData::Element>* elements) { |
| for (NSDictionary* bookmark_dict in input) { |
| NSString* type = |
| base::apple::ObjCCast<NSString>(bookmark_dict[kWebBookmarkTypeKey]); |
| if (!type) |
| continue; |
| |
| BOOL is_folder = [type isEqualToString:kWebBookmarkTypeList]; |
| |
| GURL url = GURL(); |
| if (!is_folder) { |
| NSString* url_string = |
| base::apple::ObjCCast<NSString>(bookmark_dict[kURLStringKey]); |
| if (!url_string) |
| continue; |
| url = GURL(base::SysNSStringToUTF8(url_string)); |
| } |
| |
| auto new_node = std::make_unique<BookmarkNode>( |
| /*id=*/0, base::Uuid::GenerateRandomV4(), url); |
| |
| NSNumber* node_id = |
| base::apple::ObjCCast<NSNumber>(bookmark_dict[kChromiumBookmarkIdKey]); |
| if (node_id) |
| new_node->set_id(node_id.longLongValue); |
| |
| NSDictionary* meta_info = base::apple::ObjCCast<NSDictionary>( |
| bookmark_dict[kChromiumBookmarkMetaInfoKey]); |
| if (meta_info) |
| new_node->SetMetaInfoMap(MetaInfoMapFromDictionary(meta_info)); |
| |
| NSString* title = base::apple::ObjCCast<NSString>(bookmark_dict[kTitleKey]); |
| new_node->SetTitle(base::SysNSStringToUTF16(title)); |
| |
| BookmarkNodeData::Element e = BookmarkNodeData::Element(new_node.get()); |
| // BookmarkNodeData::Element::ReadFromPickle explicitly zeroes out the two |
| // date fields so do so too. TODO(avi): Refactor this code to be a member |
| // function of BookmarkNodeData::Element so that it can write the id_ field |
| // directly and avoid the round-trip through BookmarkNode. |
| e.date_added = base::Time(); |
| e.date_folder_modified = base::Time(); |
| |
| if (is_folder) { |
| ConvertNSArrayToElements(bookmark_dict[kChildrenKey], &e.children); |
| } |
| |
| elements->push_back(e); |
| } |
| } |
| |
| bool ReadChromiumBookmarks(NSPasteboard* pb, |
| std::vector<BookmarkNodeData::Element>* elements) { |
| id bookmarks = [pb propertyListForType:kUTTypeChromiumBookmarkDictionaryList]; |
| if (!bookmarks) |
| return false; |
| |
| NSArray* bookmarks_array = base::apple::ObjCCast<NSArray>(bookmarks); |
| if (!bookmarks_array) |
| return false; |
| |
| ConvertNSArrayToElements(bookmarks_array, elements); |
| return true; |
| } |
| |
| bool ReadStandardBookmarks(NSPasteboard* pb, |
| std::vector<BookmarkNodeData::Element>* elements) { |
| NSArray<URLAndTitle*>* urls_and_titles = |
| ui::clipboard_util::URLsAndTitlesFromPasteboard(pb, |
| /*include_files=*/false); |
| |
| if (!urls_and_titles.count) { |
| return false; |
| } |
| |
| for (URLAndTitle* url_and_title in urls_and_titles) { |
| std::string url = base::SysNSStringToUTF8(url_and_title.URL); |
| std::u16string title = base::SysNSStringToUTF16(url_and_title.title); |
| if (!url.empty()) { |
| BookmarkNodeData::Element element; |
| element.is_url = true; |
| element.url = GURL(url); |
| element.title = title; |
| elements->push_back(element); |
| } |
| } |
| return true; |
| } |
| |
| // Transforms a list of bookmark nodes into an `NSArray` of `NSDictionaries` |
| // encoding them. |
| NSArray* GetNSArrayForBookmarkList( |
| const std::vector<BookmarkNodeData::Element>& elements) { |
| NSMutableArray* array = [NSMutableArray array]; |
| for (const auto& element : elements) { |
| NSDictionary* meta_info = |
| DictionaryFromBookmarkMetaInfo(element.meta_info_map); |
| NSString* title = base::SysUTF16ToNSString(element.title); |
| NSNumber* element_id = @(element.id()); |
| |
| NSDictionary* object; |
| if (element.is_url) { |
| NSString* url = base::SysUTF8ToNSString(element.url.spec()); |
| object = @{ |
| kTitleKey : title, |
| kURLStringKey : url, |
| kWebBookmarkTypeKey : kWebBookmarkTypeLeaf, |
| kChromiumBookmarkIdKey : element_id, |
| kChromiumBookmarkMetaInfoKey : meta_info |
| }; |
| } else { |
| NSArray* children = GetNSArrayForBookmarkList(element.children); |
| object = @{ |
| kTitleKey : title, |
| kChildrenKey : children, |
| kWebBookmarkTypeKey : kWebBookmarkTypeList, |
| kChromiumBookmarkIdKey : element_id, |
| kChromiumBookmarkMetaInfoKey : meta_info |
| }; |
| } |
| [array addObject:object]; |
| } |
| return array; |
| } |
| |
| void CollectUrlsAndTitlesOfBookmarks( |
| const std::vector<BookmarkNodeData::Element>& elements, |
| NSMutableArray* url_titles, |
| NSMutableArray* urls) { |
| for (const auto& element : elements) { |
| NSString* title = base::SysUTF16ToNSString(element.title); |
| if (element.is_url) { |
| NSString* url = base::SysUTF8ToNSString(element.url.spec()); |
| [url_titles addObject:title]; |
| [urls addObject:url]; |
| } else { |
| CollectUrlsAndTitlesOfBookmarks(element.children, url_titles, urls); |
| } |
| } |
| } |
| |
| // Generates a list of pasteboard items representing bookmarks. Note that the |
| // special items are included only on the first of the items. |
| NSArray<NSPasteboardItem*>* PasteboardItemsFromBookmarks( |
| const std::vector<BookmarkNodeData::Element>& elements, |
| const base::FilePath& profile_path) { |
| // Bookmarks are encoded into pasteboard items in two ways: |
| // |
| // 1. As a flat array of pasteboard items, one for each of the bookmarks. This |
| // loses the hierarchical information, but this makes the bookmark drags |
| // interoperable with other applications on the system. |
| // 2. As a plist and path containing full information about everything. |
| // |
| // The OS pasteboard provides support for multiple items, so the array of |
| // items created as part of step 1 is set to be the items on the pasteboard. |
| // Blobs of data that are only useful to Chromium are added to the first item |
| // to go along for the ride. |
| |
| // 1. The flat array of URLs for interoperability. |
| |
| NSMutableArray* url_titles = [NSMutableArray array]; |
| NSMutableArray* urls = [NSMutableArray array]; |
| CollectUrlsAndTitlesOfBookmarks(elements, url_titles, urls); |
| |
| NSArray<NSPasteboardItem*>* items = |
| ui::clipboard_util::PasteboardItemsFromUrls(urls, url_titles); |
| |
| // 2. The plist and path for Chromium use. |
| |
| if (!items.count) { |
| // There were no bookmark URLs encoded, therefore the elements being encoded |
| // consist of bookmark folders. The data for those folders will be contained |
| // in the Chromium-specific data, so make a single pasteboard item to hold |
| // it. |
| items = @[ [[NSPasteboardItem alloc] init] ]; |
| } |
| |
| [items.firstObject setPropertyList:GetNSArrayForBookmarkList(elements) |
| forType:kUTTypeChromiumBookmarkDictionaryList]; |
| |
| [items.firstObject setString:base::SysUTF8ToNSString(profile_path.value()) |
| forType:kUTTypeChromiumProfilePath]; |
| |
| return items; |
| } |
| |
| } // namespace |
| |
| void WriteBookmarksToPasteboard( |
| NSPasteboard* pb, |
| const std::vector<BookmarkNodeData::Element>& elements, |
| const base::FilePath& profile_path, |
| bool is_off_the_record) { |
| if (elements.empty()) { |
| return; |
| } |
| |
| NSArray<NSPasteboardItem*>* items = |
| PasteboardItemsFromBookmarks(elements, profile_path); |
| [pb clearContents]; |
| if (is_off_the_record) { |
| // Make the pasteboard content current host only. |
| [pb prepareForNewContentsWithOptions:NSPasteboardContentsCurrentHostOnly]; |
| } |
| [pb writeObjects:items]; |
| } |
| |
| bool ReadBookmarksFromPasteboard( |
| NSPasteboard* pb, |
| std::vector<BookmarkNodeData::Element>* elements, |
| base::FilePath* profile_path) { |
| elements->clear(); |
| NSString* profile = [pb stringForType:kUTTypeChromiumProfilePath]; |
| *profile_path = base::FilePath(base::SysNSStringToUTF8(profile)); |
| |
| // Corresponding to the two types of data written above in |
| // `PasteboardItemsFromBookmarks()`, first attempt to read the Chromium-only |
| // data that has more fidelity, and then fall back to reading standard URL |
| // types. |
| |
| return ReadChromiumBookmarks(pb, elements) || |
| ReadStandardBookmarks(pb, elements); |
| } |
| |
| bool PasteboardContainsBookmarks(NSPasteboard* pb) { |
| NSArray* availableTypes = @[ |
| ui::kUTTypeWebKitWebURLsWithTitles, |
| kUTTypeChromiumBookmarkDictionaryList, |
| NSPasteboardTypeURL, |
| ]; |
| return [pb availableTypeFromArray:availableTypes] != nil; |
| } |
| |
| } // namespace bookmarks |