chromium/components/bookmarks/browser/bookmark_pasteboard_helper_mac.mm

// 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