diff --git a/README.md b/README.md index ac0ae09a..2b731f39 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,10 @@ If set to true, links with `target="_blank"` or `window.open` will be opened in Set `sendCookies` to true to copy cookies from `sharedHTTPCookieStorage` when calling loadRequest. This emulates the behavior of react-native's `WebView` component. You can set cookies using `react-native-cookies` Default is false. +- **useWKCookieStore** + +Set `useWKCookieStore` to true to use the webView's `WKHTTPCookieStorage`. All Cookies from `sharedHTTPCookieStorage` will be copied to it. + - **source={{file: '', allowingReadAccessToURL: '' }}** This allows WKWebView loads a local HTML file. Please note the underlying API is only introduced in iOS 9+. So in iOS 8, it will simple ignores these two properties. diff --git a/WKWebView.ios.js b/WKWebView.ios.js index f9a37eb1..5b508e5b 100644 --- a/WKWebView.ios.js +++ b/WKWebView.ios.js @@ -232,6 +232,10 @@ class WKWebView extends React.Component { * Set this to true to emulate behavior of WebView component. */ sendCookies: PropTypes.bool, + /** + * Initializes the webView's WKHTTPCookieStorage and copies all cookies from sharedHTTPCookieStorage + */ + useWKCookieStore: PropTypes.bool, /** * If set to true, target="_blank" or window.open will be opened in WebView, instead * of new window. Default is false to be backward compatible. @@ -316,7 +320,8 @@ class WKWebView extends React.Component { if (this.props.source && typeof this.props.source === 'object') { source = Object.assign({}, this.props.source, { sendCookies: this.props.sendCookies, - customUserAgent: this.props.customUserAgent || this.props.userAgent + customUserAgent: this.props.customUserAgent || this.props.userAgent, + useWKCookieStore: this.props.useWKCookieStore }); if (this.props.html) { diff --git a/ios/RCTWKWebView/RCTWKWebView.m b/ios/RCTWKWebView/RCTWKWebView.m index e1348c81..e497e01d 100644 --- a/ios/RCTWKWebView/RCTWKWebView.m +++ b/ios/RCTWKWebView/RCTWKWebView.m @@ -35,6 +35,7 @@ @interface RCTWKWebView () = 110000 /* __IPHONE_11_0 */ // `contentInsetAdjustmentBehavior` is only available since iOS 11. // We set the default behavior to "never" so that iOS @@ -163,7 +164,7 @@ - (void)loadRequest:(NSURLRequest *)request request = mutableRequest; } } - + [_webView loadRequest:request]; } @@ -179,29 +180,29 @@ -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView if (!hideKeyboardAccessoryView) { return; } - + UIView* subview; for (UIView* view in _webView.scrollView.subviews) { if([[view.class description] hasPrefix:@"WKContent"]) subview = view; } - + if(subview == nil) return; - + NSString* name = [NSString stringWithFormat:@"%@_SwizzleHelperWK", subview.class.superclass]; Class newClass = NSClassFromString(name); - + if(newClass == nil) { newClass = objc_allocateClassPair(subview.class, [name cStringUsingEncoding:NSASCIIStringEncoding], 0); if(!newClass) return; - + Method method = class_getInstanceMethod([_SwizzleHelperWK class], @selector(inputAccessoryView)); class_addMethod(newClass, @selector(inputAccessoryView), method_getImplementation(method), method_getTypeEncoding(method)); - + objc_registerClassPair(newClass); } - + object_setClass(subview, newClass); } @@ -212,7 +213,7 @@ -(void)setKeyboardDisplayRequiresUserAction:(BOOL)keyboardDisplayRequiresUserAct if (!keyboardDisplayRequiresUserAction) { Class class = NSClassFromString(@"WKContentView"); NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0}; - + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) { SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:"); Method method = class_getInstanceMethod(class, selector); @@ -306,28 +307,88 @@ - (void)stopLoading [_webView stopLoading]; } +- (NSString *) cookieDescription:(NSHTTPCookie *)cookie { + + NSMutableString *cDesc = [[NSMutableString alloc] init]; + [cDesc appendFormat:@"%@=%@;", + [[cookie name] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding], + [[cookie value] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + if ([cookie.domain length] > 0) + [cDesc appendFormat:@"domain=%@;", [cookie domain]]; + if ([cookie.path length] > 0) + [cDesc appendFormat:@"path=%@;", [cookie path]]; + if (cookie.expiresDate != nil) + [cDesc appendFormat:@"expiresDate=%@;", [cookie expiresDate]]; + if (cookie.HTTPOnly == YES) + [cDesc appendString:@"HttpOnly;"]; + if (cookie.secure == YES) + [cDesc appendString:@"Secure;"]; + + + return cDesc; +} + +- (void) copyCookies { + + NSHTTPCookieStorage* storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + NSArray* array = [storage cookies]; + + + if (@available(ios 11,*)) { + + // The webView websiteDataStore only gets initialized, when needed. Setting cookies on the dataStore's + // httpCookieStore doesn't seem to initialize it. That's why fetchDataRecordsOfTypes is called. + // All the cookies of the sharedHttpCookieStorage, which is used in react-native-cookie, + // are copied to the webSiteDataStore's httpCookieStore. + // https://bugs.webkit.org/show_bug.cgi?id=185483 + [_webView.configuration.websiteDataStore fetchDataRecordsOfTypes:[NSSet setWithObject:WKWebsiteDataTypeCookies] completionHandler:^(NSArray *records) { + for (NSHTTPCookie* cookie in array) { + [_webView.configuration.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil]; + } + }]; + } else { + // Create WKUserScript for each cookie + // Cookies are injected with Javascript AtDocumentStart + for (NSHTTPCookie* cookie in array){ + NSString* cookieSource = [NSString stringWithFormat:@"document.cookie = '%@'", [self cookieDescription:cookie]]; + WKUserScript* cookieScript = [[WKUserScript alloc] + initWithSource:cookieSource + injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; + + + [_webView.configuration.userContentController addUserScript:cookieScript]; + } + } +} + - (void)setSource:(NSDictionary *)source { if (![_source isEqualToDictionary:source]) { _source = [source copy]; _sendCookies = [source[@"sendCookies"] boolValue]; + _useWKCookieStore = [source[@"useWKCookieStore"] boolValue]; + + if (_useWKCookieStore) { + [self copyCookies]; + } + if ([source[@"customUserAgent"] length] != 0 && [_webView respondsToSelector:@selector(setCustomUserAgent:)]) { [_webView setCustomUserAgent:source[@"customUserAgent"]]; } - + // Allow loading local files: // // Only works for iOS 9+. So iOS 8 will simply ignore those two values NSString *file = [RCTConvert NSString:source[@"file"]]; NSString *allowingReadAccessToURL = [RCTConvert NSString:source[@"allowingReadAccessToURL"]]; - + if (file && [_webView respondsToSelector:@selector(loadFileURL:allowingReadAccessToURL:)]) { NSURL *fileURL = [RCTConvert NSURL:file]; NSURL *baseURL = [RCTConvert NSURL:allowingReadAccessToURL]; [_webView loadFileURL:fileURL allowingReadAccessToURL:baseURL]; return; } - + // Check for a static html source first NSString *html = [RCTConvert NSString:source[@"html"]]; if (html) { @@ -338,7 +399,7 @@ - (void)setSource:(NSDictionary *)source [_webView loadHTMLString:html baseURL:baseURL]; return; } - + NSURLRequest *request = [RCTConvert NSURLRequest:source]; // Because of the way React works, as pages redirect, we actually end up // passing the redirect urls back here, so we ignore them if trying to load @@ -391,7 +452,7 @@ - (UIColor *)backgroundColor @"canGoBack": @(_webView.canGoBack), @"canGoForward" : @(_webView.canGoForward), }]; - + return event; } @@ -455,6 +516,30 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView #pragma mark - WKNavigationDelegate methods +- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { + if (_sendCookies) { + NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response; + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL]; + for (NSHTTPCookie *cookie in cookies) { + [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie]; + } + } + + if (_onNavigationResponse) { + NSDictionary *headers = ((NSHTTPURLResponse *)navigationResponse.response).allHeaderFields; + NSInteger statusCode = ((NSHTTPURLResponse *)navigationResponse.response).statusCode; + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary:@{ + @"headers": headers, + @"status": [NSHTTPURLResponse localizedStringForStatusCode:statusCode], + @"statusCode": @(statusCode), + }]; + _onNavigationResponse(event); + } + + decisionHandler(WKNavigationResponsePolicyAllow); +} + #if DEBUG - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { NSURLCredential * credential = [[NSURLCredential alloc] initWithTrust:[challenge protectionSpace].serverTrust]; @@ -468,9 +553,9 @@ - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(W NSURLRequest *request = navigationAction.request; NSURL* url = request.URL; NSString* scheme = url.scheme; - + BOOL isJSNavigation = [scheme isEqualToString:RCTJSNavigationScheme]; - + // handle mailto and tel schemes if ([scheme isEqualToString:@"mailto"] || [scheme isEqualToString:@"tel"]) { if ([app canOpenURL:url]) { @@ -479,7 +564,7 @@ - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(W return; } } - + // skip this for the JS Navigation handler if (!isJSNavigation && _onShouldStartLoadWithRequest) { NSMutableDictionary *event = [self baseEvent]; @@ -493,7 +578,7 @@ - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(W return decisionHandler(WKNavigationActionPolicyCancel); } } - + if (_onLoadingStart) { // We have this check to filter out iframe requests and whatnot BOOL isTopFrame = [url isEqual:request.mainDocumentURL]; @@ -506,7 +591,7 @@ - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(W _onLoadingStart(event); } } - + if (isJSNavigation) { decisionHandler(WKNavigationActionPolicyCancel); } @@ -525,7 +610,7 @@ - (void)webView:(__unused WKWebView *)webView didFailProvisionalNavigation:(__un // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os return; } - + NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary:@{ @"domain": error.domain, @@ -548,7 +633,7 @@ - (void)webView:(WKWebView *)webView didFinishNavigation:(__unused WKNavigation - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; - + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(); }]]; @@ -557,7 +642,7 @@ - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSStrin } - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler { - + // TODO We have to think message to confirm "YES" UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { @@ -571,17 +656,17 @@ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSStr } - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler { - + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert]; [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.text = defaultText; }]; - + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { NSString *input = ((UITextField *)alertController.textFields.firstObject).text; completionHandler(input); }]]; - + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(nil); }]]; @@ -609,20 +694,5 @@ - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView RCTLogWarn(@"Webview Process Terminated"); } -- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { - if (_onNavigationResponse) { - NSDictionary *headers = ((NSHTTPURLResponse *)navigationResponse.response).allHeaderFields; - NSInteger statusCode = ((NSHTTPURLResponse *)navigationResponse.response).statusCode; - NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary:@{ - @"headers": headers, - @"status": [NSHTTPURLResponse localizedStringForStatusCode:statusCode], - @"statusCode": @(statusCode), - }]; - _onNavigationResponse(event); - } - - decisionHandler(WKNavigationResponsePolicyAllow); -} @end