Skip to content

Commit 6ba4aca

Browse files
committed
refactor: Define a protocol for scheme handling plugins
We don't rely on the protocol itself being implemented by the plugins (we continue to check with `-respondsToSelector:`) but this allows us to avoid `objc_msgSend` and provides a way to document some of this plugin behaviour that is not otherwise explained. This should also resolve the unsafe plugin iteration issue that was mentioned in GH-1272 and GH-1030 by always iterating over an array of plugin objects that is a copy (due to calling `-allValues`).
1 parent 458f5f3 commit 6ba4aca

File tree

11 files changed

+157
-90
lines changed

11 files changed

+157
-90
lines changed

CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.h

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,14 @@
1717
under the License.
1818
*/
1919

20-
#import <Foundation/Foundation.h>
2120
#import <WebKit/WebKit.h>
22-
#import <Cordova/CDVViewController.h>
23-
#import <Cordova/CDVPlugin.h>
2421

22+
@class CDVViewController;
2523

2624
@interface CDVURLSchemeHandler : NSObject <WKURLSchemeHandler>
25+
NS_ASSUME_NONNULL_BEGIN
2726

28-
@property (nonatomic, weak) CDVViewController* viewController;
29-
30-
@property (nonatomic) CDVPlugin* schemePlugin;
31-
32-
- (instancetype)initWithVC:(CDVViewController *)controller;
33-
27+
- (instancetype)initWithViewController:(CDVViewController *)controller;
3428

29+
NS_ASSUME_NONNULL_END
3530
@end

CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m

Lines changed: 68 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -19,99 +19,104 @@ Licensed to the Apache Software Foundation (ASF) under one
1919

2020

2121
#import "CDVURLSchemeHandler.h"
22+
#import <Cordova/CDVViewController.h>
23+
#import <Cordova/CDVPlugin.h>
24+
#import <Foundation/Foundation.h>
2225
#import <MobileCoreServices/MobileCoreServices.h>
2326

24-
#import <objc/message.h>
27+
@interface CDVURLSchemeHandler ()
2528

26-
@implementation CDVURLSchemeHandler
29+
@property (nonatomic, weak) CDVViewController *viewController;
30+
@property (nonatomic) NSMapTable <id <WKURLSchemeTask>, CDVPlugin <CDVPluginSchemeHandler> *> *handlerMap;
31+
32+
@end
2733

34+
@implementation CDVURLSchemeHandler
2835

29-
- (instancetype)initWithVC:(CDVViewController *)controller
36+
- (instancetype)initWithViewController:(CDVViewController *)controller
3037
{
3138
self = [super init];
3239
if (self) {
3340
_viewController = controller;
41+
_handlerMap = [NSMapTable weakToWeakObjectsMapTable];
3442
}
3543
return self;
3644
}
3745

