| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "content/app_shim_remote_cocoa/web_drag_source_mac.h" |
| |
| #include <Cocoa/Cocoa.h> |
| #include <UniformTypeIdentifiers/UniformTypeIdentifiers.h> |
| #include <sys/param.h> |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/apple/bridging.h" |
| #include "base/apple/foundation_util.h" |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/pickle.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/remote_cocoa/app_shim/application_bridge.h" |
| #include "content/browser/download/drag_download_file.h" |
| #include "content/browser/download/drag_download_util.h" |
| #include "content/common/web_contents_ns_view_bridge.mojom.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/drop_data.h" |
| #include "net/base/apple/url_conversions.h" |
| #include "net/base/filename_util.h" |
| #include "net/base/mime_util.h" |
| #include "ui/base/clipboard/clipboard_constants.h" |
| #include "ui/base/clipboard/custom_data_helper.h" |
| #include "ui/base/cocoa/cocoa_base_utils.h" |
| #include "url/origin.h" |
| #include "url/url_constants.h" |
| |
| @implementation WebDragSource { |
| // The host through which to communicate with the WebContents. Owns |
| // this object. This pointer gets reset when the WebContents goes away with |
| // `webContentsIsGone`. |
| raw_ptr<remote_cocoa::mojom::WebContentsNSViewHost> _host; |
| |
| // The drop data. |
| content::DropData _dropData; |
| |
| // The source origin the drop data came from. |
| url::Origin _sourceOrigin; |
| |
| // Whether to mark the drag as having come from a privileged WebContents. |
| BOOL _privileged; |
| |
| // The file name to be saved to for a drag-out download. |
| base::FilePath _downloadFileName; |
| |
| // The URL to download from for a drag-out download. |
| GURL _downloadURL; |
| |
| // The file type associated with the file drag, if any. |
| UTType* __strong _fileType; |
| } |
| |
| - (instancetype)initWithHost:(remote_cocoa::mojom::WebContentsNSViewHost*)host |
| dropData:(const content::DropData&)dropData |
| sourceOrigin:(const url::Origin&)sourceOrigin |
| isPrivileged:(BOOL)privileged { |
| if ((self = [super init])) { |
| _host = host; |
| _dropData = dropData; |
| _sourceOrigin = sourceOrigin; |
| _privileged = privileged; |
| } |
| |
| return self; |
| } |
| |
| - (void)webContentsIsGone { |
| _host = nullptr; |
| } |
| |
| - (NSArray<NSPasteboardType>*)writableTypesForPasteboard: |
| (NSPasteboard*)pasteboard { |
| NSMutableArray<NSPasteboardType>* writableTypes = [NSMutableArray array]; |
| |
| // Always add kUTTypeChromiumInitiatedDrag to mark this drag as something to |
| // accept. |
| [writableTypes addObject:ui::kUTTypeChromiumInitiatedDrag]; |
| |
| // Always add kUTTypeChromiumRendererInitiatedDrag as all drags initiated here |
| // are drags from the web. |
| [writableTypes addObject:ui::kUTTypeChromiumRendererInitiatedDrag]; |
| |
| // Tag the drag as coming from a privileged WebContents if needed. |
| if (_privileged) { |
| [writableTypes addObject:ui::kUTTypeChromiumPrivilegedInitiatedDrag]; |
| } |
| |
| // URL (and title). |
| if (!_dropData.url_infos.empty()) { |
| [writableTypes addObject:NSPasteboardTypeURL]; |
| [writableTypes addObject:ui::kUTTypeUrlName]; |
| } |
| |
| // File. |
| if (!_dropData.file_contents.empty() || |
| !_dropData.download_metadata.empty()) { |
| std::string mimeType; |
| |
| // TODO(crbug.com/40599578): The |downloadFileName_| and |
| // |downloadURL_| values should be computed by the caller. |
| if (_dropData.download_metadata.empty()) { |
| std::optional<base::FilePath> suggestedFilename = |
| _dropData.GetSafeFilenameForImageFileContents(); |
| if (suggestedFilename) { |
| _downloadFileName = std::move(*suggestedFilename); |
| net::GetMimeTypeFromFile(_downloadFileName, &mimeType); |
| } |
| } else { |
| std::u16string mimeType16; |
| base::FilePath filename; |
| if (content::ParseDownloadMetadata(_dropData.download_metadata, |
| &mimeType16, &filename, |
| &_downloadURL)) { |
| // Generate the file name based on both mime type and proposed file |
| // name. |
| std::string defaultName = content::GetContentClient()->browser() |
| ? content::GetContentClient() |
| ->browser() |
| ->GetDefaultDownloadName() |
| : std::string(); |
| mimeType = base::UTF16ToUTF8(mimeType16); |
| _downloadFileName = |
| net::GenerateFileName(_downloadURL, std::string(), std::string(), |
| filename.value(), mimeType, defaultName); |
| } |
| } |
| |
| if (!mimeType.empty()) { |
| _fileType = [UTType typeWithMIMEType:base::SysUTF8ToNSString(mimeType)]; |
| |
| // Promise both the file's contents... |
| if (!_dropData.file_contents.empty()) { |
| [writableTypes addObject:_fileType.identifier]; |
| } |
| |
| // ... and materialization of the file if requested. |
| |
| // NB: Why not use `NSFilePromiseProvider`? Its design is fundamentally |
| // broken. It insists on being added to the pasteboard as its own object, |
| // but this code needs to add many, many flavors as one object. The only |
| // way to get it to share a pasteboard item with other flavors is to play |
| // the game of subclassing it, but that would involve a big rewrite of all |
| // of this code. FB11876926 |
| // |
| // https://buckleyisms.com/blog/how-to-actually-implement-file-dragging-from-your-app-on-mac/ |
| |
| [writableTypes |
| addObject:base::apple::CFToNSPtrCast(kPasteboardTypeFileURLPromise)]; |
| [writableTypes addObject:base::apple::CFToNSPtrCast( |
| kPasteboardTypeFilePromiseContent)]; |
| } |
| } |
| |
| // HTML. |
| bool hasHTMLData = _dropData.html && !_dropData.html->empty(); |
| // Mail.app and TextEdit accept drags that have both HTML and image flavors on |
| // them, but don't process them correctly <http://crbug.com/55879>. Therefore, |
| // if there is an image flavor, don't put the HTML data on as HTML, but rather |
| // put it on as this Chrome-only flavor. |
| // |
| // (The only time that Blink fills in the DropData::file_contents is with |
| // an image drop, but the MIME time is tested anyway for paranoia's sake.) |
| bool hasImageData = !_dropData.file_contents.empty() && _fileType && |
| [_fileType conformsToType:UTTypeImage]; |
| if (hasHTMLData) { |
| if (hasImageData) { |
| [writableTypes addObject:ui::kUTTypeChromiumImageAndHtml]; |
| } else { |
| [writableTypes addObject:NSPasteboardTypeHTML]; |
| } |
| } |
| |
| // Plain text. |
| if (_dropData.text && !_dropData.text->empty()) { |
| [writableTypes addObject:NSPasteboardTypeString]; |
| } |
| |
| if (!_dropData.custom_data.empty()) { |
| [writableTypes addObject:ui::kUTTypeChromiumDataTransferCustomData]; |
| } |
| |
| return writableTypes; |
| } |
| |
| - (id)pasteboardPropertyListForType:(NSPasteboardType)type { |
| // HTML. |
| if ([type isEqualToString:NSPasteboardTypeHTML] || |
| [type isEqualToString:ui::kUTTypeChromiumImageAndHtml]) { |
| DCHECK(_dropData.html && !_dropData.html->empty()); |
| |
| // NSPasteboardTypeHTML requires the character set to be declared. |
| // Otherwise, it assumes US-ASCII. Awesome. |
| static constexpr char16_t kHtmlHeader[] = |
| u"<meta http-equiv=\"Content-Type\" " |
| u"content=\"text/html;charset=UTF-8\">"; |
| return base::SysUTF16ToNSString(kHtmlHeader + *_dropData.html); |
| } |
| |
| // URL. |
| if ([type isEqualToString:NSPasteboardTypeURL]) { |
| DCHECK(!_dropData.url_infos.empty()); |
| NSURL* url = net::NSURLWithGURL(_dropData.url_infos.front().url); |
| // If NSURL creation failed, check for a badly-escaped JavaScript URL. |
| // Strip out any existing escapes and then re-escape uniformly. |
| if (!url && |
| _dropData.url_infos.front().url.SchemeIs(url::kJavaScriptScheme)) { |
| std::string unescapedUrlString = base::UnescapeBinaryURLComponent( |
| _dropData.url_infos.front().url.spec()); |
| std::string escapedUrlString = |
| base::EscapeUrlEncodedData(unescapedUrlString, false); |
| url = [NSURL URLWithString:base::SysUTF8ToNSString(escapedUrlString)]; |
| } |
| return url.absoluteString; |
| } |
| |
| // URL title. |
| if ([type isEqualToString:ui::kUTTypeUrlName]) { |
| DCHECK(!_dropData.url_infos.empty()); |
| return base::SysUTF16ToNSString(_dropData.url_infos.front().title); |
| } |
| |
| // File contents. |
| if ([type isEqualToString:_fileType.identifier]) { |
| return [NSData dataWithBytes:_dropData.file_contents.data() |
| length:_dropData.file_contents.length()]; |
| } |
| |
| // File instantiation promise. |
| if ([type isEqualToString:base::apple::CFToNSPtrCast( |
| kPasteboardTypeFilePromiseContent)]) { |
| return _fileType.identifier; |
| } |
| if ([type isEqualToString:base::apple::CFToNSPtrCast( |
| kPasteboardTypeFileURLPromise)]) { |
| // The official way of getting the drop destination is to call |
| // `PasteboardCopyPasteLocation` on the Carbon Pasteboard Manager, but what |
| // that function does is pull the location from "com.apple.pastelocation". |
| // Therefore, do that directly rather than indirecting to a different API |
| // set that does no useful bridging. |
| NSPasteboard* pasteboard = |
| [NSPasteboard pasteboardWithName:NSPasteboardNameDrag]; |
| NSString* dropDestination = |
| [pasteboard stringForType:@"com.apple.pastelocation"]; |
| if (!dropDestination || !_host) { |
| // Something has gone wrong, but understandably. Chromium leaves the data |
| // around on the pasteboard after the drag, and it's possible that some |
| // app is rummaging around for what it can find. Silently fail in this |
| // case. |
| return [NSData data]; |
| } |
| |
| base::FilePath filePath = |
| base::apple::NSURLToFilePath([NSURL URLWithString:dropDestination]); |
| filePath = filePath.Append(_downloadFileName); |
| _host->DragPromisedFileTo(filePath, _dropData, _downloadURL, _sourceOrigin, |
| &filePath); |
| |
| // The process of writing the file may have altered the value of |
| // `filePath` if, say, an existing file at the drop site already had that |
| // name. Return the actual URL to the file that was written. |
| return base::apple::FilePathToNSURL(filePath).absoluteString; |
| } |
| |
| // Plain text. |
| if ([type isEqualToString:NSPasteboardTypeString]) { |
| DCHECK(_dropData.text && !_dropData.text->empty()); |
| return base::SysUTF16ToNSString(*_dropData.text); |
| } |
| |
| // Custom MIME data. |
| if ([type isEqualToString:ui::kUTTypeChromiumDataTransferCustomData]) { |
| base::Pickle pickle; |
| ui::WriteCustomDataToPickle(_dropData.custom_data, &pickle); |
| return [NSData dataWithBytes:pickle.data() length:pickle.size()]; |
| } |
| |
| // Source origin of the drop data. |
| if ([type isEqualToString:ui::kUTTypeChromiumRendererInitiatedDrag]) { |
| return _sourceOrigin.opaque() |
| ? [NSString string] |
| : base::SysUTF8ToNSString(_sourceOrigin.Serialize()); |
| } |
| |
| // Flavors used to tag. |
| if ([type isEqualToString:ui::kUTTypeChromiumInitiatedDrag] || |
| [type isEqualToString:ui::kUTTypeChromiumPrivilegedInitiatedDrag]) { |
| // The type _was_ promised and someone decided to call the bluff. |
| return [NSData data]; |
| } |
| |
| // Oops! Unknown drag pasteboard type. |
| NOTREACHED(); |
| } |
| |
| - (NSPasteboardWritingOptions)writingOptionsForType:(NSString*)type |
| pasteboard:(NSPasteboard*)pasteboard { |
| // It is critical to return 0 here if we're in an app shim process. Otherwise |
| // for drags that end in the host chrome browser process, we might end up |
| // deadlocked with the app shim waiting for a sync IPC reply from chrome, |
| // while chrome is blocked trying to get the promised drag data. |
| return remote_cocoa::ApplicationBridge::IsOutOfProcessAppShim() |
| ? 0 |
| : NSPasteboardWritingPromised; |
| } |
| |
| @end // @implementation WebDragSource |