@@ -24,6 +24,12 @@ Licensed to the Apache Software Foundation (ASF) under one
24
24
#import < Foundation/Foundation.h>
25
25
#import < MobileCoreServices/MobileCoreServices.h>
26
26
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
+
27
33
@interface CDVURLSchemeHandler ()
28
34
29
35
@property (nonatomic , weak ) CDVViewController *viewController;
@@ -57,86 +63,192 @@ - (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)ur
57
63
}
58
64
}
59
65
66
+
67
+ NSURLRequest *req = urlSchemeTask.request ;
68
+ if (![req.URL.scheme isEqualToString: self .viewController.appScheme]) {
69
+ return ;
70
+ }
71
+
60
72
// Indicate that we are handling this task, by adding an entry with a null plugin
61
73
// We do this so that we can (in future) detect if the task is cancelled before we finished feeding it response data
62
74
[self .handlerMap setObject: (id )[NSNull null ] forKey: urlSchemeTask];
63
75
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;
68
79
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];
77
84
}
85
+
86
+ @synchronized (self.handlerMap ) {
87
+ [self .handlerMap removeObjectForKey: urlSchemeTask];
88
+ }
89
+ return ;
78
90
}
79
- }
80
91
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
+ }
102
117
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
+ }
108
168
109
- [self .handlerMap removeObjectForKey: urlSchemeTask];
169
+ @synchronized (self.handlerMap ) {
170
+ [self .handlerMap removeObjectForKey: urlSchemeTask];
171
+ }
172
+ }];
110
173
}
111
174
112
175
- (void )webView : (WKWebView *)webView stopURLSchemeTask : (id <WKURLSchemeTask >)urlSchemeTask
113
176
{
114
- CDVPlugin <CDVPluginSchemeHandler> *plugin = [self .handlerMap objectForKey: urlSchemeTask];
177
+ CDVPlugin <CDVPluginSchemeHandler> *plugin;
178
+ @synchronized (self.handlerMap ) {
179
+ plugin = [self .handlerMap objectForKey: urlSchemeTask];
180
+ }
181
+
115
182
if (![plugin isEqual: [NSNull null ]] && [plugin respondsToSelector: @selector (stopSchemeTask: )]) {
116
183
[plugin stopSchemeTask: urlSchemeTask];
117
184
}
118
185
119
- [self .handlerMap removeObjectForKey: urlSchemeTask];
186
+ @synchronized (self.handlerMap ) {
187
+ [self .handlerMap removeObjectForKey: urlSchemeTask];
188
+ }
120
189
}
121
190
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];
127
201
} 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
+ }
129
207
}
208
+
209
+ return filePath.URLByStandardizingPath ;
130
210
}
131
211
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 ];
137
219
}
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 );
139
225
}
140
226
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
+ }
141
252
142
253
@end
254
+
0 commit comments