@@ -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