/* * Copyright (C) 2010-2016 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 "AppDelegate.h" #import "BrowserWindowController.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import @implementation NSApplication (PlaywrightApplicationExtensions) - (BrowserAppDelegate *)browserAppDelegate { return (BrowserAppDelegate *)[self delegate]; } @end @interface NSApplication (TouchBar) @property (getter=isAutomaticCustomizeTouchBarMenuItemEnabled) BOOL automaticCustomizeTouchBarMenuItemEnabled; @property (readonly, nonatomic) WKWebViewConfiguration *defaultConfiguration; @end @implementation WebViewDialog - (void)dealloc { [_webView release]; _webView = nil; [super dealloc]; } @end enum { _NSBackingStoreUnbuffered = 3 }; NSString* const ActivityReason = @"Batch headless process"; const NSActivityOptions ActivityOptions = (NSActivityUserInitiatedAllowingIdleSystemSleep | NSActivityLatencyCritical) & ~(NSActivitySuddenTerminationDisabled | NSActivityAutomaticTerminationDisabled); @implementation BrowserAppDelegate - (id)init { self = [super init]; if (!self) return nil; _initialURL = nil; _userDataDir = nil; _proxyServer = nil; _proxyBypassList = nil; NSArray *arguments = [[NSProcessInfo processInfo] arguments]; NSRange subargs = NSMakeRange(1, [arguments count] - 1); NSArray *subArray = [arguments subarrayWithRange:subargs]; for (NSString *argument in subArray) { if (![argument hasPrefix:@"--"]) _initialURL = argument; if ([argument hasPrefix:@"--user-data-dir="]) { NSRange range = NSMakeRange(16, [argument length] - 16); _userDataDir = [[argument substringWithRange:range] copy]; } if ([argument hasPrefix:@"--proxy="]) { NSRange range = NSMakeRange(8, [argument length] - 8); _proxyServer = [[argument substringWithRange:range] copy]; } if ([argument hasPrefix:@"--proxy-bypass-list="]) { NSRange range = NSMakeRange(20, [argument length] - 20); _proxyBypassList = [[argument substringWithRange:range] copy]; } } _headless = [arguments containsObject: @"--headless"]; _noStartupWindow = [arguments containsObject: @"--no-startup-window"]; _browserContexts = [[NSMutableSet alloc] init]; if (_headless) { _headlessWindows = [[NSMutableSet alloc] init]; [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; [[NSProcessInfo processInfo] beginActivityWithOptions:ActivityOptions reason:ActivityReason]; _dialogs = [[NSMutableSet alloc] init]; } else { [NSApp activateIgnoringOtherApps:YES]; } if ([arguments containsObject: @"--inspector-pipe"]) [_WKBrowserInspector initializeRemoteInspectorPipe:self headless:_headless]; return self; } - (void)awakeFromNib { if ([NSApp respondsToSelector:@selector(setAutomaticCustomizeTouchBarMenuItemEnabled:)]) [NSApp setAutomaticCustomizeTouchBarMenuItemEnabled:YES]; } - (NSDictionary *)proxyConfiguration:(NSString *)proxyServer WithBypassList:(NSString *)proxyBypassList { if (!proxyServer || ![proxyServer length]) return nil; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" NSMutableDictionary *dictionary = [[[NSMutableDictionary alloc] init] autorelease]; NSURL *proxyURL = [NSURL URLWithString:proxyServer]; NSString *host = [proxyURL host]; NSNumber *port = [proxyURL port]; if ([proxyServer hasPrefix:@"socks5://"]) { [dictionary setObject:host forKey:(NSString *)kCFStreamPropertySOCKSProxyHost]; if (port) [dictionary setObject:port forKey:(NSString *)kCFStreamPropertySOCKSProxyPort]; } else { [dictionary setObject:host forKey:(NSString *)kCFStreamPropertyHTTPSProxyHost]; [dictionary setObject:host forKey:(NSString *)kCFStreamPropertyHTTPProxyHost]; if (port) { [dictionary setObject:port forKey:(NSString *)kCFStreamPropertyHTTPSProxyPort]; [dictionary setObject:port forKey:(NSString *)kCFStreamPropertyHTTPProxyPort]; } } if (proxyBypassList && [proxyBypassList length]) { NSArray* bypassList = [proxyBypassList componentsSeparatedByString:@","]; [dictionary setObject:bypassList forKey:@"ExceptionsList"]; } #pragma clang diagnostic pop return dictionary; } - (WKWebsiteDataStore *)persistentDataStore { static WKWebsiteDataStore *dataStore; if (!dataStore) { _WKWebsiteDataStoreConfiguration *configuration = [[[_WKWebsiteDataStoreConfiguration alloc] init] autorelease]; if (_userDataDir) { // Local storage state should be stored in separate dirs for persistent contexts. [configuration setShouldUseCustomStoragePaths:YES]; NSURL *cookieFile = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/cookie.db", _userDataDir]]; [configuration _setCookieStorageFile:cookieFile]; NSURL *applicationCacheDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/ApplicationCache", _userDataDir]]; [configuration setApplicationCacheDirectory:applicationCacheDirectory]; NSURL *cacheStorageDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/CacheStorage", _userDataDir]]; [configuration _setCacheStorageDirectory:cacheStorageDirectory]; NSURL *indexedDBDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/IndexedDB", _userDataDir]]; [configuration _setIndexedDBDatabaseDirectory:indexedDBDirectory]; NSURL *localStorageDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/LocalStorage", _userDataDir]]; [configuration _setWebStorageDirectory:localStorageDirectory]; NSURL *mediaCacheDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/MediaCache", _userDataDir]]; [configuration setMediaCacheDirectory:mediaCacheDirectory]; NSURL *mediaKeysDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/MediaKeys", _userDataDir]]; [configuration setMediaKeysStorageDirectory:mediaKeysDirectory]; NSURL *networkCacheDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/NetworkCache", _userDataDir]]; [configuration setNetworkCacheDirectory:networkCacheDirectory]; NSURL *loadStatsDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/ResourceLoadStatistics", _userDataDir]]; [configuration _setResourceLoadStatisticsDirectory:loadStatsDirectory]; NSURL *serviceWorkersDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/ServiceWorkers", _userDataDir]]; [configuration _setServiceWorkerRegistrationDirectory:serviceWorkersDirectory]; NSURL *webSqlDirectory = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/WebSQL", _userDataDir]]; [configuration _setWebSQLDatabaseDirectory:webSqlDirectory]; } [configuration setProxyConfiguration:[self proxyConfiguration:_proxyServer WithBypassList:_proxyBypassList]]; dataStore = [[WKWebsiteDataStore alloc] _initWithConfiguration:configuration]; } return dataStore; } - (WKWebViewConfiguration *)defaultConfiguration { static WKWebViewConfiguration *configuration; if (!configuration) { configuration = [[WKWebViewConfiguration alloc] init]; configuration.websiteDataStore = [self persistentDataStore]; configuration.preferences._fullScreenEnabled = YES; configuration.preferences._developerExtrasEnabled = YES; configuration.preferences._mediaDevicesEnabled = YES; configuration.preferences._mockCaptureDevicesEnabled = YES; configuration.preferences._hiddenPageDOMTimerThrottlingEnabled = NO; configuration.preferences._hiddenPageDOMTimerThrottlingAutoIncreases = NO; configuration.preferences._pageVisibilityBasedProcessSuppressionEnabled = NO; configuration.preferences._domTimersThrottlingEnabled = NO; _WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease]; processConfiguration.forceOverlayScrollbars = YES; configuration.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease]; } return configuration; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { if (!_headless) [self _updateNewWindowKeyEquivalents]; if (_noStartupWindow) return; // Force creation of the default browser context. [self defaultConfiguration]; // Creating the first NSWindow immediately makes it invisible in headless mode, // so we postpone it for 50ms. Experiments show that 10ms is not enough, and 20ms is enough. // We give it 50ms just in case. [NSTimer scheduledTimerWithTimeInterval: 0.05 repeats: NO block:(void *)^(NSTimer* timer) { [self createNewPage:0 withURL:_initialURL ? _initialURL : @"about:blank"]; _initialURL = nil; }]; } - (void)_updateNewWindowKeyEquivalents { NSString *normalWindowEquivalent = @"n"; _newWebKit2WindowItem.keyEquivalentModifierMask = NSEventModifierFlagCommand; _newWebKit2WindowItem.keyEquivalent = normalWindowEquivalent; } #pragma mark WKBrowserInspectorDelegate - (WKWebViewConfiguration *) sessionConfiguration:(uint64_t)sessionID { for (_WKBrowserContext *browserContext in _browserContexts) { if ([[browserContext dataStore] sessionID] != sessionID) continue; WKWebViewConfiguration *configuration = [[[self defaultConfiguration] copy] autorelease]; configuration.websiteDataStore = [browserContext dataStore]; configuration.processPool = [browserContext processPool]; return configuration; } return [self defaultConfiguration]; } - (WKWebView *)createNewPage:(uint64_t)sessionID { return [self createNewPage:sessionID withURL:@"about:blank"]; } - (WKWebView *)createNewPage:(uint64_t)sessionID withURL:(NSString*)urlString { WKWebViewConfiguration *configuration = [self sessionConfiguration:sessionID]; if (_headless) return [self createHeadlessPage:configuration withURL:urlString]; return [self createHeadfulPage:configuration withURL:urlString]; } - (WKWebView *)createHeadfulPage:(WKWebViewConfiguration *)configuration withURL:(NSString*)urlString { // WebView lifecycle will control the BrowserWindowController life times. BrowserWindowController *controller = [[BrowserWindowController alloc] initWithConfiguration:configuration]; if (!controller) return nil; [controller loadURLString:urlString]; NSWindow *window = controller.window; [window setIsVisible:YES]; return [controller webView]; } - (WKWebView *)createHeadlessPage:(WKWebViewConfiguration *)configuration withURL:(NSString*)urlString { NSRect rect = NSMakeRect(0, 0, 1280, 720); NSScreen *firstScreen = [[NSScreen screens] objectAtIndex:0]; NSRect windowRect = NSOffsetRect(rect, -10000, [firstScreen frame].size.height - rect.size.height + 10000); NSWindow* window = [[NSWindow alloc] initWithContentRect:windowRect styleMask:NSWindowStyleMaskBorderless backing:(NSBackingStoreType)_NSBackingStoreUnbuffered defer:YES]; WKWebView* webView = [[WKWebView alloc] initWithFrame:[window.contentView bounds] configuration:configuration]; webView._windowOcclusionDetectionEnabled = NO; if (!webView) return nil; webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; [window.contentView addSubview:webView]; [window setIsVisible:YES]; if (urlString) { NSURL *url = [NSURL _webkit_URLWithUserTypedString:urlString]; [webView loadRequest:[NSURLRequest requestWithURL:url]]; } [_headlessWindows addObject:window]; webView.navigationDelegate = self; webView.UIDelegate = self; return [webView autorelease]; } - (_WKBrowserContext *)createBrowserContext:(NSString *)proxyServer WithBypassList:(NSString *) proxyBypassList { _WKBrowserContext *browserContext = [[_WKBrowserContext alloc] init]; _WKProcessPoolConfiguration *processConfiguration = [[[_WKProcessPoolConfiguration alloc] init] autorelease]; processConfiguration.forceOverlayScrollbars = YES; _WKWebsiteDataStoreConfiguration *dataStoreConfiguration = [[[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration] autorelease]; if (!proxyServer || ![proxyServer length]) proxyServer = _proxyServer; if (!proxyBypassList || ![proxyBypassList length]) proxyBypassList = _proxyBypassList; [dataStoreConfiguration setProxyConfiguration:[self proxyConfiguration:proxyServer WithBypassList:proxyBypassList]]; browserContext.dataStore = [[[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration] autorelease]; browserContext.processPool = [[[WKProcessPool alloc] _initWithConfiguration:processConfiguration] autorelease]; [_browserContexts addObject:browserContext]; return browserContext; } - (void)deleteBrowserContext:(uint64_t)sessionID { for (_WKBrowserContext *browserContext in _browserContexts) { if ([[browserContext dataStore] sessionID] != sessionID) continue; [_browserContexts removeObject:browserContext]; return; } } - (void)quit { [NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0]; } #pragma mark WKUIDelegate - (void)webViewDidClose:(WKWebView *)webView { [self webView:webView handleJavaScriptDialog:false value:nil]; for (NSWindow *window in _headlessWindows) { if (webView.window != window) continue; [webView removeFromSuperview]; [window close]; [_headlessWindows removeObject:window]; break; } } - (void)_webView:(WKWebView *)webView getWindowFrameWithCompletionHandler:(void (^)(CGRect))completionHandler { completionHandler([webView.window frame]); } - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { WebViewDialog* dialog = [[WebViewDialog alloc] autorelease]; dialog.webView = webView; dialog.completionHandler = ^void (BOOL accept, NSString* value) { completionHandler(); }; [_dialogs addObject:dialog]; } - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler { WebViewDialog* dialog = [[WebViewDialog alloc] autorelease]; dialog.webView = webView; dialog.completionHandler = ^void (BOOL accept, NSString* value) { completionHandler(accept); }; [_dialogs addObject:dialog]; } - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler { WebViewDialog* dialog = [[WebViewDialog alloc] autorelease]; dialog.webView = webView; dialog.completionHandler = ^void (BOOL accept, NSString* value) { completionHandler(accept && value ? value : nil); }; [_dialogs addObject:dialog]; } - (void)_webView:(WKWebView *)webView runBeforeUnloadConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler { WebViewDialog* dialog = [[WebViewDialog alloc] autorelease]; dialog.webView = webView; dialog.completionHandler = ^void (BOOL accept, NSString* value) { completionHandler(accept); }; [_dialogs addObject:dialog]; } - (void)webView:(WKWebView *)webView handleJavaScriptDialog:(BOOL)accept value:(NSString *)value { for (WebViewDialog *dialog in _dialogs) { if (dialog.webView != webView) continue; dialog.completionHandler(accept, value); [_dialogs removeObject:dialog]; break; } } - (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures { return [self createHeadlessPage:configuration withURL:nil]; } - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { LOG(@"decidePolicyForNavigationAction"); if (navigationAction.shouldPerformDownload) { decisionHandler(WKNavigationActionPolicyDownload); return; } if (navigationAction.buttonNumber == 1 && (navigationAction.modifierFlags & (NSEventModifierFlagCommand | NSEventModifierFlagShift)) != 0) { WKWindowFeatures* windowFeatures = [[[WKWindowFeatures alloc] init] autorelease]; WKWebView* newView = [self webView:webView createWebViewWithConfiguration:webView.configuration forNavigationAction:navigationAction windowFeatures:windowFeatures]; [newView loadRequest:navigationAction.request]; decisionHandler(WKNavigationActionPolicyCancel); return; } if (navigationAction._canHandleRequest) { decisionHandler(WKNavigationActionPolicyAllow); return; } decisionHandler(WKNavigationActionPolicyCancel); } - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { if (![navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { decisionHandler(WKNavigationResponsePolicyAllow); return; } NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)navigationResponse.response; NSString *disposition = [[httpResponse allHeaderFields] objectForKey:@"Content-Disposition"]; if (disposition && [disposition hasPrefix:@"attachment"]) { decisionHandler(WKNavigationResponsePolicyDownload); return; } decisionHandler(WKNavigationResponsePolicyAllow); } - (void)webView:(WKWebView *)webView navigationAction:(WKNavigationAction *)navigationAction didBecomeDownload:(WKDownload *)download { download.delegate = self; } - (void)webView:(WKWebView *)webView navigationResponse:(WKNavigationResponse *)navigationResponse didBecomeDownload:(WKDownload *)download { download.delegate = self; } #pragma mark WKDownloadDelegate - (void)download:(WKDownload *)download decideDestinationUsingResponse:(NSURLResponse *)response suggestedFilename:(NSString *)suggestedFilename completionHandler:(void (^)(NSURL * _Nullable destination))completionHandler { completionHandler(nil); } @end