Source/WebKit:
[WebKit-https.git] / Tools / WebKitTestRunner / cocoa / TestControllerCocoa.mm
1 /*
2  * Copyright (C) 2015-2016 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 "TestController.h"
28
29 #import "CrashReporterInfo.h"
30 #import "PlatformWebView.h"
31 #import "StringFunctions.h"
32 #import "TestInvocation.h"
33 #import "TestRunnerWKWebView.h"
34 #import <Foundation/Foundation.h>
35 #import <Security/SecItem.h>
36 #import <WebKit/WKContextConfigurationRef.h>
37 #import <WebKit/WKCookieManager.h>
38 #import <WebKit/WKPreferencesRefPrivate.h>
39 #import <WebKit/WKProcessPoolPrivate.h>
40 #import <WebKit/WKStringCF.h>
41 #import <WebKit/WKUserContentControllerPrivate.h>
42 #import <WebKit/WKWebView.h>
43 #import <WebKit/WKWebViewConfiguration.h>
44 #import <WebKit/WKWebViewConfigurationPrivate.h>
45 #import <WebKit/WKWebViewPrivate.h>
46 #import <WebKit/WKWebsiteDataRecordPrivate.h>
47 #import <WebKit/WKWebsiteDataStorePrivate.h>
48 #import <WebKit/WKWebsiteDataStoreRef.h>
49 #import <WebKit/_WKApplicationManifest.h>
50 #import <WebKit/_WKUserContentExtensionStore.h>
51 #import <WebKit/_WKUserContentExtensionStorePrivate.h>
52 #import <wtf/MainThread.h>
53
54 namespace WTR {
55
56 static WKWebViewConfiguration *globalWebViewConfiguration;
57
58 void initializeWebViewConfiguration(const char* libraryPath, WKStringRef injectedBundlePath, WKContextRef context, WKContextConfigurationRef contextConfiguration)
59 {
60 #if WK_API_ENABLED
61     [globalWebViewConfiguration release];
62     globalWebViewConfiguration = [[WKWebViewConfiguration alloc] init];
63
64     globalWebViewConfiguration.processPool = (__bridge WKProcessPool *)context;
65     globalWebViewConfiguration.websiteDataStore = (__bridge WKWebsiteDataStore *)WKContextGetWebsiteDataStore(context);
66     globalWebViewConfiguration._allowUniversalAccessFromFileURLs = YES;
67     globalWebViewConfiguration._applePayEnabled = YES;
68
69     WKCookieManagerSetStorageAccessAPIEnabled(WKContextGetCookieManager(context), true);
70
71     WKWebsiteDataStore* poolWebsiteDataStore = (__bridge WKWebsiteDataStore *)WKContextGetWebsiteDataStore((__bridge WKContextRef)globalWebViewConfiguration.processPool);
72     [poolWebsiteDataStore _setCacheStoragePerOriginQuota: 400 * 1024];
73     if (libraryPath) {
74         String cacheStorageDirectory = String(libraryPath) + '/' + "CacheStorage";
75         [poolWebsiteDataStore _setCacheStorageDirectory: cacheStorageDirectory];
76
77         String serviceWorkerRegistrationDirectory = String(libraryPath) + '/' + "ServiceWorkers";
78         [poolWebsiteDataStore _setServiceWorkerRegistrationDirectory: serviceWorkerRegistrationDirectory];
79     }
80
81     [globalWebViewConfiguration.websiteDataStore _setResourceLoadStatisticsEnabled:YES];
82     [globalWebViewConfiguration.websiteDataStore _resourceLoadStatisticsSetShouldSubmitTelemetry:NO];
83
84 #if PLATFORM(IOS_FAMILY)
85     globalWebViewConfiguration.allowsInlineMediaPlayback = YES;
86     globalWebViewConfiguration._inlineMediaPlaybackRequiresPlaysInlineAttribute = NO;
87     globalWebViewConfiguration._invisibleAutoplayNotPermitted = NO;
88     globalWebViewConfiguration._mediaDataLoadsAutomatically = YES;
89     globalWebViewConfiguration.requiresUserActionForMediaPlayback = NO;
90 #endif
91     globalWebViewConfiguration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
92 #endif
93
94 #if USE(SYSTEM_PREVIEW)
95     globalWebViewConfiguration._systemPreviewEnabled = YES;
96 #endif
97 }
98
99 void TestController::cocoaPlatformInitialize()
100 {
101     const char* dumpRenderTreeTemp = libraryPathForTesting();
102     if (!dumpRenderTreeTemp)
103         return;
104
105     String resourceLoadStatisticsFolder = String(dumpRenderTreeTemp) + '/' + "ResourceLoadStatistics";
106     [[NSFileManager defaultManager] createDirectoryAtPath:resourceLoadStatisticsFolder withIntermediateDirectories:YES attributes:nil error: nil];
107     String fullBrowsingSessionResourceLog = resourceLoadStatisticsFolder + '/' + "full_browsing_session_resourceLog.plist";
108     NSDictionary *resourceLogPlist = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithInt:1], @"version", nil];
109     if (![resourceLogPlist writeToFile:fullBrowsingSessionResourceLog atomically:YES])
110         WTFCrash();
111     [resourceLogPlist release];
112 }
113
114 WKContextRef TestController::platformContext()
115 {
116 #if WK_API_ENABLED
117     return (__bridge WKContextRef)globalWebViewConfiguration.processPool;
118 #else
119     return nullptr;
120 #endif
121 }
122
123 WKPreferencesRef TestController::platformPreferences()
124 {
125 #if WK_API_ENABLED
126     return (__bridge WKPreferencesRef)globalWebViewConfiguration.preferences;
127 #else
128     return nullptr;
129 #endif
130 }
131
132 void TestController::platformAddTestOptions(TestOptions& options) const
133 {
134     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EnableProcessSwapOnNavigation"])
135         options.enableProcessSwapOnNavigation = true;
136     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EnableProcessSwapOnWindowOpen"])
137         options.enableProcessSwapOnWindowOpen = true;
138 }
139
140 void TestController::platformCreateWebView(WKPageConfigurationRef, const TestOptions& options)
141 {
142 #if WK_API_ENABLED
143     RetainPtr<WKWebViewConfiguration> copiedConfiguration = adoptNS([globalWebViewConfiguration copy]);
144
145 #if PLATFORM(IOS_FAMILY)
146     if (options.useDataDetection)
147         [copiedConfiguration setDataDetectorTypes:WKDataDetectorTypeAll];
148     if (options.ignoresViewportScaleLimits)
149         [copiedConfiguration setIgnoresViewportScaleLimits:YES];
150     if (options.useCharacterSelectionGranularity)
151         [copiedConfiguration setSelectionGranularity:WKSelectionGranularityCharacter];
152     if (options.useCharacterSelectionGranularity)
153         [copiedConfiguration setSelectionGranularity:WKSelectionGranularityCharacter];
154 #endif
155
156     if (options.enableAttachmentElement)
157         [copiedConfiguration _setAttachmentElementEnabled: YES];
158
159     if (options.enableColorFilter)
160         [copiedConfiguration _setColorFilterEnabled: YES];
161
162     if (options.applicationManifest.length()) {
163         auto manifestPath = [NSString stringWithUTF8String:options.applicationManifest.c_str()];
164         NSString *text = [NSString stringWithContentsOfFile:manifestPath usedEncoding:nullptr error:nullptr];
165         [copiedConfiguration _setApplicationManifest:[_WKApplicationManifest applicationManifestFromJSON:text manifestURL:nil documentURL:nil]];
166     }
167
168     m_mainWebView = std::make_unique<PlatformWebView>(copiedConfiguration.get(), options);
169
170     if (options.punchOutWhiteBackgroundsInDarkMode)
171         m_mainWebView->setDrawsBackground(false);
172 #else
173     m_mainWebView = std::make_unique<PlatformWebView>(globalWebViewConfiguration, options);
174 #endif
175 }
176
177 PlatformWebView* TestController::platformCreateOtherPage(PlatformWebView* parentView, WKPageConfigurationRef, const TestOptions& options)
178 {
179 #if WK_API_ENABLED
180     WKWebViewConfiguration *newConfiguration = [[globalWebViewConfiguration copy] autorelease];
181     newConfiguration._relatedWebView = static_cast<WKWebView*>(parentView->platformView());
182     return new PlatformWebView(newConfiguration, options);
183 #else
184     return nullptr;
185 #endif
186 }
187
188 WKContextRef TestController::platformAdjustContext(WKContextRef context, WKContextConfigurationRef contextConfiguration)
189 {
190 #if WK_API_ENABLED
191     initializeWebViewConfiguration(libraryPathForTesting(), injectedBundlePath(), context, contextConfiguration);
192     return (__bridge WKContextRef)globalWebViewConfiguration.processPool;
193 #else
194     return nullptr;
195 #endif
196 }
197
198 void TestController::platformRunUntil(bool& done, WTF::Seconds timeout)
199 {
200     NSDate *endDate = (timeout > 0_s) ? [NSDate dateWithTimeIntervalSinceNow:timeout.seconds()] : [NSDate distantFuture];
201
202     while (!done && [endDate compare:[NSDate date]] == NSOrderedDescending)
203         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:endDate];
204 }
205
206 ClassMethodSwizzler::ClassMethodSwizzler(Class cls, SEL originalSelector, IMP implementation)
207     : m_method(class_getClassMethod(objc_getMetaClass(NSStringFromClass(cls).UTF8String), originalSelector))
208     , m_originalImplementation(method_setImplementation(m_method, implementation))
209 {
210 }
211
212 ClassMethodSwizzler::~ClassMethodSwizzler()
213 {
214     method_setImplementation(m_method, m_originalImplementation);
215 }
216     
217 static NSCalendar *swizzledCalendar()
218 {
219     return [NSCalendar calendarWithIdentifier:TestController::singleton().getOverriddenCalendarIdentifier().get()];
220 }
221     
222 RetainPtr<NSString> TestController::getOverriddenCalendarIdentifier() const
223 {
224     return m_overriddenCalendarIdentifier;
225 }
226
227 void TestController::setDefaultCalendarType(NSString *identifier)
228 {
229     m_overriddenCalendarIdentifier = identifier;
230     if (!m_calendarSwizzler)
231         m_calendarSwizzler = std::make_unique<ClassMethodSwizzler>([NSCalendar class], @selector(currentCalendar), reinterpret_cast<IMP>(swizzledCalendar));
232 }
233     
234 void TestController::cocoaResetStateToConsistentValues(const TestOptions& options)
235 {
236 #if WK_API_ENABLED
237     __block bool doneRemoving = false;
238     [[_WKUserContentExtensionStore defaultStore] removeContentExtensionForIdentifier:@"TestContentExtensions" completionHandler:^(NSError *error) {
239         doneRemoving = true;
240     }];
241     platformRunUntil(doneRemoving, noTimeout);
242     [[_WKUserContentExtensionStore defaultStore] _removeAllContentExtensions];
243
244
245     m_calendarSwizzler = nullptr;
246     m_overriddenCalendarIdentifier = nil;
247     
248     if (auto* webView = mainWebView()) {
249         TestRunnerWKWebView *platformView = webView->platformView();
250         [platformView.configuration.userContentController _removeAllUserContentFilters];
251         platformView._viewScale = 1;
252
253         // Toggle on before the test, and toggle off after the test.
254         if (options.shouldShowSpellCheckingDots)
255             [platformView toggleContinuousSpellChecking:nil];
256     }
257 #endif
258 }
259
260 void TestController::platformWillRunTest(const TestInvocation& testInvocation)
261 {
262     setCrashReportApplicationSpecificInformationToURL(testInvocation.url());
263 }
264
265 static NSString * const WebArchivePboardType = @"Apple Web Archive pasteboard type";
266 static NSString * const WebSubresourcesKey = @"WebSubresources";
267 static NSString * const WebSubframeArchivesKey = @"WebResourceMIMEType like 'image*'";
268
269 unsigned TestController::imageCountInGeneralPasteboard() const
270 {
271 #if PLATFORM(MAC)
272     NSData *data = [[NSPasteboard generalPasteboard] dataForType:WebArchivePboardType];
273 #elif PLATFORM(IOS_FAMILY)
274     NSData *data = [[UIPasteboard generalPasteboard] valueForPasteboardType:WebArchivePboardType];
275 #endif
276     if (!data)
277         return 0;
278     
279     NSError *error = nil;
280     id webArchive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:&error];
281     if (error) {
282         NSLog(@"Encountered error while serializing Web Archive pasteboard data: %@", error);
283         return 0;
284     }
285     
286     NSArray *subItems = [NSArray arrayWithArray:[webArchive objectForKey:WebSubresourcesKey]];
287     NSPredicate *predicate = [NSPredicate predicateWithFormat:WebSubframeArchivesKey];
288     NSArray *imagesArray = [subItems filteredArrayUsingPredicate:predicate];
289     
290     if (!imagesArray)
291         return 0;
292     
293     return imagesArray.count;
294 }
295
296 void TestController::removeAllSessionCredentials()
297 {
298 #if WK_API_ENABLED
299     auto types = adoptNS([[NSSet alloc] initWithObjects:_WKWebsiteDataTypeCredentials, nil]);
300     [globalWebViewConfiguration.websiteDataStore removeDataOfTypes:types.get() modifiedSince:[NSDate distantPast] completionHandler:^() {
301         m_currentInvocation->didRemoveAllSessionCredentials();
302     }];
303 #endif
304 }
305
306 void TestController::getAllStorageAccessEntries()
307 {
308 #if WK_API_ENABLED
309     auto* parentView = mainWebView();
310     if (!parentView)
311         return;
312
313     [globalWebViewConfiguration.websiteDataStore _getAllStorageAccessEntriesFor:parentView->platformView() completionHandler:^(NSArray<NSString *> *nsDomains) {
314         Vector<String> domains;
315         domains.reserveInitialCapacity(nsDomains.count);
316         for (NSString *domain : nsDomains)
317             domains.uncheckedAppend(domain);
318         m_currentInvocation->didReceiveAllStorageAccessEntries(domains);
319     }];
320 #endif
321 }
322
323 void TestController::injectUserScript(WKStringRef script)
324 {
325 #if WK_API_ENABLED
326     auto userScript = adoptNS([[WKUserScript alloc] initWithSource: toWTFString(script) injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]);
327
328     [[globalWebViewConfiguration userContentController] addUserScript: userScript.get()];
329 #endif
330 }
331
332 void TestController::addTestKeyToKeychain(const String& privateKeyBase64, const String& attrLabel, const String& applicationTagBase64)
333 {
334     // FIXME(182772)
335 #if PLATFORM(IOS_FAMILY)
336     NSDictionary* options = @{
337         (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
338         (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
339         (id)kSecAttrKeySizeInBits: @256
340     };
341     CFErrorRef errorRef = nullptr;
342     auto key = adoptCF(SecKeyCreateWithData(
343         (__bridge CFDataRef)adoptNS([[NSData alloc] initWithBase64EncodedString:privateKeyBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]).get(),
344         (__bridge CFDictionaryRef)options,
345         &errorRef
346     ));
347     ASSERT(!errorRef);
348
349     NSDictionary* addQuery = @{
350         (id)kSecValueRef: (id)key.get(),
351         (id)kSecClass: (id)kSecClassKey,
352         (id)kSecAttrLabel: attrLabel,
353         (id)kSecAttrApplicationTag: adoptNS([[NSData alloc] initWithBase64EncodedString:applicationTagBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]).get()
354     };
355     OSStatus status = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL);
356     ASSERT_UNUSED(status, !status);
357 #endif
358 }
359
360 void TestController::cleanUpKeychain(const String& attrLabel)
361 {
362     // FIXME(182772)
363 #if PLATFORM(IOS_FAMILY)
364     NSDictionary* deleteQuery = @{
365         (id)kSecClass: (id)kSecClassKey,
366         (id)kSecAttrLabel: attrLabel
367     };
368     SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
369 #endif
370 }
371
372 bool TestController::keyExistsInKeychain(const String& attrLabel, const String& applicationTagBase64)
373 {
374     // FIXME(182772)
375 #if PLATFORM(IOS_FAMILY)
376     NSDictionary *query = @{
377         (id)kSecClass: (id)kSecClassKey,
378         (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
379         (id)kSecAttrLabel: attrLabel,
380         (id)kSecAttrApplicationTag: adoptNS([[NSData alloc] initWithBase64EncodedString:applicationTagBase64 options:NSDataBase64DecodingIgnoreUnknownCharacters]).get(),
381     };
382     OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
383     if (!status)
384         return true;
385     ASSERT(status == errSecItemNotFound);
386 #endif
387     return false;
388 }
389
390 #if PLATFORM(MAC)
391 void TestController::toggleCapsLock()
392 {
393     m_capsLockOn = !m_capsLockOn;
394     NSEvent *fakeEvent = [NSEvent keyEventWithType:NSEventTypeFlagsChanged
395         location:NSZeroPoint
396         modifierFlags:m_capsLockOn ? NSEventModifierFlagCapsLock : 0
397         timestamp:0
398         windowNumber:[mainWebView()->platformWindow() windowNumber]
399         context:nullptr
400         characters:@""
401         charactersIgnoringModifiers:@""
402         isARepeat:NO
403         keyCode:57];
404     
405     [mainWebView()->platformWindow() sendEvent:fakeEvent];
406 }
407 #endif
408
409 } // namespace WTR