1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXFileDownloader.h"
4#import "EXVersions.h"
5#import "EXKernelUtil.h"
6
7#import <React/RCTUtils.h>
8
9@import UIKit;
10
11#import <sys/utsname.h>
12
13NSString * const EXNetworkErrorDomain = @"EXNetwork";
14NSTimeInterval const EXFileDownloaderDefaultTimeoutInterval = 60;
15
16@interface EXFileDownloader () <NSURLSessionDataDelegate>
17@end
18
19@implementation EXFileDownloader
20
21- (instancetype)init
22{
23  if (self = [super init]) {
24    _timeoutInterval = EXFileDownloaderDefaultTimeoutInterval;
25  }
26  return self;
27}
28
29- (void)downloadFileFromURL:(NSURL *)url
30                     successBlock:(EXFileDownloaderSuccessBlock)successBlock
31                       errorBlock:(EXFileDownloaderErrorBlock)errorBlock
32{
33  NSURLSessionConfiguration *configuration = _urlSessionConfiguration ?: [NSURLSessionConfiguration defaultSessionConfiguration];
34
35  // also pass any custom cache policy onto this specific request
36  NSURLRequestCachePolicy cachePolicy = _urlSessionConfiguration ? _urlSessionConfiguration.requestCachePolicy : NSURLRequestUseProtocolCachePolicy;
37
38  NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
39  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:cachePolicy timeoutInterval:_timeoutInterval];
40  [self setHTTPHeaderFields:request];
41
42  __weak typeof(self) weakSelf = self;
43  NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
44    if (!error && [response isKindOfClass:[NSHTTPURLResponse class]]) {
45      NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
46      if (httpResponse.statusCode != 200) {
47        NSStringEncoding encoding = [weakSelf _encodingFromResponse:response];
48        NSString *body = [[NSString alloc] initWithData:data encoding:encoding];
49        error = [weakSelf _errorFromResponse:httpResponse body:body];
50      }
51    }
52
53    if (error) {
54      errorBlock(error, response);
55    } else {
56      successBlock(data, response);
57    }
58  }];
59  [task resume];
60  [session finishTasksAndInvalidate];
61}
62
63#pragma mark - Configuring the request
64
65- (void)setHTTPHeaderFields:(NSMutableURLRequest *)request
66{
67  [request setValue:[self _userAgentString] forHTTPHeaderField:@"User-Agent"];
68
69  NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
70  [request setValue:version forHTTPHeaderField:@"Exponent-Version"];
71  NSString *requestAbiVersion;
72  if (_abiVersion && _abiVersion.length) {
73    requestAbiVersion = _abiVersion;
74  } else {
75    NSArray *versionsAvailable = [EXVersions sharedInstance].versions[@"sdkVersions"];
76    if (versionsAvailable) {
77      requestAbiVersion = [versionsAvailable componentsJoinedByString:@","];
78    } else {
79      requestAbiVersion = [EXVersions sharedInstance].temporarySdkVersion;
80    }
81  }
82  NSString *releaseChannel;
83  if (_releaseChannel) {
84    releaseChannel = _releaseChannel;
85  } else {
86    releaseChannel = @"default";
87  }
88  [request setValue:releaseChannel forHTTPHeaderField:@"Expo-Release-Channel"];
89  [request setValue:@"true" forHTTPHeaderField:@"Expo-JSON-Error"];
90  [request setValue:requestAbiVersion forHTTPHeaderField:@"Exponent-SDK-Version"];
91  [request setValue:@"ios" forHTTPHeaderField:@"Exponent-Platform"];
92  [request setValue:@"true" forHTTPHeaderField:@"Exponent-Accept-Signature"];
93  [request setValue:@"application/expo+json,application/json" forHTTPHeaderField:@"Accept"];
94}
95
96- (NSString *)_userAgentString
97{
98  struct utsname systemInfo;
99  uname(&systemInfo);
100  NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
101  return [NSString stringWithFormat:@"Exponent/%@ (%@; %@ %@; Scale/%.2f; %@)",
102          [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
103          deviceModel,
104          [UIDevice currentDevice].systemName,
105          [UIDevice currentDevice].systemVersion,
106          [UIScreen mainScreen].scale,
107          [NSLocale autoupdatingCurrentLocale].localeIdentifier];
108}
109
110#pragma mark - NSURLSessionTaskDelegate
111
112- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler
113{
114  completionHandler(request);
115}
116
117#pragma mark - NSURLSessionDataDelegate
118
119- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
120{
121  completionHandler(proposedResponse);
122}
123
124#pragma mark - Parsing the response
125
126- (NSStringEncoding)_encodingFromResponse:(NSURLResponse *)response
127{
128  if (response.textEncodingName) {
129    CFStringRef cfEncodingName = (__bridge CFStringRef)response.textEncodingName;
130    CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding(cfEncodingName);
131    if (cfEncoding != kCFStringEncodingInvalidId) {
132      return CFStringConvertEncodingToNSStringEncoding(cfEncoding);
133    }
134  }
135  // Default to UTF-8
136  return NSUTF8StringEncoding;
137}
138
139- (NSError *)_errorFromResponse:(NSHTTPURLResponse *)response body:(NSString *)body
140{
141  NSDictionary *userInfo;
142  id errorInfo = RCTJSONParse(body, nil);
143  if ([errorInfo isKindOfClass:[NSDictionary class]]) {
144    userInfo = [self _formattedErrorInfo:(NSDictionary *)errorInfo];
145  } else {
146    userInfo = @{
147                 NSLocalizedDescriptionKey: body,
148                 };
149  }
150  return [NSError errorWithDomain:EXNetworkErrorDomain code:response.statusCode userInfo:userInfo];
151}
152
153- (NSDictionary *)_formattedErrorInfo:(NSDictionary *)errorInfo
154{
155  NSString *message = errorInfo[@"message"] ?: errorInfo[@"message"] ?: @"There was a server error";
156  NSString *errorCode = errorInfo[@"errorCode"] ?: @"UNEXPECTED_ERROR";
157  NSString *errorMetadata = errorInfo[@"metadata"] ?: @{};
158  NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: message,
159                                                                                   @"errorCode": errorCode,
160                                                                                   @"metadata": errorMetadata,
161                                                                                   }];
162
163  if ([errorInfo[@"errors"] isKindOfClass:[NSArray class]]) {
164    NSMutableArray *formattedErrorItems = [NSMutableArray array];
165    for (NSDictionary *errorItem in errorInfo[@"errors"]) {
166      if ([errorItem isKindOfClass:[NSDictionary class]]) {
167        NSMutableDictionary *formattedErrorItem = [NSMutableDictionary dictionary];
168        if (errorItem[@"description"]) {
169          formattedErrorItem[@"methodName"] = errorItem[@"description"];
170        }
171        if (errorItem[@"filename"]) {
172          formattedErrorItem[@"file"] = errorItem[@"filename"];
173        }
174        if (errorItem[@"lineNumber"]) {
175          formattedErrorItem[@"lineNumber"] = errorItem[@"lineNumber"];
176        }
177        [formattedErrorItems addObject:formattedErrorItem];
178      }
179    }
180    userInfo[@"stack"] = formattedErrorItems;
181  }
182
183  return userInfo;
184}
185
186@end
187