Skip to content

Commit 417c088

Browse files
committed
refactor(schemes): Read files on a background thread
Closes apacheGH-909.
1 parent b6ae567 commit 417c088

File tree

5 files changed

+152
-64
lines changed

5 files changed

+152
-64
lines changed

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

Lines changed: 131 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ Licensed to the Apache Software Foundation (ASF) under one
2424
#import <Foundation/Foundation.h>
2525
#import <MobileCoreServices/MobileCoreServices.h>
2626

27+
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
28+
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
29+
#endif
30+
31+
static const NSUInteger FILE_BUFFER_SIZE = 1024 * 1024 * 4; // 4 MiB
32+
2733
@interface CDVURLSchemeHandler ()
2834

2935
@property (nonatomic, weak) CDVViewController *viewController;
@@ -57,86 +63,157 @@ - (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)ur
5763
}
5864
}
5965

66+
67+
NSURLRequest *req = urlSchemeTask.request;
68+
if (![req.URL.scheme isEqualToString:self.viewController.appScheme]) {
69+
return;
70+
}
71+
6072
// Indicate that we are handling this task, by adding an entry with a null plugin
6173
// We do this so that we can (in future) detect if the task is cancelled before we finished feeding it response data
6274
[self.handlerMap setObject:(id)[NSNull null] forKey:urlSchemeTask];
6375

64-
NSString * startPath = [[NSBundle mainBundle] pathForResource:self.viewController.webContentFolderName ofType: nil];
65-
NSURL * url = urlSchemeTask.request.URL;
66-
NSString * stringToLoad = url.path;
67-
NSString * scheme = url.scheme;
76+
[self.viewController.commandDelegate runInBackground:^{
77+
NSURL *fileURL = [self fileURLForRequestURL:req.URL];
78+
NSError *error;
6879

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];
75-
} else {
76-
startPath = [startPath stringByAppendingPathComponent:stringToLoad];
80+
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:&error];
81+
if (!fileHandle || error) {
82+
if ([self taskActive:urlSchemeTask]) {
83+
[urlSchemeTask didFailWithError:error];
7784
}
85+
86+
@synchronized(self.handlerMap) {
87+
[self.handlerMap removeObjectForKey:urlSchemeTask];
88+
}
89+
return;
7890
}
79-
}
8091

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-
}
92+
NSString *mimeType = [self getMimeType:fileURL] ?: @"application/octet-stream";
93+
NSNumber *fileLength;
94+
[fileURL getResourceValue:&fileLength forKey:NSURLFileSizeKey error:nil];
10295

103-
[urlSchemeTask didReceiveResponse:response];
104-
if (data) {
105-
[urlSchemeTask didReceiveData:data];
106-
}
107-
[urlSchemeTask didFinish];
96+
NSNumber *responseSize = fileLength;
97+
98+
NSDictionary *headers = @{
99+
@"Content-Length" : [responseSize stringValue],
100+
@"Content-Type" : mimeType,
101+
@"Cache-Control": @"no-cache"
102+
};
103+
104+
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:req.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:headers];
105+
if ([self taskActive:urlSchemeTask]) {
106+
[urlSchemeTask didReceiveResponse:response];
107+
}
108+
109+
NSUInteger responseSent = 0;
110+
while ([self taskActive:urlSchemeTask] && responseSent < [responseSize unsignedIntegerValue]) {
111+
@autoreleasepool {
112+
NSData *data = [self readFromFileHandle:fileHandle upTo:FILE_BUFFER_SIZE error:&error];
113+
if (!data || error) {
114+
if ([self taskActive:urlSchemeTask]) {
115+
[urlSchemeTask didFailWithError:error];
116+
}
117+
break;
118+
}
119+
120+
if ([self taskActive:urlSchemeTask]) {
121+
[urlSchemeTask didReceiveData:data];
122+
}
123+
124+
responseSent += data.length;
125+
}
126+
}
127+
128+
[fileHandle closeFile];
129+
130+
if ([self taskActive:urlSchemeTask]) {
131+
[urlSchemeTask didFinish];
132+
}
108133

