Move URL from WebCore to WTF
[WebKit-https.git] / Source / WebKit / UIProcess / ios / WKPDFView.mm
1 /*
2  * Copyright (C) 2018 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 "WKPDFView.h"
28
29 #if ENABLE(WKPDFVIEW)
30
31 #import "APIUIClient.h"
32 #import "FindClient.h"
33 #import "PDFKitSPI.h"
34 #import "UIKitSPI.h"
35 #import "WKActionSheetAssistant.h"
36 #import "WKKeyboardScrollingAnimator.h"
37 #import "WKUIDelegatePrivate.h"
38 #import "WKWebEvent.h"
39 #import "WKWebViewInternal.h"
40 #import "WebPageProxy.h"
41 #import "_WKWebViewPrintFormatterInternal.h"
42 #import <MobileCoreServices/MobileCoreServices.h>
43 #import <WebCore/DataDetection.h>
44 #import <wtf/BlockPtr.h>
45 #import <wtf/MainThread.h>
46 #import <wtf/RetainPtr.h>
47 #import <wtf/WeakObjCPtr.h>
48 #import <wtf/cocoa/NSURLExtras.h>
49
50 @interface WKPDFView () <PDFHostViewControllerDelegate, WKActionSheetAssistantDelegate>
51 @end
52
53 @implementation WKPDFView {
54     RetainPtr<WKActionSheetAssistant> _actionSheetAssistant;
55     RetainPtr<NSData> _data;
56     RetainPtr<CGPDFDocumentRef> _documentForPrinting;
57     BlockPtr<void()> _findCompletion;
58     RetainPtr<NSString> _findString;
59     NSUInteger _findStringCount;
60     NSUInteger _findStringMaxCount;
61     RetainPtr<UIView> _fixedOverlayView;
62     std::optional<NSUInteger> _focusedSearchResultIndex;
63     NSInteger _focusedSearchResultPendingOffset;
64     RetainPtr<PDFHostViewController> _hostViewController;
65     CGSize _overlaidAccessoryViewsInset;
66     RetainPtr<UIView> _pageNumberIndicator;
67     RetainPtr<NSString> _password;
68     WebKit::InteractionInformationAtPosition _positionInformation;
69     RetainPtr<NSString> _suggestedFilename;
70     WeakObjCPtr<WKWebView> _webView;
71     RetainPtr<WKKeyboardScrollViewAnimator> _keyboardScrollingAnimator;
72 }
73
74 - (void)dealloc
75 {
76     [_actionSheetAssistant cleanupSheet];
77     [[_hostViewController view] removeFromSuperview];
78     [_pageNumberIndicator removeFromSuperview];
79     [_keyboardScrollingAnimator invalidate];
80     [super dealloc];
81 }
82
83 - (BOOL)web_handleKeyEvent:(::UIEvent *)event
84 {
85     auto webEvent = adoptNS([[WKWebEvent alloc] initWithEvent:event]);
86
87     if ([_keyboardScrollingAnimator beginWithEvent:webEvent.get()])
88         return YES;
89     [_keyboardScrollingAnimator handleKeyEvent:webEvent.get()];
90     return NO;
91 }
92
93 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
94 {
95     return [_hostViewController gestureRecognizerShouldBegin:gestureRecognizer];
96 }
97
98
99 #pragma mark WKApplicationStateTrackingView
100
101 - (UIView *)_contentView
102 {
103     return _hostViewController ? [_hostViewController view] : self;
104 }
105
106
107 #pragma mark WKWebViewContentProvider
108
109 - (instancetype)web_initWithFrame:(CGRect)frame webView:(WKWebView *)webView mimeType:(NSString *)mimeType
110 {
111     if (!(self = [super initWithFrame:frame webView:webView]))
112         return nil;
113
114     self.backgroundColor = UIColor.grayColor;
115     webView.scrollView.backgroundColor = UIColor.grayColor;
116
117     _keyboardScrollingAnimator = adoptNS([[WKKeyboardScrollViewAnimator alloc] initWithScrollView:webView.scrollView]);
118
119     _webView = webView;
120     return self;
121 }
122
123 - (void)web_setContentProviderData:(NSData *)data suggestedFilename:(NSString *)filename
124 {
125     _data = adoptNS([data copy]);
126     _suggestedFilename = adoptNS([filename copy]);
127
128     [PDFHostViewController createHostView:[self, weakSelf = WeakObjCPtr<WKPDFView>(self)](PDFHostViewController *hostViewController) {
129         ASSERT(isMainThread());
130
131         WKPDFView *autoreleasedSelf = weakSelf.getAutoreleased();
132         if (!autoreleasedSelf)
133             return;
134
135         WKWebView *webView = _webView.getAutoreleased();
136         if (!webView)
137             return;
138
139         if (!hostViewController)
140             return;
141         _hostViewController = hostViewController;
142
143         UIView *hostView = hostViewController.view;
144         hostView.frame = webView.bounds;
145         hostView.backgroundColor = UIColor.grayColor;
146
147         UIScrollView *scrollView = webView.scrollView;
148         [self removeFromSuperview];
149         [scrollView addSubview:hostView];
150
151         _actionSheetAssistant = adoptNS([[WKActionSheetAssistant alloc] initWithView:hostView]);
152         [_actionSheetAssistant setDelegate:self];
153
154         _pageNumberIndicator = hostViewController.pageNumberIndicator;
155         [_fixedOverlayView addSubview:_pageNumberIndicator.get()];
156
157         hostViewController.delegate = self;
158         [hostViewController setDocumentData:_data.get() withScrollView:scrollView];
159     } forExtensionIdentifier:nil];
160 }
161
162 - (CGPoint)_offsetForPageNumberIndicator
163 {
164     WKWebView *webView = _webView.getAutoreleased();
165     if (!webView)
166         return CGPointZero;
167
168     UIEdgeInsets insets = UIEdgeInsetsAdd(webView._computedUnobscuredSafeAreaInset, webView._computedObscuredInset, UIRectEdgeAll);
169     return CGPointMake(insets.left, insets.top + _overlaidAccessoryViewsInset.height);
170 }
171
172 - (void)_movePageNumberIndicatorToPoint:(CGPoint)point animated:(BOOL)animated
173 {
174     void (^setFrame)() = ^{
175         static const CGFloat margin = 20;
176         const CGRect frame = { CGPointMake(point.x + margin, point.y + margin), [_pageNumberIndicator frame].size };
177         [_pageNumberIndicator setFrame:frame];
178     };
179
180     if (animated) {
181         static const NSTimeInterval duration = 0.3;
182         [UIView animateWithDuration:duration animations:setFrame];
183         return;
184     }
185
186     setFrame();
187 }
188
189 - (void)_updateLayoutAnimated:(BOOL)animated
190 {
191     [_hostViewController updatePDFViewLayout];
192     [self _movePageNumberIndicatorToPoint:self._offsetForPageNumberIndicator animated:animated];
193 }
194
195 - (void)web_setMinimumSize:(CGSize)size
196 {
197     self.frame = { self.frame.origin, size };
198     [self _updateLayoutAnimated:NO];
199 }
200
201 - (void)web_setOverlaidAccessoryViewsInset:(CGSize)inset
202 {
203     _overlaidAccessoryViewsInset = inset;
204     [self _updateLayoutAnimated:YES];
205 }
206
207 - (void)web_computedContentInsetDidChange
208 {
209     [self _updateLayoutAnimated:NO];
210 }
211
212 - (void)web_setFixedOverlayView:(UIView *)fixedOverlayView
213 {
214     _fixedOverlayView = fixedOverlayView;
215 }
216
217 - (void)_scrollToURLFragment:(NSString *)fragment
218 {
219     NSInteger pageIndex = 0;
220     if ([fragment hasPrefix:@"page"])
221         pageIndex = [[fragment substringFromIndex:4] integerValue] - 1;
222
223     if (pageIndex >= 0 && pageIndex < [_hostViewController pageCount] && pageIndex != [_hostViewController currentPageIndex])
224         [_hostViewController goToPageIndex:pageIndex];
225 }
226
227 - (void)web_didSameDocumentNavigation:(WKSameDocumentNavigationType)navigationType
228 {
229     if (navigationType == kWKSameDocumentNavigationSessionStatePop)
230         [self _scrollToURLFragment:[_webView URL].fragment];
231 }
232
233 static NSStringCompareOptions stringCompareOptions(_WKFindOptions findOptions)
234 {
235     NSStringCompareOptions compareOptions = 0;
236     if (findOptions & _WKFindOptionsBackwards)
237         compareOptions |= NSBackwardsSearch;
238     if (findOptions & _WKFindOptionsCaseInsensitive)
239         compareOptions |= NSCaseInsensitiveSearch;
240     return compareOptions;
241 }
242
243 - (void)_resetFind
244 {
245     if (_findCompletion)
246         [_hostViewController cancelFindString];
247
248     _findCompletion = nil;
249     _findString = nil;
250     _findStringCount = 0;
251     _findStringMaxCount = 0;
252     _focusedSearchResultIndex = std::nullopt;
253     _focusedSearchResultPendingOffset = 0;
254 }
255
256 - (void)_findString:(NSString *)string withOptions:(_WKFindOptions)options maxCount:(NSUInteger)maxCount completion:(void(^)())completion
257 {
258     [self _resetFind];
259
260     _findCompletion = completion;
261     _findString = adoptNS([string copy]);
262     _findStringMaxCount = maxCount;
263     [_hostViewController findString:_findString.get() withOptions:stringCompareOptions(options)];
264 }
265
266 - (void)web_countStringMatches:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount
267 {
268     [self _findString:string withOptions:options maxCount:maxCount completion:^{
269         ASSERT([_findString isEqualToString:string]);
270         if (auto page = [_webView _page])
271             page->findClient().didCountStringMatches(page, _findString.get(), _findStringCount);
272     }];
273 }
274
275 - (BOOL)_computeFocusedSearchResultIndexWithOptions:(_WKFindOptions)options didWrapAround:(BOOL *)didWrapAround
276 {
277     BOOL isBackwards = options & _WKFindOptionsBackwards;
278     NSInteger singleOffset = isBackwards ? -1 : 1;
279
280     if (_findCompletion) {
281         ASSERT(!_focusedSearchResultIndex);
282         _focusedSearchResultPendingOffset += singleOffset;
283         return NO;
284     }
285
286     if (!_findStringCount)
287         return NO;
288
289     NSInteger newIndex;
290     if (_focusedSearchResultIndex) {
291         ASSERT(!_focusedSearchResultPendingOffset);
292         newIndex = *_focusedSearchResultIndex + singleOffset;
293     } else {
294         newIndex = isBackwards ? _findStringCount - 1 : 0;
295         newIndex += std::exchange(_focusedSearchResultPendingOffset, 0);
296     }
297
298     if (newIndex < 0 || static_cast<NSUInteger>(newIndex) >= _findStringCount) {
299         if (!(options & _WKFindOptionsWrapAround))
300             return NO;
301
302         NSUInteger wrappedIndex = std::abs(newIndex) % _findStringCount;
303         if (newIndex < 0)
304             wrappedIndex = _findStringCount - wrappedIndex;
305         newIndex = wrappedIndex;
306         *didWrapAround = YES;
307     }
308
309     _focusedSearchResultIndex = newIndex;
310     ASSERT(*_focusedSearchResultIndex < _findStringCount);
311     return YES;
312 }
313
314 - (void)_focusOnSearchResultWithOptions:(_WKFindOptions)options
315 {
316     auto page = [_webView _page];
317     if (!page)
318         return;
319
320     BOOL didWrapAround = NO;
321     if (![self _computeFocusedSearchResultIndexWithOptions:options didWrapAround:&didWrapAround]) {
322         if (!_findCompletion)
323             page->findClient().didFailToFindString(page, _findString.get());
324         return;
325     }
326
327     auto focusedIndex = *_focusedSearchResultIndex;
328     [_hostViewController focusOnSearchResultAtIndex:focusedIndex];
329     page->findClient().didFindString(page, _findString.get(), { }, _findStringCount, focusedIndex, didWrapAround);
330 }
331
332 - (void)web_findString:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount
333 {
334     if ([_findString isEqualToString:string]) {
335         [self _focusOnSearchResultWithOptions:options];
336         return;
337     }
338
339     [self _findString:string withOptions:options maxCount:maxCount completion:^{
340         ASSERT([_findString isEqualToString:string]);
341         [self _focusOnSearchResultWithOptions:options];
342     }];
343 }
344
345 - (void)web_hideFindUI
346 {
347     [self _resetFind];
348 }
349
350 - (UIView *)web_contentView
351 {
352     return self._contentView;
353 }
354
355 - (void)web_scrollViewDidScroll:(UIScrollView *)scrollView
356 {
357     [_hostViewController updatePDFViewLayout];
358 }
359
360 - (void)web_scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
361 {
362     [_hostViewController updatePDFViewLayout];
363 }
364
365 - (void)web_scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale
366 {
367     [_hostViewController updatePDFViewLayout];
368 }
369
370 - (void)web_scrollViewDidZoom:(UIScrollView *)scrollView
371 {
372     [_hostViewController updatePDFViewLayout];
373 }
374
375 - (void)web_beginAnimatedResizeWithUpdates:(void (^)(void))updateBlock
376 {
377     [_hostViewController beginPDFViewRotation];
378     updateBlock();
379     [_hostViewController endPDFViewRotation];
380 }
381
382 - (NSData *)web_dataRepresentation
383 {
384     return _data.get();
385 }
386
387 - (NSString *)web_suggestedFilename
388 {
389     return _suggestedFilename.get();
390 }
391
392 - (BOOL)web_isBackground
393 {
394     return self.isBackground;
395 }
396
397
398 #pragma mark PDFHostViewControllerDelegate
399
400 - (void)pdfHostViewController:(PDFHostViewController *)controller updatePageCount:(NSInteger)pageCount
401 {
402     [self _scrollToURLFragment:[_webView URL].fragment];
403 }
404
405 - (void)pdfHostViewController:(PDFHostViewController *)controller documentDidUnlockWithPassword:(NSString *)password
406 {
407     _password = adoptNS([password copy]);
408 }
409
410 - (void)pdfHostViewController:(PDFHostViewController *)controller findStringUpdate:(NSUInteger)numFound done:(BOOL)done
411 {
412     if (numFound > _findStringMaxCount && !done) {
413         [controller cancelFindStringWithHighlightsCleared:NO];
414         done = YES;
415     }
416     
417     if (!done)
418         return;
419     
420     if (auto findCompletion = std::exchange(_findCompletion, nil)) {
421         _findStringCount = numFound;
422         findCompletion();
423     }
424 }
425
426 - (NSURL *)_URLWithPageIndex:(NSInteger)pageIndex
427 {
428     return [NSURL URLWithString:[NSString stringWithFormat:@"#page%ld", (long)pageIndex + 1] relativeToURL:[_webView URL]];
429 }
430
431 - (void)_goToURL:(NSURL *)url atLocation:(CGPoint)location
432 {
433     auto page = [_webView _page];
434     if (!page)
435         return;
436
437     UIView *hostView = [_hostViewController view];
438     CGPoint locationInScreen = [hostView.window convertPoint:[hostView convertPoint:location toView:nil] toWindow:nil];
439     page->navigateToPDFLinkWithSimulatedClick(url.absoluteString, WebCore::roundedIntPoint(location), WebCore::roundedIntPoint(locationInScreen));
440 }
441
442 - (void)pdfHostViewController:(PDFHostViewController *)controller goToURL:(NSURL *)url
443 {
444     // FIXME: We'd use the real tap location if we knew it.
445     [self _goToURL:url atLocation:CGPointMake(0, 0)];
446 }
447
448 - (void)pdfHostViewController:(PDFHostViewController *)controller goToPageIndex:(NSInteger)pageIndex withViewFrustum:(CGRect)documentViewRect
449 {
450     [self _goToURL:[self _URLWithPageIndex:pageIndex] atLocation:documentViewRect.origin];
451 }
452
453 - (void)_showActionSheetForURL:(NSURL *)url atLocation:(CGPoint)location withAnnotationRect:(CGRect)annotationRect
454 {
455     WKWebView *webView = _webView.getAutoreleased();
456     if (!webView)
457         return;
458
459     WebKit::InteractionInformationAtPosition positionInformation;
460     positionInformation.bounds = WebCore::roundedIntRect(annotationRect);
461     positionInformation.request.point = WebCore::roundedIntPoint(location);
462     positionInformation.url = url;
463
464     _positionInformation = WTFMove(positionInformation);
465 #if ENABLE(DATA_DETECTION)
466     if (WebCore::DataDetection::canBePresentedByDataDetectors(_positionInformation.url))
467         [_actionSheetAssistant showDataDetectorsSheet];
468     else
469 #endif
470         [_actionSheetAssistant showLinkSheet];
471 }
472
473 - (void)pdfHostViewController:(PDFHostViewController *)controller didLongPressURL:(NSURL *)url atLocation:(CGPoint)location withAnnotationRect:(CGRect)annotationRect
474 {
475     [self _showActionSheetForURL:url atLocation:location withAnnotationRect:annotationRect];
476 }
477
478 - (void)pdfHostViewController:(PDFHostViewController *)controller didLongPressPageIndex:(NSInteger)pageIndex atLocation:(CGPoint)location withAnnotationRect:(CGRect)annotationRect
479 {
480     [self _showActionSheetForURL:[self _URLWithPageIndex:pageIndex] atLocation:location withAnnotationRect:annotationRect];
481 }
482
483 - (void)pdfHostViewControllerExtensionProcessDidCrash:(PDFHostViewController *)controller
484 {
485     // FIXME 40916725: PDFKit should dispatch this message to the main thread like it does for other delegate messages.
486     dispatch_async(dispatch_get_main_queue(), [webView = _webView] {
487         if (auto page = [webView _page])
488             page->dispatchProcessDidTerminate(WebKit::ProcessTerminationReason::Crash);
489     });
490 }
491
492
493 #pragma mark WKActionSheetAssistantDelegate
494
495 - (std::optional<WebKit::InteractionInformationAtPosition>)positionInformationForActionSheetAssistant:(WKActionSheetAssistant *)assistant
496 {
497     return _positionInformation;
498 }
499
500 - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant performAction:(WebKit::SheetAction)action
501 {
502     if (action != WebKit::SheetAction::Copy)
503         return;
504
505     NSDictionary *representations = @{
506         (NSString *)kUTTypeUTF8PlainText : (NSString *)_positionInformation.url,
507         (NSString *)kUTTypeURL : (NSURL *)_positionInformation.url,
508     };
509
510     [UIPasteboard generalPasteboard].items = @[ representations ];
511 }
512
513 - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant openElementAtLocation:(CGPoint)location
514 {
515     [self _goToURL:_positionInformation.url atLocation:location];
516 }
517
518 - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant shareElementWithURL:(NSURL *)url rect:(CGRect)boundingRect
519 {
520     auto selectionAssistant = adoptNS([[UIWKSelectionAssistant alloc] initWithView:[_hostViewController view]]);
521     [selectionAssistant showShareSheetFor:WTF::userVisibleString(url) fromRect:boundingRect];
522 }
523
524 #if HAVE(APP_LINKS)
525 - (BOOL)actionSheetAssistant:(WKActionSheetAssistant *)assistant shouldIncludeAppLinkActionsForElement:(_WKActivatedElementInfo *)element
526 {
527     auto page = [_webView _page];
528     if (!page)
529         return NO;
530
531     return page->uiClient().shouldIncludeAppLinkActionsForElement(element);
532 }
533 #endif
534
535 - (RetainPtr<NSArray>)actionSheetAssistant:(WKActionSheetAssistant *)assistant decideActionsForElement:(_WKActivatedElementInfo *)element defaultActions:(RetainPtr<NSArray>)defaultActions
536 {
537     auto page = [_webView _page];
538     if (!page)
539         return nil;
540
541     return page->uiClient().actionsForElement(element, WTFMove(defaultActions));
542 }
543
544 - (NSDictionary *)dataDetectionContextForActionSheetAssistant:(WKActionSheetAssistant *)assistant
545 {
546     auto webView = _webView.getAutoreleased();
547     if (!webView)
548         return nil;
549
550     id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>(webView.UIDelegate);
551     if (![uiDelegate respondsToSelector:@selector(_dataDetectionContextForWebView:)])
552         return nil;
553
554     return [uiDelegate _dataDetectionContextForWebView:webView];
555 }
556
557 @end
558
559
560 #pragma mark _WKWebViewPrintProvider
561
562 #if !PLATFORM(IOSMAC)
563
564 @interface WKPDFView (_WKWebViewPrintFormatter) <_WKWebViewPrintProvider>
565 @end
566
567 @implementation WKPDFView (_WKWebViewPrintFormatter)
568
569 - (CGPDFDocumentRef)_ensureDocumentForPrinting
570 {
571     if (_documentForPrinting)
572         return _documentForPrinting.get();
573
574     auto dataProvider = adoptCF(CGDataProviderCreateWithCFData((CFDataRef)_data.get()));
575     auto pdfDocument = adoptCF(CGPDFDocumentCreateWithProvider(dataProvider.get()));
576     if (!CGPDFDocumentIsUnlocked(pdfDocument.get()))
577         CGPDFDocumentUnlockWithPassword(pdfDocument.get(), [_password UTF8String]);
578
579     _documentForPrinting = WTFMove(pdfDocument);
580     return _documentForPrinting.get();
581 }
582
583 - (NSUInteger)_wk_pageCountForPrintFormatter:(_WKWebViewPrintFormatter *)printFormatter
584 {
585     CGPDFDocumentRef documentForPrinting = [self _ensureDocumentForPrinting];
586     if (!CGPDFDocumentAllowsPrinting(documentForPrinting))
587         return 0;
588
589     size_t pageCount = CGPDFDocumentGetNumberOfPages(documentForPrinting);
590     if (printFormatter.snapshotFirstPage)
591         return std::min<NSUInteger>(pageCount, 1);
592     return pageCount;
593 }
594
595 - (CGPDFDocumentRef)_wk_printedDocument
596 {
597     return [self _ensureDocumentForPrinting];
598 }
599
600 @end
601
602 #endif // !PLATFORM(IOSMAC)
603
604 #endif // ENABLE(WKPDFVIEW)