File Upload: Photo upload name is always generic (image.jpg)
[WebKit-https.git] / Source / WebKit2 / UIProcess / ios / forms / WKFileUploadPanel.mm
1 /*
2  * Copyright (C) 2014, 2016 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 #import "config.h"
27 #import "WKFileUploadPanel.h"
28
29 #if PLATFORM(IOS)
30
31 #import "APIArray.h"
32 #import "APIData.h"
33 #import "APIOpenPanelParameters.h"
34 #import "APIString.h"
35 #import "PhotosSPI.h"
36 #import "UIKitSPI.h"
37 #import "WKContentViewInteraction.h"
38 #import "WKData.h"
39 #import "WKStringCF.h"
40 #import "WKURLCF.h"
41 #import "WebOpenPanelResultListenerProxy.h"
42 #import "WebPageProxy.h"
43 #import <AVFoundation/AVFoundation.h>
44 #import <CoreMedia/CoreMedia.h>
45 #import <MobileCoreServices/MobileCoreServices.h>
46 #import <WebCore/LocalizedStrings.h>
47 #import <WebCore/SoftLinking.h>
48 #import <WebKit/WebNSFileManagerExtras.h>
49 #import <wtf/RetainPtr.h>
50
51 using namespace WebKit;
52
53 SOFT_LINK_FRAMEWORK(AVFoundation);
54 SOFT_LINK_CLASS(AVFoundation, AVAssetImageGenerator);
55 SOFT_LINK_CLASS(AVFoundation, AVURLAsset);
56
57 SOFT_LINK_FRAMEWORK(CoreMedia);
58 SOFT_LINK_CONSTANT(CoreMedia, kCMTimeZero, CMTime);
59
60 SOFT_LINK_FRAMEWORK(Photos);
61 SOFT_LINK_CLASS(Photos, PHAsset);
62 SOFT_LINK_CLASS(Photos, PHImageManager);
63 SOFT_LINK_CLASS(Photos, PHImageRequestOptions);
64 SOFT_LINK_CONSTANT(Photos, PHImageRequestOptionsResizeModeNone, NSString *);
65 SOFT_LINK_CONSTANT(Photos, PHImageRequestOptionsVersionCurrent, NSString *);
66
67 #define kCMTimeZero getkCMTimeZero()
68
69 #pragma clang diagnostic push
70 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
71
72 #pragma mark - Document picker icons
73
74 static inline UIImage *photoLibraryIcon()
75 {
76     return _UIImageGetWebKitPhotoLibraryIcon();
77 }
78
79 static inline UIImage *cameraIcon()
80 {
81     return _UIImageGetWebKitTakePhotoOrVideoIcon();
82 }
83
84 #pragma mark - Icon generation
85
86 static const CGFloat iconSideLength = 100;
87
88 static CGRect squareCropRectForSize(CGSize size)
89 {
90     CGFloat smallerSide = MIN(size.width, size.height);
91     CGRect cropRect = CGRectMake(0, 0, smallerSide, smallerSide);
92
93     if (size.width < size.height)
94         cropRect.origin.y = std::round((size.height - smallerSide) / 2);
95     else
96         cropRect.origin.x = std::round((size.width - smallerSide) / 2);
97
98     return cropRect;
99 }
100
101 static UIImage *squareImage(CGImageRef image)
102 {
103     if (!image)
104         return nil;
105
106     CGSize imageSize = CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image));
107     if (imageSize.width == imageSize.height)
108         return [UIImage imageWithCGImage:image];
109
110     CGRect squareCropRect = squareCropRectForSize(imageSize);
111     RetainPtr<CGImageRef> squareImage = adoptCF(CGImageCreateWithImageInRect(image, squareCropRect));
112     return [UIImage imageWithCGImage:squareImage.get()];
113 }
114
115 static UIImage *thumbnailSizedImageForImage(CGImageRef image)
116 {
117     UIImage *squaredImage = squareImage(image);
118     if (!squaredImage)
119         return nil;
120
121     CGRect destRect = CGRectMake(0, 0, iconSideLength, iconSideLength);
122     UIGraphicsBeginImageContext(destRect.size);
123     CGContextSetInterpolationQuality(UIGraphicsGetCurrentContext(), kCGInterpolationHigh);
124     [squaredImage drawInRect:destRect];
125     UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
126     UIGraphicsEndImageContext();
127     return resultImage;
128 }
129
130 static UIImage* fallbackIconForFile(NSURL *file)
131 {
132     ASSERT_ARG(file, [file isFileURL]);
133
134     UIDocumentInteractionController *interactionController = [UIDocumentInteractionController interactionControllerWithURL:file];
135     return thumbnailSizedImageForImage(interactionController.icons[0].CGImage);
136 }
137
138 static UIImage* iconForImageFile(NSURL *file)
139 {
140     ASSERT_ARG(file, [file isFileURL]);
141
142     NSDictionary *options = @{
143         (id)kCGImageSourceCreateThumbnailFromImageIfAbsent: @YES,
144         (id)kCGImageSourceThumbnailMaxPixelSize: @(iconSideLength),
145         (id)kCGImageSourceCreateThumbnailWithTransform: @YES,
146     };
147     RetainPtr<CGImageSource> imageSource = adoptCF(CGImageSourceCreateWithURL((CFURLRef)file, 0));
148     RetainPtr<CGImageRef> thumbnail = adoptCF(CGImageSourceCreateThumbnailAtIndex(imageSource.get(), 0, (CFDictionaryRef)options));
149     if (!thumbnail) {
150         LOG_ERROR("WKFileUploadPanel: Error creating thumbnail image for image: %@", file);
151         return fallbackIconForFile(file);
152     }
153
154     return thumbnailSizedImageForImage(thumbnail.get());
155 }
156
157 static UIImage* iconForVideoFile(NSURL *file)
158 {
159     ASSERT_ARG(file, [file isFileURL]);
160
161     RetainPtr<AVURLAsset> asset = adoptNS([allocAVURLAssetInstance() initWithURL:file options:nil]);
162     RetainPtr<AVAssetImageGenerator> generator = adoptNS([allocAVAssetImageGeneratorInstance() initWithAsset:asset.get()]);
163     [generator setAppliesPreferredTrackTransform:YES];
164
165     NSError *error = nil;
166     RetainPtr<CGImageRef> imageRef = adoptCF([generator copyCGImageAtTime:kCMTimeZero actualTime:nil error:&error]);
167     if (!imageRef) {
168         LOG_ERROR("WKFileUploadPanel: Error creating image for video '%@': %@", file, error);
169         return fallbackIconForFile(file);
170     }
171
172     return thumbnailSizedImageForImage(imageRef.get());
173 }
174
175 static UIImage* iconForFile(NSURL *file)
176 {
177     ASSERT_ARG(file, [file isFileURL]);
178
179     NSString *fileExtension = file.pathExtension;
180     if (!fileExtension.length)
181         return nil;
182
183     RetainPtr<CFStringRef> fileUTI = adoptCF(UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)fileExtension, 0));
184
185     if (UTTypeConformsTo(fileUTI.get(), kUTTypeImage))
186         return iconForImageFile(file);
187
188     if (UTTypeConformsTo(fileUTI.get(), kUTTypeMovie))
189         return iconForVideoFile(file);
190
191     return fallbackIconForFile(file);
192 }
193
194
195 #pragma mark - _WKFileUploadItem
196
197 @interface _WKFileUploadItem : NSObject
198 - (instancetype)initWithFileURL:(NSURL *)fileURL;
199 @property (nonatomic, readonly, getter=isVideo) BOOL video;
200 @property (nonatomic, readonly) NSURL *fileURL;
201 @property (nonatomic, readonly) UIImage *displayImage;
202 @end
203
204 @implementation _WKFileUploadItem {
205     RetainPtr<NSURL> _fileURL;
206 }
207
208 - (instancetype)initWithFileURL:(NSURL *)fileURL
209 {
210     if (!(self = [super init]))
211         return nil;
212
213     ASSERT([fileURL isFileURL]);
214     ASSERT([[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]);
215     _fileURL = fileURL;
216     return self;
217 }
218
219 - (BOOL)isVideo
220 {
221     ASSERT_NOT_REACHED();
222     return NO;
223 }
224
225 - (NSURL *)fileURL
226 {
227     return _fileURL.get();
228 }
229
230 - (UIImage *)displayImage
231 {
232     ASSERT_NOT_REACHED();
233     return nil;
234 }
235
236 @end
237
238
239 @interface _WKImageFileUploadItem : _WKFileUploadItem
240 @end
241
242 @implementation _WKImageFileUploadItem
243
244 - (BOOL)isVideo
245 {
246     return NO;
247 }
248
249 - (UIImage *)displayImage
250 {
251     return iconForImageFile(self.fileURL);
252 }
253
254 @end
255
256
257 @interface _WKVideoFileUploadItem : _WKFileUploadItem
258 @end
259
260 @implementation _WKVideoFileUploadItem
261
262 - (BOOL)isVideo
263 {
264     return YES;
265 }
266
267 - (UIImage *)displayImage
268 {
269     return iconForVideoFile(self.fileURL);
270 }
271
272 @end
273
274
275 #pragma mark - WKFileUploadPanel
276
277 @interface WKFileUploadPanel () <UIPopoverControllerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIDocumentPickerDelegate, UIDocumentMenuDelegate>
278 @end
279
280 @implementation WKFileUploadPanel {
281     WKContentView *_view;
282     RefPtr<WebKit::WebOpenPanelResultListenerProxy> _listener;
283     RetainPtr<NSArray> _mimeTypes;
284     CGPoint _interactionPoint;
285     BOOL _allowMultipleFiles;
286     BOOL _usingCamera;
287     RetainPtr<UIImagePickerController> _imagePicker;
288     RetainPtr<UIViewController> _presentationViewController; // iPhone always. iPad for Fullscreen Camera.
289 #pragma clang diagnostic push
290 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
291     RetainPtr<UIPopoverController> _presentationPopover; // iPad for action sheet and Photo Library.
292 #pragma clang diagnostic pop
293     RetainPtr<UIDocumentMenuViewController> _documentMenuController;
294     RetainPtr<UIAlertController> _actionSheetController;
295 }
296
297 - (instancetype)initWithView:(WKContentView *)view
298 {
299     if (!(self = [super init]))
300         return nil;
301     _view = view;
302     return self;
303 }
304
305 - (void)dealloc
306 {
307     [_imagePicker setDelegate:nil];
308     [_presentationPopover setDelegate:nil];
309     [_documentMenuController setDelegate:nil];
310
311     [super dealloc];
312 }
313
314 - (void)_dispatchDidDismiss
315 {
316     if ([_delegate respondsToSelector:@selector(fileUploadPanelDidDismiss:)])
317         [_delegate fileUploadPanelDidDismiss:self];
318 }
319
320 #pragma mark - Panel Completion (one of these must be called)
321
322 - (void)_cancel
323 {
324     _listener->cancel();
325     [self _dispatchDidDismiss];
326 }
327
328 - (void)_chooseFiles:(NSArray *)fileURLs displayString:(NSString *)displayString iconImage:(UIImage *)iconImage
329 {
330     NSUInteger count = [fileURLs count];
331     if (!count) {
332         [self _cancel];
333         return;
334     }
335
336     Vector<String> filenames;
337     filenames.reserveInitialCapacity(count);
338     for (NSURL *fileURL in fileURLs)
339         filenames.uncheckedAppend(fileURL.fileSystemRepresentation);
340
341     NSData *jpeg = UIImageJPEGRepresentation(iconImage, 1.0);
342     RefPtr<API::Data> iconImageDataRef = adoptRef(toImpl(WKDataCreate(reinterpret_cast<const unsigned char*>([jpeg bytes]), [jpeg length])));
343
344     _listener->chooseFiles(filenames, displayString, iconImageDataRef.get());
345     [self _dispatchDidDismiss];
346 }
347
348 #pragma mark - Present / Dismiss API
349
350 - (void)presentWithParameters:(API::OpenPanelParameters*)parameters resultListener:(WebKit::WebOpenPanelResultListenerProxy*)listener
351 {
352     ASSERT(!_listener);
353
354     _listener = listener;
355     _allowMultipleFiles = parameters->allowMultipleFiles();
356     _interactionPoint = [_view lastInteractionLocation];
357
358     Ref<API::Array> acceptMimeTypes = parameters->acceptMIMETypes();
359     NSMutableArray *mimeTypes = [NSMutableArray arrayWithCapacity:acceptMimeTypes->size()];
360     for (const auto& mimeType : acceptMimeTypes->elementsOfType<API::String>())
361         [mimeTypes addObject:mimeType->string()];
362     _mimeTypes = adoptNS([mimeTypes copy]);
363
364     [self _showDocumentPickerMenu];
365 }
366
367 - (void)dismiss
368 {
369     [self _dismissDisplayAnimated:NO];
370     [self _cancel];
371 }
372
373 - (void)_dismissDisplayAnimated:(BOOL)animated
374 {
375     if (_presentationPopover) {
376         [_presentationPopover dismissPopoverAnimated:animated];
377         [_presentationPopover setDelegate:nil];
378         _presentationPopover = nil;
379     }
380
381     if (_presentationViewController) {
382         [_presentationViewController dismissViewControllerAnimated:animated completion:^{
383             _presentationViewController = nil;
384         }];
385     }
386 }
387
388 #pragma mark - Media Types
389
390 static bool stringHasPrefixCaseInsensitive(NSString *str, NSString *prefix)
391 {
392     NSRange range = [str rangeOfString:prefix options:(NSCaseInsensitiveSearch | NSAnchoredSearch)];
393     return range.location != NSNotFound;
394 }
395
396 static NSArray *UTIsForMIMETypes(NSArray *mimeTypes)
397 {
398     // The HTML5 spec mentions the literal "image/*" and "video/*" strings.
399     // We support these and go a step further, if the MIME type starts with
400     // "image/" or "video/" we adjust the picker's image or video filters.
401     // So, "image/jpeg" would make the picker display all images types.
402     NSMutableSet *mediaTypes = [NSMutableSet set];
403     for (NSString *mimeType in mimeTypes) {
404         // FIXME: We should support more MIME type -> UTI mappings. <http://webkit.org/b/142614>
405         if (stringHasPrefixCaseInsensitive(mimeType, @"image/"))
406             [mediaTypes addObject:(NSString *)kUTTypeImage];
407         else if (stringHasPrefixCaseInsensitive(mimeType, @"video/"))
408             [mediaTypes addObject:(NSString *)kUTTypeMovie];
409     }
410
411     return mediaTypes.allObjects;
412 }
413
414 - (NSArray *)_mediaTypesForPickerSourceType:(UIImagePickerControllerSourceType)sourceType
415 {
416     NSArray *mediaTypes = UTIsForMIMETypes(_mimeTypes.get());
417     if (mediaTypes.count)
418         return mediaTypes;
419
420     // Fallback to every supported media type if there is no filter.
421     return [UIImagePickerController availableMediaTypesForSourceType:sourceType];
422 }
423
424 - (NSArray *)_documentPickerMenuMediaTypes
425 {
426     NSArray *mediaTypes = UTIsForMIMETypes(_mimeTypes.get());
427     if (mediaTypes.count)
428         return mediaTypes;
429
430     // Fallback to every supported media type if there is no filter.
431     return @[@"public.item"];
432 }
433
434 #pragma mark - Source selection menu
435
436 - (NSString *)_photoLibraryButtonLabel
437 {
438     return WEB_UI_STRING_KEY("Photo Library", "Photo Library (file upload action sheet)", "File Upload alert sheet button string for choosing an existing media item from the Photo Library");
439 }
440
441 - (NSString *)_cameraButtonLabel
442 {
443     if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
444         return nil;
445
446     // Choose the appropriate string for the camera button.
447     NSArray *filteredMediaTypes = [self _mediaTypesForPickerSourceType:UIImagePickerControllerSourceTypeCamera];
448     BOOL containsImageMediaType = [filteredMediaTypes containsObject:(NSString *)kUTTypeImage];
449     BOOL containsVideoMediaType = [filteredMediaTypes containsObject:(NSString *)kUTTypeMovie];
450     ASSERT(containsImageMediaType || containsVideoMediaType);
451     if (containsImageMediaType && containsVideoMediaType)
452         return WEB_UI_STRING_KEY("Take Photo or Video", "Take Photo or Video (file upload action sheet)", "File Upload alert sheet camera button string for taking photos or videos");
453
454     if (containsVideoMediaType)
455         return WEB_UI_STRING_KEY("Take Video", "Take Video (file upload action sheet)", "File Upload alert sheet camera button string for taking only videos");
456
457     return WEB_UI_STRING_KEY("Take Photo", "Take Photo (file upload action sheet)", "File Upload alert sheet camera button string for taking only photos");
458 }
459
460 - (void)_showDocumentPickerMenu
461 {
462     // FIXME: Support multiple file selection when implemented. <rdar://17177981>
463     _documentMenuController = adoptNS([[UIDocumentMenuViewController alloc] _initIgnoringApplicationEntitlementForImportOfTypes:[self _documentPickerMenuMediaTypes]]);
464     [_documentMenuController setDelegate:self];
465
466     [_documentMenuController addOptionWithTitle:[self _photoLibraryButtonLabel] image:photoLibraryIcon() order:UIDocumentMenuOrderFirst handler:^{
467         [self _showPhotoPickerWithSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
468     }];
469
470     if (NSString *cameraString = [self _cameraButtonLabel]) {
471         [_documentMenuController addOptionWithTitle:cameraString image:cameraIcon() order:UIDocumentMenuOrderFirst handler:^{
472             _usingCamera = YES;
473             [self _showPhotoPickerWithSourceType:UIImagePickerControllerSourceTypeCamera];
474         }];
475     }
476
477     [self _presentForCurrentInterfaceIdiom:_documentMenuController.get()];
478 }
479
480 #pragma mark - Image Picker
481
482 - (void)_showPhotoPickerWithSourceType:(UIImagePickerControllerSourceType)sourceType
483 {
484     _imagePicker = adoptNS([[UIImagePickerController alloc] init]);
485     [_imagePicker setDelegate:self];
486     [_imagePicker setSourceType:sourceType];
487     [_imagePicker setAllowsEditing:NO];
488     [_imagePicker setModalPresentationStyle:UIModalPresentationFullScreen];
489     [_imagePicker _setAllowsMultipleSelection:_allowMultipleFiles];
490     [_imagePicker setMediaTypes:[self _mediaTypesForPickerSourceType:sourceType]];
491
492     // Use a popover on the iPad if the source type is not the camera.
493     // The camera will use a fullscreen, modal view controller.
494     BOOL usePopover = UICurrentUserInterfaceIdiomIsPad() && sourceType != UIImagePickerControllerSourceTypeCamera;
495     if (usePopover)
496         [self _presentPopoverWithContentViewController:_imagePicker.get() animated:YES];
497     else
498         [self _presentFullscreenViewController:_imagePicker.get() animated:YES];
499 }
500
501 #pragma mark - Presenting View Controllers
502
503 - (void)_presentForCurrentInterfaceIdiom:(UIViewController *)viewController
504 {
505     if (UICurrentUserInterfaceIdiomIsPad())
506         [self _presentPopoverWithContentViewController:viewController animated:YES];
507     else
508         [self _presentFullscreenViewController:viewController animated:YES];
509 }
510
511 - (void)_presentPopoverWithContentViewController:(UIViewController *)contentViewController animated:(BOOL)animated
512 {
513     [self _dismissDisplayAnimated:animated];
514
515 #pragma clang diagnostic push
516 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
517     _presentationPopover = adoptNS([[UIPopoverController alloc] initWithContentViewController:contentViewController]);
518 #pragma clang diagnostic pop
519     [_presentationPopover setDelegate:self];
520     [_presentationPopover presentPopoverFromRect:CGRectIntegral(CGRectMake(_interactionPoint.x, _interactionPoint.y, 1, 1)) inView:_view permittedArrowDirections:UIPopoverArrowDirectionAny animated:animated];
521 }
522
523 - (void)_presentFullscreenViewController:(UIViewController *)viewController animated:(BOOL)animated
524 {
525     [self _dismissDisplayAnimated:animated];
526
527     _presentationViewController = [UIViewController _viewControllerForFullScreenPresentationFromView:_view];
528     [_presentationViewController presentViewController:viewController animated:animated completion:nil];
529 }
530
531 #pragma mark - UIPopoverControllerDelegate
532
533 - (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
534 {
535     [self _cancel];
536 }
537
538 #pragma mark - UIDocumentMenuDelegate implementation
539
540 - (void)documentMenu:(UIDocumentMenuViewController *)documentMenu didPickDocumentPicker:(UIDocumentPickerViewController *)documentPicker
541 {
542     documentPicker.delegate = self;
543     [self _presentFullscreenViewController:documentPicker animated:YES];
544 }
545
546 - (void)documentMenuWasCancelled:(UIDocumentMenuViewController *)documentMenu
547 {
548     [self _dismissDisplayAnimated:YES];
549     [self _cancel];
550 }
551
552 #pragma mark - UIDocumentPickerControllerDelegate implementation
553
554 - (void)documentPicker:(UIDocumentPickerViewController *)documentPicker didPickDocumentAtURL:(NSURL *)url
555 {
556     [self _dismissDisplayAnimated:YES];
557     [self _chooseFiles:@[url] displayString:url.lastPathComponent iconImage:iconForFile(url)];
558 }
559
560 - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)documentPicker
561 {
562     [self _dismissDisplayAnimated:YES];
563     [self _cancel];
564 }
565
566 #pragma mark - UIImagePickerControllerDelegate implementation
567
568 - (BOOL)_willMultipleSelectionDelegateBeCalled
569 {
570     // The multiple selection delegate will not be called when the UIImagePicker was not multiple selection.
571     if (!_allowMultipleFiles)
572         return NO;
573
574     // The multiple selection delegate will not be called when we used the camera in the UIImagePicker.
575     if (_usingCamera)
576         return NO;
577
578     return YES;
579 }
580
581 - (void)imagePickerController:(UIImagePickerController *)imagePicker didFinishPickingMediaWithInfo:(NSDictionary *)info
582 {
583     // Sometimes both delegates get called, sometimes just one. Always let the
584     // multiple selection delegate handle everything if it will get called.
585     if ([self _willMultipleSelectionDelegateBeCalled])
586         return;
587
588     [self _dismissDisplayAnimated:YES];
589
590     [self _processMediaInfoDictionaries:[NSArray arrayWithObject:info]
591         successBlock:^(NSArray *processedResults, NSString *displayString) {
592             ASSERT([processedResults count] == 1);
593             _WKFileUploadItem *result = [processedResults objectAtIndex:0];
594             dispatch_async(dispatch_get_main_queue(), ^{
595                 [self _chooseFiles:[NSArray arrayWithObject:result.fileURL] displayString:displayString iconImage:result.displayImage];
596             });
597         }
598         failureBlock:^{
599             dispatch_async(dispatch_get_main_queue(), ^{
600                 [self _cancel];
601             });
602         }
603     ];
604 }
605
606 - (void)imagePickerController:(UIImagePickerController *)imagePicker didFinishPickingMultipleMediaWithInfo:(NSArray *)infos
607 {
608     [self _dismissDisplayAnimated:YES];
609
610     [self _processMediaInfoDictionaries:infos
611         successBlock:^(NSArray *processedResults, NSString *displayString) {
612             UIImage *iconImage = nil;
613             NSMutableArray *fileURLs = [NSMutableArray array];
614             for (_WKFileUploadItem *result in processedResults) {
615                 NSURL *fileURL = result.fileURL;
616                 if (!fileURL)
617                     continue;
618                 [fileURLs addObject:result.fileURL];
619                 if (!iconImage)
620                     iconImage = result.displayImage;
621             }
622
623             dispatch_async(dispatch_get_main_queue(), ^{
624                 [self _chooseFiles:fileURLs displayString:displayString iconImage:iconImage];
625             });
626         }
627         failureBlock:^{
628             dispatch_async(dispatch_get_main_queue(), ^{
629                 [self _cancel];
630             });
631         }
632     ];
633 }
634
635 - (void)imagePickerControllerDidCancel:(UIImagePickerController *)imagePicker
636 {
637     [self _dismissDisplayAnimated:YES];
638     [self _cancel];
639 }
640
641 #pragma mark - Process UIImagePicker results
642
643 - (void)_processMediaInfoDictionaries:(NSArray *)infos successBlock:(void (^)(NSArray *processedResults, NSString *displayString))successBlock failureBlock:(void (^)(void))failureBlock
644 {
645     [self _processMediaInfoDictionaries:infos atIndex:0 processedResults:[NSMutableArray array] processedImageCount:0 processedVideoCount:0 successBlock:successBlock failureBlock:failureBlock];
646 }
647
648 - (void)_processMediaInfoDictionaries:(NSArray *)infos atIndex:(NSUInteger)index processedResults:(NSMutableArray *)processedResults processedImageCount:(NSUInteger)processedImageCount processedVideoCount:(NSUInteger)processedVideoCount successBlock:(void (^)(NSArray *processedResults, NSString *displayString))successBlock failureBlock:(void (^)(void))failureBlock
649 {
650     NSUInteger count = [infos count];
651     if (index == count) {
652         NSString *displayString = [self _displayStringForPhotos:processedImageCount videos:processedVideoCount];
653         successBlock(processedResults, displayString);
654         return;
655     }
656
657     NSDictionary *info = [infos objectAtIndex:index];
658     ASSERT(index < count);
659     index++;
660
661     auto uploadItemSuccessBlock = ^(_WKFileUploadItem *uploadItem) {
662         NSUInteger newProcessedVideoCount = processedVideoCount + (uploadItem.isVideo ? 1 : 0);
663         NSUInteger newProcessedImageCount = processedImageCount + (uploadItem.isVideo ? 0 : 1);
664         [processedResults addObject:uploadItem];
665         [self _processMediaInfoDictionaries:infos atIndex:index processedResults:processedResults processedImageCount:newProcessedImageCount processedVideoCount:newProcessedVideoCount successBlock:successBlock failureBlock:failureBlock];
666     };
667
668     [self _uploadItemFromMediaInfo:info successBlock:uploadItemSuccessBlock failureBlock:failureBlock];
669 }
670
671 - (void)_uploadItemForImageData:(NSData *)imageData imageName:(NSString *)imageName successBlock:(void (^)(_WKFileUploadItem *))successBlock failureBlock:(void (^)(void))failureBlock
672 {
673     ASSERT_ARG(imageData, imageData);
674     ASSERT(!isMainThread());
675
676     NSString * const kTemporaryDirectoryName = @"WKWebFileUpload";
677
678     // Build temporary file path.
679     NSFileManager *fileManager = [NSFileManager defaultManager];
680     NSString *temporaryDirectory = [fileManager _webkit_createTemporaryDirectoryWithTemplatePrefix:kTemporaryDirectoryName];
681     NSString *filePath = [temporaryDirectory stringByAppendingPathComponent:imageName];
682     if (!filePath) {
683         LOG_ERROR("WKFileUploadPanel: Failed to create temporary directory to save image");
684         failureBlock();
685         return;
686     }
687
688     // Save the image to the temporary file.
689     NSError *error = nil;
690     [imageData writeToFile:filePath options:NSDataWritingAtomic error:&error];
691     if (error) {
692         LOG_ERROR("WKFileUploadPanel: Error writing image data to temporary file: %@", error);
693         failureBlock();
694         return;
695     }
696
697     successBlock(adoptNS([[_WKImageFileUploadItem alloc] initWithFileURL:[NSURL fileURLWithPath:filePath]]).get());
698 }
699
700 - (void)_uploadItemForJPEGRepresentationOfImage:(UIImage *)image successBlock:(void (^)(_WKFileUploadItem *))successBlock failureBlock:(void (^)(void))failureBlock
701 {
702     ASSERT_ARG(image, image);
703
704     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
705         // FIXME: Different compression for different devices?
706         // FIXME: Different compression for different UIImage sizes?
707         // FIXME: Should EXIF data be maintained?
708         const CGFloat compression = 0.8;
709         NSData *jpeg = UIImageJPEGRepresentation(image, compression);
710         if (!jpeg) {
711             LOG_ERROR("WKFileUploadPanel: Failed to create JPEG representation for image");
712             failureBlock();
713             return;
714         }
715
716         // FIXME: Should we get the photo asset and get the actual filename for the photo instead of
717         // naming each of the individual uploads image.jpg? This won't work for photos taken with
718         // the camera, but would work for photos picked from the library.
719         NSString * const kUploadImageName = @"image.jpg";
720         [self _uploadItemForImageData:jpeg imageName:kUploadImageName successBlock:successBlock failureBlock:failureBlock];
721     });
722 }
723
724 - (void)_uploadItemForImage:(UIImage *)image withAssetURL:(NSURL *)assetURL successBlock:(void (^)(_WKFileUploadItem *))successBlock failureBlock:(void (^)(void))failureBlock
725 {
726     ASSERT_ARG(image, image);
727     ASSERT_ARG(assetURL, assetURL);
728
729     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
730         PHFetchResult *result = [getPHAssetClass() fetchAssetsWithALAssetURLs:@[assetURL] options:nil];
731         if (!result.count) {
732             LOG_ERROR("WKFileUploadPanel: Failed to fetch asset with URL %@", assetURL);
733             [self _uploadItemForJPEGRepresentationOfImage:image successBlock:successBlock failureBlock:failureBlock];
734             return;
735         }
736
737         PHAsset *firstAsset = result[0];
738         [firstAsset fetchPropertySetsIfNeeded];
739         NSString *originalFilename = [[firstAsset originalMetadataProperties] originalFilename];
740         ASSERT(originalFilename);
741
742         RetainPtr<PHImageRequestOptions> options = adoptNS([allocPHImageRequestOptionsInstance() init]);
743         [options setVersion:PHImageRequestOptionsVersionCurrent];
744         [options setSynchronous:YES];
745         [options setResizeMode:PHImageRequestOptionsResizeModeNone];
746
747         PHImageManager *manager = (PHImageManager *)[getPHImageManagerClass() defaultManager];
748         [manager requestImageDataForAsset:firstAsset options:options.get() resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation, NSDictionary *info)
749         {
750             if (!imageData) {
751                 LOG_ERROR("WKFileUploadPanel: Failed to request image data for asset with URL %@", assetURL);
752                 [self _uploadItemForJPEGRepresentationOfImage:image successBlock:successBlock failureBlock:failureBlock];
753                 return;
754             }
755
756             [self _uploadItemForImageData:imageData imageName:originalFilename successBlock:successBlock failureBlock:failureBlock];
757         }];
758     });
759 }
760
761 - (void)_uploadItemFromMediaInfo:(NSDictionary *)info successBlock:(void (^)(_WKFileUploadItem *))successBlock failureBlock:(void (^)(void))failureBlock
762 {
763     NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
764
765     // For videos from the existing library or camera, the media URL will give us a file path.
766     if (UTTypeConformsTo((CFStringRef)mediaType, kUTTypeMovie)) {
767         NSURL *mediaURL = [info objectForKey:UIImagePickerControllerMediaURL];
768         if (![mediaURL isFileURL]) {
769             LOG_ERROR("WKFileUploadPanel: Expected media URL to be a file path, it was not");
770             ASSERT_NOT_REACHED();
771             failureBlock();
772             return;
773         }
774
775         successBlock(adoptNS([[_WKVideoFileUploadItem alloc] initWithFileURL:mediaURL]).get());
776         return;
777     }
778
779     if (!UTTypeConformsTo((CFStringRef)mediaType, kUTTypeImage)) {
780         LOG_ERROR("WKFileUploadPanel: Unexpected media type. Expected image or video, got: %@", mediaType);
781         ASSERT_NOT_REACHED();
782         failureBlock();
783         return;
784     }
785
786     UIImage *originalImage = [info objectForKey:UIImagePickerControllerOriginalImage];
787     if (!originalImage) {
788         LOG_ERROR("WKFileUploadPanel: Expected image data but there was none");
789         ASSERT_NOT_REACHED();
790         failureBlock();
791         return;
792     }
793
794     // If we have an asset URL, try to upload the native image.
795     NSURL *referenceURL = [info objectForKey:UIImagePickerControllerReferenceURL];
796     if (referenceURL) {
797         [self _uploadItemForImage:originalImage withAssetURL:referenceURL successBlock:successBlock failureBlock:failureBlock];
798         return;
799     }
800
801     // Photos taken with the camera will not have an asset URL. Fall back to a JPEG representation.
802     [self _uploadItemForJPEGRepresentationOfImage:originalImage successBlock:successBlock failureBlock:failureBlock];
803 }
804
805 - (NSString *)_displayStringForPhotos:(NSUInteger)imageCount videos:(NSUInteger)videoCount
806 {
807     if (!imageCount && !videoCount)
808         return nil;
809
810     NSString *title;
811     NSString *countString;
812     NSString *imageString;
813     NSString *videoString;
814     NSUInteger numberOfTypes = 2;
815
816     RetainPtr<NSNumberFormatter> countFormatter = adoptNS([[NSNumberFormatter alloc] init]);
817     [countFormatter setLocale:[NSLocale currentLocale]];
818     [countFormatter setGeneratesDecimalNumbers:YES];
819     [countFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
820
821     // Generate the individual counts for each type.
822     switch (imageCount) {
823     case 0:
824         imageString = nil;
825         --numberOfTypes;
826         break;
827     case 1:
828         imageString = WEB_UI_STRING_KEY("1 Photo", "1 Photo (file upload on page label for one photo)", "File Upload single photo label");
829         break;
830     default:
831         countString = [countFormatter stringFromNumber:@(imageCount)];
832         imageString = [NSString stringWithFormat:WEB_UI_STRING_KEY("%@ Photos", "# Photos (file upload on page label for multiple photos)", "File Upload multiple photos label"), countString];
833         break;
834     }
835
836     switch (videoCount) {
837     case 0:
838         videoString = nil;
839         --numberOfTypes;
840         break;
841     case 1:
842         videoString = WEB_UI_STRING_KEY("1 Video", "1 Video (file upload on page label for one video)", "File Upload single video label");
843         break;
844     default:
845         countString = [countFormatter stringFromNumber:@(videoCount)];
846         videoString = [NSString stringWithFormat:WEB_UI_STRING_KEY("%@ Videos", "# Videos (file upload on page label for multiple videos)", "File Upload multiple videos label"), countString];
847         break;
848     }
849
850     // Combine into a single result string if needed.
851     switch (numberOfTypes) {
852     case 2:
853         // FIXME: For localization we should build a complete string. We should have a localized string for each different combination.
854         title = [NSString stringWithFormat:WEB_UI_STRING_KEY("%@ and %@", "# Photos and # Videos (file upload on page label for image and videos)", "File Upload images and videos label"), imageString, videoString];
855         break;
856     case 1:
857         title = imageString ? imageString : videoString;
858         break;
859     default:
860         ASSERT_NOT_REACHED();
861         title = nil;
862         break;
863     }
864
865     return [title lowercaseString];
866 }
867
868 @end
869
870 #pragma clang diagnostic pop
871
872 #endif // PLATFORM(IOS)