Unreviewed, fix iOS build with recent SDKs.
[WebKit-https.git] / Source / WebKit / UIProcess / API / Cocoa / NSAttributedString.mm
1 /*
2  * Copyright (C) 2019 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
28 #import "NSAttributedStringPrivate.h"
29
30 #import "WKErrorInternal.h"
31 #import "WKNavigationActionPrivate.h"
32 #import "WKNavigationDelegate.h"
33 #import "WKPreferences.h"
34 #import "WKProcessPoolPrivate.h"
35 #import "WKWebViewConfigurationPrivate.h"
36 #import "WKWebViewPrivate.h"
37 #import "WKWebsiteDataStore.h"
38 #import "_WKProcessPoolConfiguration.h"
39 #import <wtf/Deque.h>
40 #import <wtf/MemoryPressureHandler.h>
41 #import <wtf/RetainPtr.h>
42
43 #if PLATFORM(IOS_FAMILY)
44 #import <UIKitSPI.h>
45 #endif
46
47 NSString * const NSReadAccessURLDocumentOption = @"ReadAccessURL";
48
49 constexpr NSRect webViewRect = {{0, 0}, {800, 600}};
50 constexpr NSTimeInterval defaultTimeoutInterval = 60;
51 constexpr NSTimeInterval purgeWebViewCacheDelay = 15;
52 constexpr NSUInteger maximumWebViewCacheSize = 3;
53
54 @interface _WKAttributedStringNavigationDelegate : NSObject <WKNavigationDelegate>
55
56 @property (nonatomic, copy) void (^webContentProcessDidTerminate)(WKWebView *);
57 @property (nonatomic, copy) void (^decidePolicyForNavigationAction)(WKNavigationAction *, void (^)(WKNavigationActionPolicy));
58 @property (nonatomic, copy) void (^didFailProvisionalNavigation)(WKWebView *, WKNavigation *, NSError *);
59 @property (nonatomic, copy) void (^didFailNavigation)(WKWebView *, WKNavigation *, NSError *);
60 @property (nonatomic, copy) void (^didFinishNavigation)(WKWebView *, WKNavigation *);
61
62 @end
63
64 @implementation _WKAttributedStringNavigationDelegate
65
66 - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView
67 {
68     if (_webContentProcessDidTerminate)
69         _webContentProcessDidTerminate(webView);
70 }
71
72 - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
73 {
74     if (_decidePolicyForNavigationAction)
75         return _decidePolicyForNavigationAction(navigationAction, decisionHandler);
76     decisionHandler(WKNavigationActionPolicyAllow);
77 }
78
79 - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error
80 {
81     if (_didFailProvisionalNavigation)
82         _didFailProvisionalNavigation(webView, navigation, error);
83 }
84
85 - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error
86 {
87     if (_didFailNavigation)
88         _didFailNavigation(webView, navigation, error);
89 }
90
91 - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
92 {
93     if (_didFinishNavigation)
94         _didFinishNavigation(webView, navigation);
95 }
96
97 @end
98
99 @interface _WKAttributedStringWebViewCache : NSObject
100
101 + (RetainPtr<WKWebView>)retrieveOrCreateWebView;
102 + (void)cacheWebView:(WKWebView *)webView;
103
104 @end
105
106 @implementation _WKAttributedStringWebViewCache
107
108 + (NSMutableArray<WKWebView *> *)cache
109 {
110     static auto* cache = [[NSMutableArray alloc] initWithCapacity:maximumWebViewCacheSize];
111     return cache;
112 }
113
114 static WKWebViewConfiguration *configuration;
115
116 + (WKWebViewConfiguration *)configuration
117 {
118     if (!configuration) {
119         configuration = [[WKWebViewConfiguration alloc] init];
120         configuration.processPool = [[[WKProcessPool alloc] init] autorelease];
121         configuration.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
122         configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll;
123         configuration._allowsJavaScriptMarkup = NO;
124         configuration._allowsMetaRefresh = NO;
125         configuration._attachmentElementEnabled = YES;
126         configuration._invisibleAutoplayNotPermitted = YES;
127         configuration._mediaDataLoadsAutomatically = NO;
128         configuration._needsStorageAccessFromFileURLsQuirk = NO;
129 #if PLATFORM(IOS_FAMILY)
130         configuration.allowsInlineMediaPlayback = NO;
131         configuration._alwaysRunsAtForegroundPriority = YES;
132 #endif
133     }
134
135     return configuration;
136 }
137
138 + (void)clearConfiguration
139 {
140     [configuration release];
141     configuration = nil;
142 }
143
144 + (RetainPtr<WKWebView>)retrieveOrCreateWebView
145 {
146     [self resetPurgeDelay];
147
148     static dispatch_once_t onceToken;
149     dispatch_once(&onceToken, ^{
150         auto& memoryPressureHandler = MemoryPressureHandler::singleton();
151         memoryPressureHandler.setLowMemoryHandler([self] (Critical, Synchronous) {
152             [self purgeAllWebViews];
153         });
154     });
155
156     auto* cache = self.cache;
157     if (cache.count) {
158         RetainPtr<WKWebView> webView = cache.lastObject;
159         [cache removeLastObject];
160         return webView;
161     }
162
163     return adoptNS([[WKWebView alloc] initWithFrame:webViewRect configuration:self.configuration]);
164 }
165
166 + (void)cacheWebView:(WKWebView *)webView
167 {
168     auto* cache = self.cache;
169     if (cache.count >= maximumWebViewCacheSize)
170         return;
171
172     [cache addObject:webView];
173
174     // Load a blank page to clear anything loaded while in the cache.
175     [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]];
176 }
177
178 + (void)resetPurgeDelay
179 {
180     static const auto purgeSelector = @selector(purgeSingleWebView);
181     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:purgeSelector object:nil];
182     [self performSelector:purgeSelector withObject:nil afterDelay:purgeWebViewCacheDelay];
183 }
184
185 + (void)purgeSingleWebView
186 {
187     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:_cmd object:nil];
188
189     auto* cache = self.cache;
190     if (!cache.count)
191         return;
192
193     [cache.lastObject _close];
194     [cache removeLastObject];
195
196     if (!cache.count) {
197         [self clearConfiguration];
198         return;
199     }
200
201     // Keep going until every view is removed, or the delay is reset.
202     [self performSelector:_cmd withObject:nil afterDelay:purgeWebViewCacheDelay];
203 }
204
205 + (void)purgeAllWebViews
206 {
207     auto* cache = self.cache;
208     if (!cache.count)
209         return;
210
211     [cache makeObjectsPerformSelector:@selector(_close)];
212     [cache removeAllObjects];
213
214     [self clearConfiguration];
215 }
216
217 @end
218
219 @implementation NSAttributedString (WKPrivate)
220
221 + (void)_loadFromHTMLWithOptions:(NSDictionary<NSAttributedStringDocumentReadingOptionKey, id> *)options contentLoader:(WKNavigation *(^)(WKWebView *))loadWebContent completionHandler:(NSAttributedStringCompletionHandler)completionHandler
222 {
223     if (options[NSWebPreferencesDocumentOption])
224         [NSException raise:NSInvalidArgumentException format:@"NSWebPreferencesDocumentOption option is not supported"];
225     if (options[NSWebResourceLoadDelegateDocumentOption])
226         [NSException raise:NSInvalidArgumentException format:@"NSWebResourceLoadDelegateDocumentOption option is not supported"];
227     if (options[@"WebPolicyDelegate"])
228         [NSException raise:NSInvalidArgumentException format:@"WebPolicyDelegate option is not supported"];
229
230     auto runConversion = ^{
231         __block auto finished = NO;
232         __block auto webView = [_WKAttributedStringWebViewCache retrieveOrCreateWebView];
233         __block auto navigationDelegate = adoptNS([[_WKAttributedStringNavigationDelegate alloc] init]);
234
235         __block RetainPtr<WKNavigation> contentNavigation;
236
237         webView.get().navigationDelegate = navigationDelegate.get();
238
239         if (auto* textZoomFactor = dynamic_objc_cast<NSNumber>(options[NSTextSizeMultiplierDocumentOption]))
240             webView.get()._textZoomFactor = textZoomFactor.doubleValue;
241         else
242             webView.get()._textZoomFactor = 1;
243
244         auto finish = ^(NSAttributedString *attributedString, NSDictionary<NSAttributedStringDocumentAttributeKey, id> *attributes, NSError *error) {
245             if (finished)
246                 return;
247
248             finished = YES;
249
250             webView.get().navigationDelegate = nil;
251             navigationDelegate = nil;
252             contentNavigation = nil;
253
254             if (!error)
255                 [_WKAttributedStringWebViewCache cacheWebView:webView.get()];
256             webView = nil;
257
258             // Make the string be an instance of the receiver class.
259             if (attributedString && self != attributedString.class)
260                 attributedString = [[[self alloc] initWithAttributedString:attributedString] autorelease];
261
262             // Make the document attributes immutable.
263             if ([attributes isKindOfClass:NSMutableDictionary.class])
264                 attributes = [[[NSDictionary alloc] initWithDictionary:attributes] autorelease];
265
266             completionHandler(attributedString, attributes, error);
267         };
268
269         auto cancel = ^(WKErrorCode errorCode, NSError* underlyingError) {
270             finish(nil, nil, createNSError(errorCode, underlyingError).get());
271         };
272
273         navigationDelegate.get().decidePolicyForNavigationAction = ^(WKNavigationAction *action, void (^decisionHandler)(WKNavigationActionPolicy)) {
274             if ([action._mainFrameNavigation isEqual:contentNavigation.get()])
275                 return decisionHandler(WKNavigationActionPolicyAllow);
276             decisionHandler(WKNavigationActionPolicyCancel);
277         };
278
279         navigationDelegate.get().webContentProcessDidTerminate = ^(WKWebView *) {
280             cancel(WKErrorWebContentProcessTerminated, nil);
281         };
282
283         navigationDelegate.get().didFailProvisionalNavigation = ^(WKWebView *, WKNavigation *, NSError *error) {
284             cancel(WKErrorAttributedStringContentFailedToLoad, error);
285         };
286
287         navigationDelegate.get().didFailNavigation = ^(WKWebView *, WKNavigation *, NSError *error) {
288             cancel(WKErrorAttributedStringContentFailedToLoad, error);
289         };
290
291         navigationDelegate.get().didFinishNavigation = ^(WKWebView *, WKNavigation *) {
292             if (finished)
293                 return;
294
295             navigationDelegate = nil;
296
297             [webView _getContentsAsAttributedStringWithCompletionHandler:^(NSAttributedString *attributedString, NSDictionary<NSAttributedStringDocumentAttributeKey, id> *documentAttributes, NSError *error) {
298                 if (error)
299                     return cancel(WKErrorUnknown, error);
300                 finish([[attributedString retain] autorelease], [[documentAttributes retain] autorelease], nil);
301             }];
302         };
303
304         auto timeoutInterval = defaultTimeoutInterval;
305         if (auto* timeoutOption = dynamic_objc_cast<NSNumber>(options[NSTimeoutDocumentOption])) {
306             if (timeoutOption.doubleValue >= 0)
307                 timeoutInterval = timeoutOption.doubleValue;
308         }
309
310         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeoutInterval * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
311             if (finished)
312                 return;
313             cancel(WKErrorAttributedStringContentLoadTimedOut, nil);
314         });
315
316         contentNavigation = loadWebContent(webView.get());
317
318         ASSERT(contentNavigation);
319         ASSERT(webView.get().loading);
320     };
321
322     if ([NSThread isMainThread])
323         runConversion();
324     else
325         dispatch_async(dispatch_get_main_queue(), runConversion);
326 }
327
328 @end
329
330 @implementation NSAttributedString (NSAttributedStringWebKitAdditions)
331
332 + (void)loadFromHTMLWithRequest:(NSURLRequest *)request options:(NSDictionary<NSAttributedStringDocumentReadingOptionKey, id> *)options completionHandler:(NSAttributedStringCompletionHandler)completionHandler
333 {
334     [self _loadFromHTMLWithOptions:options contentLoader:^WKNavigation *(WKWebView *webView) {
335         return [webView loadRequest:request];
336     } completionHandler:completionHandler];
337 }
338
339 + (void)loadFromHTMLWithFileURL:(NSURL *)fileURL options:(NSDictionary<NSAttributedStringDocumentReadingOptionKey, id> *)options completionHandler:(NSAttributedStringCompletionHandler)completionHandler
340 {
341     [self _loadFromHTMLWithOptions:options contentLoader:^WKNavigation *(WKWebView *webView) {
342         auto* readAccessURL = dynamic_objc_cast<NSURL>(options[NSReadAccessURLDocumentOption]);
343         return [webView loadFileURL:fileURL allowingReadAccessToURL:readAccessURL];
344     } completionHandler:completionHandler];
345 }
346
347 + (void)loadFromHTMLWithString:(NSString *)string options:(NSDictionary<NSAttributedStringDocumentReadingOptionKey, id> *)options completionHandler:(NSAttributedStringCompletionHandler)completionHandler
348 {
349     [self _loadFromHTMLWithOptions:options contentLoader:^WKNavigation *(WKWebView *webView) {
350         auto* baseURL = dynamic_objc_cast<NSURL>(options[NSBaseURLDocumentOption]);
351         return [webView loadHTMLString:string baseURL:baseURL];
352     } completionHandler:completionHandler];
353 }
354
355 + (void)loadFromHTMLWithData:(NSData *)data options:(NSDictionary<NSAttributedStringDocumentReadingOptionKey, id> *)options completionHandler:(NSAttributedStringCompletionHandler)completionHandler
356 {
357     [self _loadFromHTMLWithOptions:options contentLoader:^WKNavigation *(WKWebView *webView) {
358         auto* textEncodingName = dynamic_objc_cast<NSString>(options[NSTextEncodingNameDocumentOption]);
359         auto characterEncoding = static_cast<NSStringEncoding>(dynamic_objc_cast<NSNumber>(options[NSCharacterEncodingDocumentOption]).unsignedIntegerValue);
360         auto* baseURL = dynamic_objc_cast<NSURL>(options[NSBaseURLDocumentOption]);
361
362         if (characterEncoding && !textEncodingName) {
363             auto stringEncoding = CFStringConvertNSStringEncodingToEncoding(characterEncoding);
364             if (stringEncoding != kCFStringEncodingInvalidId)
365                 textEncodingName = (__bridge NSString *)CFStringConvertEncodingToIANACharSetName(stringEncoding);
366         }
367
368         return [webView loadData:data MIMEType:@"text/html" characterEncodingName:textEncodingName baseURL:baseURL];
369     } completionHandler:completionHandler];
370 }
371
372 @end