diff --git a/STMScriptMessageHandler.podspec b/STMScriptMessageHandler.podspec index 327aaca..9c053ae 100644 --- a/STMScriptMessageHandler.podspec +++ b/STMScriptMessageHandler.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'STMScriptMessageHandler' - s.version = '3.0.0' + s.version = '3.1.0' s.summary = 'A script message handler that conform to the WKScriptMessageHandler protocol. It is used to communicate with js.' s.description = <<-DESC @@ -13,6 +13,7 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/douking/STMScriptMessageHandler.git', :tag => s.version.to_s } s.ios.deployment_target = '8.0' + s.osx.deployment_target = '10.10' s.source_files = 'STMScriptMessageHandler/Source/**/*' end diff --git a/STMScriptMessageHandler/Demo/STMWebViewController.m b/STMScriptMessageHandler/Demo/STMWebViewController.m index 7aa6315..9359cfa 100644 --- a/STMScriptMessageHandler/Demo/STMWebViewController.m +++ b/STMScriptMessageHandler/Demo/STMWebViewController.m @@ -10,7 +10,7 @@ static NSString * const kMDFWebViewObserverKeyPathTitle = @"title"; static NSString * const kMDFWebViewObserverKeyPathEstimatedProgress = @"estimatedProgress"; -@interface STMWebViewController () +@interface STMWebViewController () @property (nonatomic, strong, readwrite) WKWebView *webView; @@ -40,6 +40,7 @@ - (void)viewDidLoad { [super viewDidLoad]; [self.view addSubview:self.webView]; [self.view addSubview:self.progressView]; + [self _copyNSHTTPCookieStorageToWKWebViewWithCompletionHandler:nil]; } - (void)viewDidLayoutSubviews { @@ -77,6 +78,140 @@ - (void)_setProgressHidden:(BOOL)hidden animated:(BOOL)animated { } } +#pragma mark cookies +/** + cookie持久化路径 NSLibraryDirectory + .../Library/Cookies/ + Cookie.binarycookies WKWebview + .binarycookies NSHTTPCookieStorage + + session级别的cookie + WKProcessPool + */ + +- (void)_copyNSHTTPCookieStorageToWKWebViewWithCompletionHandler:(nullable void (^)(void))completionHandler { + if (@available(iOS 11.0, *)) { + NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; + WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore; + if (cookies.count == 0) { + !completionHandler ?: completionHandler(); + return; + } + for (NSHTTPCookie *cookie in cookies) { + [cookieStroe setCookie:cookie completionHandler:^{ + if ([[cookies lastObject] isEqual:cookie]) { + !completionHandler ?: completionHandler(); + return; + } + }]; + } + } else { + NSString *cookiestring = [self _formatCookies:[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]]; + WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:cookiestring + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:NO]; + [self.webView.configuration.userContentController addUserScript:cookieScript]; + !completionHandler ?: completionHandler(); + } +} + +- (void)_syncCookiesToRequest:(NSMutableURLRequest *)request { + if (!request.URL) { + return; + } + + void (^block)(NSArray *) = ^(NSArray *availableCookie){ + if (availableCookie.count > 0) { + NSDictionary *reqHeader = [NSHTTPCookie requestHeaderFieldsWithCookies:availableCookie]; + NSString *cookieStr = [reqHeader objectForKey:@"Cookie"]; + [request setValue:cookieStr forHTTPHeaderField:@"Cookie"]; + } + }; + + if (@available(iOS 11.0, *)) { + WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore; + [cookieStroe getAllCookies:^(NSArray * _Nonnull availableCookie) { + block(availableCookie); + }]; + } else { + NSArray *availableCookie = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL]; + block(availableCookie); + } +} + + +- (void)_clearCookies { + NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; + for (NSHTTPCookie *cookie in cookies) { + [[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie]; + } + + if (@available(iOS 11.0, *)) { + WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore; + [cookieStroe getAllCookies:^(NSArray * _Nonnull cookies) { + for (NSHTTPCookie *cookie in cookies) { + [cookieStroe deleteCookie:cookie completionHandler:^{ + NSLog(@"[STMWebViewController] WKWebView 清除cookie %@", cookie.name); + }]; + } + }]; + } +} + +- (void)clearWKWebViewCache { + NSString *libraryDir = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject; + NSString *bundleId = [[NSBundle mainBundle] infoDictionary][@"CFBundleIdentifier"]; + NSString *webkitFolderInLib = [NSString stringWithFormat:@"%@/WebKit", libraryDir]; + NSString *webKitFolderInCaches = [NSString stringWithFormat:@"%@/Caches/%@/WebKit", libraryDir, bundleId]; + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:webKitFolderInCaches error:&error]; + [[NSFileManager defaultManager] removeItemAtPath:webkitFolderInLib error:nil]; + + if (@available(iOS 9.0, *)) { + WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; + [dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] + completionHandler:^(NSArray * _Nonnull records) { + for (WKWebsiteDataRecord *record in records) { + [dataStore removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{ + NSLog(@"[STMWebViewController] WKWebView 清除缓存 %@", record.displayName); + }]; + } + }]; + } +} + +- (NSString *)_formatCookies:(NSArray *)cookies { + NSMutableString *cookieScript = [NSMutableString string]; + for (NSHTTPCookie *cookie in cookies) { + // Skip cookies that will break our script + if ([cookie.value rangeOfString:@"'"].location != NSNotFound) { + continue; + } + // Create a line that appends this cookie to the web view's document's cookies + [cookieScript appendFormat:@"document.cookie='%@=%@;", cookie.name, cookie.value]; + if (cookie.domain || cookie.domain.length > 0) { + [cookieScript appendFormat:@"domain=%@;", cookie.domain]; + } + if (cookie.path || cookie.path.length > 0) { + [cookieScript appendFormat:@"path=%@;", cookie.path]; + } + if (cookie.expiresDate) { + [cookieScript appendFormat:@"expires=%@;", [[self cookieDateFormatter] stringFromDate:cookie.expiresDate]]; + } + if (cookie.secure) { + [cookieScript appendString:@"Secure;"]; + } + if (cookie.HTTPOnly) { + // 保持 native 的 cookie 完整性,当 HTTPOnly 时,不能通过 document.cookie 来读取该 cookie。 + [cookieScript appendString:@"HTTPOnly;"]; + } + [cookieScript appendFormat:@"'\n"]; + } + + // document.cookie='%@=%@;domain=%@;path=%@;expires=%@;Secure;HTTPOnly;' + return cookieScript; +} + #pragma mark - Delegates & Notifications - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { @@ -116,6 +251,16 @@ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSStr [self presentViewController:alert animated:YES completion:nil]; } +#pragma mark - WKNavigationDelegate + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + // 302 + if ([navigationAction.request isKindOfClass:[NSMutableURLRequest class]]) { + [self _syncCookiesToRequest:(NSMutableURLRequest *)navigationAction.request]; + } + decisionHandler(WKNavigationActionPolicyAllow); +} + #pragma mark - setter & getter - (WKWebView *)webView { @@ -133,9 +278,11 @@ - (WKWebView *)webView { config.allowsInlineMediaPlayback = YES; config.userContentController = userContentController; config.preferences = preferences; + config.processPool = [self processPool]; _webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config]; _webView.UIDelegate = self; + _webView.navigationDelegate = self; } return _webView; } @@ -149,4 +296,25 @@ - (UIProgressView *)progressView { return _progressView; } +- (WKProcessPool *)processPool { + static WKProcessPool *pool = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pool = [[WKProcessPool alloc] init]; + }); + return pool; +} + +- (NSDateFormatter *)cookieDateFormatter { + static NSDateFormatter *formatter = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // expires=Mon, 01 Aug 2050 06:44:35 GMT + formatter = [NSDateFormatter new]; + formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + formatter.dateFormat = @"EEE, d MMM yyyy HH:mm:ss zzz"; + }); + return formatter; +} + @end diff --git a/STMScriptMessageHandler/Source/STMScriptMessageHandler.h b/STMScriptMessageHandler/Source/STMScriptMessageHandler.h index 44b3335..66be11f 100644 --- a/STMScriptMessageHandler/Source/STMScriptMessageHandler.h +++ b/STMScriptMessageHandler/Source/STMScriptMessageHandler.h @@ -22,8 +22,7 @@ // THE SOFTWARE. // -@import UIKit; -@import WebKit; +#import NS_ASSUME_NONNULL_BEGIN @@ -33,6 +32,7 @@ typedef void (^STMHandler)(id data, STMResponseCallback _Nullable responseCallba @interface STMScriptMessageHandler : NSObject @property (nonatomic, copy, readonly) NSString *handlerName; +@property (nullable, nonatomic, copy) void (^didReceiveScriptMessage)(id message); + (void)enableLog; diff --git a/STMScriptMessageHandler/Source/STMScriptMessageHandler.m b/STMScriptMessageHandler/Source/STMScriptMessageHandler.m index 860be86..8d8ab9d 100644 --- a/STMScriptMessageHandler/Source/STMScriptMessageHandler.m +++ b/STMScriptMessageHandler/Source/STMScriptMessageHandler.m @@ -151,6 +151,7 @@ - (void)_flushReceivedMessage:(NSDictionary *)message { } STMHandler handler = self.methodHandlers[message[kSTMMessageParameterNameKey]]; if (!handler) { + responseCallback(@{}); return; } handler(message[kSTMMessageParameterInfoKey], responseCallback); @@ -224,11 +225,13 @@ - (void)_log:(NSString *)message { - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if (![message.name isEqualToString:self.handlerName]) { return; } + !self.didReceiveScriptMessage ?: self.didReceiveScriptMessage(message.body); [self _flushReceivedMessage:message.body]; } - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message replyHandler:(void (^)(id _Nullable reply, NSString *_Nullable errorMessage))replyHandler API_AVAILABLE(macos(11.0), ios(14.0)) { if (![message.name isEqualToString:self.handlerName]) { return; } + !self.didReceiveScriptMessage ?: self.didReceiveScriptMessage(message.body); NSMutableDictionary *parameters = [message.body mutableCopy]; parameters[kSTMMessageParameterReplyKey] = replyHandler ?: ^(id _Nullable reply, NSString *_Nullable errorMessage){ diff --git a/STMScriptMessageHandler/Source/WKWebView+STMScriptMessage.m b/STMScriptMessageHandler/Source/WKWebView+STMScriptMessage.m index 6c787b8..f215924 100644 --- a/STMScriptMessageHandler/Source/WKWebView+STMScriptMessage.m +++ b/STMScriptMessageHandler/Source/WKWebView+STMScriptMessage.m @@ -62,7 +62,11 @@ - (void)stm_addScriptMessageHandler:(__kindof STMScriptMessageHandler *)msgHandl - (void)_stm_addScriptMessageHandler:(__kindof STMScriptMessageHandler *)messageHandler { WKUserContentController *userContentController = self.configuration.userContentController; [userContentController removeScriptMessageHandlerForName:messageHandler.handlerName]; +#if TARHET_OS_MAC + if (@available(macOS 11.0, *)) { +#else if (@available(iOS 14.0, *)) { +#endif [userContentController addScriptMessageHandlerWithReply:messageHandler contentWorld:WKContentWorld.pageWorld name:messageHandler.handlerName];