[Attachment Support] Support dragging attachment elements out as files on iOS
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 11 Jan 2018 15:41:39 +0000 (15:41 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 11 Jan 2018 15:41:39 +0000 (15:41 +0000)
https://bugs.webkit.org/show_bug.cgi?id=181199
<rdar://problem/36299316>

Reviewed by Tim Horton, Andy Estes and Joseph Pecoraro.

Source/WebCore:

Adds support for dragging "files" (i.e. creating item providers with preferred attachment presentation styles)
from attachment elements on iOS for Mail. See below for more detail.

Tests:  WKAttachmentTestsIOS.DragAttachmentInsertedAsData
        WKAttachmentTestsIOS.DragAttachmentInsertedAsFile

* page/DragController.cpp:
(WebCore::DragController::platformContentTypeForBlobType const):
(WebCore::DragController::dragAttachmentElement):
* page/DragController.h:
* page/mac/DragControllerMac.mm:
(WebCore::DragController::platformContentTypeForBlobType const):

Add a private method to convert the type of a promised blob to a platform type. For Cocoa platforms, this
converts the blob type (either a UTI or a MIME type) to a UTI for the platform to consume.

* platform/ios/WebItemProviderPasteboard.h:
* platform/ios/WebItemProviderPasteboard.mm:

Refactor WebItemProviderRegistrationInfo. WebItemProviderRegistrationInfo currently encapsulates a single item
provider registration call, and contains either a type identifier and data buffer, or an NSItemProviderWriting-
conformant object. To register an item provider using a WebItemProviderRegistrationInfo, the item provider
pasteboard currently checks to see whether the info contains an object or a type and data.

This patch removes WebItemProviderRegistrationInfo and replaces it with WebItemProviderDataRegistrar. Objects
that implement this protocol know how to take an NSItemProvider and register data to it. So far, there are
three implementations below.

(-[WebItemProviderDataRegistrar initWithData:type:]):
(-[WebItemProviderDataRegistrar typeIdentifier]):
(-[WebItemProviderDataRegistrar data]):
(-[WebItemProviderDataRegistrar typeIdentifierForClient]):
(-[WebItemProviderDataRegistrar dataForClient]):
(-[WebItemProviderDataRegistrar registerItemProvider:]):
(-[WebItemProviderDataRegistrar description]):

A data registrar takes a UTI and data buffer, and registers the UTI to the data. This replaces a
WebItemProviderRegistrationInfo with both a type and data, but no representing object.

(-[WebItemProviderWritableObjectRegistrar initWithObject:]):
(-[WebItemProviderWritableObjectRegistrar representingObjectForClient]):
(-[WebItemProviderWritableObjectRegistrar registerItemProvider:]):
(-[WebItemProviderWritableObjectRegistrar description]):

The writable object registrar writes an NSItemProviderWriting-conformant object to an item provider. This
replaces a WebItemProviderRegistrationInfo with only a representing object.

(-[WebItemProviderPromisedFileRegistrar initWithType:callback:]):
(-[WebItemProviderPromisedFileRegistrar registerItemProvider:]):
(-[WebItemProviderPromisedFileRegistrar description]):
(-[WebItemProviderRegistrationInfoList addData:forType:]):
(-[WebItemProviderRegistrationInfoList addRepresentingObject:]):
(-[WebItemProviderRegistrationInfoList addPromisedType:fileCallback:]):

Helper methods to add new registrars to a registration info list.

(-[WebItemProviderRegistrationInfoList itemAtIndex:]):
(-[WebItemProviderRegistrationInfoList enumerateItems:]):
(-[WebItemProviderRegistrationInfoList itemProvider]):
(-[WebItemProviderRegistrationInfoList description]):
(-[WebItemProviderRegistrationInfo initWithRepresentingObject:typeIdentifier:data:]): Deleted.
(-[WebItemProviderRegistrationInfo representingObject]): Deleted.
(-[WebItemProviderRegistrationInfo typeIdentifier]): Deleted.

Source/WebKit:

Implement support for registering and beginning a drag with promised blob info. See below for more detail.

* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKDragSessionContext addTemporaryDirectory:]):
(-[WKDragSessionContext cleanUpTemporaryDirectories]):

Introduce WKDragSessionContext, which represents the localContext of a UIDragSession initiated in WebKit. The
blob promise dragging codepath uses this to register temporary directories when saving blob data to a location
on disk; when all data transfers are finished, or if the drag interaction is being reset, we then use
-cleanUpTemporaryDirectories to remove each temporary directory.

(existingLocalDragSessionContext):
(ensureLocalDragSessionContext):

Helper methods to set the UIDragSession's localContext to a WKDragSessionContext and query for any existing
context.

(-[WKContentView cleanupInteraction]):

Before the content view's UIDragInteraction goes away, clean up any temporary directories added to the
UIDragSession.

(-[WKContentView _prepareToDragPromisedBlob:]):

When dragging with a promised blob, register a new item provider on the pasteboard representing the blob data,
along with any additional metadata associated with the blob. For the promise callback, call out to the network
process to write the blob data to a temporary path; when done, call the NSItemProvider's completion handler with
the temporary blob data location.

(-[WKContentView _itemsForBeginningOrAddingToSessionWithRegistrationList:stagedDragSource:]):
(-[WKContentView dragInteraction:sessionDidTransferItems:]):

Use this delegate hook as an opportunity to remove any temporary directories created when promised blob data is
requested upon drop. Since we know the drag session that has finished transferring data, we simply ask its local
context (a WKDragSessionContext) to remove any temporary filepaths it has created.

Tools:

Add support in the drag and drop simulator for testing blob-backed attachment element dragging, and also add new
attachment API tests.

* TestWebKitAPI/Tests/WebKitCocoa/WKAttachmentTests.mm:
(-[NSItemProvider expectType:withData:]):
(TestWebKitAPI::TEST):

Add two new WKAttachmentTests to exercise dragging data- and file-backed blobs via attachment elements. These
tests first insert attachments via drop or WKWebView SPI, and then drag these attachments out and use the
-expectType:withData: helper to inspect the item providers created from the drag source.

* TestWebKitAPI/Tests/ios/DataInteractionTests.mm:
(TestWebKitAPI::TEST):
* TestWebKitAPI/ios/DataInteractionSimulator.h:
* TestWebKitAPI/ios/DataInteractionSimulator.mm:
(-[MockDragSession localContext]):
(-[MockDragSession setLocalContext:]):
(-[DataInteractionSimulator _resetSimulatedState]):
(-[DataInteractionSimulator simulateAllTouchesCanceled:]):
(-[DataInteractionSimulator _concludeDataInteractionAndPerformOperationIfNecessary]):
(-[DataInteractionSimulator _advanceProgress]):
(-[DataInteractionSimulator endDataTransfer]):

