diff --git a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.h b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.h index 067d7e34a..bb2f998af 100644 --- a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.h +++ b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.h @@ -17,19 +17,14 @@ under the License. */ -#import #import -#import -#import +@class CDVViewController; @interface CDVURLSchemeHandler : NSObject +NS_ASSUME_NONNULL_BEGIN -@property (nonatomic, weak) CDVViewController* viewController; - -@property (nonatomic) CDVPlugin* schemePlugin; - -- (instancetype)initWithVC:(CDVViewController *)controller; - +- (instancetype)initWithViewController:(CDVViewController *)controller; +NS_ASSUME_NONNULL_END @end diff --git a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m index 33ae5cb95..17e797648 100644 --- a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m +++ b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m @@ -19,99 +19,104 @@ Licensed to the Apache Software Foundation (ASF) under one #import "CDVURLSchemeHandler.h" +#import +#import +#import #import -#import +@interface CDVURLSchemeHandler () -@implementation CDVURLSchemeHandler +@property (nonatomic, weak) CDVViewController *viewController; +@property (nonatomic) NSMapTable , CDVPlugin *> *handlerMap; + +@end +@implementation CDVURLSchemeHandler -- (instancetype)initWithVC:(CDVViewController *)controller +- (instancetype)initWithViewController:(CDVViewController *)controller { self = [super init]; if (self) { _viewController = controller; + _handlerMap = [NSMapTable weakToWeakObjectsMapTable]; } return self; } - (void)webView:(WKWebView *)webView startURLSchemeTask:(id )urlSchemeTask { + // Give plugins the chance to handle the url + for (CDVPlugin *plugin in self.viewController.enumerablePlugins) { + if ([plugin respondsToSelector:@selector(overrideSchemeTask:)]) { + CDVPlugin *schemePlugin = (CDVPlugin *)plugin; + if ([schemePlugin overrideSchemeTask:urlSchemeTask]) { + // Store the plugin that is handling this particular request + [self.handlerMap setObject:schemePlugin forKey:urlSchemeTask]; + return; + } + } + } + + // Indicate that we are handling this task, by adding an entry with a null plugin + // We do this so that we can (in future) detect if the task is cancelled before we finished feeding it response data + [self.handlerMap setObject:(id)[NSNull null] forKey:urlSchemeTask]; + NSString * startPath = [[NSBundle mainBundle] pathForResource:self.viewController.webContentFolderName ofType: nil]; NSURL * url = urlSchemeTask.request.URL; NSString * stringToLoad = url.path; NSString * scheme = url.scheme; - - CDVViewController* vc = (CDVViewController*)self.viewController; - - /* - * Give plugins the chance to handle the url - */ - BOOL anyPluginsResponded = NO; - BOOL handledRequest = NO; - - NSDictionary *pluginObjects = [[vc pluginObjects] copy]; - for (NSString* pluginName in pluginObjects) { - self.schemePlugin = [vc.pluginObjects objectForKey:pluginName]; - SEL selector = NSSelectorFromString(@"overrideSchemeTask:"); - if ([self.schemePlugin respondsToSelector:selector]) { - handledRequest = (((BOOL (*)(id, SEL, id ))objc_msgSend)(self.schemePlugin, selector, urlSchemeTask)); - if (handledRequest) { - anyPluginsResponded = YES; - break; - } - } - } - if (!anyPluginsResponded) { - if ([scheme isEqualToString:self.viewController.appScheme]) { - if ([stringToLoad hasPrefix:@"/_app_file_"]) { - startPath = [stringToLoad stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""]; + if ([scheme isEqualToString:self.viewController.appScheme]) { + if ([stringToLoad hasPrefix:@"/_app_file_"]) { + startPath = [stringToLoad stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""]; + } else { + if ([stringToLoad isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) { + startPath = [startPath stringByAppendingPathComponent:self.viewController.startPage]; } else { - if ([stringToLoad isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) { - startPath = [startPath stringByAppendingPathComponent:self.viewController.startPage]; - } else { - startPath = [startPath stringByAppendingPathComponent:stringToLoad]; - } + startPath = [startPath stringByAppendingPathComponent:stringToLoad]; } } + } - NSError * fileError = nil; - NSData * data = nil; - if ([self isMediaExtension:url.pathExtension]) { - data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError]; - } - if (!data || fileError) { - data = [[NSData alloc] initWithContentsOfFile:startPath]; - } - NSInteger statusCode = 200; - if (!data) { - statusCode = 404; - } - NSURL * localUrl = [NSURL URLWithString:url.absoluteString]; - NSString * mimeType = [self getMimeType:url.pathExtension]; - id response = nil; - if (data && [self isMediaExtension:url.pathExtension]) { - response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil]; - } else { - NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"}; - response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers]; - } + NSError * fileError = nil; + NSData * data = nil; + if ([self isMediaExtension:url.pathExtension]) { + data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError]; + } + if (!data || fileError) { + data = [[NSData alloc] initWithContentsOfFile:startPath]; + } + NSInteger statusCode = 200; + if (!data) { + statusCode = 404; + } + NSURL * localUrl = [NSURL URLWithString:url.absoluteString]; + NSString * mimeType = [self getMimeType:url.pathExtension]; + id response = nil; + if (data && [self isMediaExtension:url.pathExtension]) { + response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil]; + } else { + NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"}; + response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers]; + } - [urlSchemeTask didReceiveResponse:response]; - if (data) { - [urlSchemeTask didReceiveData:data]; - } - [urlSchemeTask didFinish]; + [urlSchemeTask didReceiveResponse:response]; + if (data) { + [urlSchemeTask didReceiveData:data]; } + [urlSchemeTask didFinish]; + + [self.handlerMap removeObjectForKey:urlSchemeTask]; } -- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id)urlSchemeTask +- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id )urlSchemeTask { - SEL selector = NSSelectorFromString(@"stopSchemeTask:"); - if (self.schemePlugin != nil && [self.schemePlugin respondsToSelector:selector]) { - (((void (*)(id, SEL, id ))objc_msgSend)(self.schemePlugin, selector, urlSchemeTask)); + CDVPlugin *plugin = [self.handlerMap objectForKey:urlSchemeTask]; + if (![plugin isEqual:[NSNull null]] && [plugin respondsToSelector:@selector(stopSchemeTask:)]) { + [plugin stopSchemeTask:urlSchemeTask]; } + + [self.handlerMap removeObjectForKey:urlSchemeTask]; } -(NSString *) getMimeType:(NSString *)fileExtension { diff --git a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m index c47cd8556..bae6838fc 100644 --- a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m +++ b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m @@ -215,7 +215,7 @@ - (void)pluginInitialize // Do not configure the scheme handler if the scheme is default (file) if(!self.cdvIsFileScheme) { - self.schemeHandler = [[CDVURLSchemeHandler alloc] initWithVC:vc]; + self.schemeHandler = [[CDVURLSchemeHandler alloc] initWithViewController:vc]; [configuration setURLSchemeHandler:self.schemeHandler forURLScheme:scheme]; } @@ -551,8 +551,7 @@ - (void) webView: (WKWebView *) webView decidePolicyForNavigationAction: (WKNavi BOOL anyPluginsResponded = NO; BOOL shouldAllowRequest = NO; - for (NSString* pluginName in vc.pluginObjects) { - CDVPlugin* plugin = [vc.pluginObjects objectForKey:pluginName]; + for (CDVPlugin *plugin in vc.enumerablePlugins) { SEL selector = NSSelectorFromString(@"shouldOverrideLoadWithRequest:navigationType:"); if ([plugin respondsToSelector:selector]) { anyPluginsResponded = YES; diff --git a/CordovaLib/Classes/Public/CDVViewController.m b/CordovaLib/Classes/Public/CDVViewController.m index 825f55251..2825e31b8 100644 --- a/CordovaLib/Classes/Public/CDVViewController.m +++ b/CordovaLib/Classes/Public/CDVViewController.m @@ -67,6 +67,7 @@ @implementation CDVViewController @synthesize splashBackgroundColor = _splashBackgroundColor; @synthesize settings = _settings; @dynamic webView; +@dynamic enumerablePlugins; #pragma mark - Initializers @@ -152,6 +153,13 @@ - (void)dealloc #pragma mark - Getters & Setters +- (NSArray *)enumerablePlugins +{ + @synchronized(_pluginObjects) { + return [_pluginObjects allValues]; + } +} + - (NSString *)wwwFolderName { return self.webContentFolderName; @@ -460,15 +468,11 @@ - (void)onAppDidBecomeActive:(NSNotification *)notification - (void)didReceiveMemoryWarning { - // iterate through all the plugin objects, and call hasPendingOperation - // if at least one has a pending operation, we don't call [super didReceiveMemoryWarning] - - NSEnumerator* enumerator = [self.pluginObjects objectEnumerator]; - CDVPlugin* plugin; - BOOL doPurge = YES; - while ((plugin = [enumerator nextObject])) { + // iterate through all the plugin objects, and call hasPendingOperation + // if at least one has a pending operation, we don't call [super didReceiveMemoryWarning] + for (CDVPlugin *plugin in self.enumerablePlugins) { if (plugin.hasPendingOperation) { NSLog(@"Plugin '%@' has a pending operation, memory purge is delayed for didReceiveMemoryWarning.", NSStringFromClass([plugin class])); doPurge = NO; @@ -676,9 +680,13 @@ - (nullable CDVPlugin *)getCommandInstance:(NSString *)pluginName return nil; } - id obj = [self.pluginObjects objectForKey:className]; + id obj = nil; + @synchronized(_pluginObjects) { + obj = [_pluginObjects objectForKey:className]; + } + if (!obj) { - obj = [[NSClassFromString(className)alloc] initWithWebViewEngine:_webViewEngine]; + obj = [[NSClassFromString(className) alloc] initWithWebViewEngine:_webViewEngine]; if (!obj) { NSString* fullClassName = [NSString stringWithFormat:@"%@.%@", NSBundle.mainBundle.infoDictionary[@"CFBundleExecutable"], diff --git a/CordovaLib/Classes/Public/CDVWebViewProcessPoolFactory.m b/CordovaLib/Classes/Public/CDVWebViewProcessPoolFactory.m index 4631c7fdc..43de7222c 100644 --- a/CordovaLib/Classes/Public/CDVWebViewProcessPoolFactory.m +++ b/CordovaLib/Classes/Public/CDVWebViewProcessPoolFactory.m @@ -17,6 +17,7 @@ Licensed to the Apache Software Foundation (ASF) under one under the License. */ +#import #import static CDVWebViewProcessPoolFactory *factory = nil; diff --git a/CordovaLib/CordovaLib.docc/CordovaLib.md b/CordovaLib/CordovaLib.docc/CordovaLib.md index 44e9bf180..ea25f75d7 100644 --- a/CordovaLib/CordovaLib.docc/CordovaLib.md +++ b/CordovaLib/CordovaLib.docc/CordovaLib.md @@ -36,6 +36,9 @@ For more information about Apache Cordova, visit [https://cordova.apache.org](ht ### Cordova plugins - ``CDVPlugin`` +- ``CDVPluginSchemeHandler`` + +### Plugin communication - ``CDVPluginResult`` - ``CDVCommandStatus`` - ``CDVInvokedUrlCommand`` diff --git a/CordovaLib/include/Cordova/CDVPlugin.h b/CordovaLib/include/Cordova/CDVPlugin.h index fc37a9c72..d878539be 100644 --- a/CordovaLib/include/Cordova/CDVPlugin.h +++ b/CordovaLib/include/Cordova/CDVPlugin.h @@ -27,6 +27,9 @@ #import #import +// Forward declaration to avoid bringing WebKit API into public headers +@protocol WKURLSchemeTask; + #ifndef __swift__ // This global extension to the UIView class causes issues for Swift subclasses // of UIView with their own scrollView properties, so we're removing it from @@ -79,3 +82,51 @@ extern const NSNotificationName CDVViewWillTransitionToSizeNotification; - (id)appDelegate; @end + +#pragma mark - Plugin protocols + +/** + A protocol for Cordova plugins to intercept handling of WebKit resource + loading for a custom URL scheme. + + Your plugin should implement this protocol if it wants to intercept requests + to a custom URL scheme and provide its own resource loading. Otherwise, + Cordova will use its default resource loading behavior from the app bundle. + + When a WebKit-based web view encounters a resource that uses a custom scheme, + it creates a WKURLSchemeTask object and Cordova passes it to the methods of + your scheme handler plugin for processing. Use the ``overrideSchemeTask:`` + method to indicate that your plugin will handle the request and to begin + loading the resource. While your handler loads the object, Cordova may call + your plugin’s ``stopSchemeTask:`` method to notify you that the resource is no + longer needed. + */ +@protocol CDVPluginSchemeHandler + +/** + Asks your plugin to handle the specified request and begin loading data. + + If your plugin intends to handle the request and return data, this method + should return `YES` as soon as possible to prevent the default request + handling. If this method returns `NO`, Cordova will handle the resource + loading using its default behavior. + + Note that all methods of the task object must be called on the main thread. + + - Parameters: + - task: The task object that identifies the resource to load. You also use + this object to report the progress of the load operation back to the web + view. + - Returns: A Boolean value indicating if the plugin is handling the request. + */ +- (BOOL)overrideSchemeTask:(id )task; + +/** + Asks your plugin to stop loading the data for the specified resource. + + - Parameters: + - task: The task object that identifies the resource the web view no + longer needs. + */ +- (void)stopSchemeTask:(id )task; +@end diff --git a/CordovaLib/include/Cordova/CDVViewController.h b/CordovaLib/include/Cordova/CDVViewController.h index 8670fd63f..e9f4751c9 100644 --- a/CordovaLib/include/Cordova/CDVViewController.h +++ b/CordovaLib/include/Cordova/CDVViewController.h @@ -18,8 +18,6 @@ */ #import -#import -#import #import #import #import @@ -67,9 +65,16 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readonly, nullable, weak) IBOutlet UIView *webView; -@property (nonatomic, readonly, strong) NSDictionary *pluginObjects; +@property (nonatomic, readonly, strong) NSDictionary *pluginObjects CDV_DEPRECATED(8, "Internal implementation detail, should not be used"); @property (nullable, nonatomic, readonly, strong) NSDictionary *pluginsMap CDV_DEPRECATED(8, "Internal implementation detail, should not be used"); +/** + An array of loaded Cordova plugin instances. + + This array is safe to iterate using a `for...in` loop. + */ +@property (nonatomic, readonly, copy) NSArray *enumerablePlugins; + @property (nonatomic, readwrite, copy) NSString *appScheme; @property (nonatomic, readonly, strong) CDVCommandQueue *commandQueue; diff --git a/CordovaLib/include/Cordova/CDVWebViewEngineProtocol.h b/CordovaLib/include/Cordova/CDVWebViewEngineProtocol.h index 0d3f0bf77..875c46dae 100644 --- a/CordovaLib/include/Cordova/CDVWebViewEngineProtocol.h +++ b/CordovaLib/include/Cordova/CDVWebViewEngineProtocol.h @@ -18,13 +18,14 @@ */ #import -#import #define kCDVWebViewEngineScriptMessageHandlers @"kCDVWebViewEngineScriptMessageHandlers" #define kCDVWebViewEngineWKNavigationDelegate @"kCDVWebViewEngineWKNavigationDelegate" #define kCDVWebViewEngineWKUIDelegate @"kCDVWebViewEngineWKUIDelegate" #define kCDVWebViewEngineWebViewPreferences @"kCDVWebViewEngineWebViewPreferences" +@class WKWebViewConfiguration; + @protocol CDVWebViewEngineProtocol NS_ASSUME_NONNULL_BEGIN diff --git a/CordovaLib/include/Cordova/CDVWebViewProcessPoolFactory.h b/CordovaLib/include/Cordova/CDVWebViewProcessPoolFactory.h index b15628914..7a5bf76bf 100644 --- a/CordovaLib/include/Cordova/CDVWebViewProcessPoolFactory.h +++ b/CordovaLib/include/Cordova/CDVWebViewProcessPoolFactory.h @@ -17,7 +17,7 @@ under the License. */ -#import +@class WKProcessPool; @interface CDVWebViewProcessPoolFactory : NSObject @property (nonatomic, retain) WKProcessPool* sharedPool; diff --git a/CordovaLib/include/Cordova/NSDictionary+CordovaPreferences.h b/CordovaLib/include/Cordova/NSDictionary+CordovaPreferences.h index 0d41c83d4..db7b086b0 100644 --- a/CordovaLib/include/Cordova/NSDictionary+CordovaPreferences.h +++ b/CordovaLib/include/Cordova/NSDictionary+CordovaPreferences.h @@ -18,7 +18,6 @@ */ #import -#import #import #ifndef __CORDOVA_SILENCE_HEADER_DEPRECATIONS