/* * Copyright (C) 2018 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "WKPDFView.h" #if ENABLE(WKPDFVIEW) #import "APIUIClient.h" #import "FindClient.h" #import "PDFKitSPI.h" #import "UIKitSPI.h" #import "WKActionSheetAssistant.h" #import "WKKeyboardScrollingAnimator.h" #import "WKUIDelegatePrivate.h" #import "WKWebEvent.h" #import "WKWebViewIOS.h" #import "WebPageProxy.h" #import "_WKWebViewPrintFormatterInternal.h" #import <MobileCoreServices/MobileCoreServices.h> #import <WebCore/DataDetection.h> #import <WebCore/ShareData.h> #import <wtf/BlockPtr.h> #import <wtf/MainThread.h> #import <wtf/RetainPtr.h> #import <wtf/WeakObjCPtr.h> #import <wtf/cocoa/Entitlements.h> #import <wtf/cocoa/NSURLExtras.h> @interface WKPDFView () <PDFHostViewControllerDelegate, WKActionSheetAssistantDelegate> @end @implementation WKPDFView { RetainPtr<WKActionSheetAssistant> _actionSheetAssistant; RetainPtr<NSData> _data; RetainPtr<CGPDFDocumentRef> _documentForPrinting; BlockPtr<void()> _findCompletion; RetainPtr<NSString> _findString; NSUInteger _findStringCount; NSUInteger _findStringMaxCount; RetainPtr<UIView> _fixedOverlayView; Optional<NSUInteger> _focusedSearchResultIndex; NSInteger _focusedSearchResultPendingOffset; RetainPtr<PDFHostViewController> _hostViewController; CGSize _overlaidAccessoryViewsInset; RetainPtr<UIView> _pageNumberIndicator; CString _passwordForPrinting; WebKit::InteractionInformationAtPosition _positionInformation; RetainPtr<NSString> _suggestedFilename; WeakObjCPtr<WKWebView> _webView; RetainPtr<WKKeyboardScrollViewAnimator> _keyboardScrollingAnimator; RetainPtr<WKShareSheet> _shareSheet; } - (void)dealloc { if (_shareSheet) { [_shareSheet setDelegate:nil]; [_shareSheet dismiss]; _shareSheet = nil; } [_actionSheetAssistant cleanupSheet]; [[_hostViewController view] removeFromSuperview]; [_pageNumberIndicator removeFromSuperview]; [_keyboardScrollingAnimator invalidate]; std::memset(_passwordForPrinting.mutableData(), 0, _passwordForPrinting.length()); [super dealloc]; } - (BOOL)web_handleKeyEvent:(::UIEvent *)event { auto webEvent = adoptNS([[WKWebEvent alloc] initWithEvent:event]); if ([_keyboardScrollingAnimator beginWithEvent:webEvent.get()]) return YES; [_keyboardScrollingAnimator handleKeyEvent:webEvent.get()]; return NO; } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { return [_hostViewController gestureRecognizerShouldBegin:gestureRecognizer]; } #pragma mark WKApplicationStateTrackingView - (UIView *)_contentView { return _hostViewController ? [_hostViewController view] : self; } #pragma mark WKWebViewContentProvider - (instancetype)web_initWithFrame:(CGRect)frame webView:(WKWebView *)webView mimeType:(NSString *)mimeType { if (!(self = [super initWithFrame:frame webView:webView])) return nil; #if USE(PDFKIT_BACKGROUND_COLOR) UIColor *backgroundColor = PDFHostViewController.backgroundColor; #else UIColor *backgroundColor = UIColor.grayColor; #endif self.backgroundColor = backgroundColor; webView.scrollView.backgroundColor = backgroundColor; _keyboardScrollingAnimator = adoptNS([[WKKeyboardScrollViewAnimator alloc] initWithScrollView:webView.scrollView]); _webView = webView; return self; } - (void)web_setContentProviderData:(NSData *)data suggestedFilename:(NSString *)filename { _data = adoptNS([data copy]); _suggestedFilename = adoptNS([filename copy]); [PDFHostViewController createHostView:[self, weakSelf = WeakObjCPtr<WKPDFView>(self)](PDFHostViewController *hostViewController) { ASSERT(isMainThread()); WKPDFView *autoreleasedSelf = weakSelf.getAutoreleased(); if (!autoreleasedSelf) return; WKWebView *webView = _webView.getAutoreleased(); if (!webView) return; if (!hostViewController) return; _hostViewController = hostViewController; UIView *hostView = hostViewController.view; hostView.frame = webView.bounds; UIScrollView *scrollView = webView.scrollView; [self removeFromSuperview]; [scrollView addSubview:hostView]; _actionSheetAssistant = adoptNS([[WKActionSheetAssistant alloc] initWithView:hostView]); [_actionSheetAssistant setDelegate:self]; _pageNumberIndicator = hostViewController.pageNumberIndicator; [_fixedOverlayView addSubview:_pageNumberIndicator.get()]; hostViewController.delegate = self; [hostViewController setDocumentData:_data.get() withScrollView:scrollView]; } forExtensionIdentifier:nil]; } - (CGPoint)_offsetForPageNumberIndicator { WKWebView *webView = _webView.getAutoreleased(); if (!webView) return CGPointZero; UIEdgeInsets insets = UIEdgeInsetsAdd(webView._computedUnobscuredSafeAreaInset, webView._computedObscuredInset, UIRectEdgeAll); return CGPointMake(insets.left, insets.top + _overlaidAccessoryViewsInset.height); } - (void)_movePageNumberIndicatorToPoint:(CGPoint)point animated:(BOOL)animated { void (^setFrame)() = ^{ static const CGFloat margin = 20; const CGRect frame = { CGPointMake(point.x + margin, point.y + margin), [_pageNumberIndicator frame].size }; [_pageNumberIndicator setFrame:frame]; }; if (animated) { static const NSTimeInterval duration = 0.3; [UIView animateWithDuration:duration animations:setFrame]; return; } setFrame(); } - (void)_updateLayoutAnimated:(BOOL)animated { [_hostViewController updatePDFViewLayout]; [self _movePageNumberIndicatorToPoint:self._offsetForPageNumberIndicator animated:animated]; } - (void)web_setMinimumSize:(CGSize)size { self.frame = { self.frame.origin, size }; [self _updateLayoutAnimated:NO]; } - (void)web_setOverlaidAccessoryViewsInset:(CGSize)inset { _overlaidAccessoryViewsInset = inset; [self _updateLayoutAnimated:YES]; } - (void)web_computedContentInsetDidChange { [self _updateLayoutAnimated:NO]; } - (void)web_setFixedOverlayView:(UIView *)fixedOverlayView { _fixedOverlayView = fixedOverlayView; } - (void)_scrollToURLFragment:(NSString *)fragment { NSInteger pageIndex = 0; if ([fragment hasPrefix:@"page"]) pageIndex = [[fragment substringFromIndex:4] integerValue] - 1; if (pageIndex >= 0 && pageIndex < [_hostViewController pageCount] && pageIndex != [_hostViewController currentPageIndex]) [_hostViewController goToPageIndex:pageIndex]; } - (void)web_didSameDocumentNavigation:(WKSameDocumentNavigationType)navigationType { if (navigationType == kWKSameDocumentNavigationSessionStatePop) [self _scrollToURLFragment:[_webView URL].fragment]; } static NSStringCompareOptions stringCompareOptions(_WKFindOptions findOptions) { NSStringCompareOptions compareOptions = 0; if (findOptions & _WKFindOptionsBackwards) compareOptions |= NSBackwardsSearch; if (findOptions & _WKFindOptionsCaseInsensitive) compareOptions |= NSCaseInsensitiveSearch; return compareOptions; } - (void)_resetFind { if (_findCompletion) [_hostViewController cancelFindString]; _findCompletion = nil; _findString = nil; _findStringCount = 0; _findStringMaxCount = 0; _focusedSearchResultIndex = WTF::nullopt; _focusedSearchResultPendingOffset = 0; } - (void)_findString:(NSString *)string withOptions:(_WKFindOptions)options maxCount:(NSUInteger)maxCount completion:(void(^)())completion { [self _resetFind]; _findCompletion = completion; _findString = adoptNS([string copy]); _findStringMaxCount = maxCount; [_hostViewController findString:_findString.get() withOptions:stringCompareOptions(options)]; } - (void)web_countStringMatches:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount { [self _findString:string withOptions:options maxCount:maxCount completion:^{ ASSERT([_findString isEqualToString:string]); if (auto page = [_webView _page]) page->findClient().didCountStringMatches(page, _findString.get(), _findStringCount); }]; } - (BOOL)_computeFocusedSearchResultIndexWithOptions:(_WKFindOptions)options didWrapAround:(BOOL *)didWrapAround { BOOL isBackwards = options & _WKFindOptionsBackwards; NSInteger singleOffset = isBackwards ? -1 : 1; if (_findCompletion) { ASSERT(!_focusedSearchResultIndex); _focusedSearchResultPendingOffset += singleOffset; return NO; } if (!_findStringCount) return NO; NSInteger newIndex; if (_focusedSearchResultIndex) { ASSERT(!_focusedSearchResultPendingOffset); newIndex = *_focusedSearchResultIndex + singleOffset; } else { newIndex = isBackwards ? _findStringCount - 1 : 0; newIndex += std::exchange(_focusedSearchResultPendingOffset, 0); } if (newIndex < 0 || static_cast<NSUInteger>(newIndex) >= _findStringCount) { if (!(options & _WKFindOptionsWrapAround)) return NO; NSUInteger wrappedIndex = std::abs(newIndex) % _findStringCount; if (newIndex < 0) wrappedIndex = _findStringCount - wrappedIndex; newIndex = wrappedIndex; *didWrapAround = YES; } _focusedSearchResultIndex = newIndex; ASSERT(*_focusedSearchResultIndex < _findStringCount); return YES; } - (void)_focusOnSearchResultWithOptions:(_WKFindOptions)options { auto page = [_webView _page]; if (!page) return; BOOL didWrapAround = NO; if (![self _computeFocusedSearchResultIndexWithOptions:options didWrapAround:&didWrapAround]) { if (!_findCompletion) page->findClient().didFailToFindString(page, _findString.get()); return; } auto focusedIndex = *_focusedSearchResultIndex; [_hostViewController focusOnSearchResultAtIndex:focusedIndex]; page->findClient().didFindString(page, _findString.get(), { }, _findStringCount, focusedIndex, didWrapAround); } - (void)web_findString:(NSString *)string options:(_WKFindOptions)options maxCount:(NSUInteger)maxCount { if ([_findString isEqualToString:string]) { [self _focusOnSearchResultWithOptions:options]; return; } [self _findString:string withOptions:options maxCount:maxCount completion:^{ ASSERT([_findString isEqualToString:string]); [self _focusOnSearchResultWithOptions:options]; }]; } - (void)web_hideFindUI { [self _resetFind]; } - (UIView *)web_contentView { return self._contentView; } + (BOOL)web_requiresCustomSnapshotting { #if HAVE(PDFHOSTVIEWCONTROLLER_SNAPSHOTTING) static bool hasGlobalCaptureEntitlement = WTF::processHasEntitlement("com.apple.QuartzCore.global-capture"); return !hasGlobalCaptureEntitlement; #else return false; #endif } - (void)web_scrollViewDidScroll:(UIScrollView *)scrollView { [_hostViewController updatePDFViewLayout]; } - (void)web_scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view { [_hostViewController updatePDFViewLayout]; } - (void)web_scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale { [_hostViewController updatePDFViewLayout]; } - (void)web_scrollViewDidZoom:(UIScrollView *)scrollView { [_hostViewController updatePDFViewLayout]; } - (void)web_beginAnimatedResizeWithUpdates:(void (^)(void))updateBlock { [_hostViewController beginPDFViewRotation]; updateBlock(); [_hostViewController endPDFViewRotation]; } - (void)web_snapshotRectInContentViewCoordinates:(CGRect)rectInContentViewCoordinates snapshotWidth:(CGFloat)snapshotWidth completionHandler:(void (^)(CGImageRef))completionHandler { #if HAVE(PDFHOSTVIEWCONTROLLER_SNAPSHOTTING) CGRect rectInHostViewCoordinates = [self._contentView convertRect:rectInContentViewCoordinates toView:[_hostViewController view]]; [_hostViewController snapshotViewRect:rectInHostViewCoordinates snapshotWidth:@(snapshotWidth) afterScreenUpdates:NO withResult:^(UIImage *image) { completionHandler(image.CGImage); }]; #endif } - (NSData *)web_dataRepresentation { return _data.get(); } - (NSString *)web_suggestedFilename { return _suggestedFilename.get(); } - (BOOL)web_isBackground { return self.isBackground; } #pragma mark PDFHostViewControllerDelegate - (void)pdfHostViewController:(PDFHostViewController *)controller updatePageCount:(NSInteger)pageCount { [self _scrollToURLFragment:[_webView URL].fragment]; } - (void)pdfHostViewController:(PDFHostViewController *)controller documentDidUnlockWithPassword:(NSString *)password { _passwordForPrinting = [password UTF8String]; } - (void)pdfHostViewController:(PDFHostViewController *)controller findStringUpdate:(NSUInteger)numFound done:(BOOL)done { if (numFound > _findStringMaxCount && !done) { [controller cancelFindStringWithHighlightsCleared:NO]; done = YES; } if (!done) return; if (auto findCompletion = std::exchange(_findCompletion, nil)) { _findStringCount = numFound; findCompletion(); } } - (NSURL *)_URLWithPageIndex:(NSInteger)pageIndex { return [NSURL URLWithString:[NSString stringWithFormat:@"#page%ld", (long)pageIndex + 1] relativeToURL:[_webView URL]]; } - (void)_goToURL:(NSURL *)url atLocation:(CGPoint)location { auto page = [_webView _page]; if (!page) return; UIView *hostView = [_hostViewController view]; CGPoint locationInScreen = [hostView.window convertPoint:[hostView convertPoint:location toView:nil] toWindow:nil]; page->navigateToPDFLinkWithSimulatedClick(url.absoluteString, WebCore::roundedIntPoint(location), WebCore::roundedIntPoint(locationInScreen)); } - (void)pdfHostViewController:(PDFHostViewController *)controller goToURL:(NSURL *)url { // FIXME: We'd use the real tap location if we knew it. [self _goToURL:url atLocation:CGPointMake(0, 0)]; } - (void)pdfHostViewController:(PDFHostViewController *)controller goToPageIndex:(NSInteger)pageIndex withViewFrustum:(CGRect)documentViewRect { [self _goToURL:[self _URLWithPageIndex:pageIndex] atLocation:documentViewRect.origin]; } - (void)_showActionSheetForURL:(NSURL *)url atLocation:(CGPoint)location withAnnotationRect:(CGRect)annotationRect { WKWebView *webView = _webView.getAutoreleased(); if (!webView) return; WebKit::InteractionInformationAtPosition positionInformation; positionInformation.bounds = WebCore::roundedIntRect(annotationRect); positionInformation.request.point = WebCore::roundedIntPoint(location); positionInformation.url = url; _positionInformation = WTFMove(positionInformation); #if ENABLE(DATA_DETECTION) if (WebCore::DataDetection::canBePresentedByDataDetectors(_positionInformation.url)) [_actionSheetAssistant showDataDetectorsSheet]; else #endif [_actionSheetAssistant showLinkSheet]; } - (void)pdfHostViewController:(PDFHostViewController *)controller didLongPressURL:(NSURL *)url atLocation:(CGPoint)location withAnnotationRect:(CGRect)annotationRect { [self _showActionSheetForURL:url atLocation:location withAnnotationRect:annotationRect]; } - (void)pdfHostViewController:(PDFHostViewController *)controller didLongPressPageIndex:(NSInteger)pageIndex atLocation:(CGPoint)location withAnnotationRect:(CGRect)annotationRect { [self _showActionSheetForURL:[self _URLWithPageIndex:pageIndex] atLocation:location withAnnotationRect:annotationRect]; } - (void)pdfHostViewControllerExtensionProcessDidCrash:(PDFHostViewController *)controller { // FIXME 40916725: PDFKit should dispatch this message to the main thread like it does for other delegate messages. dispatch_async(dispatch_get_main_queue(), [webView = _webView] { if (auto page = [webView _page]) page->dispatchProcessDidTerminate(WebKit::ProcessTerminationReason::Crash); }); } #pragma mark WKActionSheetAssistantDelegate - (Optional<WebKit::InteractionInformationAtPosition>)positionInformationForActionSheetAssistant:(WKActionSheetAssistant *)assistant { return _positionInformation; } - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant performAction:(WebKit::SheetAction)action { if (action != WebKit::SheetAction::Copy) return; NSDictionary *representations = @{ (NSString *)kUTTypeUTF8PlainText : (NSString *)_positionInformation.url, (NSString *)kUTTypeURL : (NSURL *)_positionInformation.url, }; [UIPasteboard generalPasteboard].items = @[ representations ]; } - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant openElementAtLocation:(CGPoint)location { [self _goToURL:_positionInformation.url atLocation:location]; } - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant shareElementWithURL:(NSURL *)url rect:(CGRect)boundingRect { WKWebView *webView = _webView.getAutoreleased(); if (!webView) return; WebCore::ShareDataWithParsedURL shareData; shareData.url = { url }; [_shareSheet dismiss]; _shareSheet = adoptNS([[WKShareSheet alloc] initWithView:webView]); [_shareSheet setDelegate:self]; [_shareSheet presentWithParameters:shareData inRect: { [[_hostViewController view] convertRect:boundingRect toView:webView] } completionHandler:[] (bool success) { }]; } - (void)shareSheetDidDismiss:(WKShareSheet *)shareSheet { ASSERT(_shareSheet == shareSheet); [_shareSheet setDelegate:nil]; _shareSheet = nil; } #if HAVE(APP_LINKS) - (BOOL)actionSheetAssistant:(WKActionSheetAssistant *)assistant shouldIncludeAppLinkActionsForElement:(_WKActivatedElementInfo *)element { auto page = [_webView _page]; if (!page) return NO; return page->uiClient().shouldIncludeAppLinkActionsForElement(element); } #endif - (RetainPtr<NSArray>)actionSheetAssistant:(WKActionSheetAssistant *)assistant decideActionsForElement:(_WKActivatedElementInfo *)element defaultActions:(RetainPtr<NSArray>)defaultActions { auto page = [_webView _page]; if (!page) return nil; return page->uiClient().actionsForElement(element, WTFMove(defaultActions)); } - (NSDictionary *)dataDetectionContextForActionSheetAssistant:(WKActionSheetAssistant *)assistant { auto webView = _webView.getAutoreleased(); if (!webView) return nil; id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>(webView.UIDelegate); if (![uiDelegate respondsToSelector:@selector(_dataDetectionContextForWebView:)]) return nil; return [uiDelegate _dataDetectionContextForWebView:webView]; } @end #pragma mark _WKWebViewPrintProvider @interface WKPDFView (_WKWebViewPrintFormatter) <_WKWebViewPrintProvider> @end @implementation WKPDFView (_WKWebViewPrintFormatter) - (CGPDFDocumentRef)_ensureDocumentForPrinting { if (_documentForPrinting) return _documentForPrinting.get(); auto dataProvider = adoptCF(CGDataProviderCreateWithCFData((CFDataRef)_data.get())); auto pdfDocument = adoptCF(CGPDFDocumentCreateWithProvider(dataProvider.get())); if (!CGPDFDocumentIsUnlocked(pdfDocument.get())) CGPDFDocumentUnlockWithPassword(pdfDocument.get(), _passwordForPrinting.data()); _documentForPrinting = WTFMove(pdfDocument); return _documentForPrinting.get(); } - (NSUInteger)_wk_pageCountForPrintFormatter:(_WKWebViewPrintFormatter *)printFormatter { CGPDFDocumentRef documentForPrinting = [self _ensureDocumentForPrinting]; if (!CGPDFDocumentAllowsPrinting(documentForPrinting)) return 0; size_t pageCount = CGPDFDocumentGetNumberOfPages(documentForPrinting); if (printFormatter.snapshotFirstPage) return std::min<NSUInteger>(pageCount, 1); return pageCount; } - (CGPDFDocumentRef)_wk_printedDocument { return [self _ensureDocumentForPrinting]; } @end #endif // ENABLE(WKPDFVIEW)