Using -[WebItemProviderPasteboard setItemProviders:] to swap out item providers befor...
[WebKit-https.git] / Source / WebCore / platform / ios / WebItemProviderPasteboard.mm
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #import "WebItemProviderPasteboard.h"
28
29 #if ENABLE(DATA_INTERACTION)
30
31 #import "SoftLinking.h"
32 #import "UIKitSPI.h"
33 #import <Foundation/NSProgress.h>
34 #import <MobileCoreServices/MobileCoreServices.h>
35 #import <UIKit/NSString+UIItemProvider.h>
36 #import <UIKit/NSURL+UIItemProvider.h>
37 #import <UIKit/UIColor.h>
38 #import <UIKit/UIImage.h>
39 #import <UIKit/UIItemProviderReading.h>
40 #import <UIKit/UIItemProviderWriting.h>
41 #import <WebCore/FileSystemIOS.h>
42 #import <wtf/BlockPtr.h>
43 #import <wtf/OSObjectPtr.h>
44 #import <wtf/RetainPtr.h>
45
46 SOFT_LINK_FRAMEWORK(UIKit)
47 SOFT_LINK_CLASS(UIKit, UIColor)
48 SOFT_LINK_CLASS(UIKit, UIImage)
49 SOFT_LINK_CLASS(UIKit, UIItemProvider)
50
51 typedef void(^ItemProviderDataLoadCompletionHandler)(NSData *, NSError *);
52 typedef NSDictionary<NSString *, NSURL *> TypeToFileURLMap;
53
54 @interface WebItemProviderRegistrationInfo ()
55 {
56     RetainPtr<id <UIItemProviderWriting>> _representingObject;
57     RetainPtr<NSString> _typeIdentifier;
58     RetainPtr<NSData> _data;
59 }
60 @end
61
62 @implementation WebItemProviderRegistrationInfo
63
64 - (instancetype)initWithRepresentingObject:(id <UIItemProviderWriting>)representingObject typeIdentifier:(NSString *)typeIdentifier data:(NSData *)data
65 {
66     if (representingObject)
67         ASSERT(!typeIdentifier && !data);
68     else
69         ASSERT(typeIdentifier && data);
70
71     if (self = [super init]) {
72         _representingObject = representingObject;
73         _typeIdentifier = typeIdentifier;
74         _data = data;
75     }
76     return self;
77 }
78
79 - (id <UIItemProviderWriting>)representingObject
80 {
81     return _representingObject.get();
82 }
83
84 - (NSString *)typeIdentifier
85 {
86     return _typeIdentifier.get();
87 }
88
89 - (NSData *)data
90 {
91     return _data.get();
92 }
93
94 @end
95
96 @interface WebItemProviderRegistrationInfoList ()
97 {
98     RetainPtr<NSMutableArray> _items;
99 }
100 @end
101
102 @implementation WebItemProviderRegistrationInfoList
103
104 - (instancetype)init
105 {
106     if (self = [super init]) {
107         _items = adoptNS([[NSMutableArray alloc] init]);
108         _estimatedDisplayedSize = CGSizeZero;
109     }
110
111     return self;
112 }
113
114 - (void)addData:(NSData *)data forType:(NSString *)typeIdentifier
115 {
116     [_items addObject:[[[WebItemProviderRegistrationInfo alloc] initWithRepresentingObject:nil typeIdentifier:typeIdentifier data:data] autorelease]];
117 }
118
119 - (void)addRepresentingObject:(id <UIItemProviderWriting>)object
120 {
121     ASSERT([object conformsToProtocol:@protocol(UIItemProviderWriting)]);
122     [_items addObject:[[[WebItemProviderRegistrationInfo alloc] initWithRepresentingObject:object typeIdentifier:nil data:nil] autorelease]];
123 }
124
125 - (NSUInteger)numberOfItems
126 {
127     return [_items count];
128 }
129
130 - (WebItemProviderRegistrationInfo *)itemAtIndex:(NSUInteger)index
131 {
132     if (index >= self.numberOfItems)
133         return nil;
134
135     return [_items objectAtIndex:index];
136 }
137
138 - (void)enumerateItems:(void (^)(WebItemProviderRegistrationInfo *, NSUInteger))block
139 {
140     for (NSUInteger index = 0; index < self.numberOfItems; ++index)
141         block([self itemAtIndex:index], index);
142 }
143
144 @end
145
146 @interface WebItemProviderPasteboard ()
147
148 @property (nonatomic) NSInteger numberOfItems;
149 @property (nonatomic) NSInteger changeCount;
150 @property (nonatomic) NSInteger pendingOperationCount;
151
152 @end
153
154 @implementation WebItemProviderPasteboard {
155     // FIXME: These ivars should be refactored to be Vector<RetainPtr<Type>> instead of generic NSArrays.
156     RetainPtr<NSArray> _itemProviders;
157     RetainPtr<NSArray> _cachedTypeIdentifiers;
158     RetainPtr<NSArray> _typeToFileURLMaps;
159     RetainPtr<NSArray> _supportedTypeIdentifiers;
160     RetainPtr<NSArray> _registrationInfoLists;
161 }
162
163 + (instancetype)sharedInstance
164 {
165     static WebItemProviderPasteboard *sharedPasteboard = nil;
166     static dispatch_once_t onceToken;
167     dispatch_once(&onceToken, ^() {
168         sharedPasteboard = [[WebItemProviderPasteboard alloc] init];
169     });
170     return sharedPasteboard;
171 }
172
173 - (instancetype)init
174 {
175     if (self = [super init]) {
176         _itemProviders = adoptNS([[NSArray alloc] init]);
177         _changeCount = 0;
178         _pendingOperationCount = 0;
179         _typeToFileURLMaps = adoptNS([[NSArray alloc] init]);
180         _supportedTypeIdentifiers = nil;
181         _registrationInfoLists = nil;
182     }
183     return self;
184 }
185
186 - (void)updateSupportedTypeIdentifiers:(NSArray<NSString *> *)types
187 {
188     _supportedTypeIdentifiers = types;
189 }
190
191 - (NSArray<NSString *> *)pasteboardTypesByFidelityForItemAtIndex:(NSUInteger)index
192 {
193     return [self itemProviderAtIndex:index].registeredTypeIdentifiers ?: @[ ];
194 }
195
196 - (NSArray<NSString *> *)pasteboardTypes
197 {
198     if (_cachedTypeIdentifiers)
199         return _cachedTypeIdentifiers.get();
200
201     NSMutableSet<NSString *> *allTypes = [NSMutableSet set];
202     NSMutableArray<NSString *> *allTypesInOrder = [NSMutableArray array];
203     for (UIItemProvider *provider in _itemProviders.get()) {
204         for (NSString *typeIdentifier in provider.registeredTypeIdentifiers) {
205             if ([allTypes containsObject:typeIdentifier])
206                 continue;
207
208             [allTypes addObject:typeIdentifier];
209             [allTypesInOrder addObject:typeIdentifier];
210         }
211     }
212     _cachedTypeIdentifiers = allTypesInOrder;
213     return _cachedTypeIdentifiers.get();
214 }
215
216 - (NSArray<UIItemProvider *> *)itemProviders
217 {
218     return _itemProviders.get();
219 }
220
221 - (void)setItemProviders:(NSArray<UIItemProvider *> *)itemProviders
222 {
223     itemProviders = itemProviders ?: [NSArray array];
224     if (_itemProviders == itemProviders || [_itemProviders isEqualToArray:itemProviders])
225         return;
226
227     _itemProviders = itemProviders;
228     _changeCount++;
229     _cachedTypeIdentifiers = nil;
230     _registrationInfoLists = nil;
231
232     NSMutableArray *typeToFileURLMaps = [NSMutableArray arrayWithCapacity:itemProviders.count];
233     [itemProviders enumerateObjectsUsingBlock:[typeToFileURLMaps] (UIItemProvider *, NSUInteger, BOOL *) {
234         [typeToFileURLMaps addObject:@{ }];
235     }];
236 }
237
238 - (NSInteger)numberOfItems
239 {
240     return [_itemProviders count];
241 }
242
243 - (void)setItemsUsingRegistrationInfoLists:(NSArray<WebItemProviderRegistrationInfoList *> *)itemLists
244 {
245     NSMutableArray *providers = [NSMutableArray array];
246     for (WebItemProviderRegistrationInfoList *itemList in itemLists) {
247         if (!itemList.numberOfItems)
248             continue;
249
250         auto itemProvider = adoptNS([allocUIItemProviderInstance() init]);
251         [itemList enumerateItems:[itemProvider] (WebItemProviderRegistrationInfo *item, NSUInteger) {
252             if (item.representingObject) {
253                 [itemProvider registerObject:item.representingObject visibility:UIItemProviderRepresentationOptionsVisibilityAll];
254                 return;
255             }
256
257             if (!item.typeIdentifier.length || !item.data.length)
258                 return;
259
260             RetainPtr<NSData> itemData = item.data;
261             [itemProvider registerDataRepresentationForTypeIdentifier:item.typeIdentifier visibility:UIItemProviderRepresentationOptionsVisibilityAll loadHandler:[itemData] (ItemProviderDataLoadCompletionHandler completionHandler) -> NSProgress * {
262                 completionHandler(itemData.get(), nil);
263                 return nil;
264             }];
265         }];
266 #pragma clang diagnostic push
267 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
268         [itemProvider setEstimatedDisplayedSize:itemList.estimatedDisplayedSize];
269 #pragma clang diagnostic pop
270         [itemProvider setSuggestedName:itemList.suggestedName];
271         [providers addObject:itemProvider.get()];
272     }
273
274     self.itemProviders = providers;
275     _registrationInfoLists = itemLists;
276 }
277
278 - (NSData *)_preLoadedDataConformingToType:(NSString *)typeIdentifier forItemProviderAtIndex:(NSUInteger)index
279 {
280     if ([_typeToFileURLMaps count] != [_itemProviders count]) {
281         ASSERT_NOT_REACHED();
282         return nil;
283     }
284
285     TypeToFileURLMap *typeToFileURLMap = [_typeToFileURLMaps objectAtIndex:index];
286     for (NSString *loadedType in typeToFileURLMap) {
287         if (!UTTypeConformsTo((CFStringRef)loadedType, (CFStringRef)typeIdentifier))
288             continue;
289
290         // We've already loaded data relevant for this UTI type onto disk, so there's no need to ask the UIItemProvider for the same data again.
291         if (NSData *result = [NSData dataWithContentsOfURL:typeToFileURLMap[loadedType] options:NSDataReadingMappedIfSafe error:nil])
292             return result;
293     }
294     return nil;
295 }
296
297 - (NSArray *)dataForPasteboardType:(NSString *)pasteboardType inItemSet:(NSIndexSet *)itemSet
298 {
299     auto values = adoptNS([[NSMutableArray alloc] init]);
300     RetainPtr<WebItemProviderPasteboard> retainedSelf = self;
301     [itemSet enumerateIndexesUsingBlock:[retainedSelf, pasteboardType, values] (NSUInteger index, BOOL *) {
302         UIItemProvider *provider = [retainedSelf itemProviderAtIndex:index];
303         if (!provider)
304             return;
305
306         if (NSData *loadedData = [retainedSelf _preLoadedDataConformingToType:pasteboardType forItemProviderAtIndex:index])
307             [values addObject:loadedData];
308     }];
309     return values.autorelease();
310 }
311
312 static NSArray<Class<UIItemProviderReading>> *allLoadableClasses()
313 {
314     return @[ [getUIColorClass() class], [getUIImageClass() class], [NSURL class], [NSString class], [NSAttributedString class] ];
315 }
316
317 static Class classForTypeIdentifier(NSString *typeIdentifier, NSString *&outTypeIdentifierToLoad)
318 {
319     outTypeIdentifierToLoad = typeIdentifier;
320
321     // First, try to load a platform UIItemProviderReading-conformant object as-is.
322     for (Class<UIItemProviderReading> loadableClass in allLoadableClasses()) {
323         if ([[loadableClass readableTypeIdentifiersForItemProvider] containsObject:(NSString *)typeIdentifier])
324             return loadableClass;
325     }
326
327     // If we were unable to load any object, check if the given type identifier is still something
328     // WebKit knows how to handle.
329     if ([typeIdentifier isEqualToString:(NSString *)kUTTypeHTML]) {
330         // Load kUTTypeHTML as a plain text HTML string.
331         outTypeIdentifierToLoad = (NSString *)kUTTypePlainText;
332         return [NSString class];
333     }
334
335     return nil;
336 }
337
338 - (NSArray *)valuesForPasteboardType:(NSString *)pasteboardType inItemSet:(NSIndexSet *)itemSet
339 {
340     auto values = adoptNS([[NSMutableArray alloc] init]);
341     RetainPtr<WebItemProviderPasteboard> retainedSelf = self;
342     [itemSet enumerateIndexesUsingBlock:[retainedSelf, pasteboardType, values] (NSUInteger index, BOOL *) {
343         UIItemProvider *provider = [retainedSelf itemProviderAtIndex:index];
344         if (!provider)
345             return;
346
347         NSString *typeIdentifierToLoad;
348         Class readableClass = classForTypeIdentifier(pasteboardType, typeIdentifierToLoad);
349         if (!readableClass)
350             return;
351
352         NSData *preloadedData = [retainedSelf _preLoadedDataConformingToType:pasteboardType forItemProviderAtIndex:index];
353         if (!preloadedData)
354             return;
355
356         if (auto readObject = adoptNS([[readableClass alloc] initWithItemProviderData:preloadedData typeIdentifier:(NSString *)typeIdentifierToLoad error:nil]))
357             [values addObject:readObject.get()];
358     }];
359
360     return values.autorelease();
361 }
362
363 - (NSInteger)changeCount
364 {
365     return _changeCount;
366 }
367
368 - (NSArray<NSURL *> *)fileURLsForDataInteraction
369 {
370     NSMutableArray<NSURL *> *fileURLs = [NSMutableArray array];
371     for (TypeToFileURLMap *typeToFileURLMap in _typeToFileURLMaps.get())
372         [fileURLs addObjectsFromArray:[typeToFileURLMap allValues]];
373     return fileURLs;
374 }
375
376 - (NSInteger)numberOfFiles
377 {
378     NSInteger numberOfFiles = 0;
379     for (UIItemProvider *itemProvider in _itemProviders.get()) {
380         for (NSString *identifier in itemProvider.registeredTypeIdentifiers) {
381             if (!UTTypeConformsTo((__bridge CFStringRef)identifier, kUTTypeContent))
382                 continue;
383             ++numberOfFiles;
384             break;
385         }
386     }
387     return numberOfFiles;
388 }
389
390 static NSURL *temporaryFileURLForDataInteractionContent(NSURL *url, NSString *suggestedName)
391 {
392     static NSString *defaultDataInteractionFileName = @"file";
393     static NSString *dataInteractionDirectoryPrefix = @"data-interaction";
394     if (!url)
395         return nil;
396
397     NSString *temporaryDataInteractionDirectory = WebCore::createTemporaryDirectory(dataInteractionDirectoryPrefix);
398     if (!temporaryDataInteractionDirectory)
399         return nil;
400
401     suggestedName = suggestedName ?: defaultDataInteractionFileName;
402     if (![suggestedName containsString:@"."])
403         suggestedName = [suggestedName stringByAppendingPathExtension:url.pathExtension];
404
405     return [NSURL fileURLWithPath:[temporaryDataInteractionDirectory stringByAppendingPathComponent:suggestedName ?: url.lastPathComponent]];
406 }
407
408 - (BOOL)typeIsAppropriateForSupportedTypes:(NSString *)type
409 {
410     // A type is considered appropriate to load if it conforms to one or more supported types.
411     for (NSString *supportedTypeIdentifier in _supportedTypeIdentifiers.get()) {
412         if (UTTypeConformsTo((CFStringRef)type, (CFStringRef)supportedTypeIdentifier))
413             return YES;
414     }
415     return NO;
416 }
417
418 - (NSString *)typeIdentifierToLoadForRegisteredTypeIdentfiers:(NSArray<NSString *> *)registeredTypeIdentifiers
419 {
420     NSString *highestFidelityContentType = nil;
421     for (NSString *registeredTypeIdentifier in registeredTypeIdentifiers) {
422         if ([self typeIsAppropriateForSupportedTypes:registeredTypeIdentifier])
423             return registeredTypeIdentifier;
424
425         if (!highestFidelityContentType && UTTypeConformsTo((CFStringRef)registeredTypeIdentifier, kUTTypeContent))
426             highestFidelityContentType = registeredTypeIdentifier;
427     }
428     return highestFidelityContentType;
429 }
430
431 - (void)doAfterLoadingProvidedContentIntoFileURLs:(WebItemProviderFileLoadBlock)action
432 {
433     [self doAfterLoadingProvidedContentIntoFileURLs:action synchronousTimeout:0];
434 }
435
436 - (void)doAfterLoadingProvidedContentIntoFileURLs:(WebItemProviderFileLoadBlock)action synchronousTimeout:(NSTimeInterval)synchronousTimeout
437 {
438     auto changeCountBeforeLoading = _changeCount;
439     auto typeToFileURLMaps = adoptNS([[NSMutableArray alloc] initWithCapacity:[_itemProviders count]]);
440
441     // First, figure out which item providers we want to try and load files from.
442     auto itemProvidersToLoad = adoptNS([[NSMutableArray alloc] init]);
443     auto typeIdentifiersToLoad = adoptNS([[NSMutableArray alloc] init]);
444     auto indicesOfitemProvidersToLoad = adoptNS([[NSMutableArray alloc] init]);
445     RetainPtr<WebItemProviderPasteboard> protectedSelf = self;
446     [_itemProviders enumerateObjectsUsingBlock:[protectedSelf, itemProvidersToLoad, typeIdentifiersToLoad, indicesOfitemProvidersToLoad, typeToFileURLMaps] (UIItemProvider *itemProvider, NSUInteger index, BOOL *) {
447         NSString *typeIdentifierToLoad = [protectedSelf typeIdentifierToLoadForRegisteredTypeIdentfiers:itemProvider.registeredTypeIdentifiers];
448         if (typeIdentifierToLoad) {
449             [itemProvidersToLoad addObject:itemProvider];
450             [typeIdentifiersToLoad addObject:typeIdentifierToLoad];
451             [indicesOfitemProvidersToLoad addObject:@(index)];
452         }
453         [typeToFileURLMaps addObject:@{ }];
454     }];
455
456     if (![itemProvidersToLoad count]) {
457         action(@[ ]);
458         return;
459     }
460
461     auto setFileURLsLock = adoptNS([[NSLock alloc] init]);
462     auto synchronousFileLoadingGroup = adoptOSObject(dispatch_group_create());
463     auto fileLoadingGroup = adoptOSObject(dispatch_group_create());
464     for (NSUInteger index = 0; index < [itemProvidersToLoad count]; ++index) {
465         RetainPtr<UIItemProvider> itemProvider = [itemProvidersToLoad objectAtIndex:index];
466         RetainPtr<NSString> typeIdentifier = [typeIdentifiersToLoad objectAtIndex:index];
467         NSUInteger indexInItemProviderArray = [[indicesOfitemProvidersToLoad objectAtIndex:index] unsignedIntegerValue];
468         RetainPtr<NSString> suggestedName = [itemProvider suggestedName];
469         dispatch_group_enter(fileLoadingGroup.get());
470         dispatch_group_enter(synchronousFileLoadingGroup.get());
471         [itemProvider loadFileRepresentationForTypeIdentifier:typeIdentifier.get() completionHandler:[synchronousFileLoadingGroup, setFileURLsLock, indexInItemProviderArray, suggestedName, typeIdentifier, typeToFileURLMaps, fileLoadingGroup] (NSURL *url, NSError *error) {
472             // After executing this completion block, UIKit removes the file at the given URL. However, we need this data to persist longer for the web content process.
473             // To address this, we hard link the given URL to a new temporary file in the temporary directory. This follows the same flow as regular file upload, in
474             // WKFileUploadPanel.mm. The temporary files are cleaned up by the system at a later time.
475             RetainPtr<NSURL> destinationURL = temporaryFileURLForDataInteractionContent(url, suggestedName.get());
476             if (destinationURL && !error && [[NSFileManager defaultManager] linkItemAtURL:url toURL:destinationURL.get() error:nil]) {
477                 [setFileURLsLock lock];
478                 [typeToFileURLMaps setObject:[NSDictionary dictionaryWithObject:destinationURL.get() forKey:typeIdentifier.get()] atIndexedSubscript:indexInItemProviderArray];
479                 [setFileURLsLock unlock];
480             }
481             dispatch_group_leave(fileLoadingGroup.get());
482             dispatch_group_leave(synchronousFileLoadingGroup.get());
483         }];
484     }
485
486     RetainPtr<WebItemProviderPasteboard> retainedSelf = self;
487     auto itemLoadCompletion = [retainedSelf, synchronousFileLoadingGroup, fileLoadingGroup, typeToFileURLMaps, completionBlock = makeBlockPtr(action), changeCountBeforeLoading] {
488         if (changeCountBeforeLoading == retainedSelf->_changeCount)
489             retainedSelf->_typeToFileURLMaps = typeToFileURLMaps;
490
491         completionBlock([retainedSelf fileURLsForDataInteraction]);
492     };
493
494     if (synchronousTimeout > 0 && !dispatch_group_wait(synchronousFileLoadingGroup.get(), dispatch_time(DISPATCH_TIME_NOW, synchronousTimeout * NSEC_PER_SEC))) {
495         itemLoadCompletion();
496         return;
497     }
498
499     dispatch_group_notify(fileLoadingGroup.get(), dispatch_get_main_queue(), itemLoadCompletion);
500 }
501
502 - (WebItemProviderRegistrationInfoList *)registrationInfoAtIndex:(NSUInteger)index
503 {
504     return index < [_registrationInfoLists count] ? [_registrationInfoLists objectAtIndex:index] : nil;
505 }
506
507 - (UIItemProvider *)itemProviderAtIndex:(NSUInteger)index
508 {
509     return index < [_itemProviders count] ? [_itemProviders objectAtIndex:index] : nil;
510 }
511
512 - (BOOL)hasPendingOperation
513 {
514     return _pendingOperationCount;
515 }
516
517 - (void)incrementPendingOperationCount
518 {
519     _pendingOperationCount++;
520 }
521
522 - (void)decrementPendingOperationCount
523 {
524     _pendingOperationCount--;
525 }
526
527 - (void)enumerateItemProvidersWithBlock:(void (^)(UIItemProvider *itemProvider, NSUInteger index, BOOL *stop))block
528 {
529     [_itemProviders enumerateObjectsUsingBlock:block];
530 }
531
532 @end
533
534 #endif // ENABLE(DATA_INTERACTION)