Make some tweaks to the iOS drag and drop simulator. In particular, this patch (1) adds a new hook to tell
WebKit that data transfers have been completed, (2) fixes incorrect drop proposal handling when returning
UIDropOperationForbidden by replacing _shouldPerformOperation with a UIDropProposal, and (3) teach the
MockDragSession to hold on to a localContext.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@226779 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
Source/WebCore/ChangeLog
Source/WebCore/page/DragController.cpp
Source/WebCore/page/DragController.h
Source/WebCore/page/mac/DragControllerMac.mm
Source/WebCore/platform/ios/WebItemProviderPasteboard.h
Source/WebCore/platform/ios/WebItemProviderPasteboard.mm
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WebKitCocoa/WKAttachmentTests.mm
Tools/TestWebKitAPI/Tests/ios/DataInteractionTests.mm
Tools/TestWebKitAPI/ios/DataInteractionSimulator.h
Tools/TestWebKitAPI/ios/DataInteractionSimulator.mm

index d46310c..d421669 100644 (file)
@@ -1,3 +1,75 @@
+2018-01-11  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Attachment Support] Support dragging attachment elements out as files on iOS
+        https://bugs.webkit.org/show_bug.cgi?id=181199
+        <rdar://problem/36299316>
+
+        Reviewed by Tim Horton, Andy Estes and Joseph Pecoraro.
+
+        Adds support for dragging "files" (i.e. creating item providers with preferred attachment presentation styles)
+        from attachment elements on iOS for Mail. See below for more detail.
+
+        Tests:  WKAttachmentTestsIOS.DragAttachmentInsertedAsData
+                WKAttachmentTestsIOS.DragAttachmentInsertedAsFile
+
+        * page/DragController.cpp:
+        (WebCore::DragController::platformContentTypeForBlobType const):
+        (WebCore::DragController::dragAttachmentElement):
+        * page/DragController.h:
+        * page/mac/DragControllerMac.mm:
+        (WebCore::DragController::platformContentTypeForBlobType const):
+
+        Add a private method to convert the type of a promised blob to a platform type. For Cocoa platforms, this
+        converts the blob type (either a UTI or a MIME type) to a UTI for the platform to consume.
+
+        * platform/ios/WebItemProviderPasteboard.h:
+        * platform/ios/WebItemProviderPasteboard.mm:
+
+        Refactor WebItemProviderRegistrationInfo. WebItemProviderRegistrationInfo currently encapsulates a single item
+        provider registration call, and contains either a type identifier and data buffer, or an NSItemProviderWriting-
+        conformant object. To register an item provider using a WebItemProviderRegistrationInfo, the item provider
+        pasteboard currently checks to see whether the info contains an object or a type and data.
+
+        This patch removes WebItemProviderRegistrationInfo and replaces it with WebItemProviderDataRegistrar. Objects
+        that implement this protocol know how to take an NSItemProvider and register data to it. So far, there are
+        three implementations below.
+
+        (-[WebItemProviderDataRegistrar initWithData:type:]):
+        (-[WebItemProviderDataRegistrar typeIdentifier]):
+        (-[WebItemProviderDataRegistrar data]):
+        (-[WebItemProviderDataRegistrar typeIdentifierForClient]):
+        (-[WebItemProviderDataRegistrar dataForClient]):
+        (-[WebItemProviderDataRegistrar registerItemProvider:]):
+        (-[WebItemProviderDataRegistrar description]):
+
+        A data registrar takes a UTI and data buffer, and registers the UTI to the data. This replaces a
+        WebItemProviderRegistrationInfo with both a type and data, but no representing object.
+
+        (-[WebItemProviderWritableObjectRegistrar initWithObject:]):
+        (-[WebItemProviderWritableObjectRegistrar representingObjectForClient]):
+        (-[WebItemProviderWritableObjectRegistrar registerItemProvider:]):
+        (-[WebItemProviderWritableObjectRegistrar description]):
+
+        The writable object registrar writes an NSItemProviderWriting-conformant object to an item provider. This
+        replaces a WebItemProviderRegistrationInfo with only a representing object.
+
+        (-[WebItemProviderPromisedFileRegistrar initWithType:callback:]):
+        (-[WebItemProviderPromisedFileRegistrar registerItemProvider:]):
+        (-[WebItemProviderPromisedFileRegistrar description]):
+        (-[WebItemProviderRegistrationInfoList addData:forType:]):
+        (-[WebItemProviderRegistrationInfoList addRepresentingObject:]):
+        (-[WebItemProviderRegistrationInfoList addPromisedType:fileCallback:]):
+
+        Helper methods to add new registrars to a registration info list.
+
+        (-[WebItemProviderRegistrationInfoList itemAtIndex:]):
+        (-[WebItemProviderRegistrationInfoList enumerateItems:]):
+        (-[WebItemProviderRegistrationInfoList itemProvider]):
+        (-[WebItemProviderRegistrationInfoList description]):
+        (-[WebItemProviderRegistrationInfo initWithRepresentingObject:typeIdentifier:data:]): Deleted.
+        (-[WebItemProviderRegistrationInfo representingObject]): Deleted.
+        (-[WebItemProviderRegistrationInfo typeIdentifier]): Deleted.
+
 2018-01-11  Michael Saboff  <msaboff@apple.com>
 
         Add a DOM gadget for Spectre testing
index 55beecb..658e07f 100644 (file)
@@ -1274,6 +1274,15 @@ bool DragController::shouldUseCachedImageForDragImage(const Image& image) const
 #endif
 }
 
+#if !PLATFORM(COCOA)
+
+String DragController::platformContentTypeForBlobType(const String& type) const
+{
+    return type;
+}
+
+#endif
+
 #if ENABLE(ATTACHMENT_ELEMENT)
 
 bool DragController::dragAttachmentElement(Frame& frame, HTMLAttachmentElement& attachment)
@@ -1289,7 +1298,7 @@ bool DragController::dragAttachmentElement(Frame& frame, HTMLAttachmentElement&
 #endif
 
     auto& file = *attachment.file();
-    m_client.prepareToDragPromisedBlob({ file.url(), file.type(), file.name(), additionalTypes, additionalData });
+    m_client.prepareToDragPromisedBlob({ file.url(), platformContentTypeForBlobType(file.type()), file.name(), WTFMove(additionalTypes), WTFMove(additionalData) });
 
     return true;
 }
index d77e648..7413609 100644 (file)
@@ -128,6 +128,8 @@ struct DragState;
 #endif
         }
 