109-
[self.handlerMap removeObjectForKey:urlSchemeTask];
134+
@synchronized(self.handlerMap) {
135+
[self.handlerMap removeObjectForKey:urlSchemeTask];
136+
}
137+
}];
110138
}
111139

112140
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
113141
{
114-
CDVPlugin <CDVPluginSchemeHandler> *plugin = [self.handlerMap objectForKey:urlSchemeTask];
142+
CDVPlugin <CDVPluginSchemeHandler> *plugin;
143+
@synchronized(self.handlerMap) {
144+
plugin = [self.handlerMap objectForKey:urlSchemeTask];
145+
}
146+
115147
if (![plugin isEqual:[NSNull null]] && [plugin respondsToSelector:@selector(stopSchemeTask:)]) {
116148
[plugin stopSchemeTask:urlSchemeTask];
117149
}
118150

119-
[self.handlerMap removeObjectForKey:urlSchemeTask];
151+
@synchronized(self.handlerMap) {
152+
[self.handlerMap removeObjectForKey:urlSchemeTask];
153+
}
120154
}
121155

122-
-(NSString *) getMimeType:(NSString *)fileExtension {
123-
if (fileExtension && ![fileExtension isEqualToString:@""]) {
124-
NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL);
125-
NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
126-
return contentType ? contentType : @"application/octet-stream";
156+
#pragma mark - Utility methods
157+
158+
- (NSURL *)fileURLForRequestURL:(NSURL *)url
159+
{
160+
NSURL *resDir = [[NSBundle mainBundle] URLForResource:self.viewController.webContentFolderName withExtension:nil];
161+
NSURL *filePath;
162+
163+
if ([url.path hasPrefix:@"/_app_file_"]) {
164+
NSString *path = [url.path stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""];
165+
filePath = [resDir URLByAppendingPathComponent:path];
127166
} else {
128-
return @"text/html";
167+
if ([url.path isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) {
168+
filePath = [resDir URLByAppendingPathComponent:self.viewController.startPage];
169+
} else {
170+
filePath = [resDir URLByAppendingPathComponent:url.path];
171+
}
172+
}
173+
174+
return filePath.URLByStandardizingPath;
175+
}
176+
177+
-(NSString *)getMimeType:(NSURL *)url
178+
{
179+
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
180+
if (@available(iOS 14.0, *)) {
181+
UTType *uti;
182+
[url getResourceValue:&uti forKey:NSURLContentTypeKey error:nil];
183+
return [uti preferredMIMEType];
129184
}
185+
#endif
186+
187+
NSString *type;
188+
[url getResourceValue:&type forKey:NSURLTypeIdentifierKey error:nil];
189+
return (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)type, kUTTagClassMIMEType);
130190
}
131191

132-
-(BOOL) isMediaExtension:(NSString *) pathExtension {
133-
NSArray * mediaExtensions = @[@"m4v", @"mov", @"mp4",
134-
@"aac", @"ac3", @"aiff", @"au", @"flac", @"m4a", @"mp3", @"wav"];
135-
if ([mediaExtensions containsObject:pathExtension.lowercaseString]) {
136-
return YES;
192+
- (nullable NSData *)readFromFileHandle:(NSFileHandle *)handle upTo:(NSUInteger)length error:(NSError **)err
193+
{
194+
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
195+
if (@available(iOS 14.0, *)) {
196+
return [handle readDataUpToLength:length error:err];
197+
}
198+
#endif
199+
200+
@try {
201+
return [handle readDataOfLength:length];
202+
}
203+
@catch (NSError *error) {
204+
if (err != nil) {
205+
*err = error;
206+
}
207+
return nil;
137208
}
138-
return NO;
139209
}
140210

211+
- (BOOL)taskActive:(id <WKURLSchemeTask>)task
212+
{
213+
@synchronized(self.handlerMap) {
214+
return [self.handlerMap objectForKey:task] != nil;
215+
}
216+
}
141217

142218
@end
219+

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,21 +165,19 @@ - (WKWebViewConfiguration*) createConfigurationFromSettings:(CDVSettingsDictiona
165165

166166
- (void)pluginInitialize
167167
{
168-
// viewController would be available now. we attempt to set all possible delegates to it, by default
169-
CDVViewController* vc = (CDVViewController*)self.viewController;
170168
CDVSettingsDictionary* settings = self.commandDelegate.settings;
171169

172-
NSString *scheme = [settings cordovaSettingForKey:@"scheme"];
170+
NSString *scheme = self.viewController.appScheme;
173171

174172
// If scheme is file or nil, then default to file scheme
175-
self.cdvIsFileScheme = [scheme isEqualToString: @"file"] || scheme == nil;
173+
self.cdvIsFileScheme = [scheme isEqualToString:@"file"] || scheme == nil;
176174

177175
NSString *hostname = @"";
178176
if(!self.cdvIsFileScheme) {
179177
if(scheme == nil || [WKWebView handlesURLScheme:scheme]){
180178
scheme = @"app";
179+
self.viewController.appScheme = scheme;
181180
}
182-
vc.appScheme = scheme;
183181

184182
hostname = [settings cordovaSettingForKey:@"hostname"];
185183
if(hostname == nil){
@@ -189,7 +187,7 @@ - (void)pluginInitialize
189187
self.CDV_ASSETS_URL = [NSString stringWithFormat:@"%@://%@", scheme, hostname];
190188
}
191189

192-
CDVWebViewUIDelegate* uiDelegate = [[CDVWebViewUIDelegate alloc] initWithViewController:vc];
190+
CDVWebViewUIDelegate* uiDelegate = [[CDVWebViewUIDelegate alloc] initWithViewController:self.viewController];
193191
uiDelegate.title = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
194192
uiDelegate.allowNewWindows = [settings cordovaBoolSettingForKey:@"AllowNewWindows" defaultValue:NO];
195193
self.uiDelegate = uiDelegate;
@@ -213,7 +211,7 @@ - (void)pluginInitialize
213211

214212
// Do not configure the scheme handler if the scheme is default (file)
215213
if(!self.cdvIsFileScheme) {
216-
self.schemeHandler = [[CDVURLSchemeHandler alloc] initWithViewController:vc];
214+
self.schemeHandler = [[CDVURLSchemeHandler alloc] initWithViewController:self.viewController];
217215
[configuration setURLSchemeHandler:self.schemeHandler forURLScheme:scheme];
218216
}
219217

CordovaLib/Classes/Public/CDVViewController.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,8 @@ - (void)loadSettings
523523
if (self.startPage == nil) {
524524
self.startPage = @"index.html";
525525
}
526+
527+
self.appScheme = [self.settings cordovaSettingForKey:@"Scheme"] ?: @"app";
526528
}
527529

528530
/// Retrieves the view from a newwly initialized webViewEngine

CordovaLib/include/Cordova/CDVPlugin.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,6 @@ extern const NSNotificationName CDVViewWillTransitionToSizeNotification;
111111
handling. If this method returns `NO`, Cordova will handle the resource
112112
loading using its default behavior.
113113
114-
Note that all methods of the task object must be called on the main thread.
115-
116114
- Parameters:
117115
- task: The task object that identifies the resource to load. You also use
118116
this object to report the progress of the load operation back to the web

CordovaLib/include/Cordova/CDVViewController.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,20 @@ NS_ASSUME_NONNULL_BEGIN
7575
*/
7676
@property (nonatomic, readonly, copy) NSArray <CDVPlugin *> *enumerablePlugins;
7777

78-
@property (nonatomic, readwrite, copy) NSString *appScheme;
78+
/*
79+
The scheme being used to load web content from the app bundle into the Cordova
80+
web view.
81+
82+
The default value is `app` but can be customized via the `Scheme` preference
83+
in the Cordova XML configuration file. Setting this to `file` will results in
84+
web content being loaded using the File URL protocol, which has inherent
85+
security limitations. It is encouraged that you use a custom scheme to load
86+
your app content.
87+
88+
It is not valid to set this to an existing protocol scheme such as `http` or
89+
`https`.
90+
*/
91+
@property (nonatomic, nullable, readwrite, copy) NSString *appScheme;
7992

8093
@property (nonatomic, readonly, strong) CDVCommandQueue *commandQueue;
8194
@property (nonatomic, readonly, strong) id <CDVCommandDelegate> commandDelegate;

0 commit comments

Comments
 (0)