a694071607c89969e009d6ec936677a3cd1d1ae4
[WebKit-https.git] / Tools / TestWebKitAPI / Tests / WebKitCocoa / StorageQuota.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 #import <WebKit/WebKit.h>
28
29 #import "PlatformUtilities.h"
30 #import "Test.h"
31 #import "TestNavigationDelegate.h"
32 #import "TestWKWebView.h"
33 #import <WebKit/WKPreferencesPrivate.h>
34 #import <WebKit/WKProcessPoolPrivate.h>
35 #import <WebKit/WKURLSchemeHandler.h>
36 #import <WebKit/WKURLSchemeTaskPrivate.h>
37 #import <WebKit/WKWebViewConfigurationPrivate.h>
38 #import <WebKit/WKWebsiteDataStorePrivate.h>
39 #import <WebKit/WKWebsiteDataStoreRef.h>
40 #import <WebKit/_WKWebsiteDataStoreConfiguration.h>
41 #import <wtf/BlockPtr.h>
42 #import <wtf/HashMap.h>
43 #import <wtf/RetainPtr.h>
44 #import <wtf/Vector.h>
45 #include <wtf/text/StringConcatenateNumbers.h>
46 #import <wtf/text/StringHash.h>
47 #import <wtf/text/WTFString.h>
48
49 using namespace TestWebKitAPI;
50
51 static bool didFinishNavigation;
52
53 @interface QuotaDelegate : NSObject <WKUIDelegate>
54 -(bool)quotaDelegateCalled;
55 -(void)grantQuota;
56 -(void)denyQuota;
57 @end
58
59 static bool receivedQuotaDelegateCalled;
60
61 @implementation QuotaDelegate {
62     bool _quotaDelegateCalled;
63     unsigned long long _currentQuota;
64     unsigned long long _expectedUsage;
65     BlockPtr<void(unsigned long long newQuota)> _decisionHandler;
66 }
67
68 - (instancetype)init
69 {
70     if (!(self = [super init]))
71         return nil;
72
73     _quotaDelegateCalled = false;
74     _expectedUsage = 0;
75     _currentQuota = 0;
76     
77     return self;
78 }
79
80 - (void)_webView:(WKWebView *)webView decideDatabaseQuotaForSecurityOrigin:(WKSecurityOrigin *)securityOrigin currentQuota:(unsigned long long)currentQuota currentOriginUsage:(unsigned long long)currentOriginUsage currentDatabaseUsage:(unsigned long long)currentUsage expectedUsage:(unsigned long long)expectedUsage decisionHandler:(void (^)(unsigned long long newQuota))decisionHandler
81 {
82     receivedQuotaDelegateCalled = true;
83     _quotaDelegateCalled = true;
84     _currentQuota = currentQuota;
85     _expectedUsage = expectedUsage;
86     _decisionHandler = decisionHandler;
87 }
88
89 -(bool)quotaDelegateCalled {
90     return _quotaDelegateCalled;
91 }
92
93 -(void)grantQuota {
94     if (_quotaDelegateCalled)
95         _decisionHandler(_expectedUsage);
96     _quotaDelegateCalled = false;
97 }
98
99 -(void)denyQuota {
100     if (_quotaDelegateCalled)
101         _decisionHandler(_currentQuota);
102     _quotaDelegateCalled = false;
103 }
104
105 @end
106
107 struct ResourceInfo {
108     RetainPtr<NSString> mimeType;
109     const char* data;
110 };
111
112 @interface StorageSchemes : NSObject <WKURLSchemeHandler> {
113 @public
114     HashMap<String, ResourceInfo> resources;
115 }
116 @end
117
118 @implementation StorageSchemes {
119 }
120
121 - (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)task
122 {
123     auto entry = resources.find([task.request.URL absoluteString]);
124     if (entry == resources.end()) {
125         NSLog(@"Did not find resource entry for URL %@", task.request.URL);
126         return;
127     }
128     
129     RetainPtr<NSURLResponse> response = adoptNS([[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:entry->value.mimeType.get() expectedContentLength:1 textEncodingName:nil]);
130     [task didReceiveResponse:response.get()];
131     
132     [task didReceiveData:[NSData dataWithBytesNoCopy:(void*)entry->value.data length:strlen(entry->value.data) freeWhenDone:NO]];
133     [task didFinish];
134 }
135
136 - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)task
137 {
138 }
139
140 @end
141
142 static bool receivedMessage;
143
144 @interface QuotaMessageHandler : NSObject <WKScriptMessageHandler>
145 -(void)setExpectedMessage:(NSString *)message;
146 @end
147
148 @implementation QuotaMessageHandler {
149     NSString *_expectedMessage;
150 }
151
152 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
153 {
154     if (_expectedMessage) {
155         EXPECT_TRUE([[message body] isEqualToString:_expectedMessage]);
156         _expectedMessage = nil;
157     }
158     receivedMessage = true;
159 }
160
161 -(void)setExpectedMessage:(NSString *)message {
162     _expectedMessage = message;
163 }
164 @end
165
166 static const char* TestBytes = R"SWRESOURCE(
167 <script>
168
169 async function doTest()
170 {
171     const cache = await window.caches.open("mycache");
172     const promise = cache.put("http://example.org/test", new Response(new ArrayBuffer(1024 * 500)));
173     window.webkit.messageHandlers.qt.postMessage("start");
174     promise.then(() => {
175         window.webkit.messageHandlers.qt.postMessage("pass");
176     }, () => {
177         window.webkit.messageHandlers.qt.postMessage("fail");
178     });
179 }
180 doTest();
181
182 function doTestAgain()
183 {
184     doTest();
185 }
186 </script>
187 )SWRESOURCE";
188
189 static const char* TestUrlBytes = R"SWRESOURCE(
190 <script>
191
192 var index = 0;
193 async function test(num)
194 {
195     index++;
196     url = "http://example.org/test" + index;
197
198     const cache = await window.caches.open("mycache");
199     const promise = cache.put(url, new Response(new ArrayBuffer(num * 1024 * 1024)));
200     promise.then(() => {
201         window.webkit.messageHandlers.qt.postMessage("pass");
202     }, () => {
203         window.webkit.messageHandlers.qt.postMessage("fail");
204     });
205 }
206
207 function doTest(num)
208 {
209     test(num);
210 }
211 </script>
212 )SWRESOURCE";
213
214 static bool done;
215
216 static inline void setVisible(TestWKWebView *webView)
217 {
218 #if PLATFORM(MAC)
219     [webView.window setIsVisible:YES];
220 #else
221     webView.window.hidden = NO;
222 #endif
223 }
224
225 TEST(WebKit, QuotaDelegate)
226 {
227     done = false;
228     _WKWebsiteDataStoreConfiguration *storeConfiguration = [[[_WKWebsiteDataStoreConfiguration alloc] init] autorelease];
229     storeConfiguration.perOriginStorageQuota = 1024 * 400;
230     WKWebsiteDataStore *dataStore = [[[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration] autorelease];
231     [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() {
232         done = true;
233     }];
234     TestWebKitAPI::Util::run(&done);
235
236     auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
237     [configuration setWebsiteDataStore:dataStore];
238
239     auto messageHandler = adoptNS([[QuotaMessageHandler alloc] init]);
240     [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"qt"];
241
242     auto handler1 = adoptNS([[StorageSchemes alloc] init]);
243     handler1->resources.set("qt1://test1.html", ResourceInfo { @"text/html", TestBytes });
244     [configuration setURLSchemeHandler:handler1.get() forURLScheme:@"QT1"];
245     [configuration.get().processPool _registerURLSchemeServiceWorkersCanHandle:@"qt1"];
246
247     auto handler2 = adoptNS([[StorageSchemes alloc] init]);
248     handler2->resources.set("qt2://test2.html", ResourceInfo { @"text/html", TestBytes });
249     [configuration setURLSchemeHandler:handler2.get() forURLScheme:@"QT2"];
250     [configuration.get().processPool _registerURLSchemeServiceWorkersCanHandle:@"qt2"];
251
252     auto webView1 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get() addToWindow:YES]);
253     auto delegate1 = adoptNS([[QuotaDelegate alloc] init]);
254     [webView1 setUIDelegate:delegate1.get()];
255     setVisible(webView1.get());
256     
257     receivedQuotaDelegateCalled = false;
258     [webView1 loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"qt1://test1.html"]]];
259     Util::run(&receivedQuotaDelegateCalled);
260
261     auto webView2 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get() addToWindow:YES]);
262     auto delegate2 = adoptNS([[QuotaDelegate alloc] init]);
263     [webView2 setUIDelegate:delegate2.get()];
264     setVisible(webView2.get());
265
266     receivedMessage = false;
267     [webView2 loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"qt2://test2.html"]]];
268     [messageHandler setExpectedMessage: @"start"];
269     Util::run(&receivedMessage);
270
271     EXPECT_FALSE(delegate2.get().quotaDelegateCalled);
272     [delegate1 grantQuota];
273
274     [messageHandler setExpectedMessage: @"pass"];
275     receivedMessage = false;
276     Util::run(&receivedMessage);
277
278     while (!delegate2.get().quotaDelegateCalled)
279         TestWebKitAPI::Util::sleep(0.1);
280
281     [delegate2 denyQuota];
282
283     [messageHandler setExpectedMessage: @"fail"];
284     receivedMessage = false;
285     Util::run(&receivedMessage);
286 }
287
288 TEST(WebKit, QuotaDelegateReload)
289 {
290     done = false;
291     _WKWebsiteDataStoreConfiguration *storeConfiguration = [[[_WKWebsiteDataStoreConfiguration alloc] init] autorelease];
292     storeConfiguration.perOriginStorageQuota = 1024 * 400;
293     WKWebsiteDataStore *dataStore = [[[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration] autorelease];
294     [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() {
295         done = true;
296     }];
297     TestWebKitAPI::Util::run(&done);
298     
299     auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
300     [configuration setWebsiteDataStore:dataStore];
301     
302     auto messageHandler = adoptNS([[QuotaMessageHandler alloc] init]);
303     [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"qt"];
304     
305     auto handler = adoptNS([[StorageSchemes alloc] init]);
306     handler->resources.set("qt://test1.html", ResourceInfo { @"text/html", TestBytes });
307     [configuration setURLSchemeHandler:handler.get() forURLScheme:@"QT"];
308     [configuration.get().processPool _registerURLSchemeServiceWorkersCanHandle:@"qt"];
309     
310     auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get() addToWindow:YES]);
311     auto delegate = adoptNS([[QuotaDelegate alloc] init]);
312     [webView setUIDelegate:delegate.get()];
313     setVisible(webView.get());
314
315     receivedQuotaDelegateCalled = false;
316     [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"qt://test1.html"]]];
317     Util::run(&receivedQuotaDelegateCalled);
318
319     [delegate denyQuota];
320
321     [messageHandler setExpectedMessage: @"fail"];
322     receivedMessage = false;
323     Util::run(&receivedMessage);
324
325     receivedQuotaDelegateCalled = false;
326     [webView reload];
327     Util::run(&receivedQuotaDelegateCalled);
328
329     [delegate grantQuota];
330
331     [messageHandler setExpectedMessage: @"pass"];
332     receivedMessage = false;
333     Util::run(&receivedMessage);
334 }
335
336 TEST(WebKit, QuotaDelegateNavigateFragment)
337 {
338     done = false;
339     _WKWebsiteDataStoreConfiguration *storeConfiguration = [[[_WKWebsiteDataStoreConfiguration alloc] init] autorelease];
340     storeConfiguration.perOriginStorageQuota = 1024 * 400;
341     WKWebsiteDataStore *dataStore = [[[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration] autorelease];
342     [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() {
343         done = true;
344     }];
345     TestWebKitAPI::Util::run(&done);
346
347     auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
348     [configuration setWebsiteDataStore:dataStore];
349
350     auto messageHandler = adoptNS([[QuotaMessageHandler alloc] init]);
351     [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"qt"];
352
353     auto handler = adoptNS([[StorageSchemes alloc] init]);
354     handler->resources.set("qt://test1.html", ResourceInfo { @"text/html", TestBytes });
355     [configuration setURLSchemeHandler:handler.get() forURLScheme:@"QT"];
356     [configuration.get().processPool _registerURLSchemeServiceWorkersCanHandle:@"qt"];
357
358     auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get() addToWindow:YES]);
359     auto delegate = adoptNS([[QuotaDelegate alloc] init]);
360     [webView setUIDelegate:delegate.get()];
361     setVisible(webView.get());
362
363     receivedQuotaDelegateCalled = false;
364     [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"qt://test1.html"]]];
365     Util::run(&receivedQuotaDelegateCalled);
366
367     [delegate denyQuota];
368
369     [messageHandler setExpectedMessage: @"fail"];
370     receivedMessage = false;
371     Util::run(&receivedMessage);
372
373     receivedQuotaDelegateCalled = false;
374     [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"qt://test1.html#fragment"]]];
375     [webView stringByEvaluatingJavaScript:@"doTestAgain()"];
376
377     [messageHandler setExpectedMessage: @"start"];
378     receivedMessage = false;
379     Util::run(&receivedMessage);
380
381     [messageHandler setExpectedMessage: @"fail"];
382     receivedMessage = false;
383     Util::run(&receivedMessage);
384
385     EXPECT_FALSE(receivedQuotaDelegateCalled);
386 }
387
388 TEST(WebKit, DefaultQuota)
389 {
390     done = false;
391     [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() {
392         done = true;
393     }];
394     TestWebKitAPI::Util::run(&done);
395
396     auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
397
398     auto messageHandler = adoptNS([[QuotaMessageHandler alloc] init]);
399     [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"qt"];
400
401     auto handler = adoptNS([[StorageSchemes alloc] init]);
402     handler->resources.set("qt://test1.html", ResourceInfo { @"text/html", TestUrlBytes });
403     [configuration setURLSchemeHandler:handler.get() forURLScheme:@"QT"];
404     [configuration.get().processPool _registerURLSchemeServiceWorkersCanHandle:@"qt"];
405
406     auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get() addToWindow:YES]);
407     auto delegate = adoptNS([[QuotaDelegate alloc] init]);
408     [webView setUIDelegate:delegate.get()];
409     setVisible(webView.get());
410
411     auto navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]);
412     [navigationDelegate setDidFinishNavigation:^(WKWebView *, WKNavigation *) {
413         didFinishNavigation = true;
414     }];
415     [webView setNavigationDelegate:navigationDelegate.get()];
416
417     didFinishNavigation = false;
418     [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"qt://test1.html"]]];
419     Util::run(&didFinishNavigation);
420
421     receivedQuotaDelegateCalled = false;
422
423     // Storing 10 entries of 10 MB should not hit the default quota which is 1GB
424     for (int i = 0; i < 10; ++i) {
425         [webView stringByEvaluatingJavaScript:makeString("doTest(10)")];
426         [messageHandler setExpectedMessage: @"pass"];
427         receivedMessage = false;
428         Util::run(&receivedMessage);
429     }
430     EXPECT_FALSE(receivedQuotaDelegateCalled);
431 }