Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Define a protocol for scheme-handling plugins #1479

Merged
merged 1 commit into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -19,99 +19,104 @@ Licensed to the Apache Software Foundation (ASF) under one


#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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

Expand Down Expand Up @@ -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;
Expand Down
26 changes: 17 additions & 9 deletions CordovaLib/Classes/Public/CDVViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ @implementation CDVViewController
@synthesize splashBackgroundColor = _splashBackgroundColor;
@synthesize settings = _settings;
@dynamic webView;
@dynamic enumerablePlugins;

#pragma mark - Initializers

Expand Down Expand Up @@ -152,6 +153,13 @@ - (void)dealloc

#pragma mark - Getters & Setters

- (NSArray <CDVPlugin *> *)enumerablePlugins
{
@synchronized(_pluginObjects) {
return [_pluginObjects allValues];
}
}

- (NSString *)wwwFolderName
{
return self.webContentFolderName;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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"],
Expand Down
1 change: 1 addition & 0 deletions CordovaLib/Classes/Public/CDVWebViewProcessPoolFactory.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Licensed to the Apache Software Foundation (ASF) under one
under the License.
*/

#import <WebKit/WebKit.h>
#import <Cordova/CDVWebViewProcessPoolFactory.h>

static CDVWebViewProcessPoolFactory *factory = nil;
Expand Down
3 changes: 3 additions & 0 deletions CordovaLib/CordovaLib.docc/CordovaLib.md
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand Down
51 changes: 51 additions & 0 deletions CordovaLib/include/Cordova/CDVPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
11 changes: 8 additions & 3 deletions CordovaLib/include/Cordova/CDVViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion CordovaLib/include/Cordova/CDVWebViewEngineProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CordovaLib/include/Cordova/CDVWebViewProcessPoolFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
under the License.
*/

#import <WebKit/WebKit.h>
@class WKProcessPool;

@interface CDVWebViewProcessPoolFactory : NSObject
@property (nonatomic, retain) WKProcessPool* sharedPool;
Expand Down
Loading