/* * Copyright (C) 2011 Keijiro Takahashi * Copyright (C) 2012 GREE, Inc. * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. */ #if !(__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0) #import #import // NOTE: we need extern without "C" before unity 4.5 //extern UIViewController *UnityGetGLViewController(); extern "C" UIViewController *UnityGetGLViewController(); extern "C" void UnitySendMessage(const char *, const char *, const char *); // cf. https://stackoverflow.com/questions/26383031/wkwebview-causes-my-view-controller-to-leak/33365424#33365424 @interface WeakScriptMessageDelegate : NSObject @property (nonatomic, weak) id scriptDelegate; - (instancetype)initWithDelegate:(id)scriptDelegate; @end @implementation WeakScriptMessageDelegate - (instancetype)initWithDelegate:(id)scriptDelegate { self = [super init]; if (self) { _scriptDelegate = scriptDelegate; } return self; } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message]; } @end @protocol WebViewProtocol @property (nonatomic, getter=isOpaque) BOOL opaque; @property (nullable, nonatomic, copy) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; @property (nonatomic, getter=isHidden) BOOL hidden; @property (nonatomic) CGRect frame; @property (nullable, nonatomic, weak) id navigationDelegate; @property (nullable, nonatomic, weak) id UIDelegate; @property (nullable, nonatomic, readonly, copy) NSURL *URL; - (void)load:(NSURLRequest *)request; - (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl; - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler; @property (nonatomic, readonly) BOOL canGoBack; @property (nonatomic, readonly) BOOL canGoForward; - (void)goBack; - (void)goForward; - (void)reload; - (void)stopLoading; - (void)setScrollbarsVisibility:(BOOL)visibility; - (void)setScrollBounce:(BOOL)enable; @end @interface WKWebView(WebViewProtocolConformed) @end @implementation WKWebView(WebViewProtocolConformed) - (void)load:(NSURLRequest *)request { WKWebView *webView = (WKWebView *)self; NSURL *url = [request URL]; if ([url.absoluteString hasPrefix:@"file:"]) { NSURL *top = [NSURL URLWithString:[[url absoluteString] stringByDeletingLastPathComponent]]; [webView loadFileURL:url allowingReadAccessToURL:top]; } else { [webView loadRequest:request]; } } - (NSURLRequest *)constructionCustomHeader:(NSURLRequest *)originalRequest with:(NSDictionary *)headerDictionary { NSMutableURLRequest *convertedRequest = originalRequest.mutableCopy; for (NSString *key in [headerDictionary allKeys]) { [convertedRequest setValue:headerDictionary[key] forHTTPHeaderField:key]; } return (NSURLRequest *)[convertedRequest copy]; } - (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl { WKWebView *webView = (WKWebView *)self; [webView loadHTMLString:html baseURL:baseUrl]; } - (void)setScrollbarsVisibility:(BOOL)visibility { WKWebView *webView = (WKWebView *)self; webView.scrollView.showsHorizontalScrollIndicator = visibility; webView.scrollView.showsVerticalScrollIndicator = visibility; } - (void)setScrollBounce:(BOOL)enable { WKWebView *webView = (WKWebView *)self; webView.scrollView.bounces = enable; } @end @interface CWebViewPlugin : NSObject { UIView *webView; NSString *gameObjectName; NSMutableDictionary *customRequestHeader; BOOL googleAppRedirectionEnabled; BOOL alertDialogEnabled; NSRegularExpression *allowRegex; NSRegularExpression *denyRegex; NSRegularExpression *hookRegex; NSString *basicAuthUserName; NSString *basicAuthPassword; } @end @implementation CWebViewPlugin static WKProcessPool *_sharedProcessPool; static NSMutableArray *_instances = [[NSMutableArray alloc] init]; - (BOOL)isInitialized { return webView != nil; } - (id)initWithGameObjectName:(const char *)gameObjectName_ transparent:(BOOL)transparent zoom:(BOOL)zoom ua:(const char *)ua enableWKWebView:(BOOL)enableWKWebView contentMode:(WKContentMode)contentMode allowsLinkPreview:(BOOL)allowsLinkPreview allowsBackForwardNavigationGestures:(BOOL)allowsBackForwardNavigationGestures radius:(int)radius { self = [super init]; gameObjectName = [NSString stringWithUTF8String:gameObjectName_]; customRequestHeader = [[NSMutableDictionary alloc] init]; googleAppRedirectionEnabled = false; alertDialogEnabled = true; allowRegex = nil; denyRegex = nil; hookRegex = nil; basicAuthUserName = nil; basicAuthPassword = nil; UIView *view = UnityGetGLViewController().view; if (enableWKWebView && [WKWebView class]) { if (_sharedProcessPool == NULL) { _sharedProcessPool = [[WKProcessPool alloc] init]; } WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; WKUserContentController *controller = [[WKUserContentController alloc] init]; [controller addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"unityControl"]; [controller addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"saveDataURL"]; { NSString *str = @"\ window.Unity = { \ call: function(msg) { \ window.webkit.messageHandlers.unityControl.postMessage(msg); \ }, \ saveDataURL: function(fileName, dataURL) { \ window.webkit.messageHandlers.saveDataURL.postMessage(fileName + '\t' + dataURL); \ } \ }; \ "; WKUserScript *script = [[WKUserScript alloc] initWithSource:str injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; [controller addUserScript:script]; } if (!zoom) { NSString *str = @"\ (function() { \ var meta = document.querySelector('meta[name=viewport]'); \ if (meta == null) { \ meta = document.createElement('meta'); \ meta.name = 'viewport'; \ } \ meta.content += ((meta.content.length > 0) ? ',' : '') + 'user-scalable=no'; \ var head = document.getElementsByTagName('head')[0]; \ head.appendChild(meta); \ })(); \ "; WKUserScript *script = [[WKUserScript alloc] initWithSource:str injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; [controller addUserScript:script]; } configuration.userContentController = controller; configuration.allowsInlineMediaPlayback = true; if (@available(iOS 10.0, *)) { configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; } else { if (@available(iOS 9.0, *)) { configuration.requiresUserActionForMediaPlayback = NO; } else { configuration.mediaPlaybackRequiresUserAction = NO; } } configuration.websiteDataStore = [WKWebsiteDataStore defaultDataStore]; configuration.processPool = _sharedProcessPool; if (@available(iOS 13.0, *)) { configuration.defaultWebpagePreferences.preferredContentMode = contentMode; } #if UNITYWEBVIEW_IOS_ALLOW_FILE_URLS // cf. https://stackoverflow.com/questions/35554814/wkwebview-xmlhttprequest-with-file-url/44365081#44365081 try { [configuration.preferences setValue:@TRUE forKey:@"allowFileAccessFromFileURLs"]; } catch (NSException *ex) { } try { [configuration setValue:@TRUE forKey:@"allowUniversalAccessFromFileURLs"]; } catch (NSException *ex) { } #endif WKWebView *wkwebView = [[WKWebView alloc] initWithFrame:view.frame configuration:configuration]; #if UNITYWEBVIEW_DEVELOPMENT NSOperatingSystemVersion version = { 16, 4, 0 }; if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { wkwebView.inspectable = true; } #endif wkwebView.allowsLinkPreview = allowsLinkPreview; wkwebView.allowsBackForwardNavigationGestures = allowsBackForwardNavigationGestures; webView = wkwebView; webView.UIDelegate = self; webView.navigationDelegate = self; if (radius > 0) { webView.layer.cornerRadius = radius; webView.layer.masksToBounds = YES; } if (ua != NULL && strcmp(ua, "") != 0) { ((WKWebView *)webView).customUserAgent = [[NSString alloc] initWithUTF8String:ua]; } // cf. https://rick38yip.medium.com/wkwebview-weird-spacing-issue-in-ios-13-54a4fc686f72 // cf. https://stackoverflow.com/questions/44390971/automaticallyadjustsscrollviewinsets-was-deprecated-in-ios-11-0 if (@available(iOS 11.0, *)) { ((WKWebView *)webView).scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } else { //UnityGetGLViewController().automaticallyAdjustsScrollViewInsets = false; } } else { webView = nil; return self; } if (transparent) { webView.opaque = NO; webView.backgroundColor = [UIColor clearColor]; } webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; webView.hidden = YES; [webView addObserver:self forKeyPath: @"loading" options: NSKeyValueObservingOptionNew context:nil]; [view addSubview:webView]; //set webview for Unity 6 accessibility hierarchy NSMutableArray *accessibilityElements = view.accessibilityElements ? [view.accessibilityElements mutableCopy] : [NSMutableArray array]; [accessibilityElements addObject:(UIAccessibilityElement *)webView]; view.accessibilityElements = accessibilityElements; return self; } - (void)dispose { if (webView != nil) { UIView *webView0 = webView; webView = nil; if ([webView0 isKindOfClass:[WKWebView class]]) { webView0.UIDelegate = nil; webView0.navigationDelegate = nil; [((WKWebView *)webView0).configuration.userContentController removeScriptMessageHandlerForName:@"saveDataURL"]; [((WKWebView *)webView0).configuration.userContentController removeScriptMessageHandlerForName:@"unityControl"]; } [webView0 stopLoading]; [webView0 removeFromSuperview]; [webView0 removeObserver:self forKeyPath:@"loading"]; //remove the WebViewObject from Unity hierarchy tree UIView *view = UnityGetGLViewController().view; NSMutableArray *accessibilityElements = view.accessibilityElements ? [view.accessibilityElements mutableCopy] : [NSMutableArray array]; [accessibilityElements removeObject: (UIAccessibilityElement *)webView0]; view.accessibilityElements = accessibilityElements; } basicAuthPassword = nil; basicAuthUserName = nil; hookRegex = nil; denyRegex = nil; allowRegex = nil; customRequestHeader = nil; gameObjectName = nil; } + (void)resetSharedProcessPool { // cf. https://stackoverflow.com/questions/33156567/getting-all-cookies-from-wkwebview/49744695#49744695 _sharedProcessPool = [[WKProcessPool alloc] init]; [_instances enumerateObjectsUsingBlock:^(CWebViewPlugin *obj, NSUInteger idx, BOOL *stop) { if ([obj->webView isKindOfClass:[WKWebView class]]) { WKWebView *webView = (WKWebView *)obj->webView; webView.configuration.processPool = _sharedProcessPool; } }]; } + (void)clearCookie:(const char *)name of:(const char *)url { NSURL *nsurl = [NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]]; if (nsurl == nil) { return; } NSString *nsname = [NSString stringWithUTF8String:name]; if (@available(iOS 9.0, *)) { WKHTTPCookieStore *cookieStore = WKWebsiteDataStore.defaultDataStore.httpCookieStore; [cookieStore getAllCookies:^(NSArray *array) { [array enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { if ([cookie.name isEqualToString:nsname] && [cookie.domain isEqualToString:nsurl.host] && [cookie.path isEqualToString:nsurl.path]) { [cookieStore deleteCookie:cookie completionHandler:^{}]; } }]; }]; } else { NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; if (cookieStorage == nil) { // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; } [[cookieStorage cookies] enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { if ([cookie.name isEqualToString:nsname] && [cookie.domain isEqualToString:nsurl.host] && [cookie.path isEqualToString:nsurl.path]) { [cookieStorage deleteCookie:cookie]; } }]; } } + (void)clearCookies { [CWebViewPlugin resetSharedProcessPool]; // cf. https://dev.classmethod.jp/smartphone/remove-webview-cookies/ NSString *libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject; NSString *cookiesPath = [libraryPath stringByAppendingPathComponent:@"Cookies"]; NSString *webKitPath = [libraryPath stringByAppendingPathComponent:@"WebKit"]; [[NSFileManager defaultManager] removeItemAtPath:cookiesPath error:nil]; [[NSFileManager defaultManager] removeItemAtPath:webKitPath error:nil]; NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; if (cookieStorage == nil) { // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; } [[cookieStorage cookies] enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { [cookieStorage deleteCookie:cookie]; }]; NSOperatingSystemVersion version = { 9, 0, 0 }; if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { // cf. https://stackoverflow.com/questions/46465070/how-to-delete-cookies-from-wkhttpcookiestore/47928399#47928399 NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:date completionHandler:^{}]; } } + saveCookies { [CWebViewPlugin resetSharedProcessPool]; } - (void)getCookies:(const char *)url { NSOperatingSystemVersion version = { 9, 0, 0 }; if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { NSURL *nsurl = [NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]]; WKHTTPCookieStore *cookieStore = WKWebsiteDataStore.defaultDataStore.httpCookieStore; [cookieStore getAllCookies:^(NSArray *array) { NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; [formatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss zzz"]; NSMutableString *result = [NSMutableString string]; [array enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { if ([cookie.domain isEqualToString:nsurl.host]) { [result appendString:[NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value]]; if ([cookie.domain length] > 0) { [result appendString:[NSString stringWithFormat:@"; "]]; [result appendString:[NSString stringWithFormat:@"Domain=%@", cookie.domain]]; } if ([cookie.path length] > 0) { [result appendString:[NSString stringWithFormat:@"; "]]; [result appendString:[NSString stringWithFormat:@"Path=%@", cookie.path]]; } if (cookie.expiresDate != nil) { [result appendString:[NSString stringWithFormat:@"; "]]; [result appendString:[NSString stringWithFormat:@"Expires=%@", [formatter stringFromDate:cookie.expiresDate]]]; } [result appendString:[NSString stringWithFormat:@"; "]]; [result appendString:[NSString stringWithFormat:@"Version=%zd", cookie.version]]; [result appendString:[NSString stringWithFormat:@"\n"]]; } }]; UnitySendMessage([gameObjectName UTF8String], "CallOnCookies", [result UTF8String]); }]; } else { [CWebViewPlugin resetSharedProcessPool]; NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; [formatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss zzz"]; NSMutableString *result = [NSMutableString string]; NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; if (cookieStorage == nil) { // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; } [[cookieStorage cookiesForURL:[NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]]] enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { [result appendString:[NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value]]; if ([cookie.domain length] > 0) { [result appendString:[NSString stringWithFormat:@"; "]]; [result appendString:[NSString stringWithFormat:@"Domain=%@", cookie.domain]]; } if ([cookie.path length] > 0) { [result appendString:[NSString stringWithFormat:@"; "]]; [result appendString:[NSString stringWithFormat:@"Path=%@", cookie.path]]; } if (cookie.expiresDate != nil) { [result appendString:[NSString stringWithFormat:@"; "]]; [result appendString:[NSString stringWithFormat:@"Expires=%@", [formatter stringFromDate:cookie.expiresDate]]]; } [result appendString:[NSString stringWithFormat:@"; "]]; [result appendString:[NSString stringWithFormat:@"Version=%zd", cookie.version]]; [result appendString:[NSString stringWithFormat:@"\n"]]; }]; UnitySendMessage([gameObjectName UTF8String], "CallOnCookies", [result UTF8String]); } } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { // Log out the message received //NSLog(@"Received event %@", message.body); if ([message.name isEqualToString:@"unityControl"]) { UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[NSString stringWithFormat:@"%@", message.body] UTF8String]); } else if ([message.name isEqualToString:@"saveDataURL"]) { NSRange range = [message.body rangeOfString:@"\t"]; if (range.location == NSNotFound) { return; } NSString *fileName = [[message.body substringWithRange:NSMakeRange(0, range.location)] lastPathComponent]; NSString *dataURL = [message.body substringFromIndex:(range.location + 1)]; range = [dataURL rangeOfString:@"data:"]; if (range.location != 0) { return; } NSString *tmp = [dataURL substringFromIndex:[@"data:" length]]; range = [tmp rangeOfString:@";"]; if (range.location == NSNotFound) { return; } NSString *base64data = [tmp substringFromIndex:(range.location + 1 + [@"base64," length])]; NSString *type = [tmp substringWithRange:NSMakeRange(0, range.location)]; NSData *data = [[NSData alloc] initWithBase64EncodedString:base64data options:0]; NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; path = [path stringByAppendingString:@"/Downloads"]; BOOL isDir; NSError *err = nil; if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) { if (!isDir) { return; } } else { [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&err]; if (err != nil) { return; } } NSString *prefix = [path stringByAppendingString:@"/"]; path = [prefix stringByAppendingString:fileName]; int count = 0; while ([[NSFileManager defaultManager] fileExistsAtPath:path]) { count++; NSString *name = [fileName stringByDeletingPathExtension]; NSString *ext = [fileName pathExtension]; if (ext.length == 0) { path = [NSString stringWithFormat:@"%@%@ (%d)", prefix, name, count]; } else { path = [NSString stringWithFormat:@"%@%@ (%d).%@", prefix, name, count, ext]; } } [data writeToFile:path atomically:YES]; } /* // Then pull something from the device using the message body NSString *version = [[UIDevice currentDevice] valueForKey:message.body]; // Execute some JavaScript using the result? NSString *exec_template = @"set_headline(\"received: %@\");"; NSString *exec = [NSString stringWithFormat:exec_template, version]; [webView evaluateJavaScript:exec completionHandler:nil]; */ } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (webView == nil) return; if ([keyPath isEqualToString:@"loading"] && [[change objectForKey:NSKeyValueChangeNewKey] intValue] == 0 && [webView URL] != nil) { UnitySendMessage( [gameObjectName UTF8String], "CallOnLoaded", [[[webView URL] absoluteString] UTF8String]); } } - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { UnitySendMessage([gameObjectName UTF8String], "CallOnError", "webViewWebContentProcessDidTerminate"); } - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error { UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); } - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error { UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); } - (WKWebView *)webView:(WKWebView *)wkWebView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures { // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c if (!navigationAction.targetFrame.isMainFrame) { [wkWebView loadRequest:navigationAction.request]; } return nil; } - (void)webView:(WKWebView *)wkWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { if (webView == nil) { decisionHandler(WKNavigationActionPolicyCancel); return; } NSURL *nsurl = [navigationAction.request URL]; NSString *url = [nsurl absoluteString]; BOOL pass = YES; if (allowRegex != nil && [allowRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { pass = YES; } else if (denyRegex != nil && [denyRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { pass = NO; } if (!pass) { decisionHandler(WKNavigationActionPolicyCancel); return; } if ([url rangeOfString:@"//itunes.apple.com/"].location != NSNotFound) { if (@available(iOS 10.0, *)) { [[UIApplication sharedApplication] openURL:nsurl options:@{} completionHandler:nil]; } else { [[UIApplication sharedApplication] openURL:nsurl]; } decisionHandler(WKNavigationActionPolicyCancel); return; } else if ([url hasPrefix:@"unity:"]) { UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[url substringFromIndex:6] UTF8String]); decisionHandler(WKNavigationActionPolicyCancel); return; } else if (hookRegex != nil && [hookRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { UnitySendMessage([gameObjectName UTF8String], "CallOnHooked", [url UTF8String]); decisionHandler(WKNavigationActionPolicyCancel); return; } else if (![url hasPrefix:@"about:blank"] // for loadHTML(), cf. #365 && ![url hasPrefix:@"about:srcdoc"] // for iframe srcdoc attribute && ![url hasPrefix:@"file:"] && ![url hasPrefix:@"http:"] && ![url hasPrefix:@"https:"]) { if([[UIApplication sharedApplication] canOpenURL:nsurl]) { if (@available(iOS 10.0, *)) { [[UIApplication sharedApplication] openURL:nsurl options:@{} completionHandler:nil]; } else { [[UIApplication sharedApplication] openURL:nsurl]; } } decisionHandler(WKNavigationActionPolicyCancel); return; } else if (navigationAction.navigationType == WKNavigationTypeLinkActivated && (!navigationAction.targetFrame || !navigationAction.targetFrame.isMainFrame)) { // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c [webView load:navigationAction.request]; decisionHandler(WKNavigationActionPolicyCancel); return; } else { if (navigationAction.targetFrame != nil && navigationAction.targetFrame.isMainFrame) { // If the custom header is not attached, give it and make a request again. if (![self isSetupedCustomHeader:[navigationAction request]]) { //NSLog(@"navi ... %@", navigationAction); [wkWebView loadRequest:[self constructionCustomHeader:navigationAction.request]]; decisionHandler(WKNavigationActionPolicyCancel); return; } } } UnitySendMessage([gameObjectName UTF8String], "CallOnStarted", [url UTF8String]); // cf. https://stackoverflow.com/questions/37086605/disable-wkwebview-for-opening-links-to-redirect-to-apps-installed-on-my-iphone/76948270#76948270 if (!googleAppRedirectionEnabled && [url hasPrefix:@"https://www.google.com/"] && navigationAction.navigationType == WKNavigationTypeLinkActivated) { [webView load:navigationAction.request]; decisionHandler(WKNavigationActionPolicyCancel); return; } decisionHandler(WKNavigationActionPolicyAllow); } - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse * response = (NSHTTPURLResponse *)navigationResponse.response; if (response.statusCode >= 400) { UnitySendMessage([gameObjectName UTF8String], "CallOnHttpError", [[NSString stringWithFormat:@"%d", response.statusCode] UTF8String]); } } decisionHandler(WKNavigationResponsePolicyAllow); } // alert - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { if (!alertDialogEnabled) { completionHandler(); return; } UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction: [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; } // confirm - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler { if (!alertDialogEnabled) { completionHandler(NO); return; } UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { completionHandler(YES); }]]; [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(NO); }]]; [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; } // prompt - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler { if (!alertDialogEnabled) { completionHandler(nil); return; } UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert]; [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.text = defaultText; }]; [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { NSString *input = ((UITextField *)alertController.textFields.firstObject).text; completionHandler(input); }]]; [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(nil); }]]; [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; } - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler { NSURLSessionAuthChallengeDisposition disposition; NSURLCredential *credential; if (basicAuthUserName && basicAuthPassword && [challenge previousFailureCount] == 0) { disposition = NSURLSessionAuthChallengeUseCredential; credential = [NSURLCredential credentialWithUser:basicAuthUserName password:basicAuthPassword persistence:NSURLCredentialPersistenceForSession]; } else { disposition = NSURLSessionAuthChallengePerformDefaultHandling; credential = nil; } completionHandler(disposition, credential); } - (BOOL)isSetupedCustomHeader:(NSURLRequest *)targetRequest { // Check for additional custom header. for (NSString *key in [customRequestHeader allKeys]) { if (![[[targetRequest allHTTPHeaderFields] objectForKey:key] isEqualToString:[customRequestHeader objectForKey:key]]) { return NO; } } return YES; } - (NSURLRequest *)constructionCustomHeader:(NSURLRequest *)originalRequest { NSMutableURLRequest *convertedRequest = originalRequest.mutableCopy; for (NSString *key in [customRequestHeader allKeys]) { [convertedRequest setValue:customRequestHeader[key] forHTTPHeaderField:key]; } return (NSURLRequest *)[convertedRequest copy]; } - (void)setMargins:(float)left top:(float)top right:(float)right bottom:(float)bottom relative:(BOOL)relative { if (webView == nil) return; UIView *view = UnityGetGLViewController().view; CGRect frame = webView.frame; CGRect screen = view.bounds; if (relative) { frame.size.width = floor(screen.size.width * (1.0f - left - right)); frame.size.height = floor(screen.size.height * (1.0f - top - bottom)); frame.origin.x = floor(screen.size.width * left); frame.origin.y = floor(screen.size.height * top); } else { CGFloat scale = 1.0f / [self getScale:view]; frame.size.width = floor(screen.size.width - scale * (left + right)); frame.size.height = floor(screen.size.height - scale * (top + bottom)); frame.origin.x = floor(scale * left); frame.origin.y = floor(scale * top); } webView.frame = frame; } - (CGFloat)getScale:(UIView *)view { if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) return view.window.screen.nativeScale; return view.contentScaleFactor; } - (void)setVisibility:(BOOL)visibility { if (webView == nil) return; webView.hidden = visibility ? NO : YES; } - (void)setInteractionEnabled:(BOOL)enabled { if (webView == nil) return; webView.userInteractionEnabled = enabled; } - (void)setGoogleAppRedirectionEnabled:(BOOL)enabled { if (webView == nil) return; googleAppRedirectionEnabled = enabled; } - (void)setAlertDialogEnabled:(BOOL)enabled { alertDialogEnabled = enabled; } - (void)setScrollbarsVisibility:(BOOL)visibility { if (webView == nil) return; [webView setScrollbarsVisibility:visibility]; } - (void)setScrollBounceEnabled:(BOOL)enabled { if (webView == nil) return; [webView setScrollBounce:enabled]; } - (BOOL)setURLPattern:(const char *)allowPattern and:(const char *)denyPattern and:(const char *)hookPattern { NSError *err = nil; NSRegularExpression *allow = nil; NSRegularExpression *deny = nil; NSRegularExpression *hook = nil; if (allowPattern == nil || *allowPattern == '\0') { allow = nil; } else { allow = [NSRegularExpression regularExpressionWithPattern:[NSString stringWithUTF8String:allowPattern] options:0 error:&err]; if (err != nil) { return NO; } } if (denyPattern == nil || *denyPattern == '\0') { deny = nil; } else { deny = [NSRegularExpression regularExpressionWithPattern:[NSString stringWithUTF8String:denyPattern] options:0 error:&err]; if (err != nil) { return NO; } } if (hookPattern == nil || *hookPattern == '\0') { hook = nil; } else { hook = [NSRegularExpression regularExpressionWithPattern:[NSString stringWithUTF8String:hookPattern] options:0 error:&err]; if (err != nil) { return NO; } } allowRegex = allow; denyRegex = deny; hookRegex = hook; return YES; } - (void)loadURL:(const char *)url { if (webView == nil) return; NSString *urlStr = [NSString stringWithUTF8String:url]; NSURL *nsurl = [NSURL URLWithString:urlStr]; NSURLRequest *request = [NSURLRequest requestWithURL:nsurl]; [webView load:request]; } - (void)loadHTML:(const char *)html baseURL:(const char *)baseUrl { if (webView == nil) return; NSString *htmlStr = [NSString stringWithUTF8String:html]; NSString *baseStr = [NSString stringWithUTF8String:baseUrl]; NSURL *baseNSUrl = [NSURL URLWithString:baseStr]; [webView loadHTML:htmlStr baseURL:baseNSUrl]; } - (void)evaluateJS:(const char *)js { if (webView == nil) return; NSString *jsStr = [NSString stringWithUTF8String:js]; [webView evaluateJavaScript:jsStr completionHandler:^(NSString *result, NSError *error) {}]; } - (int)progress { if (webView == nil) return 0; if ([webView isKindOfClass:[WKWebView class]]) { return (int)([(WKWebView *)webView estimatedProgress] * 100); } else { return 0; } } - (BOOL)canGoBack { if (webView == nil) return false; return [webView canGoBack]; } - (BOOL)canGoForward { if (webView == nil) return false; return [webView canGoForward]; } - (void)goBack { if (webView == nil) return; [webView goBack]; } - (void)goForward { if (webView == nil) return; [webView goForward]; } - (void)reload { if (webView == nil) return; [webView reload]; } - (void)addCustomRequestHeader:(const char *)headerKey value:(const char *)headerValue { NSString *keyString = [NSString stringWithUTF8String:headerKey]; NSString *valueString = [NSString stringWithUTF8String:headerValue]; [customRequestHeader setObject:valueString forKey:keyString]; } - (void)removeCustomRequestHeader:(const char *)headerKey { NSString *keyString = [NSString stringWithUTF8String:headerKey]; if ([[customRequestHeader allKeys]containsObject:keyString]) { [customRequestHeader removeObjectForKey:keyString]; } } - (void)clearCustomRequestHeader { [customRequestHeader removeAllObjects]; } - (const char *)getCustomRequestHeaderValue:(const char *)headerKey { NSString *keyString = [NSString stringWithUTF8String:headerKey]; NSString *result = [customRequestHeader objectForKey:keyString]; if (!result) { return NULL; } const char *s = [result UTF8String]; char *r = (char *)malloc(strlen(s) + 1); strcpy(r, s); return r; } - (void)setBasicAuthInfo:(const char *)userName password:(const char *)password { basicAuthUserName = [NSString stringWithUTF8String:userName]; basicAuthPassword = [NSString stringWithUTF8String:password]; } - (void)clearCache:(BOOL)includeDiskFiles { if (webView == nil) return; NSMutableSet *types = [NSMutableSet setWithArray:@[WKWebsiteDataTypeMemoryCache]]; if (includeDiskFiles) { [types addObject:WKWebsiteDataTypeDiskCache]; } NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:types modifiedSince:date completionHandler:^{}]; } - (void)setAllMediaPlaybackSuspended:(BOOL)suspended { NSOperatingSystemVersion version = { 15, 0, 0 }; if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { if ([webView isKindOfClass:[WKWebView class]]) { [(WKWebView *)webView setAllMediaPlaybackSuspended:suspended completionHandler:nil]; } } } @end extern "C" { BOOL _CWebViewPlugin_IsInitialized(void *instance); void *_CWebViewPlugin_Init(const char *gameObjectName, BOOL transparent, BOOL zoom, const char *ua, BOOL enableWKWebView, int contentMode, BOOL allowsLinkPreview, BOOL allowsBackForwardNavigationGestures, int radius); void _CWebViewPlugin_Destroy(void *instance); void _CWebViewPlugin_SetMargins( void *instance, float left, float top, float right, float bottom, BOOL relative); void _CWebViewPlugin_SetVisibility(void *instance, BOOL visibility); void _CWebViewPlugin_SetInteractionEnabled(void *instance, BOOL enabled); void _CWebViewPlugin_SetGoogleAppRedirectionEnabled(void *instance, BOOL enabled); void _CWebViewPlugin_SetAlertDialogEnabled(void *instance, BOOL visibility); void _CWebViewPlugin_SetScrollbarsVisibility(void *instance, BOOL visibility); void _CWebViewPlugin_SetScrollBounceEnabled(void *instance, BOOL enabled); BOOL _CWebViewPlugin_SetURLPattern(void *instance, const char *allowPattern, const char *denyPattern, const char *hookPattern); void _CWebViewPlugin_LoadURL(void *instance, const char *url); void _CWebViewPlugin_LoadHTML(void *instance, const char *html, const char *baseUrl); void _CWebViewPlugin_EvaluateJS(void *instance, const char *url); int _CWebViewPlugin_Progress(void *instance); BOOL _CWebViewPlugin_CanGoBack(void *instance); BOOL _CWebViewPlugin_CanGoForward(void *instance); void _CWebViewPlugin_GoBack(void *instance); void _CWebViewPlugin_GoForward(void *instance); void _CWebViewPlugin_Reload(void *instance); void _CWebViewPlugin_AddCustomHeader(void *instance, const char *headerKey, const char *headerValue); void _CWebViewPlugin_RemoveCustomHeader(void *instance, const char *headerKey); void _CWebViewPlugin_ClearCustomHeader(void *instance); void _CWebViewPlugin_ClearCookie(const char *url, const char *name); void _CWebViewPlugin_ClearCookies(); void _CWebViewPlugin_SaveCookies(); void _CWebViewPlugin_GetCookies(void *instance, const char *url); const char *_CWebViewPlugin_GetCustomHeaderValue(void *instance, const char *headerKey); void _CWebViewPlugin_SetBasicAuthInfo(void *instance, const char *userName, const char *password); void _CWebViewPlugin_ClearCache(void *instance, BOOL includeDiskFiles); void _CWebViewPlugin_SetSuspended(void *instance, BOOL suspended); } BOOL _CWebViewPlugin_IsInitialized(void *instance) { if (instance == NULL) return NO; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; return [webViewPlugin isInitialized]; } void *_CWebViewPlugin_Init(const char *gameObjectName, BOOL transparent, BOOL zoom, const char *ua, BOOL enableWKWebView, int contentMode, BOOL allowsLinkPreview, BOOL allowsBackForwardNavigationGestures, int radius) { if (! (enableWKWebView && [WKWebView class])) return nil; WKContentMode wkContentMode = WKContentModeRecommended; switch (contentMode) { case 1: wkContentMode = WKContentModeMobile; break; case 2: wkContentMode = WKContentModeDesktop; break; default: wkContentMode = WKContentModeRecommended; break; } CWebViewPlugin *webViewPlugin = [[CWebViewPlugin alloc] initWithGameObjectName:gameObjectName transparent:transparent zoom:zoom ua:ua enableWKWebView:enableWKWebView contentMode:wkContentMode allowsLinkPreview:allowsLinkPreview allowsBackForwardNavigationGestures:allowsBackForwardNavigationGestures radius:radius]; [_instances addObject:webViewPlugin]; return (__bridge_retained void *)webViewPlugin; } void _CWebViewPlugin_Destroy(void *instance) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge_transfer CWebViewPlugin *)instance; [_instances removeObject:webViewPlugin]; [webViewPlugin dispose]; webViewPlugin = nil; } void _CWebViewPlugin_SetMargins( void *instance, float left, float top, float right, float bottom, BOOL relative) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin setMargins:left top:top right:right bottom:bottom relative:relative]; } void _CWebViewPlugin_SetVisibility(void *instance, BOOL visibility) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin setVisibility:visibility]; } void _CWebViewPlugin_SetInteractionEnabled(void *instance, BOOL enabled) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin setInteractionEnabled:enabled]; } void _CWebViewPlugin_SetGoogleAppRedirectionEnabled(void *instance, BOOL enabled) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin setGoogleAppRedirectionEnabled:enabled]; } void _CWebViewPlugin_SetAlertDialogEnabled(void *instance, BOOL enabled) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin setAlertDialogEnabled:enabled]; } void _CWebViewPlugin_SetScrollbarsVisibility(void *instance, BOOL visibility) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin setScrollbarsVisibility:visibility]; } void _CWebViewPlugin_SetScrollBounceEnabled(void *instance, BOOL enabled) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin setScrollBounceEnabled:enabled]; } BOOL _CWebViewPlugin_SetURLPattern(void *instance, const char *allowPattern, const char *denyPattern, const char *hookPattern) { if (instance == NULL) return NO; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; return [webViewPlugin setURLPattern:allowPattern and:denyPattern and:hookPattern]; } void _CWebViewPlugin_LoadURL(void *instance, const char *url) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin loadURL:url]; } void _CWebViewPlugin_LoadHTML(void *instance, const char *html, const char *baseUrl) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin loadHTML:html baseURL:baseUrl]; } void _CWebViewPlugin_EvaluateJS(void *instance, const char *js) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin evaluateJS:js]; } int _CWebViewPlugin_Progress(void *instance) { if (instance == NULL) return 0; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; return [webViewPlugin progress]; } BOOL _CWebViewPlugin_CanGoBack(void *instance) { if (instance == NULL) return false; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; return [webViewPlugin canGoBack]; } BOOL _CWebViewPlugin_CanGoForward(void *instance) { if (instance == NULL) return false; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; return [webViewPlugin canGoForward]; } void _CWebViewPlugin_GoBack(void *instance) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin goBack]; } void _CWebViewPlugin_GoForward(void *instance) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin goForward]; } void _CWebViewPlugin_Reload(void *instance) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin reload]; } void _CWebViewPlugin_AddCustomHeader(void *instance, const char *headerKey, const char *headerValue) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin addCustomRequestHeader:headerKey value:headerValue]; } void _CWebViewPlugin_RemoveCustomHeader(void *instance, const char *headerKey) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin removeCustomRequestHeader:headerKey]; } void _CWebViewPlugin_ClearCustomHeader(void *instance) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin clearCustomRequestHeader]; } void _CWebViewPlugin_ClearCookie(const char *url, const char *name) { [CWebViewPlugin clearCookie:name of:url]; } void _CWebViewPlugin_ClearCookies() { [CWebViewPlugin clearCookies]; } void _CWebViewPlugin_SaveCookies() { [CWebViewPlugin saveCookies]; } void _CWebViewPlugin_GetCookies(void *instance, const char *url) { CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin getCookies:url]; } const char *_CWebViewPlugin_GetCustomHeaderValue(void *instance, const char *headerKey) { if (instance == NULL) return NULL; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; return [webViewPlugin getCustomRequestHeaderValue:headerKey]; } void _CWebViewPlugin_SetBasicAuthInfo(void *instance, const char *userName, const char *password) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin setBasicAuthInfo:userName password:password]; } void _CWebViewPlugin_ClearCache(void *instance, BOOL includeDiskFiles) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin clearCache:includeDiskFiles]; } void _CWebViewPlugin_SetSuspended(void *instance, BOOL suspended) { if (instance == NULL) return; CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin setAllMediaPlaybackSuspended:suspended]; } #endif // !(__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0)