From 8a3b0fa9e81f74d25816e2fe17268a1ee829d70a Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Sat, 25 Apr 2015 14:52:18 -0700 Subject: [PATCH] Fix edge cases and add tests for +[RCTConvert NSURL:] --- React/Base/RCTConvert.m | 51 +++++---- React/Base/RCTJavaScriptLoader.m | 174 ++++++++++++------------------- React/Base/RCTRootView.m | 10 -- 3 files changed, 98 insertions(+), 137 deletions(-) diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index f1ed77298..eacb03b85 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -49,11 +49,11 @@ RCT_CONVERTER(NSString *, NSString, description) }); NSNumber *number = [formatter numberFromString:json]; if (!number) { - RCTLogError(@"JSON String '%@' could not be interpreted as a number", json); + RCTLogConvertError(json, "a number"); } return number; } else if (json && json != [NSNull null]) { - RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json classForCoder]); + RCTLogConvertError(json, "a number"); } return nil; } @@ -66,30 +66,38 @@ RCT_CONVERTER(NSString *, NSString, description) + (NSURL *)NSURL:(id)json { - if (!json || json == (id)kCFNull) { + NSString *path = [self NSString:json]; + if (!path.length) { return nil; } - if (![json isKindOfClass:[NSString class]]) { - RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json); - return nil; - } + @try { // NSURL has a history of crashing with bad input, so let's be safe - NSString *path = json; - if ([path isAbsolutePath]) - { + NSURL *URL = [NSURL URLWithString:path]; + if (URL.scheme) { // Was a well-formed absolute URL + return URL; + } + + // Check if it has a scheme + if ([path rangeOfString:@"[a-zA-Z][a-zA-Z._-]+:" options:NSRegularExpressionSearch].location == 0) { + path = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + URL = [NSURL URLWithString:path]; + if (URL) { + return URL; + } + } + + // Assume that it's a local path + path = [path stringByRemovingPercentEncoding]; + if (![path isAbsolutePath]) { + path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:path]; + } return [NSURL fileURLWithPath:path]; } - else if ([path length]) - { - NSURL *URL = [NSURL URLWithString:path relativeToURL:[[NSBundle mainBundle] resourceURL]]; - if ([URL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) { - RCTLogWarn(@"The file '%@' does not exist", URL); - return nil; - } - return URL; + @catch (__unused NSException *e) { + RCTLogConvertError(json, "a valid URL"); + return nil; } - return nil; } + (NSURLRequest *)NSURLRequest:(id)json @@ -112,11 +120,12 @@ RCT_CONVERTER(NSString *, NSString, description) }); NSDate *date = [formatter dateFromString:json]; if (!date) { - RCTLogError(@"JSON String '%@' could not be interpreted as a date. Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json); + RCTLogError(@"JSON String '%@' could not be interpreted as a date. " + "Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json); } return date; } else if (json && json != [NSNull null]) { - RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json classForCoder]); + RCTLogConvertError(json, "a date"); } return nil; } diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index dd8fab461..2e7d21b94 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -10,47 +10,15 @@ #import "RCTJavaScriptLoader.h" #import "RCTBridge.h" -#import "RCTInvalidating.h" -#import "RCTLog.h" -#import "RCTRedBox.h" +#import "RCTConvert.h" #import "RCTSourceCode.h" #import "RCTUtils.h" -#define NO_REMOTE_MODULE @"Could not fetch module bundle %@. Ensure node server is running.\n\nIf it timed out, try reloading." -#define NO_LOCAL_BUNDLE @"Could not load local bundle %@. Ensure file exists." - -#define CACHE_DIR @"RCTJSBundleCache" - -#pragma mark - Application Engine - -/** - * TODO: - * - Add window resize rotation events matching the DOM API. - * - Device pixel ration hooks. - * - Source maps. - */ @implementation RCTJavaScriptLoader { __weak RCTBridge *_bridge; } -/** - * `CADisplayLink` code copied from Ejecta but we've placed the JavaScriptCore - * engine in its own dedicated thread. - * - * TODO: Try adding to the `RCTJavaScriptExecutor`'s thread runloop. Removes one - * additional GCD dispatch per frame and likely makes it so that other UIThread - * operations don't delay the dispatch (so we can begin working in JS much - * faster.) Event handling must still be sent via a GCD dispatch, of course. - * - * We must add the display link to two runloops in order to get setTimeouts to - * fire during scrolling. (`NSDefaultRunLoopMode` and `UITrackingRunLoopMode`) - * TODO: We can invent a `requestAnimationFrame` and - * `requestAvailableAnimationFrame` to control if callbacks can be fired during - * an animation. - * http://stackoverflow.com/questions/12622800/why-does-uiscrollview-pause-my-cadisplaylink - * - */ - (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super init])) { @@ -61,92 +29,86 @@ - (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onComplete { - if (scriptURL == nil) { + // Sanitize the script URL + scriptURL = [RCTConvert NSURL:scriptURL.absoluteString]; + + if (!scriptURL || + ([scriptURL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:scriptURL.path])) { NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{ - NSLocalizedDescriptionKey: @"No script URL provided" + NSLocalizedDescriptionKey: scriptURL ? [NSString stringWithFormat:@"Script at '%@' could not be found.", scriptURL] : @"No script URL provided" }]; onComplete(error); return; } - if ([scriptURL isFileURL]) { - NSString *bundlePath = [[NSBundle bundleForClass:[self class]] resourcePath]; - NSString *localPath = [scriptURL.absoluteString substringFromIndex:@"file://".length]; - - if (![localPath hasPrefix:bundlePath]) { - NSString *absolutePath = [NSString stringWithFormat:@"%@/%@", bundlePath, localPath]; - scriptURL = [NSURL fileURLWithPath:absolutePath]; - } - } - NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) { - // Handle general request errors - if (error) { - if ([[error domain] isEqualToString:NSURLErrorDomain]) { - NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]]; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: desc, - NSLocalizedFailureReasonErrorKey: [error localizedDescription], - NSUnderlyingErrorKey: error, - }; - error = [NSError errorWithDomain:@"JSServer" - code:error.code - userInfo:userInfo]; - } - onComplete(error); - return; - } + // Handle general request errors + if (error) { + if ([[error domain] isEqualToString:NSURLErrorDomain]) { + NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]]; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: desc, + NSLocalizedFailureReasonErrorKey: [error localizedDescription], + NSUnderlyingErrorKey: error, + }; + error = [NSError errorWithDomain:@"JSServer" + code:error.code + userInfo:userInfo]; + } + onComplete(error); + return; + } - // Parse response as text - NSStringEncoding encoding = NSUTF8StringEncoding; - if (response.textEncodingName != nil) { - CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); - if (cfEncoding != kCFStringEncodingInvalidId) { - encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); - } - } - NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; + // Parse response as text + NSStringEncoding encoding = NSUTF8StringEncoding; + if (response.textEncodingName != nil) { + CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); + if (cfEncoding != kCFStringEncodingInvalidId) { + encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); + } + } + NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; - // Handle HTTP errors - if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) { - NSDictionary *userInfo; - NSDictionary *errorDetails = RCTJSONParse(rawText, nil); - if ([errorDetails isKindOfClass:[NSDictionary class]] && - [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) { - NSMutableArray *fakeStack = [[NSMutableArray alloc] init]; - for (NSDictionary *err in errorDetails[@"errors"]) { - [fakeStack addObject: @{ - @"methodName": err[@"description"] ?: @"", - @"file": err[@"filename"] ?: @"", - @"lineNumber": err[@"lineNumber"] ?: @0 - }]; - } - userInfo = @{ - NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided", - @"stack": fakeStack, - }; - } else { - userInfo = @{NSLocalizedDescriptionKey: rawText}; - } - error = [NSError errorWithDomain:@"JSServer" - code:[(NSHTTPURLResponse *)response statusCode] - userInfo:userInfo]; + // Handle HTTP errors + if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) { + NSDictionary *userInfo; + NSDictionary *errorDetails = RCTJSONParse(rawText, nil); + if ([errorDetails isKindOfClass:[NSDictionary class]] && + [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) { + NSMutableArray *fakeStack = [[NSMutableArray alloc] init]; + for (NSDictionary *err in errorDetails[@"errors"]) { + [fakeStack addObject: @{ + @"methodName": err[@"description"] ?: @"", + @"file": err[@"filename"] ?: @"", + @"lineNumber": err[@"lineNumber"] ?: @0 + }]; + } + userInfo = @{ + NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided", + @"stack": fakeStack, + }; + } else { + userInfo = @{NSLocalizedDescriptionKey: rawText}; + } + error = [NSError errorWithDomain:@"JSServer" + code:[(NSHTTPURLResponse *)response statusCode] + userInfo:userInfo]; - onComplete(error); - return; - } - RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; - sourceCodeModule.scriptURL = scriptURL; - sourceCodeModule.scriptText = rawText; + onComplete(error); + return; + } + RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; + sourceCodeModule.scriptURL = scriptURL; + sourceCodeModule.scriptText = rawText; - [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) { - dispatch_async(dispatch_get_main_queue(), ^{ - onComplete(scriptError); - }); - }]; - }]; + [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) { + dispatch_async(dispatch_get_main_queue(), ^{ + onComplete(scriptError); + }); + }]; + }]; [task resume]; } diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 45624efdc..0c171951e 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -23,16 +23,6 @@ #import "RCTWebViewExecutor.h" #import "UIView+React.h" -/** - * HACK(t6568049) This should be removed soon, hiding to prevent people from - * relying on it - */ -@interface RCTBridge (RCTRootView) - -- (void)setJavaScriptExecutor:(id)executor; - -@end - @interface RCTUIManager (RCTRootView) - (NSNumber *)allocateRootTag;