+        String platformContentTypeForBlobType(const String& type) const;
+
         void cleanupAfterSystemDrag();
         void declareAndWriteDragImage(DataTransfer&, Element&, const URL&, const String& label);
 
index f75d1e4..c20c3dc 100644 (file)
@@ -46,6 +46,7 @@
 #import "PlatformStrategies.h"
 #import "Range.h"
 #import "RuntimeEnabledFeatures.h"
+#import "UTIUtilities.h"
 
 #if ENABLE(DATA_INTERACTION)
 #import <MobileCoreServices/MobileCoreServices.h>
@@ -91,6 +92,14 @@ const IntSize& DragController::maxDragImageSize()
     return maxDragImageSize;
 }
 
+String DragController::platformContentTypeForBlobType(const String& type) const
+{
+    auto utiType = UTIFromMIMEType(type);
+    if (!utiType.isEmpty())
+        return utiType;
+    return type;
+}
+
 void DragController::cleanupAfterSystemDrag()
 {
 #if PLATFORM(MAC)
index cc88ff6..1a48ded 100644 (file)
@@ -40,20 +40,23 @@ typedef NS_ENUM(NSInteger, WebPreferredPresentationStyle) {
 
 NS_ASSUME_NONNULL_BEGIN
 
-/*! A WebItemProviderRegistrationInfo represents a single call to register something to an item provider.
- @discussion Either the representing object exists and the type identifier and data are nil, or the
- representing object is nil and the type identifier and data exist. The former represents a call to
- register an entire UIItemProviderWriting-conformant object to the item provider, while the latter
represents a call to register only a data representation for the given type identifier.
+/*! A WebItemProviderRegistrar encapsulates a single call to register something to an item provider.
+ @discussion Classes that implement this protocol each represent a different way of writing data to
+ an item provider. Some examples include setting a chunk of data corresponding to a type identifier,
+ or registering a NSItemProviderWriting-conformant object, or registering a type to a promised file
where the data has been written.
  */
-WEBCORE_EXPORT @interface WebItemProviderRegistrationInfo : NSObject
-
-@property (nonatomic, readonly, nullable, strong) id <UIItemProviderWriting> representingObject;
-@property (nonatomic, readonly, nullable, strong) NSString *typeIdentifier;
-@property (nonatomic, readonly, nullable, strong) NSData *data;
+@protocol WebItemProviderRegistrar <NSObject>
+- (void)registerItemProvider:(NSItemProvider *)itemProvider;
 
+@optional
+@property (nonatomic, readonly) id <NSItemProviderWriting> representingObjectForClient;
+@property (nonatomic, readonly) NSString *typeIdentifierForClient;
+@property (nonatomic, readonly) NSData *dataForClient;
 @end
 
+typedef void(^WebItemProviderFileCallback)(NSURL * _Nullable, NSError * _Nullable);
+
 /*! A WebItemProviderRegistrationInfoList represents a series of registration calls used to set up a
  single item provider.
  @discussion The order of items specified in the list (lowest indices first) is the order in which
@@ -65,6 +68,7 @@ WEBCORE_EXPORT @interface WebItemProviderRegistrationInfoList : NSObject
 
 - (void)addRepresentingObject:(id <UIItemProviderWriting>)object;
 - (void)addData:(NSData *)data forType:(NSString *)typeIdentifier;
+- (void)addPromisedType:(NSString *)typeIdentifier fileCallback:(void(^)(WebItemProviderFileCallback))callback;
 
 @property (nonatomic) CGSize preferredPresentationSize;
 @property (nonatomic, copy) NSString *suggestedName;
@@ -74,8 +78,8 @@ WEBCORE_EXPORT @interface WebItemProviderRegistrationInfoList : NSObject
 @property (nonatomic, copy) NSData *teamData;
 
 - (NSUInteger)numberOfItems;
-- (nullable WebItemProviderRegistrationInfo *)itemAtIndex:(NSUInteger)index;
-- (void)enumerateItems:(void(^)(WebItemProviderRegistrationInfo *item, NSUInteger index))block;
+- (nullable id <WebItemProviderRegistrar>)itemAtIndex:(NSUInteger)index;
+- (void)enumerateItems:(void(^)(id <WebItemProviderRegistrar> item, NSUInteger index))block;
 
 @end
 
index f8ede15..6eb38e9 100644 (file)
@@ -48,32 +48,83 @@ SOFT_LINK_CLASS(UIKit, UIImage)
 SOFT_LINK_CLASS(UIKit, UIItemProvider)
 
 typedef void(^ItemProviderDataLoadCompletionHandler)(NSData *, NSError *);
+typedef void(^ItemProviderFileLoadCompletionHandler)(NSURL *, BOOL, NSError *);
 typedef NSMutableDictionary<NSString *, NSURL *> TypeToFileURLMap;
 
 using WebCore::Pasteboard;
 using WebCore::PasteboardCustomData;
-@interface WebItemProviderRegistrationInfo ()
-{
-    RetainPtr<id <UIItemProviderWriting>> _representingObject;
+
+@interface WebItemProviderDataRegistrar : NSObject <WebItemProviderRegistrar>
+- (instancetype)initWithData:(NSData *)data type:(NSString *)utiType;
+@property (nonatomic, readonly) NSString *typeIdentifier;
+@property (nonatomic, readonly) NSData *data;
+@end
+
+@implementation WebItemProviderDataRegistrar {
     RetainPtr<NSString> _typeIdentifier;
     RetainPtr<NSData> _data;
 }
+
+- (instancetype)initWithData:(NSData *)data type:(NSString *)utiType
+{
+    if (!(self = [super init]))
+        return nil;
+
+    _data = data;
+    _typeIdentifier = utiType;
+    return self;
+}
+
+- (NSString *)typeIdentifier
+{
+    return _typeIdentifier.get();
+}
+
+- (NSData *)data
+{
+    return _data.get();
+}
+
+- (NSString *)typeIdentifierForClient
+{
+    return self.typeIdentifier;
+}
+
+- (NSData *)dataForClient
+{
+    return self.data;
+}
+
+- (void)registerItemProvider:(NSItemProvider *)itemProvider
+{
+    [itemProvider registerDataRepresentationForTypeIdentifier:self.typeIdentifier visibility:UIItemProviderRepresentationOptionsVisibilityAll loadHandler:[itemData = _data] (ItemProviderDataLoadCompletionHandler completionHandler) -> NSProgress * {
+        completionHandler(itemData.get(), nil);
+        return nil;
+    }];
+}
+
+- (NSString *)description
+{
+    return [NSString stringWithFormat:@"(%@ => %tu bytes)", _typeIdentifier.get(), [_data length]];
+}
+
 @end
 
-@implementation WebItemProviderRegistrationInfo
+@interface WebItemProviderWritableObjectRegistrar : NSObject <WebItemProviderRegistrar>
+- (instancetype)initWithObject:(id <UIItemProviderWriting>)representingObject;
+@property (nonatomic, readonly) id <UIItemProviderWriting> representingObject;
+@end
 
-- (instancetype)initWithRepresentingObject:(id <UIItemProviderWriting>)representingObject typeIdentifier:(NSString *)typeIdentifier data:(NSData *)data
+@implementation WebItemProviderWritableObjectRegistrar {
+    RetainPtr<id <UIItemProviderWriting>> _representingObject;
+}
+
+- (instancetype)initWithObject:(id <UIItemProviderWriting>)representingObject
 {
-    if (representingObject)
-        ASSERT(!typeIdentifier && !data);
-    else
-        ASSERT(typeIdentifier && data);
+    if (!(self = [super init]))
+        return nil;
 
-    if (self = [super init]) {
-        _representingObject = representingObject;
-        _typeIdentifier = typeIdentifier;
-        _data = data;
-    }
+    _representingObject = representingObject;
     return self;
 }
 
@@ -82,14 +133,61 @@ using WebCore::PasteboardCustomData;
     return _representingObject.get();
 }
 
+- (id <NSItemProviderWriting>)representingObjectForClient
+{
+    return self.representingObject;
+}
+
+- (void)registerItemProvider:(NSItemProvider *)itemProvider
+{
+    [itemProvider registerObject:self.representingObject visibility:UIItemProviderRepresentationOptionsVisibilityAll];
+}
+
+- (NSString *)description
+{
+    return [NSString stringWithFormat:@"(%@)", [_representingObject class]];
+}
+
+@end
+
+@interface WebItemProviderPromisedFileRegistrar : NSObject <WebItemProviderRegistrar>
+- (instancetype)initWithType:(NSString *)utiType callback:(void(^)(WebItemProviderFileCallback))callback;
+@property (nonatomic, readonly) NSString *typeIdentifier;
+@end
+
+@implementation WebItemProviderPromisedFileRegistrar {
+    RetainPtr<NSString> _typeIdentifier;
+    BlockPtr<void(WebItemProviderFileCallback)> _callback;
+}
+
+- (instancetype)initWithType:(NSString *)utiType callback:(void(^)(WebItemProviderFileCallback))callback
+{
+    if (!(self = [super init]))
+        return nil;
+
+    _typeIdentifier = utiType;
+    _callback = callback;
+    return self;
+}
+
+- (void)registerItemProvider:(NSItemProvider *)itemProvider
+{
+    [itemProvider registerFileRepresentationForTypeIdentifier:_typeIdentifier.get() fileOptions:0 visibility:NSItemProviderRepresentationVisibilityAll loadHandler:[callback = _callback] (ItemProviderFileLoadCompletionHandler completionHandler) -> NSProgress * {
+        callback([protectedCompletionHandler = makeBlockPtr(completionHandler)] (NSURL *fileURL, NSError *error) {
+            protectedCompletionHandler(fileURL, NO, error);
+        });
+        return nil;
+    }];
+}
+
 - (NSString *)typeIdentifier
 {
     return _typeIdentifier.get();
 }
 
-- (NSData *)data
+- (NSString *)description
 {
-    return _data.get();
+    return [NSString stringWithFormat:@"(file promise: %@)", _typeIdentifier.get()];
 }
 
 @end
@@ -121,13 +219,21 @@ using WebCore::PasteboardCustomData;
 
 - (void)addData:(NSData *)data forType:(NSString *)typeIdentifier
 {
-    [_representations addObject:[[[WebItemProviderRegistrationInfo alloc] initWithRepresentingObject:nil typeIdentifier:typeIdentifier data:data] autorelease]];
+    auto representation = adoptNS([[WebItemProviderDataRegistrar alloc] initWithData:data type:typeIdentifier]);
+    [_representations addObject:representation.get()];
 }
 
 - (void)addRepresentingObject:(id <UIItemProviderWriting>)object
 {
     ASSERT([object conformsToProtocol:@protocol(UIItemProviderWriting)]);
-    [_representations addObject:[[[WebItemProviderRegistrationInfo alloc] initWithRepresentingObject:object typeIdentifier:nil data:nil] autorelease]];
+    auto representation = adoptNS([[WebItemProviderWritableObjectRegistrar alloc] initWithObject:object]);
+    [_representations addObject:representation.get()];
+}
+
+- (void)addPromisedType:(NSString *)typeIdentifier fileCallback:(void(^)(WebItemProviderFileCallback))callback
+{
+    auto representation = adoptNS([[WebItemProviderPromisedFileRegistrar alloc] initWithType:typeIdentifier callback:callback]);
+    [_representations addObject:representation.get()];
 }
 
 - (NSUInteger)numberOfItems
@@ -135,7 +241,7 @@ using WebCore::PasteboardCustomData;
     return [_representations count];
 }
 
-- (WebItemProviderRegistrationInfo *)itemAtIndex:(NSUInteger)index
+- (id <WebItemProviderRegistrar>)itemAtIndex:(NSUInteger)index
 {
     if (index >= self.numberOfItems)
         return nil;
@@ -143,7 +249,7 @@ using WebCore::PasteboardCustomData;
     return [_representations objectAtIndex:index];
 }
 
-- (void)enumerateItems:(void (^)(WebItemProviderRegistrationInfo *, NSUInteger))block
+- (void)enumerateItems:(void (^)(id <WebItemProviderRegistrar>, NSUInteger))block
 {
     for (NSUInteger index = 0; index < self.numberOfItems; ++index)
         block([self itemAtIndex:index], index);
@@ -170,21 +276,8 @@ static UIPreferredPresentationStyle uiPreferredPresentationStyle(WebPreferredPre
         return nil;
 
     auto itemProvider = adoptNS([allocUIItemProviderInstance() init]);
-    for (WebItemProviderRegistrationInfo *representation in _representations.get()) {
-        if (representation.representingObject) {
-            [itemProvider registerObject:representation.representingObject visibility:UIItemProviderRepresentationOptionsVisibilityAll];
-            continue;
-        }
-
-        if (!representation.typeIdentifier.length || !representation.data.length)
-            continue;
-
-        RetainPtr<NSData> itemData = representation.data;
-        [itemProvider registerDataRepresentationForTypeIdentifier:representation.typeIdentifier visibility:UIItemProviderRepresentationOptionsVisibilityAll loadHandler:[itemData] (ItemProviderDataLoadCompletionHandler completionHandler) -> NSProgress * {
-            completionHandler(itemData.get(), nil);
-            return nil;
-        }];
-    }
+    for (id <WebItemProviderRegistrar> representation in _representations.get())
+        [representation registerItemProvider:itemProvider.get()];
     [itemProvider setPreferredPresentationSize:self.preferredPresentationSize];
     [itemProvider setSuggestedName:self.suggestedName];
     [itemProvider setPreferredPresentationStyle:uiPreferredPresentationStyle(self.preferredPresentationStyle)];
@@ -196,14 +289,10 @@ static UIPreferredPresentationStyle uiPreferredPresentationStyle(WebPreferredPre
 {
     __block NSMutableString *description = [NSMutableString string];
     [description appendFormat:@"<%@: %p", [self class], self];
-    [self enumerateItems:^(WebItemProviderRegistrationInfo *item, NSUInteger index) {
+    [self enumerateItems:^(id <WebItemProviderRegistrar> item, NSUInteger index) {
         if (index)
-            [description appendString:@","];
-
-        if (item.representingObject)
-            [description appendFormat:@" (%@: %p)", [item.representingObject class], item.representingObject];
-        else
-            [description appendFormat:@" ('%@' => %tu bytes)", item.typeIdentifier, item.data.length];
+            [description appendString:@", "];
+        [description appendString:[item description]];
     }];
     [description appendString:@">"];
     return description;
index 4a38c9b..1894e61 100644 (file)
@@ -1,3 +1,47 @@
+2018-01-11  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Attachment Support] Support dragging attachment elements out as files on iOS
+        https://bugs.webkit.org/show_bug.cgi?id=181199
+        <rdar://problem/36299316>
+
+        Reviewed by Tim Horton, Andy Estes and Joseph Pecoraro.
+
+        Implement support for registering and beginning a drag with promised blob info. See below for more detail.
+
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKDragSessionContext addTemporaryDirectory:]):
+        (-[WKDragSessionContext cleanUpTemporaryDirectories]):
+
+        Introduce WKDragSessionContext, which represents the localContext of a UIDragSession initiated in WebKit. The
+        blob promise dragging codepath uses this to register temporary directories when saving blob data to a location
+        on disk; when all data transfers are finished, or if the drag interaction is being reset, we then use
+        -cleanUpTemporaryDirectories to remove each temporary directory.
+
+        (existingLocalDragSessionContext):
+        (ensureLocalDragSessionContext):
+
+        Helper methods to set the UIDragSession's localContext to a WKDragSessionContext and query for any existing
+        context.
+
+        (-[WKContentView cleanupInteraction]):
+
+        Before the content view's UIDragInteraction goes away, clean up any temporary directories added to the
+        UIDragSession.
+
+        (-[WKContentView _prepareToDragPromisedBlob:]):
+
+        When dragging with a promised blob, register a new item provider on the pasteboard representing the blob data,
+        along with any additional metadata associated with the blob. For the promise callback, call out to the network
+        process to write the blob data to a temporary path; when done, call the NSItemProvider's completion handler with
+        the temporary blob data location.
+
+        (-[WKContentView _itemsForBeginningOrAddingToSessionWithRegistrationList:stagedDragSource:]):
+        (-[WKContentView dragInteraction:sessionDidTransferItems:]):
+
+        Use this delegate hook as an opportunity to remove any temporary directories created when promised blob data is
+        requested upon drop. Since we know the drag session that has finished transferring data, we simply ask its local
+        context (a WKDragSessionContext) to remove any temporary filepaths it has created.
+
 2018-01-10  Jeff Miller  <jeffm@apple.com>
 
         -[WKWebView _web_gestureEventWasNotHandledByWebCore:] should call -_gestureEventWasNotHandledByWebCore:
index 37dc79a..608302f 100644 (file)
@@ -40,6 +40,7 @@
 #import "TextInputSPI.h"
 #import "UIKitSPI.h"
 #import "WKActionSheetAssistant.h"
+#import "WKError.h"
 #import "WKFormInputControl.h"
 #import "WKFormSelectControl.h"
 #import "WKImagePreviewViewController.h"
@@ -53,6 +54,7 @@
 #import "WKWebViewConfigurationPrivate.h"
 #import "WKWebViewInternal.h"
 #import "WKWebViewPrivate.h"
+#import "WeakObjCPtr.h"
 #import "WebEvent.h"
 #import "WebIOSEventFactory.h"
 #import "WebPageMessages.h"
@@ -473,6 +475,57 @@ const CGFloat minimumTapHighlightRadius = 2.0;
 }
 @end
 
+#if ENABLE(DRAG_SUPPORT)
+
+@interface WKDragSessionContext : NSObject
+- (void)addTemporaryDirectory:(NSString *)temporaryDirectory;
+- (void)cleanUpTemporaryDirectories;
+@end
+
+@implementation WKDragSessionContext {
+    RetainPtr<NSMutableArray> _temporaryDirectories;
+}
+
+- (void)addTemporaryDirectory:(NSString *)temporaryDirectory
+{
+    if (!_temporaryDirectories)
+        _temporaryDirectories = adoptNS([NSMutableArray new]);
+    [_temporaryDirectories addObject:temporaryDirectory];
+}
+
+- (void)cleanUpTemporaryDirectories
+{
+    for (NSString *directory in _temporaryDirectories.get()) {
+        NSError *error = nil;
+        [[NSFileManager defaultManager] removeItemAtPath:directory error:&error];
+        RELEASE_LOG(DragAndDrop, "Removed temporary download directory: %@ with error: %@", directory, error);
+    }
+    _temporaryDirectories = nil;
+}
+
+@end
+
+static WKDragSessionContext *existingLocalDragSessionContext(id <UIDragSession> session)
+{
+    return [session.localContext isKindOfClass:[WKDragSessionContext class]] ? (WKDragSessionContext *)session.localContext : nil;
+}
+
+static WKDragSessionContext *ensureLocalDragSessionContext(id <UIDragSession> session)
+{
+    if (WKDragSessionContext *existingContext = existingLocalDragSessionContext(session))
+        return existingContext;
+
+    if (session.localContext) {
+        RELEASE_LOG(DragAndDrop, "Overriding existing local context: %@ on session: %@", session.localContext, session);
+        ASSERT_NOT_REACHED();
+    }
+
+    session.localContext = [[[WKDragSessionContext alloc] init] autorelease];
+    return (WKDragSessionContext *)session.localContext;
+}
+
+#endif // ENABLE(DRAG_SUPPORT)
+
 @interface WKContentView (WKInteractionPrivate)
 - (void)accessibilitySpeakSelectionSetContent:(NSString *)string;
 - (NSArray *)webSelectionRectsForSelectionRects:(const Vector<WebCore::SelectionRect>&)selectionRects;
@@ -627,6 +680,7 @@ const CGFloat minimumTapHighlightRadius = 2.0;
     _layerTreeTransactionIdAtLastTouchStart = 0;
 
 #if ENABLE(DATA_INTERACTION)
+    [existingLocalDragSessionContext(_dragDropInteractionState.dragSession()) cleanUpTemporaryDirectories];
     [self teardownDataInteractionDelegates];
 #endif
 
@@ -4472,8 +4526,52 @@ static NSArray<UIItemProvider *> *extractItemProvidersFromDropSession(id <UIDrop
 
 - (void)_prepareToDragPromisedBlob:(const PromisedBlobInfo&)info
 {
-    // FIXME: Add iOS support for dragging promised blob data as file promises.
-    UNUSED_PARAM(info);
+    auto session = retainPtr(_dragDropInteractionState.dragSession());
+    if (!session) {
+        ASSERT_NOT_REACHED();
+        return;
+    }
+
+    auto numberOfAdditionalTypes = info.additionalTypes.size();
+    ASSERT(numberOfAdditionalTypes == info.additionalData.size());
+
+    RELEASE_LOG(DragAndDrop, "Drag session: %p preparing to drag blob: %s", session.get(), info.blobURL.string().utf8().data());
+
+    auto registrationList = adoptNS([[WebItemProviderRegistrationInfoList alloc] init]);
+    [registrationList setPreferredPresentationStyle:WebPreferredPresentationStyleAttachment];
+    if (!info.filename.isEmpty())
+        [registrationList setSuggestedName:info.filename];
+    if (numberOfAdditionalTypes == info.additionalData.size() && numberOfAdditionalTypes) {
+        for (size_t index = 0; index < numberOfAdditionalTypes; ++index) {
+            auto nsData = info.additionalData[index]->createNSData();
+            [registrationList addData:nsData.get() forType:info.additionalTypes[index]];
+        }
+    }
+
+    [registrationList addPromisedType:info.contentType fileCallback:[session = WTFMove(session), weakSelf = WeakObjCPtr<WKContentView>(self), url = info.blobURL] (WebItemProviderFileCallback callback) {
+        auto strongSelf = weakSelf.get();
+        if (!strongSelf) {
+            callback(nil, [NSError errorWithDomain:WKErrorDomain code:WKErrorWebViewInvalidated userInfo:nil]);
+            return;
+        }
+
+        NSString *temporaryBlobDirectory = FileSystem::createTemporaryDirectory(@"blobs");
+        NSURL *destinationURL = [NSURL fileURLWithPath:[temporaryBlobDirectory stringByAppendingPathComponent:[NSUUID UUID].UUIDString]];
+
+        RELEASE_LOG(DragAndDrop, "Drag session: %p delivering promised blob at path: %@", session.get(), destinationURL.path);
+        strongSelf->_page->writeBlobToFilePath(url, destinationURL.path, [protectedURL = retainPtr(destinationURL), protectedCallback = makeBlockPtr(callback)] (bool success) {
+            if (success)
+                protectedCallback(protectedURL.get(), nil);
+            else
+                protectedCallback(nil, [NSError errorWithDomain:WKErrorDomain code:WKErrorUnknown userInfo:nil]);
+        });
+
+        [ensureLocalDragSessionContext(session.get()) addTemporaryDirectory:temporaryBlobDirectory];
+    }];
+
+    WebItemProviderPasteboard *pasteboard = [WebItemProviderPasteboard sharedInstance];
+    pasteboard.itemProviders = @[ [registrationList itemProvider] ];
+    [pasteboard stageRegistrationList:registrationList.get()];
 }
 
 - (WKDragDestinationAction)_dragDestinationActionForDropSession:(id <UIDropSession>)session
@@ -4514,11 +4612,11 @@ static NSArray<UIItemProvider *> *extractItemProvidersFromDropSession(id <UIDrop
     if ([uiDelegate respondsToSelector:@selector(_webView:adjustedDataInteractionItemProvidersForItemProvider:representingObjects:additionalData:)]) {
         auto representingObjects = adoptNS([[NSMutableArray alloc] init]);
         auto additionalData = adoptNS([[NSMutableDictionary alloc] init]);
-        [registrationList enumerateItems:[representingObjects, additionalData] (WebItemProviderRegistrationInfo *item, NSUInteger) {
-            if (item.representingObject)
-                [representingObjects addObject:item.representingObject];
-            if (item.typeIdentifier && item.data)
-                [additionalData setObject:item.data forKey:item.typeIdentifier];
+        [registrationList enumerateItems:[representingObjects, additionalData] (id <WebItemProviderRegistrar> item, NSUInteger) {
+            if ([item respondsToSelector:@selector(representingObjectForClient)])
+                [representingObjects addObject:item.representingObjectForClient];
+            if ([item respondsToSelector:@selector(typeIdentifierForClient)] && [item respondsToSelector:@selector(dataForClient)])
+                [additionalData setObject:item.dataForClient forKey:item.typeIdentifierForClient];
         }];
         adjustedItemProviders = [uiDelegate _webView:_webView adjustedDataInteractionItemProvidersForItemProvider:defaultItemProvider representingObjects:representingObjects.get() additionalData:additionalData.get()];
     } else
@@ -4718,6 +4816,11 @@ static NSArray<UIItemProvider *> *extractItemProvidersFromDropSession(id <UIDrop
     }];
 }
 
+- (void)dragInteraction:(UIDragInteraction *)interaction sessionDidTransferItems:(id <UIDragSession>)session
+{
+    [existingLocalDragSessionContext(session) cleanUpTemporaryDirectories];
+}
+
 #pragma mark - UIDropInteractionDelegate
 
 - (NSInteger)_dropInteraction:(UIDropInteraction *)interaction dataOwnerForSession:(id <UIDropSession>)session
index 35308e3..20ed704 100644 (file)
@@ -1,3 +1,39 @@
+2018-01-11  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [Attachment Support] Support dragging attachment elements out as files on iOS
+        https://bugs.webkit.org/show_bug.cgi?id=181199
+        <rdar://problem/36299316>
+
+        Reviewed by Tim Horton, Andy Estes and Joseph Pecoraro.
+
+        Add support in the drag and drop simulator for testing blob-backed attachment element dragging, and also add new
+        attachment API tests.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/WKAttachmentTests.mm:
+        (-[NSItemProvider expectType:withData:]):
+        (TestWebKitAPI::TEST):
+
+        Add two new WKAttachmentTests to exercise dragging data- and file-backed blobs via attachment elements. These
+        tests first insert attachments via drop or WKWebView SPI, and then drag these attachments out and use the
+        -expectType:withData: helper to inspect the item providers created from the drag source.
+
+        * TestWebKitAPI/Tests/ios/DataInteractionTests.mm:
+        (TestWebKitAPI::TEST):
+        * TestWebKitAPI/ios/DataInteractionSimulator.h:
+        * TestWebKitAPI/ios/DataInteractionSimulator.mm:
+        (-[MockDragSession localContext]):
+        (-[MockDragSession setLocalContext:]):
+        (-[DataInteractionSimulator _resetSimulatedState]):
+        (-[DataInteractionSimulator simulateAllTouchesCanceled:]):
+        (-[DataInteractionSimulator _concludeDataInteractionAndPerformOperationIfNecessary]):
+        (-[DataInteractionSimulator _advanceProgress]):
+        (-[DataInteractionSimulator endDataTransfer]):
+
+        Make some tweaks to the iOS drag and drop simulator. In particular, this patch (1) adds a new hook to tell
+        WebKit that data transfers have been completed, (2) fixes incorrect drop proposal handling when returning
+        UIDropOperationForbidden by replacing _shouldPerformOperation with a UIDropProposal, and (3) teach the
+        MockDragSession to hold on to a localContext.
+
 2018-01-11  Ali Juma  <ajuma@chromium.org>
 
         Unreviewed. Add Ali Juma as contributor
index 7af784c..c5decd2 100644 (file)
@@ -341,6 +341,28 @@ typedef void(^ItemProviderDataLoadHandler)(NSData *, NSError *);
     }];
 }
 
+- (void)expectType:(NSString *)type withData:(NSData *)expectedData
+{
+    BOOL containsType = [self.registeredTypeIdentifiers containsObject:type];
+    EXPECT_TRUE(containsType);
+    if (!containsType) {
+        NSLog(@"Expected: %@ to contain %@", self, type);
+        return;
+    }
+
+    __block bool done = false;
+    [self loadDataRepresentationForTypeIdentifier:type completionHandler:^(NSData *observedData, NSError *error) {
+        EXPECT_TRUE([observedData isEqualToData:expectedData]);
+        if (![observedData isEqualToData:expectedData])
+            NSLog(@"Expected data: <%tu bytes> to be equal to data: <%tu bytes>", observedData.length, expectedData.length);
+        EXPECT_TRUE(!error);
+        if (error)
+            NSLog(@"Encountered error when loading data: %@", error);
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+}
+
 @end
 
 #endif // PLATFORM(IOS)
@@ -1228,6 +1250,67 @@ TEST(WKAttachmentTestsIOS, InsertDroppedItemProvidersInOrder)
     EXPECT_WK_STREQ("application/pdf", [webView stringByEvaluatingJavaScript:@"document.querySelectorAll('attachment')[1].getAttribute('type')"]);
 }
 
+TEST(WKAttachmentTestsIOS, DragAttachmentInsertedAsFile)
+{
+    auto item = adoptNS([[NSItemProvider alloc] init]);
+    auto data = retainPtr(testPDFData());
+    [item registerData:data.get() type:(NSString *)kUTTypePDF];
+    [item setSuggestedName:@"document.pdf"];
+
+    auto webView = webViewForTestingAttachments();
+    auto draggingSimulator = adoptNS([[DataInteractionSimulator alloc] initWithWebView:webView.get()]);
+    [draggingSimulator setExternalItemProviders:@[ item.get() ]];
+    [draggingSimulator runFrom:CGPointZero to:CGPointMake(50, 50)];
+
+    // First, verify that the attachment was successfully dropped.
+    EXPECT_EQ(1U, [draggingSimulator insertedAttachments].count);
+    _WKAttachment *attachment = [draggingSimulator insertedAttachments].firstObject;
+    [attachment expectRequestedDataToBe:data.get()];
+    EXPECT_WK_STREQ("document.pdf", [webView valueOfAttribute:@"title" forQuerySelector:@"attachment"]);
+    EXPECT_WK_STREQ("application/pdf", [webView valueOfAttribute:@"type" forQuerySelector:@"attachment"]);
+
+    [webView evaluateJavaScript:@"getSelection().removeAllRanges()" completionHandler:nil];
+    [draggingSimulator setExternalItemProviders:@[ ]];
+    [draggingSimulator runFrom:CGPointMake(25, 25) to:CGPointMake(-100, -100)];
+
+    // Next, verify that dragging the attachment produces an item provider with a PDF attachment.
+    EXPECT_EQ(1U, [draggingSimulator sourceItemProviders].count);
+    NSItemProvider *itemProvider = [draggingSimulator sourceItemProviders].firstObject;
+    EXPECT_EQ(UIPreferredPresentationStyleAttachment, itemProvider.preferredPresentationStyle);
+    [itemProvider expectType:(NSString *)kUTTypePDF withData:data.get()];
+    EXPECT_WK_STREQ("document.pdf", [itemProvider suggestedName]);
+    [draggingSimulator endDataTransfer];
+}
+
+TEST(WKAttachmentTestsIOS, DragAttachmentInsertedAsData)
+{
+    auto webView = webViewForTestingAttachments();
+    auto data = retainPtr(testPDFData());
+    RetainPtr<_WKAttachment> attachment;
+    {
+        ObserveAttachmentUpdatesForScope observer(webView.get());
+        attachment = [webView synchronouslyInsertAttachmentWithFilename:@"document.pdf" contentType:@"application/pdf" data:data.get() options:displayOptionsWithMode(_WKAttachmentDisplayModeAsIcon)];
+        observer.expectAttachmentUpdates(@[], @[attachment.get()]);
+    }
+
+    // First, verify that the attachment was successfully inserted from raw data.
+    [attachment expectRequestedDataToBe:data.get()];
+    EXPECT_WK_STREQ("document.pdf", [webView valueOfAttribute:@"title" forQuerySelector:@"attachment"]);
+    EXPECT_WK_STREQ("application/pdf", [webView valueOfAttribute:@"type" forQuerySelector:@"attachment"]);
+
+    [webView evaluateJavaScript:@"getSelection().removeAllRanges()" completionHandler:nil];
+    auto draggingSimulator = adoptNS([[DataInteractionSimulator alloc] initWithWebView:webView.get()]);
+    [draggingSimulator runFrom:CGPointMake(25, 25) to:CGPointMake(-100, -100)];
+
+    // Next, verify that dragging the attachment produces an item provider with a PDF attachment.
+    EXPECT_EQ(1U, [draggingSimulator sourceItemProviders].count);
+    NSItemProvider *itemProvider = [draggingSimulator sourceItemProviders].firstObject;
+    EXPECT_EQ(UIPreferredPresentationStyleAttachment, itemProvider.preferredPresentationStyle);
+    [itemProvider expectType:(NSString *)kUTTypePDF withData:data.get()];
+    EXPECT_WK_STREQ("document.pdf", [itemProvider suggestedName]);
+    [draggingSimulator endDataTransfer];
+}
+
 #endif // PLATFORM(IOS)
 
 } // namespace TestWebKitAPI
index f6627f5..4de7fc3 100644 (file)
@@ -1119,7 +1119,7 @@ TEST(DataInteractionTests, OverrideDataInteractionOperation)
     [dataInteractionSimulator setOverrideDataInteractionOperationBlock:^NSUInteger(NSUInteger operation, id session)
     {
         EXPECT_EQ(0U, operation);
-        return 1;
+        return UIDropOperationCopy;
     }];
     [dataInteractionSimulator setDataInteractionOperationCompletionBlock:^(BOOL handled, NSArray *itemProviders) {
         EXPECT_FALSE(handled);
index 5694ac3..3d37205 100644 (file)
 @end
 
 @interface MockDropSession : MockDragDropSession <UIDropSession>
-@property (nonatomic, strong) id localContext;
 @end
 
 @interface MockDragSession : MockDragDropSession <UIDragSession>
-@property (nonatomic, strong) id localContext;
-@property (nonatomic, strong) id context;
 @end
 
 extern NSString * const DataInteractionEnterEventName;
@@ -142,10 +139,11 @@ typedef NS_ENUM(NSInteger, DataInteractionPhase) {
     RetainPtr<NSMutableArray<_WKAttachment *>> _removedAttachments;
 
     bool _isDoneWaitingForInputSession;
-    BOOL _shouldPerformOperation;
     double _currentProgress;
     bool _isDoneWithCurrentRun;
     DataInteractionPhase _phase;
+
+    RetainPtr<UIDropProposal> _currentDropProposal;
 }
 
 - (instancetype)initWithWebView:(TestWKWebView *)webView;
@@ -153,6 +151,7 @@ typedef NS_ENUM(NSInteger, DataInteractionPhase) {
 - (void)runFrom:(CGPoint)startLocation to:(CGPoint)endLocation;
 - (void)runFrom:(CGPoint)startLocation to:(CGPoint)endLocation additionalItemRequestLocations:(ProgressToCGPointValueMap)additionalItemRequestLocations;
 - (void)waitForInputSession;
+- (void)endDataTransfer;
 
 @property (nonatomic) BOOL allowsFocusToStartInputSession;
 @property (nonatomic) BOOL shouldEnsureUIApplication;
index 3299fb0..856b0b1 100644 (file)
@@ -235,7 +235,9 @@ NSString * const DataInteractionStartEventName = @"dragstart";
 
 @end
 
-@implementation MockDragSession
+@implementation MockDragSession {
+    RetainPtr<id> _localContext;
+}
 
 - (instancetype)initWithWindow:(UIWindow *)window allowMove:(BOOL)allowMove
 {
@@ -259,6 +261,16 @@ NSString * const DataInteractionStartEventName = @"dragstart";
     return nil;
 }
 
+- (id)localContext
+{
+    return _localContext.get();
+}
+
+- (void)setLocalContext:(id)localContext
+{
+    _localContext = localContext;
+}
+
 @end
 
 static double progressIncrementStep = 0.033;
@@ -322,7 +334,7 @@ static NSArray *dataInteractionEventNames()
     _finalSelectionRects = @[ ];
     _dragSession = nil;
     _dropSession = nil;
-    _shouldPerformOperation = NO;
+    _currentDropProposal = nil;
     _lastKnownDragCaretRect = CGRectZero;
     _remainingAdditionalItemRequestLocationsByProgress = nil;
     _queuedAdditionalItemRequestLocations = adoptNS([[NSMutableArray alloc] init]);
@@ -341,7 +353,7 @@ static NSArray *dataInteractionEventNames()
     _currentProgress = 1;
     _isDoneWithCurrentRun = true;
     if (_dragSession)
-        [[_webView dragInteractionDelegate] dragInteraction:[_webView dragInteraction] session:_dragSession.get() didEndWithOperation:UIDropOperationCopy];
+        [[_webView dragInteractionDelegate] dragInteraction:[_webView dragInteraction] session:_dragSession.get() didEndWithOperation:UIDropOperationCancel];
 }
 
 - (void)runFrom:(CGPoint)startLocation to:(CGPoint)endLocation
@@ -405,7 +417,8 @@ static NSArray *dataInteractionEventNames()
 - (void)_concludeDataInteractionAndPerformOperationIfNecessary
 {
     _lastKnownDragCaretRect = [_webView _dragCaretRect];
-    if (_shouldPerformOperation) {
+    auto operation = [_currentDropProposal operation];
+    if (operation != UIDropOperationCancel && operation != UIDropOperationForbidden) {
         [[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] performDrop:_dropSession.get()];
         _phase = DataInteractionPerforming;
     } else {
@@ -416,7 +429,7 @@ static NSArray *dataInteractionEventNames()
     [[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] sessionDidEnd:_dropSession.get()];
 
     if (_dragSession)
-        [[_webView dragInteractionDelegate] dragInteraction:[_webView dragInteraction] session:_dragSession.get() didEndWithOperation:UIDropOperationCopy];
+        [[_webView dragInteractionDelegate] dragInteraction:[_webView dragInteraction] session:_dragSession.get() didEndWithOperation:operation];
 }
 
 - (void)_enqueuePendingAdditionalItemRequestLocations
@@ -514,8 +527,9 @@ static NSArray *dataInteractionEventNames()
         _phase = DataInteractionEntered;
         break;
     case DataInteractionEntered: {
-        auto operation = static_cast<UIDropOperation>([[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] sessionDidUpdate:_dropSession.get()].operation);
-        _shouldPerformOperation = operation == UIDropOperationCopy || ([_dropSession allowsMoveOperation] && operation != UIDropOperationCancel);
+        _currentDropProposal = [[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] sessionDidUpdate:_dropSession.get()];
+        if (![self shouldAllowMoveOperation] && [_currentDropProposal operation] == UIDropOperationMove)
+            _currentDropProposal = adoptNS([[UIDropProposal alloc] initWithDropOperation:UIDropOperationCancel]);
         break;
     }
     default:
@@ -588,6 +602,11 @@ static NSArray *dataInteractionEventNames()
     return _removedAttachments.get();
 }
 
+- (void)endDataTransfer
+{
+    [[_webView dragInteractionDelegate] dragInteraction:[_webView dragInteraction] sessionDidTransferItems:_dragSession.get()];
+}
+
 #pragma mark - WKUIDelegatePrivate
 
 - (void)_webView:(WKWebView *)webView dataInteractionOperationWasHandled:(BOOL)handled forSession:(id)session itemProviders:(NSArray<UIItemProvider *> *)itemProviders