1// Copyright 2014 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 "components/bookmarks/browser/bookmark_pasteboard_helper_mac.h"
6
7#import <Cocoa/Cocoa.h>
8
9#include "base/files/file_path.h"
10#include "base/strings/sys_string_conversions.h"
11#include "components/bookmarks/browser/bookmark_node.h"
12#include "ui/base/clipboard/clipboard.h"
13
14using bookmarks::BookmarkNodeData;
15
16NSString* const kBookmarkDictionaryListPboardType =
17    @"BookmarkDictionaryListPboardType";
18
19namespace {
20
21// An unofficial standard pasteboard title type to be provided alongside the
22// |NSURLPboardType|.
23NSString* const kNSURLTitlePboardType = @"public.url-name";
24
25// Pasteboard type used to store profile path to determine which profile
26// a set of bookmarks came from.
27NSString* const kChromiumProfilePathPboardType =
28    @"ChromiumProfilePathPboardType";
29
30// Internal bookmark ID for a bookmark node.  Used only when moving inside
31// of one profile.
32NSString* const kChromiumBookmarkId = @"ChromiumBookmarkId";
33
34// Internal bookmark meta info dictionary for a bookmark node.
35NSString* const kChromiumBookmarkMetaInfo = @"ChromiumBookmarkMetaInfo";
36
37// Mac WebKit uses this type, declared in
38// WebKit/mac/History/WebURLsWithTitles.h.
39NSString* const kCrWebURLsWithTitlesPboardType = @"WebURLsWithTitlesPboardType";
40
41// Keys for the type of node in BookmarkDictionaryListPboardType.
42NSString* const kWebBookmarkType = @"WebBookmarkType";
43
44NSString* const kWebBookmarkTypeList = @"WebBookmarkTypeList";
45
46NSString* const kWebBookmarkTypeLeaf = @"WebBookmarkTypeLeaf";
47
48BookmarkNode::MetaInfoMap MetaInfoMapFromDictionary(NSDictionary* dictionary) {
49  BookmarkNode::MetaInfoMap meta_info_map;
50
51  for (NSString* key in dictionary) {
52    meta_info_map[base::SysNSStringToUTF8(key)] =
53        base::SysNSStringToUTF8([dictionary objectForKey:key]);
54  }
55
56  return meta_info_map;
57}
58
59void ConvertPlistToElements(NSArray* input,
60                            std::vector<BookmarkNodeData::Element>& elements) {
61  NSUInteger len = [input count];
62  for (NSUInteger i = 0; i < len; ++i) {
63    NSDictionary* pboardBookmark = [input objectAtIndex:i];
64    scoped_ptr<BookmarkNode> new_node(new BookmarkNode(GURL()));
65    int64 node_id =
66        [[pboardBookmark objectForKey:kChromiumBookmarkId] longLongValue];
67    new_node->set_id(node_id);
68
69    NSDictionary* metaInfoDictionary =
70        [pboardBookmark objectForKey:kChromiumBookmarkMetaInfo];
71    if (metaInfoDictionary)
72      new_node->SetMetaInfoMap(MetaInfoMapFromDictionary(metaInfoDictionary));
73
74    BOOL is_folder = [[pboardBookmark objectForKey:kWebBookmarkType]
75        isEqualToString:kWebBookmarkTypeList];
76    if (is_folder) {
77      new_node->set_type(BookmarkNode::FOLDER);
78      NSString* title = [pboardBookmark objectForKey:@"Title"];
79      new_node->SetTitle(base::SysNSStringToUTF16(title));
80    } else {
81      new_node->set_type(BookmarkNode::URL);
82      NSDictionary* uriDictionary =
83          [pboardBookmark objectForKey:@"URIDictionary"];
84      NSString* title = [uriDictionary objectForKey:@"title"];
85      NSString* urlString = [pboardBookmark objectForKey:@"URLString"];
86      new_node->SetTitle(base::SysNSStringToUTF16(title));
87      new_node->set_url(GURL(base::SysNSStringToUTF8(urlString)));
88    }
89    BookmarkNodeData::Element e = BookmarkNodeData::Element(new_node.get());
90    if (is_folder) {
91      ConvertPlistToElements([pboardBookmark objectForKey:@"Children"],
92                             e.children);
93    }
94    elements.push_back(e);
95  }
96}
97
98bool ReadBookmarkDictionaryListPboardType(
99    NSPasteboard* pb,
100    std::vector<BookmarkNodeData::Element>& elements) {
101  NSArray* bookmarks =
102      [pb propertyListForType:kBookmarkDictionaryListPboardType];
103  if (!bookmarks)
104    return false;
105  ConvertPlistToElements(bookmarks, elements);
106  return true;
107}
108
109bool ReadWebURLsWithTitlesPboardType(
110    NSPasteboard* pb,
111    std::vector<BookmarkNodeData::Element>& elements) {
112  NSArray* bookmarkPairs =
113      [pb propertyListForType:kCrWebURLsWithTitlesPboardType];
114  if (![bookmarkPairs isKindOfClass:[NSArray class]])
115    return false;
116
117  NSArray* urlsArr = [bookmarkPairs objectAtIndex:0];
118  NSArray* titlesArr = [bookmarkPairs objectAtIndex:1];
119  if ([urlsArr count] < 1)
120    return false;
121  if ([urlsArr count] != [titlesArr count])
122    return false;
123
124  NSUInteger len = [titlesArr count];
125  for (NSUInteger i = 0; i < len; ++i) {
126    base::string16 title =
127        base::SysNSStringToUTF16([titlesArr objectAtIndex:i]);
128    std::string url = base::SysNSStringToUTF8([urlsArr objectAtIndex:i]);
129    if (!url.empty()) {
130      BookmarkNodeData::Element element;
131      element.is_url = true;
132      element.url = GURL(url);
133      element.title = title;
134      elements.push_back(element);
135    }
136  }
137  return true;
138}
139
140bool ReadNSURLPboardType(NSPasteboard* pb,
141                         std::vector<BookmarkNodeData::Element>& elements) {
142  NSURL* url = [NSURL URLFromPasteboard:pb];
143  if (url == nil)
144    return false;
145
146  std::string urlString = base::SysNSStringToUTF8([url absoluteString]);
147  NSString* title = [pb stringForType:kNSURLTitlePboardType];
148  if (!title)
149    title = [pb stringForType:NSStringPboardType];
150
151  BookmarkNodeData::Element element;
152  element.is_url = true;
153  element.url = GURL(urlString);
154  element.title = base::SysNSStringToUTF16(title);
155  elements.push_back(element);
156  return true;
157}
158
159NSDictionary* DictionaryFromBookmarkMetaInfo(
160    const BookmarkNode::MetaInfoMap& meta_info_map) {
161  NSMutableDictionary* dictionary = [NSMutableDictionary dictionary];
162
163  for (BookmarkNode::MetaInfoMap::const_iterator it = meta_info_map.begin();
164      it != meta_info_map.end(); ++it) {
165    [dictionary setObject:base::SysUTF8ToNSString(it->second)
166                   forKey:base::SysUTF8ToNSString(it->first)];
167  }
168
169  return dictionary;
170}
171
172NSArray* GetPlistForBookmarkList(
173    const std::vector<BookmarkNodeData::Element>& elements) {
174  NSMutableArray* plist = [NSMutableArray array];
175  for (size_t i = 0; i < elements.size(); ++i) {
176    BookmarkNodeData::Element element = elements[i];
177    NSDictionary* metaInfoDictionary =
178        DictionaryFromBookmarkMetaInfo(element.meta_info_map);
179    if (element.is_url) {
180      NSString* title = base::SysUTF16ToNSString(element.title);
181      NSString* url = base::SysUTF8ToNSString(element.url.spec());
182      int64 elementId = element.id();
183      NSNumber* idNum = [NSNumber numberWithLongLong:elementId];
184      NSDictionary* uriDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
185              title, @"title", nil];
186      NSDictionary* object = [NSDictionary dictionaryWithObjectsAndKeys:
187          uriDictionary, @"URIDictionary",
188          url, @"URLString",
189          kWebBookmarkTypeLeaf, kWebBookmarkType,
190          idNum, kChromiumBookmarkId,
191          metaInfoDictionary, kChromiumBookmarkMetaInfo,
192          nil];
193      [plist addObject:object];
194    } else {
195      NSString* title = base::SysUTF16ToNSString(element.title);
196      NSArray* children = GetPlistForBookmarkList(element.children);
197      int64 elementId = element.id();
198      NSNumber* idNum = [NSNumber numberWithLongLong:elementId];
199      NSDictionary* object = [NSDictionary dictionaryWithObjectsAndKeys:
200          title, @"Title",
201          children, @"Children",
202          kWebBookmarkTypeList, kWebBookmarkType,
203          idNum, kChromiumBookmarkId,
204          metaInfoDictionary, kChromiumBookmarkMetaInfo,
205          nil];
206      [plist addObject:object];
207    }
208  }
209  return plist;
210}
211
212void WriteBookmarkDictionaryListPboardType(
213    NSPasteboard* pb,
214    const std::vector<BookmarkNodeData::Element>& elements) {
215  NSArray* plist = GetPlistForBookmarkList(elements);
216  [pb setPropertyList:plist forType:kBookmarkDictionaryListPboardType];
217}
218
219void FillFlattenedArraysForBookmarks(
220    const std::vector<BookmarkNodeData::Element>& elements,
221    NSMutableArray* titles,
222    NSMutableArray* urls) {
223  for (size_t i = 0; i < elements.size(); ++i) {
224    BookmarkNodeData::Element element = elements[i];
225    if (element.is_url) {
226      NSString* title = base::SysUTF16ToNSString(element.title);
227      NSString* url = base::SysUTF8ToNSString(element.url.spec());
228      [titles addObject:title];
229      [urls addObject:url];
230    } else {
231      FillFlattenedArraysForBookmarks(element.children, titles, urls);
232    }
233  }
234}
235
236void WriteSimplifiedBookmarkTypes(
237    NSPasteboard* pb,
238    const std::vector<BookmarkNodeData::Element>& elements) {
239  NSMutableArray* titles = [NSMutableArray array];
240  NSMutableArray* urls = [NSMutableArray array];
241  FillFlattenedArraysForBookmarks(elements, titles, urls);
242
243  // These bookmark types only act on urls, not folders.
244  if ([urls count] < 1)
245    return;
246
247  // Write WebURLsWithTitlesPboardType.
248  [pb setPropertyList:[NSArray arrayWithObjects:urls, titles, nil]
249              forType:kCrWebURLsWithTitlesPboardType];
250
251  // Write NSStringPboardType.
252  [pb setString:[urls componentsJoinedByString:@"\n"]
253        forType:NSStringPboardType];
254
255  // Write NSURLPboardType (with title).
256  NSURL* url = [NSURL URLWithString:[urls objectAtIndex:0]];
257  [url writeToPasteboard:pb];
258  NSString* titleString = [titles objectAtIndex:0];
259  [pb setString:titleString forType:kNSURLTitlePboardType];
260}
261
262NSPasteboard* PasteboardFromType(ui::ClipboardType type) {
263  NSString* type_string = nil;
264  switch (type) {
265    case ui::CLIPBOARD_TYPE_COPY_PASTE:
266      type_string = NSGeneralPboard;
267      break;
268    case ui::CLIPBOARD_TYPE_DRAG:
269      type_string = NSDragPboard;
270      break;
271    case ui::CLIPBOARD_TYPE_SELECTION:
272      NOTREACHED();
273      break;
274  }
275
276  return [NSPasteboard pasteboardWithName:type_string];
277}
278
279}  // namespace
280
281void WriteBookmarksToPasteboard(
282    ui::ClipboardType type,
283    const std::vector<BookmarkNodeData::Element>& elements,
284    const base::FilePath& profile_path) {
285  if (elements.empty())
286    return;
287
288  NSPasteboard* pb = PasteboardFromType(type);
289
290  NSArray* types = [NSArray arrayWithObjects:kBookmarkDictionaryListPboardType,
291                                             kCrWebURLsWithTitlesPboardType,
292                                             NSStringPboardType,
293                                             NSURLPboardType,
294                                             kNSURLTitlePboardType,
295                                             kChromiumProfilePathPboardType,
296                                             nil];
297  [pb declareTypes:types owner:nil];
298  [pb setString:base::SysUTF8ToNSString(profile_path.value())
299        forType:kChromiumProfilePathPboardType];
300  WriteBookmarkDictionaryListPboardType(pb, elements);
301  WriteSimplifiedBookmarkTypes(pb, elements);
302}
303
304bool ReadBookmarksFromPasteboard(
305    ui::ClipboardType type,
306    std::vector<BookmarkNodeData::Element>& elements,
307    base::FilePath* profile_path) {
308  NSPasteboard* pb = PasteboardFromType(type);
309
310  elements.clear();
311  NSString* profile = [pb stringForType:kChromiumProfilePathPboardType];
312  *profile_path = base::FilePath(base::SysNSStringToUTF8(profile));
313  return ReadBookmarkDictionaryListPboardType(pb, elements) ||
314         ReadWebURLsWithTitlesPboardType(pb, elements) ||
315         ReadNSURLPboardType(pb, elements);
316}
317
318bool PasteboardContainsBookmarks(ui::ClipboardType type) {
319  NSPasteboard* pb = PasteboardFromType(type);
320
321  NSArray* availableTypes =
322      [NSArray arrayWithObjects:kBookmarkDictionaryListPboardType,
323                                kCrWebURLsWithTitlesPboardType,
324                                NSURLPboardType,
325                                nil];
326  return [pb availableTypeFromArray:availableTypes] != nil;
327}
328