3846
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
3947
{
48+
// Give plugins the chance to handle the url
49+
for (CDVPlugin *plugin in self.viewController.enumerablePlugins) {
50+
if ([plugin respondsToSelector:@selector(overrideSchemeTask:)]) {
51+
CDVPlugin <CDVPluginSchemeHandler> *schemePlugin = (CDVPlugin<CDVPluginSchemeHandler> *)plugin;
52+
if ([schemePlugin overrideSchemeTask:urlSchemeTask]) {
53+
// Store the plugin that is handling this particular request
54+
[self.handlerMap setObject:schemePlugin forKey:urlSchemeTask];
55+
return;
56+
}
57+
}
58+
}
59+
60+
// Indicate that we are handling this task, by adding an entry with a null plugin
61+
// We do this so that we can (in future) detect if the task is cancelled before we finished feeding it response data
62+
[self.handlerMap setObject:(id)[NSNull null] forKey:urlSchemeTask];
63+
4064
NSString * startPath = [[NSBundle mainBundle] pathForResource:self.viewController.webContentFolderName ofType: nil];
4165
NSURL * url = urlSchemeTask.request.URL;
4266
NSString * stringToLoad = url.path;
4367
NSString * scheme = url.scheme;
44-
45-
CDVViewController* vc = (CDVViewController*)self.viewController;
46-
47-
/*
48-
* Give plugins the chance to handle the url
49-
*/
50-
BOOL anyPluginsResponded = NO;
51-
BOOL handledRequest = NO;
52-
53-
NSDictionary *pluginObjects = [[vc pluginObjects] copy];
54-
for (NSString* pluginName in pluginObjects) {
55-
self.schemePlugin = [vc.pluginObjects objectForKey:pluginName];
56-
SEL selector = NSSelectorFromString(@"overrideSchemeTask:");
57-
if ([self.schemePlugin respondsToSelector:selector]) {
58-
handledRequest = (((BOOL (*)(id, SEL, id <WKURLSchemeTask>))objc_msgSend)(self.schemePlugin, selector, urlSchemeTask));
59-
if (handledRequest) {
60-
anyPluginsResponded = YES;
61-
break;
62-
}
63-
}
64-
}
6568

66-
if (!anyPluginsResponded) {
67-
if ([scheme isEqualToString:self.viewController.appScheme]) {
68-
if ([stringToLoad hasPrefix:@"/_app_file_"]) {
69-
startPath = [stringToLoad stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""];
69+
if ([scheme isEqualToString:self.viewController.appScheme]) {
70+
if ([stringToLoad hasPrefix:@"/_app_file_"]) {
71+
startPath = [stringToLoad stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""];
72+
} else {
73+
if ([stringToLoad isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) {
74+
startPath = [startPath stringByAppendingPathComponent:self.viewController.startPage];
7075
} else {
71-
if ([stringToLoad isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) {
72-
startPath = [startPath stringByAppendingPathComponent:self.viewController.startPage];
73-
} else {
74-
startPath = [startPath stringByAppendingPathComponent:stringToLoad];
75-
}
76+
startPath = [startPath stringByAppendingPathComponent:stringToLoad];
7677
}
7778
}
79+
}
7880

79-
NSError * fileError = nil;
80-
NSData * data = nil;
81-
if ([self isMediaExtension:url.pathExtension]) {
82-
data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError];
83-
}
84-
if (!data || fileError) {
85-
data = [[NSData alloc] initWithContentsOfFile:startPath];
86-
}
87-
NSInteger statusCode = 200;
88-
if (!data) {
89-
statusCode = 404;
90-
}
91-
NSURL * localUrl = [NSURL URLWithString:url.absoluteString];
92-
NSString * mimeType = [self getMimeType:url.pathExtension];
93-
id response = nil;
94-
if (data && [self isMediaExtension:url.pathExtension]) {
95-
response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil];
96-
} else {
97-
NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"};
98-
response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers];
99-
}
81+
NSError * fileError = nil;
82+
NSData * data = nil;
83+
if ([self isMediaExtension:url.pathExtension]) {
84+
data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError];
85+
}
86+
if (!data || fileError) {
87+
data = [[NSData alloc] initWithContentsOfFile:startPath];
88+
}
89+
NSInteger statusCode = 200;
90+
if (!data) {
91+
statusCode = 404;
92+
}
93+
NSURL * localUrl = [NSURL URLWithString:url.absoluteString];
94+
NSString * mimeType = [self getMimeType:url.pathExtension];
95+
id response = nil;
96+
if (data && [self isMediaExtension:url.pathExtension]) {
97+
response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil];
98+
} else {
99+
NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"};
100+
response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers];
101+
}
100102

101-
[urlSchemeTask didReceiveResponse:response];
102-
if (data) {
103-
[urlSchemeTask didReceiveData:data];
104-
}
105-
[urlSchemeTask didFinish];
103+
[urlSchemeTask didReceiveResponse:response];
104+
if (data) {
105+
[urlSchemeTask didReceiveData:data];
106106
}
107+
[urlSchemeTask didFinish];
108+
109+
[self.handlerMap removeObjectForKey:urlSchemeTask];
107110
}
108111

