@@ -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,192 @@ - (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+ NSInteger statusCode = 200 ; // Default to 200 OK status
93+ NSString *mimeType = [self getMimeType: fileURL] ?: @" application/octet-stream" ;
94+ NSNumber *fileLength;
95+ [fileURL getResourceValue: &fileLength forKey: NSURLFileSizeKey error: nil ];
96+
97+ NSNumber *responseSize = fileLength;
98+ NSUInteger responseSent = 0 ;
99+
100+ NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithCapacity: 5 ];
101+ headers[@" Content-Type" ] = mimeType;
102+ headers[@" Cache-Control" ] = @" no-cache" ;
103+ headers[@" Content-Length" ] = [responseSize stringValue ];
104+
105+ // Check for Range header
106+ NSString *rangeHeader = [urlSchemeTask.request valueForHTTPHeaderField: @" Range" ];
107+ if (rangeHeader) {
108+ NSRange range = NSMakeRange (NSNotFound , 0 );
109+
110+ if ([rangeHeader hasPrefix: @" bytes=" ]) {
111+ NSString *byteRange = [rangeHeader substringFromIndex: 6 ];
112+ NSArray <NSString *> *rangeParts = [byteRange componentsSeparatedByString: @" -" ];
113+ NSUInteger start = (NSUInteger )[rangeParts[0 ] integerValue ];
114+ NSUInteger end = rangeParts.count > 1 && ![rangeParts[1 ] isEqualToString: @" " ] ? (NSUInteger )[rangeParts[1 ] integerValue ] : [fileLength unsignedIntegerValue ] - 1 ;
115+ range = NSMakeRange (start, end - start + 1 );
116+ }
102117
103- [urlSchemeTask didReceiveResponse: response];
104- if (data) {
105- [urlSchemeTask didReceiveData: data];
106- }
107- [urlSchemeTask didFinish ];
118+ if (range.location != NSNotFound ) {
119+ // Ensure range is valid
120+ if (range.location >= [fileLength unsignedIntegerValue ] && [self taskActive: urlSchemeTask]) {
121+ headers[@" Content-Range" ] = [NSString stringWithFormat: @" bytes */%@ " , fileLength];
122+ NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc ] initWithURL: req.URL statusCode: 416 HTTPVersion: @" HTTP/1.1" headerFields: headers];
123+ [urlSchemeTask didReceiveResponse: response];
124+ [urlSchemeTask didFinish ];
125+
126+ @synchronized (self.handlerMap ) {
127+ [self .handlerMap removeObjectForKey: urlSchemeTask];
128+ }
129+ return ;
130+ }
131+
132+ [fileHandle seekToFileOffset: range.location];
133+ responseSize = [NSNumber numberWithUnsignedInteger: range.length];
134+ statusCode = 206 ; // Partial Content
135+ headers[@" Content-Range" ] = [NSString stringWithFormat: @" bytes %lu -%lu /%@ " , (unsigned long )range.location, (unsigned long )(range.location + range.length - 1 ), fileLength];
136+ headers[@" Content-Length" ] = [NSString stringWithFormat: @" %lu " , (unsigned long )range.length];
137+ }
138+ }
139+
140+ NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc ] initWithURL: req.URL statusCode: statusCode HTTPVersion: @" HTTP/1.1" headerFields: headers];
141+ if ([self taskActive: urlSchemeTask]) {
142+ [urlSchemeTask didReceiveResponse: response];
143+ }
144+
145+ while ([self taskActive: urlSchemeTask] && responseSent < [responseSize unsignedIntegerValue ]) {
146+ @autoreleasepool {
147+ NSData *data = [self readFromFileHandle: fileHandle upTo: FILE_BUFFER_SIZE error: &error];
148+ if (!data || error) {
149+ if ([self taskActive: urlSchemeTask]) {
150+ [urlSchemeTask didFailWithError: error];
151+ }
152+ break ;
153+ }
154+
155+ if ([self taskActive: urlSchemeTask]) {
156+ [urlSchemeTask didReceiveData: data];
157+ }
158+
159+ responseSent += data.length ;
160+ }
161+ }
162+
163+ [fileHandle closeFile ];
164+
165+ if ([self taskActive: urlSchemeTask]) {
166+ [urlSchemeTask didFinish ];
167+ }
108168
109- [self .handlerMap removeObjectForKey: urlSchemeTask];
169+ @synchronized (self.handlerMap ) {
170+ [self .handlerMap removeObjectForKey: urlSchemeTask];
171+ }
172+ }];
110173}
111174
112175- (void )webView : (WKWebView *)webView stopURLSchemeTask : (id <WKURLSchemeTask >)urlSchemeTask
113176{
114- CDVPlugin <CDVPluginSchemeHandler> *plugin = [self .handlerMap objectForKey: urlSchemeTask];
177+ CDVPlugin <CDVPluginSchemeHandler> *plugin;
178+ @synchronized (self.handlerMap ) {
179+ plugin = [self .handlerMap objectForKey: urlSchemeTask];
180+ }
181+
115182 if (![plugin isEqual: [NSNull null ]] && [plugin respondsToSelector: @selector (stopSchemeTask: )]) {
116183 [plugin stopSchemeTask: urlSchemeTask];
117184 }
118185
119- [self .handlerMap removeObjectForKey: urlSchemeTask];
186+ @synchronized (self.handlerMap ) {
187+ [self .handlerMap removeObjectForKey: urlSchemeTask];
188+ }
120189}
121190
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" ;
191+ #pragma mark - Utility methods
192+
193+ - (NSURL *)fileURLForRequestURL : (NSURL *)url
194+ {
195+ NSURL *resDir = [[NSBundle mainBundle ] URLForResource: self .viewController.webContentFolderName withExtension: nil ];
196+ NSURL *filePath;
197+
198+ if ([url.path hasPrefix: @" /_app_file_" ]) {
199+ NSString *path = [url.path stringByReplacingOccurrencesOfString: @" /_app_file_" withString: @" " ];
200+ filePath = [resDir URLByAppendingPathComponent: path];
127201 } else {
128- return @" text/html" ;
202+ if ([url.path isEqualToString: @" " ] || [url.pathExtension isEqualToString: @" " ]) {
203+ filePath = [resDir URLByAppendingPathComponent: self .viewController.startPage];
204+ } else {
205+ filePath = [resDir URLByAppendingPathComponent: url.path];
206+ }
129207 }
208+
209+ return filePath.URLByStandardizingPath ;
130210}
131211
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 ;
212+ -(NSString *)getMimeType : (NSURL *)url
213+ {
214+ #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
215+ if (@available (iOS 14.0 , *)) {
216+ UTType *uti;
217+ [url getResourceValue: &uti forKey: NSURLContentTypeKey error: nil ];
218+ return [uti preferredMIMEType ];
137219 }
138- return NO ;
220+ #endif
221+
222+ NSString *type;
223+ [url getResourceValue: &type forKey: NSURLTypeIdentifierKey error: nil ];
224+ return (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass ((__bridge CFStringRef)type, kUTTagClassMIMEType );
139225}
140226
227+ - (nullable NSData *)readFromFileHandle : (NSFileHandle *)handle upTo : (NSUInteger )length error : (NSError **)err
228+ {
229+ #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
230+ if (@available (iOS 14.0 , *)) {
231+ return [handle readDataUpToLength: length error: err];
232+ }
233+ #endif
234+
235+ @try {
236+ return [handle readDataOfLength: length];
237+ }
238+ @catch (NSError *error) {
239+ if (err != nil ) {
240+ *err = error;
241+ }
242+ return nil ;
243+ }
244+ }
245+
246+ - (BOOL )taskActive : (id <WKURLSchemeTask >)task
247+ {
248+ @synchronized (self.handlerMap ) {
249+ return [self .handlerMap objectForKey: task] != nil ;
250+ }
251+ }
141252
142253@end
254+
0 commit comments