/* * Copyright (C) 2019-2022 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 "NSAttributedStringPrivate.h" #import "WKErrorInternal.h" #import #import #import #import #import #import #import #import #import #import #import #if PLATFORM(IOS_FAMILY) #import #endif NSString * const NSReadAccessURLDocumentOption = @"ReadAccessURL"; constexpr NSRect webViewRect = {{0, 0}, {800, 600}}; constexpr NSTimeInterval defaultTimeoutInterval = 60; constexpr NSTimeInterval purgeWebViewCacheDelay = 15; constexpr NSUInteger maximumWebViewCacheSize = 3; @interface _WKAttributedStringNavigationDelegate : NSObject @property (nonatomic, copy) void (^webContentProcessDidTerminate)(WKWebView *); @property (nonatomic, copy) void (^decidePolicyForNavigationAction)(WKNavigationAction *, void (^)(WKNavigationActionPolicy)); @property (nonatomic, copy) void (^didFailProvisionalNavigation)(WKWebView *, WKNavigation *, NSError *); @property (nonatomic, copy) void (^didFailNavigation)(WKWebView *, WKNavigation *, NSError *); @property (nonatomic, copy) void (^didFinishNavigation)(WKWebView *, WKNavigation *); @end @implementation _WKAttributedStringNavigationDelegate - (void)dealloc { [_webContentProcessDidTerminate release]; _webContentProcessDidTerminate = nil; [_decidePolicyForNavigationAction release]; _decidePolicyForNavigationAction = nil; [_didFailProvisionalNavigation release]; _didFailProvisionalNavigation = nil; [_didFailNavigation release]; _didFailNavigation = nil; [_didFinishNavigation release]; _didFinishNavigation = nil; [super dealloc]; } - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { if (_webContentProcessDidTerminate) _webContentProcessDidTerminate(webView); } - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { if (_decidePolicyForNavigationAction) return _decidePolicyForNavigationAction(navigationAction, decisionHandler); decisionHandler(WKNavigationActionPolicyAllow); } - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error { if (_didFailProvisionalNavigation) _didFailProvisionalNavigation(webView, navigation, error); } - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error { if (_didFailNavigation) _didFailNavigation(webView, navigation, error); } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { if (_didFinishNavigation) _didFinishNavigation(webView, navigation); } @end @interface _WKAttributedStringWebViewCache : NSObject + (RetainPtr)retrieveOrCreateWebView; + (void)cacheWebView:(WKWebView *)webView; @end @implementation _WKAttributedStringWebViewCache + (NSMutableArray *)cache { static auto* cache = [[NSMutableArray alloc] initWithCapacity:maximumWebViewCacheSize]; return cache; } static RetainPtr& globalConfiguration() { static NeverDestroyed> configuration; return configuration; } + (WKWebViewConfiguration *)configuration { auto& configuration = globalConfiguration(); if (!configuration) { configuration = adoptNS([[WKWebViewConfiguration alloc] init]); [configuration setProcessPool:adoptNS([[WKProcessPool alloc] init]).get()]; [configuration setWebsiteDataStore:[WKWebsiteDataStore nonPersistentDataStore]]; [configuration setMediaTypesRequiringUserActionForPlayback:WKAudiovisualMediaTypeAll]; [configuration _setAllowsJavaScriptMarkup:NO]; [configuration _setAllowsMetaRefresh:NO]; [configuration _setAttachmentElementEnabled:YES]; [configuration preferences]._extensibleSSOEnabled = NO; [configuration _setInvisibleAutoplayNotPermitted:YES]; [configuration _setMediaDataLoadsAutomatically:NO]; [configuration _setNeedsStorageAccessFromFileURLsQuirk:NO]; #if PLATFORM(IOS_FAMILY) [configuration setAllowsInlineMediaPlayback:NO]; [configuration _setClientNavigationsRunAtForegroundPriority:YES]; #endif [configuration preferences]._defaultFontSize = 12; } return configuration.get(); } + (void)clearConfiguration { globalConfiguration() = nil; } + (RetainPtr)retrieveOrCreateWebView { [self resetPurgeDelay]; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ auto& memoryPressureHandler = MemoryPressureHandler::singleton(); memoryPressureHandler.setLowMemoryHandler([self] (Critical, Synchronous) { [self purgeAllWebViews]; }); }); auto* cache = self.cache; if (cache.count) { RetainPtr webView = cache.lastObject; [cache removeLastObject]; return webView; } return adoptNS([[WKWebView alloc] initWithFrame:webViewRect configuration:self.configuration]); } + (void)cacheWebView:(WKWebView *)webView { auto* cache = self.cache; if (cache.count >= maximumWebViewCacheSize) return; [cache addObject:webView]; } + (void)resetPurgeDelay { static const auto purgeSelector = @selector(purgeSingleWebView); [NSObject cancelPreviousPerformRequestsWithTarget:self selector:purgeSelector object:nil]; [self performSelector:purgeSelector withObject:nil afterDelay:purgeWebViewCacheDelay]; } + (void)purgeSingleWebView { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:_cmd object:nil]; auto* cache = self.cache; if (!cache.count) return; [cache.lastObject _close]; [cache removeLastObject]; if (!cache.count) { [self clearConfiguration]; return; } // Keep going until every view is removed, or the delay is reset. [self performSelector:_cmd withObject:nil afterDelay:purgeWebViewCacheDelay]; } + (void)purgeAllWebViews { auto* cache = self.cache; if (!cache.count) return; [cache makeObjectsPerformSelector:@selector(_close)]; [cache removeAllObjects]; [self clearConfiguration]; } @end @implementation NSAttributedString (WKPrivate) + (void)_loadFromHTMLWithOptions:(NSDictionary *)options contentLoader:(WKNavigation *(^)(WKWebView *))loadWebContent completionHandler:(NSAttributedStringCompletionHandler)completionHandler { if (options[NSWebPreferencesDocumentOption]) [NSException raise:NSInvalidArgumentException format:@"NSWebPreferencesDocumentOption option is not supported"]; if (options[NSWebResourceLoadDelegateDocumentOption]) [NSException raise:NSInvalidArgumentException format:@"NSWebResourceLoadDelegateDocumentOption option is not supported"]; if (options[@"WebPolicyDelegate"]) [NSException raise:NSInvalidArgumentException format:@"WebPolicyDelegate option is not supported"]; auto runConversion = ^{ __block auto finished = NO; __block auto webView = [_WKAttributedStringWebViewCache retrieveOrCreateWebView]; __block auto navigationDelegate = adoptNS([[_WKAttributedStringNavigationDelegate alloc] init]); __block RetainPtr contentNavigation; webView.get().navigationDelegate = navigationDelegate.get(); if (auto* textZoomFactor = dynamic_objc_cast(options[NSTextSizeMultiplierDocumentOption])) webView.get()._textZoomFactor = textZoomFactor.doubleValue; else webView.get()._textZoomFactor = 1; auto finish = ^(NSAttributedString *attributedString, NSDictionary *attributes, NSError *error) { if (finished) return; finished = YES; webView.get().navigationDelegate = nil; navigationDelegate = nil; contentNavigation = nil; if (!error) [_WKAttributedStringWebViewCache cacheWebView:webView.get()]; webView = nil; // Make the string be an instance of the receiver class. RetainPtr newAttributedString; if (attributedString && self != attributedString.class) { newAttributedString = adoptNS([[self alloc] initWithAttributedString:attributedString]); attributedString = newAttributedString.get(); } // Make the document attributes immutable. RetainPtr> newAttributes; if ([attributes isKindOfClass:NSMutableDictionary.class]) { newAttributes = adoptNS([[NSDictionary alloc] initWithDictionary:attributes]); attributes = newAttributes.get(); } completionHandler(attributedString, attributes, error); }; auto cancel = ^(WKErrorCode errorCode, NSError* underlyingError) { finish(nil, nil, createNSError(errorCode, underlyingError).get()); }; navigationDelegate.get().decidePolicyForNavigationAction = ^(WKNavigationAction *action, void (^decisionHandler)(WKNavigationActionPolicy)) { if ([action._mainFrameNavigation isEqual:contentNavigation.get()]) return decisionHandler(WKNavigationActionPolicyAllow); decisionHandler(WKNavigationActionPolicyCancel); }; navigationDelegate.get().webContentProcessDidTerminate = ^(WKWebView *) { cancel(WKErrorWebContentProcessTerminated, nil); }; navigationDelegate.get().didFailProvisionalNavigation = ^(WKWebView *, WKNavigation *, NSError *error) { cancel(WKErrorAttributedStringContentFailedToLoad, error); }; navigationDelegate.get().didFailNavigation = ^(WKWebView *, WKNavigation *, NSError *error) { cancel(WKErrorAttributedStringContentFailedToLoad, error); }; navigationDelegate.get().didFinishNavigation = ^(WKWebView *, WKNavigation *) { if (finished) return; navigationDelegate = nil; [webView _getContentsAsAttributedStringWithCompletionHandler:^(NSAttributedString *attributedString, NSDictionary *documentAttributes, NSError *error) { if (error) return cancel(WKErrorUnknown, error); finish(attributedString, documentAttributes, nil); }]; }; auto timeoutInterval = defaultTimeoutInterval; if (auto* timeoutOption = dynamic_objc_cast(options[NSTimeoutDocumentOption])) { if (timeoutOption.doubleValue >= 0) timeoutInterval = timeoutOption.doubleValue; } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeoutInterval * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ if (finished) return; cancel(WKErrorAttributedStringContentLoadTimedOut, nil); }); contentNavigation = loadWebContent(webView.get()); ASSERT(contentNavigation); ASSERT(webView.get().loading); }; if ([NSThread isMainThread]) runConversion(); else dispatch_async(dispatch_get_main_queue(), runConversion); } @end @implementation NSAttributedString (NSAttributedStringWebKitAdditions) + (void)loadFromHTMLWithRequest:(NSURLRequest *)request options:(NSDictionary *)options completionHandler:(NSAttributedStringCompletionHandler)completionHandler { [self _loadFromHTMLWithOptions:options contentLoader:^WKNavigation *(WKWebView *webView) { return [webView loadRequest:request]; } completionHandler:completionHandler]; } + (void)loadFromHTMLWithFileURL:(NSURL *)fileURL options:(NSDictionary *)options completionHandler:(NSAttributedStringCompletionHandler)completionHandler { [self _loadFromHTMLWithOptions:options contentLoader:^WKNavigation *(WKWebView *webView) { auto* readAccessURL = dynamic_objc_cast(options[NSReadAccessURLDocumentOption]); return [webView loadFileURL:fileURL allowingReadAccessToURL:readAccessURL]; } completionHandler:completionHandler]; } + (void)loadFromHTMLWithString:(NSString *)string options:(NSDictionary *)options completionHandler:(NSAttributedStringCompletionHandler)completionHandler { [self _loadFromHTMLWithOptions:options contentLoader:^WKNavigation *(WKWebView *webView) { auto* baseURL = dynamic_objc_cast(options[NSBaseURLDocumentOption]); return [webView loadHTMLString:string baseURL:baseURL]; } completionHandler:completionHandler]; } + (void)loadFromHTMLWithData:(NSData *)data options:(NSDictionary *)options completionHandler:(NSAttributedStringCompletionHandler)completionHandler { [self _loadFromHTMLWithOptions:options contentLoader:^WKNavigation *(WKWebView *webView) { auto* textEncodingName = dynamic_objc_cast(options[NSTextEncodingNameDocumentOption]); auto characterEncoding = static_cast(dynamic_objc_cast(options[NSCharacterEncodingDocumentOption]).unsignedIntegerValue); auto* baseURL = dynamic_objc_cast(options[NSBaseURLDocumentOption]); if (characterEncoding && !textEncodingName) { auto stringEncoding = CFStringConvertNSStringEncodingToEncoding(characterEncoding); if (stringEncoding != kCFStringEncodingInvalidId) textEncodingName = (__bridge NSString *)CFStringConvertEncodingToIANACharSetName(stringEncoding); } return [webView loadData:data MIMEType:@"text/html" characterEncodingName:textEncodingName baseURL:baseURL]; } completionHandler:completionHandler]; } @end