This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
365 lines
14 KiB
365 lines
14 KiB
/** |
|
* Copyright (c) 2015-present, Facebook, Inc. |
|
* |
|
* This source code is licensed under the MIT license found in the |
|
* LICENSE file in the root directory of this source tree. |
|
*/ |
|
|
|
#import "RCTJavaScriptLoader.h" |
|
|
|
#import <sys/stat.h> |
|
|
|
#import <cxxreact/JSBundleType.h> |
|
#import <jschelpers/JavaScriptCore.h> |
|
|
|
#import "RCTBridge.h" |
|
#import "RCTConvert.h" |
|
#import "RCTMultipartDataTask.h" |
|
#import "RCTPerformanceLogger.h" |
|
#import "RCTUtils.h" |
|
|
|
NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain"; |
|
|
|
@interface RCTSource() |
|
{ |
|
@public |
|
NSURL *_url; |
|
NSData *_data; |
|
NSUInteger _length; |
|
NSInteger _filesChangedCount; |
|
} |
|
|
|
@end |
|
|
|
@implementation RCTSource |
|
|
|
static RCTSource *RCTSourceCreate(NSURL *url, NSData *data, int64_t length) NS_RETURNS_RETAINED |
|
{ |
|
RCTSource *source = [RCTSource new]; |
|
source->_url = url; |
|
source->_data = data; |
|
source->_length = length; |
|
source->_filesChangedCount = RCTSourceFilesChangedCountNotBuiltByBundler; |
|
return source; |
|
} |
|
|
|
@end |
|
|
|
@implementation RCTLoadingProgress |
|
|
|
- (NSString *)description |
|
{ |
|
NSMutableString *desc = [NSMutableString new]; |
|
[desc appendString:_status ?: @"Loading"]; |
|
|
|
if ([_total integerValue] > 0) { |
|
[desc appendFormat:@" %ld%% (%@/%@)", (long)(100 * [_done integerValue] / [_total integerValue]), _done, _total]; |
|
} |
|
[desc appendString:@"\u2026"]; |
|
return desc; |
|
} |
|
|
|
@end |
|
|
|
@implementation RCTJavaScriptLoader |
|
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)init) |
|
|
|
+ (void)loadBundleAtURL:(NSURL *)scriptURL onProgress:(RCTSourceLoadProgressBlock)onProgress onComplete:(RCTSourceLoadBlock)onComplete |
|
{ |
|
int64_t sourceLength; |
|
NSError *error; |
|
NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL |
|
runtimeBCVersion:JSNoBytecodeFileFormatVersion |
|
sourceLength:&sourceLength |
|
error:&error]; |
|
if (data) { |
|
onComplete(nil, RCTSourceCreate(scriptURL, data, sourceLength)); |
|
return; |
|
} |
|
|
|
const BOOL isCannotLoadSyncError = |
|
[error.domain isEqualToString:RCTJavaScriptLoaderErrorDomain] |
|
&& error.code == RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously; |
|
|
|
if (isCannotLoadSyncError) { |
|
attemptAsynchronousLoadOfBundleAtURL(scriptURL, onProgress, onComplete); |
|
} else { |
|
onComplete(error, nil); |
|
} |
|
} |
|
|
|
+ (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL |
|
runtimeBCVersion:(int32_t)runtimeBCVersion |
|
sourceLength:(int64_t *)sourceLength |
|
error:(NSError **)error |
|
{ |
|
NSString *unsanitizedScriptURLString = scriptURL.absoluteString; |
|
// Sanitize the script URL |
|
scriptURL = sanitizeURL(scriptURL); |
|
|
|
if (!scriptURL) { |
|
if (error) { |
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain |
|
code:RCTJavaScriptLoaderErrorNoScriptURL |
|
userInfo:@{NSLocalizedDescriptionKey: |
|
[NSString stringWithFormat:@"No script URL provided. Make sure the packager is " |
|
@"running or you have embedded a JS bundle in your application bundle.\n\n" |
|
@"unsanitizedScriptURLString = %@", unsanitizedScriptURLString]}]; |
|
} |
|
return nil; |
|
} |
|
|
|
// Load local script file |
|
if (!scriptURL.fileURL) { |
|
if (error) { |
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain |
|
code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously |
|
userInfo:@{NSLocalizedDescriptionKey: |
|
[NSString stringWithFormat:@"Cannot load %@ URLs synchronously", |
|
scriptURL.scheme]}]; |
|
} |
|
return nil; |
|
} |
|
|
|
// Load the first 4 bytes to check if the bundle is regular or RAM ("Random Access Modules" bundle). |
|
// The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`. |
|
// The benefit of RAM bundle over a regular bundle is that we can lazily inject |
|
// modules into JSC as they're required. |
|
FILE *bundle = fopen(scriptURL.path.UTF8String, "r"); |
|
if (!bundle) { |
|
if (error) { |
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain |
|
code:RCTJavaScriptLoaderErrorFailedOpeningFile |
|
userInfo:@{NSLocalizedDescriptionKey: |
|
[NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path]}]; |
|
} |
|
return nil; |
|
} |
|
|
|
facebook::react::BundleHeader header; |
|
size_t readResult = fread(&header, sizeof(header), 1, bundle); |
|
fclose(bundle); |
|
if (readResult != 1) { |
|
if (error) { |
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain |
|
code:RCTJavaScriptLoaderErrorFailedReadingFile |
|
userInfo:@{NSLocalizedDescriptionKey: |
|
[NSString stringWithFormat:@"Error reading bundle %@", scriptURL.path]}]; |
|
} |
|
return nil; |
|
} |
|
|
|
facebook::react::ScriptTag tag = facebook::react::parseTypeFromHeader(header); |
|
switch (tag) { |
|
case facebook::react::ScriptTag::RAMBundle: |
|
break; |
|
|
|
case facebook::react::ScriptTag::String: { |
|
#if RCT_ENABLE_INSPECTOR |
|
NSData *source = [NSData dataWithContentsOfFile:scriptURL.path |
|
options:NSDataReadingMappedIfSafe |
|
error:error]; |
|
if (sourceLength && source != nil) { |
|
*sourceLength = source.length; |
|
} |
|
return source; |
|
#else |
|
if (error) { |
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain |
|
code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously |
|
userInfo:@{NSLocalizedDescriptionKey: |
|
@"Cannot load text/javascript files synchronously"}]; |
|
} |
|
return nil; |
|
#endif |
|
} |
|
case facebook::react::ScriptTag::BCBundle: |
|
if (runtimeBCVersion == JSNoBytecodeFileFormatVersion || runtimeBCVersion < 0) { |
|
if (error) { |
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain |
|
code:RCTJavaScriptLoaderErrorBCNotSupported |
|
userInfo:@{NSLocalizedDescriptionKey: |
|
@"Bytecode bundles are not supported by this runtime."}]; |
|
} |
|
return nil; |
|
} |
|
else if ((uint32_t)runtimeBCVersion != header.version) { |
|
if (error) { |
|
NSString *errDesc = |
|
[NSString stringWithFormat:@"BC Version Mismatch. Expect: %d, Actual: %u", |
|
runtimeBCVersion, header.version]; |
|
|
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain |
|
code:RCTJavaScriptLoaderErrorBCVersion |
|
userInfo:@{NSLocalizedDescriptionKey: errDesc}]; |
|
} |
|
return nil; |
|
} |
|
break; |
|
} |
|
|
|
struct stat statInfo; |
|
if (stat(scriptURL.path.UTF8String, &statInfo) != 0) { |
|
if (error) { |
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain |
|
code:RCTJavaScriptLoaderErrorFailedStatingFile |
|
userInfo:@{NSLocalizedDescriptionKey: |
|
[NSString stringWithFormat:@"Error stating bundle %@", scriptURL.path]}]; |
|
} |
|
return nil; |
|
} |
|
if (sourceLength) { |
|
*sourceLength = statInfo.st_size; |
|
} |
|
return [NSData dataWithBytes:&header length:sizeof(header)]; |
|
} |
|
|
|
static void parseHeaders(NSDictionary *headers, RCTSource *source) { |
|
source->_filesChangedCount = [headers[@"X-Metro-Files-Changed-Count"] integerValue]; |
|
} |
|
|
|
static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoadProgressBlock onProgress, RCTSourceLoadBlock onComplete) |
|
{ |
|
scriptURL = sanitizeURL(scriptURL); |
|
|
|
if (scriptURL.fileURL) { |
|
// Reading in a large bundle can be slow. Dispatch to the background queue to do it. |
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
|
NSError *error = nil; |
|
NSData *source = [NSData dataWithContentsOfFile:scriptURL.path |
|
options:NSDataReadingMappedIfSafe |
|
error:&error]; |
|
onComplete(error, RCTSourceCreate(scriptURL, source, source.length)); |
|
}); |
|
return; |
|
} |
|
|
|
RCTMultipartDataTask *task = [[RCTMultipartDataTask alloc] initWithURL:scriptURL partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) { |
|
if (!done) { |
|
if (onProgress) { |
|
onProgress(progressEventFromData(data)); |
|
} |
|
return; |
|
} |
|
|
|
// Handle general request errors |
|
if (error) { |
|
if ([error.domain isEqualToString:NSURLErrorDomain]) { |
|
error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain |
|
code:RCTJavaScriptLoaderErrorURLLoadFailed |
|
userInfo: |
|
@{ |
|
NSLocalizedDescriptionKey: |
|
[@"Could not connect to development server.\n\n" |
|
"Ensure the following:\n" |
|
"- Node server is running and available on the same network - run 'npm start' from react-native root\n" |
|
"- Node server URL is correctly set in AppDelegate\n" |
|
"- WiFi is enabled and connected to the same network as the Node Server\n\n" |
|
"URL: " stringByAppendingString:scriptURL.absoluteString], |
|
NSLocalizedFailureReasonErrorKey: error.localizedDescription, |
|
NSUnderlyingErrorKey: error, |
|
}]; |
|
} |
|
onComplete(error, nil); |
|
return; |
|
} |
|
|
|
// For multipart responses packager sets X-Http-Status header in case HTTP status code |
|
// is different from 200 OK |
|
NSString *statusCodeHeader = headers[@"X-Http-Status"]; |
|
if (statusCodeHeader) { |
|
statusCode = [statusCodeHeader integerValue]; |
|
} |
|
|
|
if (statusCode != 200) { |
|
error = [NSError errorWithDomain:@"JSServer" |
|
code:statusCode |
|
userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding])]; |
|
onComplete(error, nil); |
|
return; |
|
} |
|
|
|
// Validate that the packager actually returned javascript. |
|
NSString *contentType = headers[@"Content-Type"]; |
|
NSString *mimeType = [[contentType componentsSeparatedByString:@";"] firstObject]; |
|
if (![mimeType isEqualToString:@"application/javascript"] && |
|
![mimeType isEqualToString:@"text/javascript"]) { |
|
NSString *description = [NSString stringWithFormat:@"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.", mimeType]; |
|
error = [NSError errorWithDomain:@"JSServer" |
|
code:NSURLErrorCannotParseResponse |
|
userInfo:@{ |
|
NSLocalizedDescriptionKey: description, |
|
@"headers": headers, |
|
@"data": data |
|
}]; |
|
onComplete(error, nil); |
|
return; |
|
} |
|
|
|
RCTSource *source = RCTSourceCreate(scriptURL, data, data.length); |
|
parseHeaders(headers, source); |
|
onComplete(nil, source); |
|
} progressHandler:^(NSDictionary *headers, NSNumber *loaded, NSNumber *total) { |
|
// Only care about download progress events for the javascript bundle part. |
|
if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"]) { |
|
onProgress(progressEventFromDownloadProgress(loaded, total)); |
|
} |
|
}]; |
|
|
|
[task startTask]; |
|
} |
|
|
|
static NSURL *sanitizeURL(NSURL *url) |
|
{ |
|
// Why we do this is lost to time. We probably shouldn't; passing a valid URL is the caller's responsibility not ours. |
|
return [RCTConvert NSURL:url.absoluteString]; |
|
} |
|
|
|
static RCTLoadingProgress *progressEventFromData(NSData *rawData) |
|
{ |
|
NSString *text = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding]; |
|
id info = RCTJSONParse(text, nil); |
|
if (!info || ![info isKindOfClass:[NSDictionary class]]) { |
|
return nil; |
|
} |
|
|
|
RCTLoadingProgress *progress = [RCTLoadingProgress new]; |
|
progress.status = info[@"status"]; |
|
progress.done = info[@"done"]; |
|
progress.total = info[@"total"]; |
|
return progress; |
|
} |
|
|
|
static RCTLoadingProgress *progressEventFromDownloadProgress(NSNumber *total, NSNumber *done) |
|
{ |
|
RCTLoadingProgress *progress = [RCTLoadingProgress new]; |
|
progress.status = @"Downloading JavaScript bundle"; |
|
// Progress values are in bytes transform them to kilobytes for smaller numbers. |
|
progress.done = done != nil ? @([done integerValue] / 1024) : nil; |
|
progress.total = total != nil ? @([total integerValue] / 1024) : nil; |
|
return progress; |
|
} |
|
|
|
static NSDictionary *userInfoForRawResponse(NSString *rawText) |
|
{ |
|
NSDictionary *parsedResponse = RCTJSONParse(rawText, nil); |
|
if (![parsedResponse isKindOfClass:[NSDictionary class]]) { |
|
return @{NSLocalizedDescriptionKey: rawText}; |
|
} |
|
NSArray *errors = parsedResponse[@"errors"]; |
|
if (![errors isKindOfClass:[NSArray class]]) { |
|
return @{NSLocalizedDescriptionKey: rawText}; |
|
} |
|
NSMutableArray<NSDictionary *> *fakeStack = [NSMutableArray new]; |
|
for (NSDictionary *err in errors) { |
|
[fakeStack addObject: @{ |
|
@"methodName": err[@"description"] ?: @"", |
|
@"file": err[@"filename"] ?: @"", |
|
@"lineNumber": err[@"lineNumber"] ?: @0 |
|
}]; |
|
} |
|
return @{NSLocalizedDescriptionKey: parsedResponse[@"message"] ?: @"No message provided", @"stack": [fakeStack copy]}; |
|
} |
|
|
|
@end
|
|
|