109-
- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask
112+
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
110113
{
111-
SEL selector = NSSelectorFromString(@"stopSchemeTask:");
112-
if (self.schemePlugin != nil && [self.schemePlugin respondsToSelector:selector]) {
113-
(((void (*)(id, SEL, id <WKURLSchemeTask>))objc_msgSend)(self.schemePlugin, selector, urlSchemeTask));
114+
CDVPlugin <CDVPluginSchemeHandler> *plugin = [self.handlerMap objectForKey:urlSchemeTask];
115+
if (![plugin isEqual:[NSNull null]] && [plugin respondsToSelector:@selector(stopSchemeTask:)]) {
116+
[plugin stopSchemeTask:urlSchemeTask];
114117
}
118+
119+
[self.handlerMap removeObjectForKey:urlSchemeTask];
115120
}
116121

117122
-(NSString *) getMimeType:(NSString *)fileExtension {

CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ - (void)pluginInitialize
215215

216216
// Do not configure the scheme handler if the scheme is default (file)
217217
if(!self.cdvIsFileScheme) {
218-
self.schemeHandler = [[CDVURLSchemeHandler alloc] initWithVC:vc];
218+
self.schemeHandler = [[CDVURLSchemeHandler alloc] initWithViewController:vc];
219219
[configuration setURLSchemeHandler:self.schemeHandler forURLScheme:scheme];
220220
}
221221

@@ -551,8 +551,7 @@ - (void) webView: (WKWebView *) webView decidePolicyForNavigationAction: (WKNavi
551551
BOOL anyPluginsResponded = NO;
552552
BOOL shouldAllowRequest = NO;
553553

554-
for (NSString* pluginName in vc.pluginObjects) {
555-
CDVPlugin* plugin = [vc.pluginObjects objectForKey:pluginName];
554+
for (CDVPlugin *plugin in vc.enumerablePlugins) {
556555
SEL selector = NSSelectorFromString(@"shouldOverrideLoadWithRequest:navigationType:");
557556
if ([plugin respondsToSelector:selector]) {
558557
anyPluginsResponded = YES;

CordovaLib/Classes/Public/CDVViewController.m

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ @implementation CDVViewController
6767
@synthesize splashBackgroundColor = _splashBackgroundColor;
6868
@synthesize settings = _settings;
6969
@dynamic webView;
70+
@dynamic enumerablePlugins;
7071

7172
#pragma mark - Initializers
7273

@@ -152,6 +153,13 @@ - (void)dealloc
152153

153154
#pragma mark - Getters & Setters
154155

156+
- (NSArray <CDVPlugin *> *)enumerablePlugins
157+
{
158+
@synchronized(_pluginObjects) {
159+
return [_pluginObjects allValues];
160+
}
161+
}
162+
155163
- (NSString *)wwwFolderName
156164
{
157165
return self.webContentFolderName;
@@ -460,15 +468,11 @@ - (void)onAppDidBecomeActive:(NSNotification *)notification
460468

461469
- (void)didReceiveMemoryWarning
462470
{
463-
// iterate through all the plugin objects, and call hasPendingOperation
464-
// if at least one has a pending operation, we don't call [super didReceiveMemoryWarning]
465-
466-
NSEnumerator* enumerator = [self.pluginObjects objectEnumerator];
467-
CDVPlugin* plugin;
468-
469471
BOOL doPurge = YES;
470472

471-
while ((plugin = [enumerator nextObject])) {
473+
// iterate through all the plugin objects, and call hasPendingOperation
474+
// if at least one has a pending operation, we don't call [super didReceiveMemoryWarning]
475+
for (CDVPlugin *plugin in self.enumerablePlugins) {
472476
if (plugin.hasPendingOperation) {
473477
NSLog(@"Plugin '%@' has a pending operation, memory purge is delayed for didReceiveMemoryWarning.", NSStringFromClass([plugin class]));
474478
doPurge = NO;
@@ -676,9 +680,13 @@ - (nullable CDVPlugin *)getCommandInstance:(NSString *)pluginName
676680
return nil;
677681
}
678682

679-
id obj = [self.pluginObjects objectForKey:className];
683+
id obj = nil;
684+
@synchronized(_pluginObjects) {
685+
obj = [_pluginObjects objectForKey:className];
686+
}
687+
680688
if (!obj) {
681-
obj = [[NSClassFromString(className)alloc] initWithWebViewEngine:_webViewEngine];
689+
obj = [[NSClassFromString(className) alloc] initWithWebViewEngine:_webViewEngine];
682690
if (!obj) {
683691
NSString* fullClassName = [NSString stringWithFormat:@"%@.%@",
684692
NSBundle.mainBundle.infoDictionary[@"CFBundleExecutable"],

CordovaLib/Classes/Public/CDVWebViewProcessPoolFactory.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Licensed to the Apache Software Foundation (ASF) under one
1717
under the License.
1818
*/
1919

20+
#import <WebKit/WebKit.h>
2021
#import <Cordova/CDVWebViewProcessPoolFactory.h>
2122

2223
static CDVWebViewProcessPoolFactory *factory = nil;

CordovaLib/CordovaLib.docc/CordovaLib.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ For more information about Apache Cordova, visit [https://cordova.apache.org](ht
3636
### Cordova plugins
3737

3838
- ``CDVPlugin``
39+
- ``CDVPluginSchemeHandler``
40+
41+
### Plugin communication
3942
- ``CDVPluginResult``
4043
- ``CDVCommandStatus``
4144
- ``CDVInvokedUrlCommand``

CordovaLib/include/Cordova/CDVPlugin.h

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
#import <Cordova/CDVWebViewEngineProtocol.h>
2828
#import <Cordova/CDVInvokedUrlCommand.h>
2929

30+
// Forward declaration to avoid bringing WebKit API into public headers
31+
@protocol WKURLSchemeTask;
32+
3033
#ifndef __swift__
3134
// This global extension to the UIView class causes issues for Swift subclasses
3235
// of UIView with their own scrollView properties, so we're removing it from
@@ -79,3 +82,51 @@ extern const NSNotificationName CDVViewWillTransitionToSizeNotification;
7982
- (id)appDelegate;
8083

8184
@end
85+
86+
#pragma mark - Plugin protocols
87+
88+
/**
89+
A protocol for Cordova plugins to intercept handling of WebKit resource
90+
loading for a custom URL scheme.
91+
92+
Your plugin should implement this protocol if it wants to intercept requests
93+
to a custom URL scheme and provide its own resource loading. Otherwise,
94+
Cordova will use its default resource loading behavior from the app bundle.
95+
96+
When a WebKit-based web view encounters a resource that uses a custom scheme,
97+
it creates a WKURLSchemeTask object and Cordova passes it to the methods of
98+
your scheme handler plugin for processing. Use the ``overrideSchemeTask:``
99+
method to indicate that your plugin will handle the request and to begin
100+
loading the resource. While your handler loads the object, Cordova may call
101+
your plugin’s ``stopSchemeTask:`` method to notify you that the resource is no
102+
longer needed.
103+
*/
104+
@protocol CDVPluginSchemeHandler <NSObject>
105+
106+
/**
107+
Asks your plugin to handle the specified request and begin loading data.
108+
109+
If your plugin intends to handle the request and return data, this method
110+
should return `YES` as soon as possible to prevent the default request
111+
handling. If this method returns `NO`, Cordova will handle the resource
112+
loading using its default behavior.
113+
114+
Note that all methods of the task object must be called on the main thread.
115+
116+
- Parameters:
117+
- task: The task object that identifies the resource to load. You also use
118+
this object to report the progress of the load operation back to the web
119+
view.
120+
- Returns: A Boolean value indicating if the plugin is handling the request.
121+
*/
122+
- (BOOL)overrideSchemeTask:(id <WKURLSchemeTask>)task;
123+
124+
/**
125+
Asks your plugin to stop loading the data for the specified resource.
126+
127+
- Parameters:
128+
- task: The task object that identifies the resource the web view no
129+
longer needs.
130+
*/
131+
- (void)stopSchemeTask:(id <WKURLSchemeTask>)task;
132+
@end

CordovaLib/include/Cordova/CDVViewController.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
*/
1919

2020
#import <UIKit/UIKit.h>
21-
#import <WebKit/WebKit.h>
22-
#import <Foundation/NSJSONSerialization.h>
2321
#import <Cordova/CDVAvailability.h>
2422
#import <Cordova/CDVInvokedUrlCommand.h>
2523
#import <Cordova/CDVCommandDelegate.h>
@@ -67,9 +65,16 @@ NS_ASSUME_NONNULL_BEGIN
6765
*/
6866
@property (nonatomic, readonly, nullable, weak) IBOutlet UIView *webView;
6967

70-
@property (nonatomic, readonly, strong) NSDictionary<NSString *, CDVPlugin *> *pluginObjects;
68+
@property (nonatomic, readonly, strong) NSDictionary<NSString *, CDVPlugin *> *pluginObjects CDV_DEPRECATED(8, "Internal implementation detail, should not be used");
7169
@property (nullable, nonatomic, readonly, strong) NSDictionary<NSString *, NSString *> *pluginsMap CDV_DEPRECATED(8, "Internal implementation detail, should not be used");
7270

71+
/**
72+
An array of loaded Cordova plugin instances.
73+
74+
This array is safe to iterate using a `for...in` loop.
75+
*/
76+
@property (nonatomic, readonly, copy) NSArray <CDVPlugin *> *enumerablePlugins;
77+
7378
@property (nonatomic, readwrite, copy) NSString *appScheme;
7479

7580
@property (nonatomic, readonly, strong) CDVCommandQueue *commandQueue;

CordovaLib/include/Cordova/CDVWebViewEngineProtocol.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
*/
1919

2020
#import <UIKit/UIKit.h>
21-
#import <WebKit/WebKit.h>
2221

2322
#define kCDVWebViewEngineScriptMessageHandlers @"kCDVWebViewEngineScriptMessageHandlers"
2423
#define kCDVWebViewEngineWKNavigationDelegate @"kCDVWebViewEngineWKNavigationDelegate"
2524
#define kCDVWebViewEngineWKUIDelegate @"kCDVWebViewEngineWKUIDelegate"
2625
#define kCDVWebViewEngineWebViewPreferences @"kCDVWebViewEngineWebViewPreferences"
2726

27+
@class WKWebViewConfiguration;
28+
2829
@protocol CDVWebViewEngineProtocol <NSObject>
2930

3031
NS_ASSUME_NONNULL_BEGIN

CordovaLib/include/Cordova/CDVWebViewProcessPoolFactory.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
under the License.
1818
*/
1919

20-
#import <WebKit/WebKit.h>
20+
@class WKProcessPool;
2121

2222
@interface CDVWebViewProcessPoolFactory : NSObject
2323
@property (nonatomic, retain) WKProcessPool* sharedPool;

0 commit comments

Comments
 (0)