You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by dp...@apache.org on 2024/08/28 09:23:15 UTC

(cordova-ios) branch master updated: refactor: Define a protocol for scheme handling plugins (#1479)

This is an automated email from the ASF dual-hosted git repository.

dpogue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-ios.git


The following commit(s) were added to refs/heads/master by this push:
     new c3d5949d refactor: Define a protocol for scheme handling plugins (#1479)
c3d5949d is described below

commit c3d5949de0ba5753cb55481eb145f9467a5380dc
Author: Darryl Pogue <da...@dpogue.ca>
AuthorDate: Wed Aug 28 02:23:07 2024 -0700

    refactor: Define a protocol for scheme handling plugins (#1479)
    
    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`).
---
 .../Plugins/CDVWebViewEngine/CDVURLSchemeHandler.h |  13 +-
 .../Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m | 131 +++++++++++----------
 .../Plugins/CDVWebViewEngine/CDVWebViewEngine.m    |   5 +-
 CordovaLib/Classes/Public/CDVViewController.m      |  26 ++--
 .../Classes/Public/CDVWebViewProcessPoolFactory.m  |   1 +
 CordovaLib/CordovaLib.docc/CordovaLib.md           |   3 +
 CordovaLib/include/Cordova/CDVPlugin.h             |  51 ++++++++
 CordovaLib/include/Cordova/CDVViewController.h     |  11 +-
 .../include/Cordova/CDVWebViewEngineProtocol.h     |   3 +-
 .../include/Cordova/CDVWebViewProcessPoolFactory.h |   2 +-
 .../Cordova/NSDictionary+CordovaPreferences.h      |   1 -
 11 files changed, 157 insertions(+), 90 deletions(-)

diff --git a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.h b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.h
index 067d7e34..bb2f998a 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 <Foundation/Foundation.h>
 #import <WebKit/WebKit.h>
-#import <Cordova/CDVViewController.h>
-#import <Cordova/CDVPlugin.h>
 
+@class CDVViewController;
 
 @interface CDVURLSchemeHandler : NSObject <WKURLSchemeHandler>
+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 33ae5cb9..17e79764 100644
--- a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m
+++ b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m
@@ -19,99 +19,104 @@
 
 
 #import "CDVURLSchemeHandler.h"
+#import <Cordova/CDVViewController.h>
+#import <Cordova/CDVPlugin.h>
+#import <Foundation/Foundation.h>
 #import <MobileCoreServices/MobileCoreServices.h>
 
-#import <objc/message.h>
+@interface CDVURLSchemeHandler ()
 
-@implementation CDVURLSchemeHandler
+@property (nonatomic, weak) CDVViewController *viewController;
+@property (nonatomic) NSMapTable <id <WKURLSchemeTask>, CDVPlugin <CDVPluginSchemeHandler> *> *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 <WKURLSchemeTask>)urlSchemeTask
 {
+    // Give plugins the chance to handle the url
+    for (CDVPlugin *plugin in self.viewController.enumerablePlugins) {
+        if ([plugin respondsToSelector:@selector(overrideSchemeTask:)]) {
+            CDVPlugin <CDVPluginSchemeHandler> *schemePlugin = (CDVPlugin<CDVPluginSchemeHandler> *)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 <WKURLSchemeTask>))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<WKURLSchemeTask>)urlSchemeTask
+- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
 {
-    SEL selector = NSSelectorFromString(@"stopSchemeTask:");
-    if (self.schemePlugin != nil && [self.schemePlugin respondsToSelector:selector]) {
-        (((void (*)(id, SEL, id <WKURLSchemeTask>))objc_msgSend)(self.schemePlugin, selector, urlSchemeTask));
+    CDVPlugin <CDVPluginSchemeHandler> *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 c47cd855..bae6838f 100644
--- a/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m
+++ b/CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m
@@ -215,7 +215,7 @@
 
     // 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 @@
     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 825f5525..2825e31b 100644
--- a/CordovaLib/Classes/Public/CDVViewController.m
+++ b/CordovaLib/Classes/Public/CDVViewController.m
@@ -67,6 +67,7 @@ static UIColor* defaultBackgroundColor(void) {
 @synthesize splashBackgroundColor = _splashBackgroundColor;
 @synthesize settings = _settings;
 @dynamic webView;
+@dynamic enumerablePlugins;
 
 #pragma mark - Initializers
 
@@ -152,6 +153,13 @@ static UIColor* defaultBackgroundColor(void) {
 
 #pragma mark - Getters & Setters
 
+- (NSArray <CDVPlugin *> *)enumerablePlugins
+{
+    @synchronized(_pluginObjects) {
+        return [_pluginObjects allValues];
+    }
+}
+
 - (NSString *)wwwFolderName
 {
     return self.webContentFolderName;
@@ -460,15 +468,11 @@ static UIColor* defaultBackgroundColor(void) {
 
 - (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 @@ static UIColor* defaultBackgroundColor(void) {
         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 4631c7fd..43de7222 100644
--- a/CordovaLib/Classes/Public/CDVWebViewProcessPoolFactory.m
+++ b/CordovaLib/Classes/Public/CDVWebViewProcessPoolFactory.m
@@ -17,6 +17,7 @@
  under the License.
  */
 
+#import <WebKit/WebKit.h>
 #import <Cordova/CDVWebViewProcessPoolFactory.h>
 
 static CDVWebViewProcessPoolFactory *factory = nil;
diff --git a/CordovaLib/CordovaLib.docc/CordovaLib.md b/CordovaLib/CordovaLib.docc/CordovaLib.md
index 44e9bf18..ea25f75d 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 fc37a9c7..d878539b 100644
--- a/CordovaLib/include/Cordova/CDVPlugin.h
+++ b/CordovaLib/include/Cordova/CDVPlugin.h
@@ -27,6 +27,9 @@
 #import <Cordova/CDVWebViewEngineProtocol.h>
 #import <Cordova/CDVInvokedUrlCommand.h>
 
+// 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 <NSObject>
+
+/**
+ 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 <WKURLSchemeTask>)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 <WKURLSchemeTask>)task;
+@end
diff --git a/CordovaLib/include/Cordova/CDVViewController.h b/CordovaLib/include/Cordova/CDVViewController.h
index 8670fd63..e9f4751c 100644
--- a/CordovaLib/include/Cordova/CDVViewController.h
+++ b/CordovaLib/include/Cordova/CDVViewController.h
@@ -18,8 +18,6 @@
  */
 
 #import <UIKit/UIKit.h>
-#import <WebKit/WebKit.h>
-#import <Foundation/NSJSONSerialization.h>
 #import <Cordova/CDVAvailability.h>
 #import <Cordova/CDVInvokedUrlCommand.h>
 #import <Cordova/CDVCommandDelegate.h>
@@ -67,9 +65,16 @@ NS_ASSUME_NONNULL_BEGIN
  */
 @property (nonatomic, readonly, nullable, weak) IBOutlet UIView *webView;
 
-@property (nonatomic, readonly, strong) NSDictionary<NSString *, CDVPlugin *> *pluginObjects;
+@property (nonatomic, readonly, strong) NSDictionary<NSString *, CDVPlugin *> *pluginObjects CDV_DEPRECATED(8, "Internal implementation detail, should not be used");
 @property (nullable, nonatomic, readonly, strong) NSDictionary<NSString *, NSString *> *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 <CDVPlugin *> *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 0d3f0bf7..875c46da 100644
--- a/CordovaLib/include/Cordova/CDVWebViewEngineProtocol.h
+++ b/CordovaLib/include/Cordova/CDVWebViewEngineProtocol.h
@@ -18,13 +18,14 @@
  */
 
 #import <UIKit/UIKit.h>
-#import <WebKit/WebKit.h>
 
 #define kCDVWebViewEngineScriptMessageHandlers @"kCDVWebViewEngineScriptMessageHandlers"
 #define kCDVWebViewEngineWKNavigationDelegate @"kCDVWebViewEngineWKNavigationDelegate"
 #define kCDVWebViewEngineWKUIDelegate @"kCDVWebViewEngineWKUIDelegate"
 #define kCDVWebViewEngineWebViewPreferences @"kCDVWebViewEngineWebViewPreferences"
 
+@class WKWebViewConfiguration;
+
 @protocol CDVWebViewEngineProtocol <NSObject>
 
 NS_ASSUME_NONNULL_BEGIN
diff --git a/CordovaLib/include/Cordova/CDVWebViewProcessPoolFactory.h b/CordovaLib/include/Cordova/CDVWebViewProcessPoolFactory.h
index b1562891..7a5bf76b 100644
--- a/CordovaLib/include/Cordova/CDVWebViewProcessPoolFactory.h
+++ b/CordovaLib/include/Cordova/CDVWebViewProcessPoolFactory.h
@@ -17,7 +17,7 @@
  under the License.
  */
 
-#import <WebKit/WebKit.h>
+@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 0d41c83d..db7b086b 100644
--- a/CordovaLib/include/Cordova/NSDictionary+CordovaPreferences.h
+++ b/CordovaLib/include/Cordova/NSDictionary+CordovaPreferences.h
@@ -18,7 +18,6 @@
  */
 
 #import <Foundation/Foundation.h>
-#import <UIKit/UIKit.h>
 #import <Cordova/CDVAvailabilityDeprecated.h>
 
 #ifndef __CORDOVA_SILENCE_HEADER_DEPRECATIONS


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org
For additional commands, e-mail: commits-help@cordova.apache.org