8ae4d1fc2179dd092ccefb744c42bc2189e2daf0
[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 <Foundation/NSProgress.h>
32 #import <MobileCoreServices/MobileCoreServices.h>
33 #import <UIKit/UIColor.h>
34 #import <UIKit/UIImage.h>
35 #import <WebCore/FileSystemIOS.h>
36 #import <WebCore/Pasteboard.h>
37 #import <pal/spi/ios/UIKitSPI.h>
38 #import <wtf/BlockPtr.h>
39 #import <wtf/OSObjectPtr.h>
40 #import <wtf/RetainPtr.h>
41 #import <wtf/SoftLinking.h>
42
43 SOFT_LINK_FRAMEWORK(UIKit)
44 SOFT_LINK_CLASS(UIKit, UIColor)
45 SOFT_LINK_CLASS(UIKit, UIImage)
46 SOFT_LINK_CLASS(UIKit, UIItemProvider)
47
48 // FIXME: Remove once +objectWithItemProviderData:typeIdentifier:error: is available in the public SDK.
49 @interface NSObject (Foundation_NSItemProvider_Staging)
50 + (id <NSItemProviderReading>)objectWithItemProviderData:(NSData *)data typeIdentifier:(NSString *)typeIdentifier error:(NSError **)outError;
51 - (id <NSItemProviderReading>)initWithItemProviderData:(NSData *)data typeIdentifier:(NSString *)typeIdentifier error:(NSError **)outError;
52 @end
53
54 using namespace WebCore;
55
56 typedef void(^ItemProviderDataLoadCompletionHandler)(NSData *, NSError *);
57 typedef NSDictionary<NSString *, NSURL *> TypeToFileURLMap;
58
59 @interface WebItemProviderRegistrationInfo ()
60 {
61     RetainPtr<id <UIItemProviderWriting>> _representingObject;
62     RetainPtr<NSString> _typeIdentifier;
63     RetainPtr<NSData> _data;
64 }
65 @end
66
67 @implementation WebItemProviderRegistrationInfo
68
69 - (instancetype)initWithRepresentingObject:(id <UIItemProviderWriting>)representingObject typeIdentifier:(NSString *)typeIdentifier data:(NSData *)data
70 {
71     if (representingObject)
72         ASSERT(!typeIdentifier && !data);
73     else
74         ASSERT(typeIdentifier && data);
75
76     if (self = [super init]) {
77         _representingObject = representingObject;
78         _typeIdentifier = typeIdentifier;
79         _data = data;
80     }
81     return self;
82 }
83
84 - (id <UIItemProviderWriting>)representingObject
85 {
86     return _representingObject.get();
87 }
88
89 - (NSString *)typeIdentifier
90 {
91     return _typeIdentifier.get();
92 }
93
94 - (NSData *)data
95 {
96     return _data.get();
97 }
98
99 @end
100
101 @interface WebItemProviderRegistrationInfoList ()
102 {
103     RetainPtr<NSMutableArray> _items;
104 }
105 @end
106
107 @implementation WebItemProviderRegistrationInfoList
108
109 - (instancetype)init
110 {
111     if (self = [super init]) {
112         _items = adoptNS([[NSMutableArray alloc] init]);
113         _estimatedDisplayedSize = CGSizeZero;
114     }
115
116     return self;
117 }
118
119 - (void)dealloc
120 {
121     [_suggestedName release];
122     [super dealloc];
123 }
124
125 - (void)addData:(NSData *)data forType:(NSString *)typeIdentifier
126 {
127     [_items addObject:[[[WebItemProviderRegistrationInfo alloc] initWithRepresentingObject:nil typeIdentifier:typeIdentifier data:data] autorelease]];
128 }
129
130 - (void)addRepresentingObject:(id <UIItemProviderWriting>)object
131 {
132     ASSERT([object conformsToProtocol:@protocol(UIItemProviderWriting)]);
133     [_items addObject:[[[WebItemProviderRegistrationInfo alloc] initWithRepresentingObject:object typeIdentifier:nil data:nil] autorelease]];
134 }
135
136 - (NSUInteger)numberOfItems
137 {
138     return [_items count];
139 }
140
141 - (WebItemProviderRegistrationInfo *)itemAtIndex:(NSUInteger)index
142 {
143     if (index >= self.numberOfItems)
144         return nil;
145
146     return [_items objectAtIndex:index];
147 }
148
149 - (void)enumerateItems:(void (^)(WebItemProviderRegistrationInfo *, NSUInteger))block
150 {
151     for (NSUInteger index = 0; index < self.numberOfItems; ++index)
152         block([self itemAtIndex:index], index);
153 }
154
155 @end
156
157 @interface WebItemProviderPasteboard ()
158
159 @property (nonatomic) NSInteger numberOfItems;
160 @property (nonatomic) NSInteger changeCount;
161 @property (nonatomic) NSInteger pendingOperationCount;
162
163 @end
164
165 @implementation WebItemProviderPasteboard {
166     // FIXME: These ivars should be refactored to be Vector<RetainPtr<Type>> instead of generic NSArrays.
167     RetainPtr<NSArray> _itemProviders;
168     RetainPtr<NSArray> _cachedTypeIdentifiers;
169     RetainPtr<NSArray> _typeToFileURLMaps;
170     RetainPtr<NSArray> _supportedTypeIdentifiers;
171     RetainPtr<NSArray> _registrationInfoLists;
172 }
173
174 + (instancetype)sharedInstance
175 {
176     static WebItemProviderPasteboard *sharedPasteboard = nil;
177     static dispatch_once_t onceToken;
178     dispatch_once(&onceToken, ^() {
179         sharedPasteboard = [[WebItemProviderPasteboard alloc] init];
180     });
181     return sharedPasteboard;
182 }
183
184 - (instancetype)init
185 {
186     if (self = [super init]) {
187         _itemProviders = adoptNS([[NSArray alloc] init]);
188         _changeCount = 0;
189         _pendingOperationCount = 0;
190         _typeToFileURLMaps = adoptNS([[NSArray alloc] init]);
191         _supportedTypeIdentifiers = nil;
192         _registrationInfoLists = nil;
193     }
194     return self;
195 }
196
197 - (void)updateSupportedTypeIdentifiers:(NSArray<NSString *> *)types
198 {
199     _supportedTypeIdentifiers = types;
200 }
201
202 - (NSArray<NSString *> *)pasteboardTypesByFidelityForItemAtIndex:(NSUInteger)index
203 {
204     return [self itemProviderAtIndex:index].registeredTypeIdentifiers ?: @[ ];
205 }
206
207 - (NSArray<NSString *> *)pasteboardTypes
208 {
209     if (_cachedTypeIdentifiers)
210         return _cachedTypeIdentifiers.get();
211
212     NSMutableSet<NSString *> *allTypes = [NSMutableSet set];
213     NSMutableArray<NSString *> *allTypesInOrder = [NSMutableArray array];
214     for (UIItemProvider *provider in _itemProviders.get()) {
215         for (NSString *typeIdentifier in provider.registeredTypeIdentifiers) {
216             if ([allTypes containsObject:typeIdentifier])
217                 continue;
218
219             [allTypes addObject:typeIdentifier];
220             [allTypesInOrder addObject:typeIdentifier];
221         }
222     }
223     _cachedTypeIdentifiers = allTypesInOrder;
224     return _cachedTypeIdentifiers.get();
225 }
226
227 - (NSArray<__kindof NSItemProvider *> *)itemProviders
228 {
229     return _itemProviders.get();
230 }
231
232 - (void)setItemProviders:(NSArray<__kindof NSItemProvider *> *)itemProviders
233 {
234     itemProviders = itemProviders ?: [NSArray array];
235     if (_itemProviders == itemProviders || [_itemProviders isEqualToArray:itemProviders])
236         return;
237
238     _itemProviders = itemProviders;
239     _changeCount++;
240     _cachedTypeIdentifiers = nil;
241     _registrationInfoLists = nil;
242
243     NSMutableArray *typeToFileURLMaps = [NSMutableArray arrayWithCapacity:itemProviders.count];
244     [itemProviders enumerateObjectsUsingBlock:[typeToFileURLMaps] (UIItemProvider *, NSUInteger, BOOL *) {
245         [typeToFileURLMaps addObject:@{ }];
246     }];
247 }
248
249 - (NSInteger)numberOfItems
250 {
251     return [_itemProviders count];
252 }
253
254 - (void)setItemsUsingRegistrationInfoLists:(NSArray<WebItemProviderRegistrationInfoList *> *)itemLists
255 {
256     NSMutableArray *providers = [NSMutableArray array];
257     for (WebItemProviderRegistrationInfoList *itemList in itemLists) {
258         if (!itemList.numberOfItems)
259             continue;
260
261         auto itemProvider = adoptNS([allocUIItemProviderInstance() init]);
262         [itemList enumerateItems:[itemProvider] (WebItemProviderRegistrationInfo *item, NSUInteger) {
263             if (item.representingObject) {
264                 [itemProvider registerObject:item.representingObject visibility:UIItemProviderRepresentationOptionsVisibilityAll];
265                 return;
266             }
267
268             if (!item.typeIdentifier.length || !item.data.length)
269                 return;
270
271             RetainPtr<NSData> itemData = item.data;
272             [itemProvider registerDataRepresentationForTypeIdentifier:item.typeIdentifier visibility:UIItemProviderRepresentationOptionsVisibilityAll loadHandler:[itemData] (ItemProviderDataLoadCompletionHandler completionHandler) -> NSProgress * {
273                 completionHandler(itemData.get(), nil);
274                 return nil;
275             }];
276         }];
277 #pragma clang diagnostic push
278 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
279         [itemProvider setEstimatedDisplayedSize:itemList.estimatedDisplayedSize];
280 #pragma clang diagnostic pop
281         [itemProvider setSuggestedName:itemList.suggestedName];
282         [providers addObject:itemProvider.get()];
283     }
284
285     self.itemProviders = providers;
286     _registrationInfoLists = itemLists;
287 }
288
289 - (NSData *)_preLoadedDataConformingToType:(NSString *)typeIdentifier forItemProviderAtIndex:(NSUInteger)index
290 {
291     if ([_typeToFileURLMaps count] != [_itemProviders count]) {
292         ASSERT_NOT_REACHED();
293         return nil;
294     }
295
296     TypeToFileURLMap *typeToFileURLMap = [_typeToFileURLMaps objectAtIndex:index];
297     for (NSString *loadedType in typeToFileURLMap) {
298         if (!UTTypeConformsTo((CFStringRef)loadedType, (CFStringRef)typeIdentifier))
299             continue;
300
301         // 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.
302         if (NSData *result = [NSData dataWithContentsOfURL:typeToFileURLMap[loadedType] options:NSDataReadingMappedIfSafe error:nil])
303             return result;
304     }
305     return nil;
306 }
307
308 - (NSArray *)dataForPasteboardType:(NSString *)pasteboardType inItemSet:(NSIndexSet *)itemSet
309 {
310     auto values = adoptNS([[NSMutableArray alloc] init]);
311     RetainPtr<WebItemProviderPasteboard> retainedSelf = self;
312     [itemSet enumerateIndexesUsingBlock:[retainedSelf, pasteboardType, values] (NSUInteger index, BOOL *) {
313         UIItemProvider *provider = [retainedSelf itemProviderAtIndex:index];
314         if (!provider)
315             return;
316
317         if (NSData *loadedData = [retainedSelf _preLoadedDataConformingToType:pasteboardType forItemProviderAtIndex:index])
318             [values addObject:loadedData];
319     }];
320     return values.autorelease();
321 }
322
323 static NSArray<Class<UIItemProviderReading>> *allLoadableClasses()
324 {
325     return @[ [getUIColorClass() class], [getUIImageClass() class], [NSURL class], [NSString class], [NSAttributedString class] ];
326 }
327
328 static Class classForTypeIdentifier(NSString *typeIdentifier, NSString *&outTypeIdentifierToLoad)
329 {
330     outTypeIdentifierToLoad = typeIdentifier;
331
332     // First, try to load a platform UIItemProviderReading-conformant object as-is.
333     for (Class<UIItemProviderReading> loadableClass in allLoadableClasses()) {
334         if ([[loadableClass readableTypeIdentifiersForItemProvider] containsObject:(NSString *)typeIdentifier])
335             return loadableClass;
336     }
337
338     // If we were unable to load any object, check if the given type identifier is still something
339     // WebKit knows how to handle.
340     if ([typeIdentifier isEqualToString:(NSString *)kUTTypeHTML]) {
341         // Load kUTTypeHTML as a plain text HTML string.
342         outTypeIdentifierToLoad = (NSString *)kUTTypePlainText;
343         return [NSString class];
344     }
345
346     return nil;
347 }
348
349 - (NSArray *)valuesForPasteboardType:(NSString *)pasteboardType inItemSet:(NSIndexSet *)itemSet
350 {
351     auto values = adoptNS([[NSMutableArray alloc] init]);
352     RetainPtr<WebItemProviderPasteboard> retainedSelf = self;
353     [itemSet enumerateIndexesUsingBlock:[retainedSelf, pasteboardType, values] (NSUInteger index, BOOL *) {
354         UIItemProvider *provider = [retainedSelf itemProviderAtIndex:index];
355         if (!provider)
356             return;
357
358         NSString *typeIdentifierToLoad;
359         Class readableClass = classForTypeIdentifier(pasteboardType, typeIdentifierToLoad);
360         if (!readableClass)
361             return;
362
363         NSData *preloadedData = [retainedSelf _preLoadedDataConformingToType:pasteboardType forItemProviderAtIndex:index];
364         if (!preloadedData)
365             return;
366
367         if ([readableClass respondsToSelector:@selector(objectWithItemProviderData:typeIdentifier:error:)]) {
368             if (id <NSItemProviderReading> readObject = [readableClass objectWithItemProviderData:preloadedData typeIdentifier:(NSString *)typeIdentifierToLoad error:nil])
369                 [values addObject:readObject];
370         } else {
371             if (auto readObject = adoptNS([[readableClass alloc] initWithItemProviderData:preloadedData typeIdentifier:(NSString *)typeIdentifierToLoad error:nil]))
372                 [values addObject:readObject.get()];
373         }
374     }];
375
376     return values.autorelease();
377 }
378
379 - (NSInteger)changeCount
380 {
381     return _changeCount;
382 }
383
384 - (NSArray<NSURL *> *)fileURLsForDataInteraction
385 {
386     NSMutableArray<NSURL *> *fileURLs = [NSMutableArray array];
387     for (TypeToFileURLMap *typeToFileURLMap in _typeToFileURLMaps.get())
388         [fileURLs addObjectsFromArray:[typeToFileURLMap allValues]];
389     return fileURLs;
390 }
391
392 static BOOL typeConformsToTypes(NSString *type, NSArray *conformsToTypes)
393 {
394     // A type is considered appropriate to load if it conforms to one or more supported types.
395     for (NSString *conformsToType in conformsToTypes) {
396         if (UTTypeConformsTo((CFStringRef)type, (CFStringRef)conformsToType))
397             return YES;
398     }
399     return NO;
400 }
401
402 - (NSInteger)numberOfFiles
403 {
404     NSArray *supportedFileTypes = Pasteboard::supportedFileUploadPasteboardTypes();
405     NSInteger numberOfFiles = 0;
406     for (UIItemProvider *itemProvider in _itemProviders.get()) {
407         for (NSString *identifier in itemProvider.registeredTypeIdentifiers) {
408             if (!typeConformsToTypes(identifier, supportedFileTypes))
409                 continue;
410             ++numberOfFiles;
411             break;
412         }
413     }
414     return numberOfFiles;
415 }
416
417 static NSURL *temporaryFileURLForDataInteractionContent(NSURL *url, NSString *suggestedName)
418 {
419     static NSString *defaultDataInteractionFileName = @"file";
420     static NSString *dataInteractionDirectoryPrefix = @"data-interaction";
421     if (!url)
422         return nil;
423
424     NSString *temporaryDataInteractionDirectory = WebCore::createTemporaryDirectory(dataInteractionDirectoryPrefix);
425     if (!temporaryDataInteractionDirectory)
426         return nil;
427
428     suggestedName = suggestedName ?: defaultDataInteractionFileName;
429     if (![suggestedName containsString:@"."])
430         suggestedName = [suggestedName stringByAppendingPathExtension:url.pathExtension];
431
432     return [NSURL fileURLWithPath:[temporaryDataInteractionDirectory stringByAppendingPathComponent:suggestedName ?: url.lastPathComponent]];
433 }
434
435 - (NSString *)typeIdentifierToLoadForRegisteredTypeIdentfiers:(NSArray<NSString *> *)registeredTypeIdentifiers
436 {
437     NSString *highestFidelityContentType = nil;
438     for (NSString *registeredTypeIdentifier in registeredTypeIdentifiers) {
439         if (typeConformsToTypes(registeredTypeIdentifier, _supportedTypeIdentifiers.get()))
440             return registeredTypeIdentifier;
441
442         if (!highestFidelityContentType && UTTypeConformsTo((CFStringRef)registeredTypeIdentifier, kUTTypeContent))
443             highestFidelityContentType = registeredTypeIdentifier;
444     }
445     return highestFidelityContentType;
446 }
447
448 - (void)doAfterLoadingProvidedContentIntoFileURLs:(WebItemProviderFileLoadBlock)action
449 {
450     [self doAfterLoadingProvidedContentIntoFileURLs:action synchronousTimeout:0];
451 }
452
453 - (void)doAfterLoadingProvidedContentIntoFileURLs:(WebItemProviderFileLoadBlock)action synchronousTimeout:(NSTimeInterval)synchronousTimeout
454 {
455     auto changeCountBeforeLoading = _changeCount;
456     auto typeToFileURLMaps = adoptNS([[NSMutableArray alloc] initWithCapacity:[_itemProviders count]]);
457
458     // First, figure out which item providers we want to try and load files from.
459     auto itemProvidersToLoad = adoptNS([[NSMutableArray alloc] init]);
460     auto typeIdentifiersToLoad = adoptNS([[NSMutableArray alloc] init]);
461     auto indicesOfitemProvidersToLoad = adoptNS([[NSMutableArray alloc] init]);
462     RetainPtr<WebItemProviderPasteboard> protectedSelf = self;
463     [_itemProviders enumerateObjectsUsingBlock:[protectedSelf, itemProvidersToLoad, typeIdentifiersToLoad, indicesOfitemProvidersToLoad, typeToFileURLMaps] (UIItemProvider *itemProvider, NSUInteger index, BOOL *) {
464         NSString *typeIdentifierToLoad = [protectedSelf typeIdentifierToLoadForRegisteredTypeIdentfiers:itemProvider.registeredTypeIdentifiers];
465         if (typeIdentifierToLoad) {
466             [itemProvidersToLoad addObject:itemProvider];
467             [typeIdentifiersToLoad addObject:typeIdentifierToLoad];
468             [indicesOfitemProvidersToLoad addObject:@(index)];
469         }
470         [typeToFileURLMaps addObject:@{ }];
471     }];
472
473     if (![itemProvidersToLoad count]) {
474         action(@[ ]);
475         return;
476     }
477
478     auto setFileURLsLock = adoptNS([[NSLock alloc] init]);
479     auto synchronousFileLoadingGroup = adoptOSObject(dispatch_group_create());
480     auto fileLoadingGroup = adoptOSObject(dispatch_group_create());
481     for (NSUInteger index = 0; index < [itemProvidersToLoad count]; ++index) {
482         RetainPtr<UIItemProvider> itemProvider = [itemProvidersToLoad objectAtIndex:index];
483         RetainPtr<NSString> typeIdentifier = [typeIdentifiersToLoad objectAtIndex:index];
484         NSUInteger indexInItemProviderArray = [[indicesOfitemProvidersToLoad objectAtIndex:index] unsignedIntegerValue];
485         RetainPtr<NSString> suggestedName = [itemProvider suggestedName];
486         dispatch_group_enter(fileLoadingGroup.get());
487         dispatch_group_enter(synchronousFileLoadingGroup.get());
488         [itemProvider loadFileRepresentationForTypeIdentifier:typeIdentifier.get() completionHandler:[synchronousFileLoadingGroup, setFileURLsLock, indexInItemProviderArray, suggestedName, typeIdentifier, typeToFileURLMaps, fileLoadingGroup] (NSURL *url, NSError *error) {
489             // 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.
490             // 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
491             // WKFileUploadPanel.mm. The temporary files are cleaned up by the system at a later time.
492             RetainPtr<NSURL> destinationURL = temporaryFileURLForDataInteractionContent(url, suggestedName.get());
493             if (destinationURL && !error && [[NSFileManager defaultManager] linkItemAtURL:url toURL:destinationURL.get() error:nil]) {
494                 [setFileURLsLock lock];
495                 [typeToFileURLMaps setObject:[NSDictionary dictionaryWithObject:destinationURL.get() forKey:typeIdentifier.get()] atIndexedSubscript:indexInItemProviderArray];
496                 [setFileURLsLock unlock];
497             }
498             dispatch_group_leave(fileLoadingGroup.get());
499             dispatch_group_leave(synchronousFileLoadingGroup.get());
500         }];
501     }
502
503     RetainPtr<WebItemProviderPasteboard> retainedSelf = self;
504     auto itemLoadCompletion = [retainedSelf, synchronousFileLoadingGroup, fileLoadingGroup, typeToFileURLMaps, completionBlock = makeBlockPtr(action), changeCountBeforeLoading] {
505         if (changeCountBeforeLoading == retainedSelf->_changeCount)
506             retainedSelf->_typeToFileURLMaps = typeToFileURLMaps;
507
508         completionBlock([retainedSelf fileURLsForDataInteraction]);
509     };
510
511     if (synchronousTimeout > 0 && !dispatch_group_wait(synchronousFileLoadingGroup.get(), dispatch_time(DISPATCH_TIME_NOW, synchronousTimeout * NSEC_PER_SEC))) {
512         itemLoadCompletion();
513         return;
514     }
515
516     dispatch_group_notify(fileLoadingGroup.get(), dispatch_get_main_queue(), itemLoadCompletion);
517 }
518
519 - (WebItemProviderRegistrationInfoList *)registrationInfoAtIndex:(NSUInteger)index
520 {
521     return index < [_registrationInfoLists count] ? [_registrationInfoLists objectAtIndex:index] : nil;
522 }
523
524 - (UIItemProvider *)itemProviderAtIndex:(NSUInteger)index
525 {
526     return index < [_itemProviders count] ? [_itemProviders objectAtIndex:index] : nil;
527 }
528
529 - (BOOL)hasPendingOperation
530 {
531     return _pendingOperationCount;
532 }
533
534 - (void)incrementPendingOperationCount
535 {
536     _pendingOperationCount++;
537 }
538
539 - (void)decrementPendingOperationCount
540 {
541     _pendingOperationCount--;
542 }
543
544 - (void)enumerateItemProvidersWithBlock:(void (^)(UIItemProvider *itemProvider, NSUInteger index, BOOL *stop))block
545 {
546     [_itemProviders enumerateObjectsUsingBlock:block];
547 }
548
549 @end
550
551 #endif // ENABLE(DATA_INTERACTION)