CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVURLSchemeHandler.m (185 lines of code) (raw):
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
#import "CDVURLSchemeHandler.h"
#import <Cordova/CDVViewController.h>
#import <Cordova/CDVPlugin.h>
#import <Foundation/Foundation.h>
#import <MobileCoreServices/MobileCoreServices.h>
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#endif
static const NSUInteger FILE_BUFFER_SIZE = 1024 * 1024 * 4; // 4 MiB
@interface CDVURLSchemeHandler ()
@property (nonatomic, weak) CDVViewController *viewController;
@property (nonatomic) NSMapTable <id <WKURLSchemeTask>, CDVPlugin <CDVPluginSchemeHandler> *> *handlerMap;
@end
@implementation CDVURLSchemeHandler
- (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;
}
}
}
NSURLRequest *req = urlSchemeTask.request;
if (![req.URL.scheme isEqualToString:self.viewController.appScheme]) {
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];
[self.viewController.commandDelegate runInBackground:^{
NSURL *fileURL = [self fileURLForRequestURL:req.URL];
NSError *error;
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:&error];
if (!fileHandle || error) {
if ([self taskActive:urlSchemeTask]) {
[urlSchemeTask didFailWithError:error];
}
@synchronized(self.handlerMap) {
[self.handlerMap removeObjectForKey:urlSchemeTask];
}
return;
}
NSInteger statusCode = 200; // Default to 200 OK status
NSString *mimeType = [self getMimeType:fileURL] ?: @"application/octet-stream";
NSNumber *fileLength;
[fileURL getResourceValue:&fileLength forKey:NSURLFileSizeKey error:nil];
NSNumber *responseSize = fileLength;
NSUInteger responseSent = 0;
NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithCapacity:5];
headers[@"Content-Type"] = mimeType;
headers[@"Cache-Control"] = @"no-cache";
headers[@"Content-Length"] = [responseSize stringValue];
// Check for Range header
NSString *rangeHeader = [urlSchemeTask.request valueForHTTPHeaderField:@"Range"];
if (rangeHeader) {
NSRange range = NSMakeRange(NSNotFound, 0);
if ([rangeHeader hasPrefix:@"bytes="]) {
NSString *byteRange = [rangeHeader substringFromIndex:6];
NSArray<NSString *> *rangeParts = [byteRange componentsSeparatedByString:@"-"];
NSUInteger start = (NSUInteger)[rangeParts[0] integerValue];
NSUInteger end = rangeParts.count > 1 && ![rangeParts[1] isEqualToString:@""] ? (NSUInteger)[rangeParts[1] integerValue] : [fileLength unsignedIntegerValue] - 1;
range = NSMakeRange(start, end - start + 1);
}
if (range.location != NSNotFound) {
// Ensure range is valid
if (range.location >= [fileLength unsignedIntegerValue] && [self taskActive:urlSchemeTask]) {
headers[@"Content-Range"] = [NSString stringWithFormat:@"bytes */%@", fileLength];
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:req.URL statusCode:416 HTTPVersion:@"HTTP/1.1" headerFields:headers];
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didFinish];
@synchronized(self.handlerMap) {
[self.handlerMap removeObjectForKey:urlSchemeTask];
}
return;
}
[fileHandle seekToFileOffset:range.location];
responseSize = [NSNumber numberWithUnsignedInteger:range.length];
statusCode = 206; // Partial Content
headers[@"Content-Range"] = [NSString stringWithFormat:@"bytes %lu-%lu/%@", (unsigned long)range.location, (unsigned long)(range.location + range.length - 1), fileLength];
headers[@"Content-Length"] = [NSString stringWithFormat:@"%lu", (unsigned long)range.length];
}
}
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:req.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headers];
if ([self taskActive:urlSchemeTask]) {
[urlSchemeTask didReceiveResponse:response];
}
while ([self taskActive:urlSchemeTask] && responseSent < [responseSize unsignedIntegerValue]) {
@autoreleasepool {
NSData *data = [self readFromFileHandle:fileHandle upTo:FILE_BUFFER_SIZE error:&error];
if (!data || error) {
if ([self taskActive:urlSchemeTask]) {
[urlSchemeTask didFailWithError:error];
}
break;
}
if ([self taskActive:urlSchemeTask]) {
[urlSchemeTask didReceiveData:data];
}
responseSent += data.length;
}
}
[fileHandle closeFile];
if ([self taskActive:urlSchemeTask]) {
[urlSchemeTask didFinish];
}
@synchronized(self.handlerMap) {
[self.handlerMap removeObjectForKey:urlSchemeTask];
}
}];
}
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
{
CDVPlugin <CDVPluginSchemeHandler> *plugin;
@synchronized(self.handlerMap) {
plugin = [self.handlerMap objectForKey:urlSchemeTask];
}
if (![plugin isEqual:[NSNull null]] && [plugin respondsToSelector:@selector(stopSchemeTask:)]) {
[plugin stopSchemeTask:urlSchemeTask];
}
@synchronized(self.handlerMap) {
[self.handlerMap removeObjectForKey:urlSchemeTask];
}
}
#pragma mark - Utility methods
- (NSURL *)fileURLForRequestURL:(NSURL *)url
{
NSURL *resDir = [[NSBundle mainBundle] URLForResource:self.viewController.webContentFolderName withExtension:nil];
NSURL *filePath;
if ([url.path hasPrefix:@"/_app_file_"]) {
NSString *path = [url.path stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""];
filePath = [resDir URLByAppendingPathComponent:path];
} else {
if ([url.path isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) {
filePath = [resDir URLByAppendingPathComponent:self.viewController.startPage];
} else {
filePath = [resDir URLByAppendingPathComponent:url.path];
}
}
return filePath.URLByStandardizingPath;
}
-(NSString *)getMimeType:(NSURL *)url
{
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
if (@available(iOS 14.0, *)) {
UTType *uti;
[url getResourceValue:&uti forKey:NSURLContentTypeKey error:nil];
return [uti preferredMIMEType];
}
#endif
NSString *type;
[url getResourceValue:&type forKey:NSURLTypeIdentifierKey error:nil];
return (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)type, kUTTagClassMIMEType);
}
- (nullable NSData *)readFromFileHandle:(NSFileHandle *)handle upTo:(NSUInteger)length error:(NSError **)err
{
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000
if (@available(iOS 14.0, *)) {
return [handle readDataUpToLength:length error:err];
}
#endif
@try {
return [handle readDataOfLength:length];
}
@catch (NSError *error) {
if (err != nil) {
*err = error;
}
return nil;
}
}
- (BOOL)taskActive:(id <WKURLSchemeTask>)task
{
@synchronized(self.handlerMap) {
return [self.handlerMap objectForKey:task] != nil;
}
}
@end