added iOS source code
[wl-app.git] / iOS / Pods / GoogleUtilities / GoogleUtilities / Network / GULNetworkURLSession.m
1 // Copyright 2017 Google
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 #import <Foundation/Foundation.h>
16
17 #import "Private/GULNetworkURLSession.h"
18
19 #import <GoogleUtilities/GULLogger.h>
20 #import "Private/GULMutableDictionary.h"
21 #import "Private/GULNetworkConstants.h"
22 #import "Private/GULNetworkMessageCode.h"
23
24 @implementation GULNetworkURLSession {
25   /// The handler to be called when the request completes or error has occurs.
26   GULNetworkURLSessionCompletionHandler _completionHandler;
27
28   /// Session ID generated randomly with a fixed prefix.
29   NSString *_sessionID;
30
31 #pragma clang diagnostic push
32 #pragma clang diagnostic ignored "-Wunguarded-availability"
33   /// The session configuration. NSURLSessionConfiguration' is only available on iOS 7.0 or newer.
34   NSURLSessionConfiguration *_sessionConfig;
35 #pragma pop
36
37   /// The path to the directory where all temporary files are stored before uploading.
38   NSURL *_networkDirectoryURL;
39
40   /// The downloaded data from fetching.
41   NSData *_downloadedData;
42
43   /// The path to the temporary file which stores the uploading data.
44   NSURL *_uploadingFileURL;
45
46   /// The current request.
47   NSURLRequest *_request;
48 }
49
50 #pragma mark - Init
51
52 - (instancetype)initWithNetworkLoggerDelegate:(id<GULNetworkLoggerDelegate>)networkLoggerDelegate {
53   self = [super init];
54   if (self) {
55     // Create URL to the directory where all temporary files to upload have to be stored.
56     NSArray *paths =
57         NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
58     NSString *applicationSupportDirectory = paths.firstObject;
59     NSArray *tempPathComponents = @[
60       applicationSupportDirectory, kGULNetworkApplicationSupportSubdirectory,
61       kGULNetworkTempDirectoryName
62     ];
63     _networkDirectoryURL = [NSURL fileURLWithPathComponents:tempPathComponents];
64     _sessionID = [NSString stringWithFormat:@"%@-%@", kGULNetworkBackgroundSessionConfigIDPrefix,
65                                             [[NSUUID UUID] UUIDString]];
66     _loggerDelegate = networkLoggerDelegate;
67   }
68   return self;
69 }
70
71 #pragma mark - External Methods
72
73 #pragma mark - To be called from AppDelegate
74
75 + (void)handleEventsForBackgroundURLSessionID:(NSString *)sessionID
76                             completionHandler:
77                                 (GULNetworkSystemCompletionHandler)systemCompletionHandler {
78   // The session may not be Analytics background. Ignore those that do not have the prefix.
79   if (![sessionID hasPrefix:kGULNetworkBackgroundSessionConfigIDPrefix]) {
80     return;
81   }
82   GULNetworkURLSession *fetcher = [self fetcherWithSessionIdentifier:sessionID];
83   if (fetcher != nil) {
84     [fetcher addSystemCompletionHandler:systemCompletionHandler forSession:sessionID];
85   } else {
86     GULLogError(kGULLoggerNetwork, NO,
87                 [NSString stringWithFormat:@"I-NET%06ld", (long)kGULNetworkMessageCodeNetwork003],
88                 @"Failed to retrieve background session with ID %@ after app is relaunched.",
89                 sessionID);
90   }
91 }
92
93 #pragma mark - External Methods
94
95 /// Sends an async POST request using NSURLSession for iOS >= 7.0, and returns an ID of the
96 /// connection.
97 - (NSString *)sessionIDFromAsyncPOSTRequest:(NSURLRequest *)request
98                           completionHandler:(GULNetworkURLSessionCompletionHandler)handler
99     API_AVAILABLE(ios(7.0)) {
100   // NSURLSessionUploadTask does not work with NSData in the background.
101   // To avoid this issue, write the data to a temporary file to upload it.
102   // Make a temporary file with the data subset.
103   _uploadingFileURL = [self temporaryFilePathWithSessionID:_sessionID];
104   NSError *writeError;
105   NSURLSessionUploadTask *postRequestTask;
106   NSURLSession *session;
107   BOOL didWriteFile = NO;
108
109   // Clean up the entire temp folder to avoid temp files that remain in case the previous session
110   // crashed and did not clean up.
111   [self maybeRemoveTempFilesAtURL:_networkDirectoryURL
112                      expiringTime:kGULNetworkTempFolderExpireTime];
113
114   // If there is no background network enabled, no need to write to file. This will allow default
115   // network session which runs on the foreground.
116   if (_backgroundNetworkEnabled && [self ensureTemporaryDirectoryExists]) {
117     didWriteFile = [request.HTTPBody writeToFile:_uploadingFileURL.path
118                                          options:NSDataWritingAtomic
119                                            error:&writeError];
120
121     if (writeError) {
122       [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
123                                    messageCode:kGULNetworkMessageCodeURLSession000
124                                        message:@"Failed to write request data to file"
125                                        context:writeError];
126     }
127   }
128
129   if (didWriteFile) {
130     // Exclude this file from backing up to iTunes. There are conflicting reports that excluding
131     // directory from backing up does not excluding files of that directory from backing up.
132     [self excludeFromBackupForURL:_uploadingFileURL];
133
134     _sessionConfig = [self backgroundSessionConfigWithSessionID:_sessionID];
135     [self populateSessionConfig:_sessionConfig withRequest:request];
136     session = [NSURLSession sessionWithConfiguration:_sessionConfig
137                                             delegate:self
138                                        delegateQueue:[NSOperationQueue mainQueue]];
139     postRequestTask = [session uploadTaskWithRequest:request fromFile:_uploadingFileURL];
140   } else {
141     // If we cannot write to file, just send it in the foreground.
142     _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
143     [self populateSessionConfig:_sessionConfig withRequest:request];
144     _sessionConfig.URLCache = nil;
145     session = [NSURLSession sessionWithConfiguration:_sessionConfig
146                                             delegate:self
147                                        delegateQueue:[NSOperationQueue mainQueue]];
148     postRequestTask = [session uploadTaskWithRequest:request fromData:request.HTTPBody];
149   }
150
151   if (!session || !postRequestTask) {
152     NSError *error = [[NSError alloc]
153         initWithDomain:kGULNetworkErrorDomain
154                   code:GULErrorCodeNetworkRequestCreation
155               userInfo:@{kGULNetworkErrorContext : @"Cannot create network session"}];
156     [self callCompletionHandler:handler withResponse:nil data:nil error:error];
157     return nil;
158   }
159
160   // Save the session into memory.
161   NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIDToFetcherMap];
162   [sessionIdentifierToFetcherMap setObject:self forKey:_sessionID];
163
164   _request = [request copy];
165
166   // Store completion handler because background session does not accept handler block but custom
167   // delegate.
168   _completionHandler = [handler copy];
169   [postRequestTask resume];
170
171   return _sessionID;
172 }
173
174 /// Sends an async GET request using NSURLSession for iOS >= 7.0, and returns an ID of the session.
175 - (NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request
176                          completionHandler:(GULNetworkURLSessionCompletionHandler)handler
177     API_AVAILABLE(ios(7.0)) {
178   if (_backgroundNetworkEnabled) {
179     _sessionConfig = [self backgroundSessionConfigWithSessionID:_sessionID];
180   } else {
181     _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
182   }
183
184   [self populateSessionConfig:_sessionConfig withRequest:request];
185
186   // Do not cache the GET request.
187   _sessionConfig.URLCache = nil;
188
189   NSURLSession *session = [NSURLSession sessionWithConfiguration:_sessionConfig
190                                                         delegate:self
191                                                    delegateQueue:[NSOperationQueue mainQueue]];
192   NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
193
194   if (!session || !downloadTask) {
195     NSError *error = [[NSError alloc]
196         initWithDomain:kGULNetworkErrorDomain
197                   code:GULErrorCodeNetworkRequestCreation
198               userInfo:@{kGULNetworkErrorContext : @"Cannot create network session"}];
199     [self callCompletionHandler:handler withResponse:nil data:nil error:error];
200     return nil;
201   }
202
203   // Save the session into memory.
204   NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIDToFetcherMap];
205   [sessionIdentifierToFetcherMap setObject:self forKey:_sessionID];
206
207   _request = [request copy];
208
209   _completionHandler = [handler copy];
210   [downloadTask resume];
211
212   return _sessionID;
213 }
214
215 #pragma mark - NSURLSessionTaskDelegate
216
217 /// Called by the NSURLSession once the download task is completed. The file is saved in the
218 /// provided URL so we need to read the data and store into _downloadedData. Once the session is
219 /// completed, URLSession:task:didCompleteWithError will be called and the completion handler will
220 /// be called with the downloaded data.
221 - (void)URLSession:(NSURLSession *)session
222                  downloadTask:(NSURLSessionDownloadTask *)task
223     didFinishDownloadingToURL:(NSURL *)url API_AVAILABLE(ios(7.0)) {
224   if (!url.path) {
225     [_loggerDelegate
226         GULNetwork_logWithLevel:kGULNetworkLogLevelError
227                     messageCode:kGULNetworkMessageCodeURLSession001
228                         message:@"Unable to read downloaded data from empty temp path"];
229     _downloadedData = nil;
230     return;
231   }
232
233   NSError *error;
234   _downloadedData = [NSData dataWithContentsOfFile:url.path options:0 error:&error];
235
236   if (error) {
237     [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
238                                  messageCode:kGULNetworkMessageCodeURLSession002
239                                      message:@"Cannot read the content of downloaded data"
240                                      context:error];
241     _downloadedData = nil;
242   }
243 }
244
245 #if TARGET_OS_IOS || TARGET_OS_TV
246 - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
247     API_AVAILABLE(ios(7.0)) {
248   [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
249                                messageCode:kGULNetworkMessageCodeURLSession003
250                                    message:@"Background session finished"
251                                    context:session.configuration.identifier];
252   [self callSystemCompletionHandler:session.configuration.identifier];
253 }
254 #endif
255
256 - (void)URLSession:(NSURLSession *)session
257                     task:(NSURLSessionTask *)task
258     didCompleteWithError:(NSError *)error API_AVAILABLE(ios(7.0)) {
259   // Avoid any chance of recursive behavior leading to it being used repeatedly.
260   GULNetworkURLSessionCompletionHandler handler = _completionHandler;
261   _completionHandler = nil;
262
263   if (task.response) {
264     // The following assertion should always be true for HTTP requests, see https://goo.gl/gVLxT7.
265     NSAssert([task.response isKindOfClass:[NSHTTPURLResponse class]], @"URL response must be HTTP");
266
267     // The server responded so ignore the error created by the system.
268     error = nil;
269   } else if (!error) {
270     error = [[NSError alloc]
271         initWithDomain:kGULNetworkErrorDomain
272                   code:GULErrorCodeNetworkInvalidResponse
273               userInfo:@{kGULNetworkErrorContext : @"Network Error: Empty network response"}];
274   }
275
276   [self callCompletionHandler:handler
277                  withResponse:(NSHTTPURLResponse *)task.response
278                          data:_downloadedData
279                         error:error];
280
281   // Remove the temp file to avoid trashing devices with lots of temp files.
282   [self removeTempItemAtURL:_uploadingFileURL];
283
284   // Try to clean up stale files again.
285   [self maybeRemoveTempFilesAtURL:_networkDirectoryURL
286                      expiringTime:kGULNetworkTempFolderExpireTime];
287 }
288
289 - (void)URLSession:(NSURLSession *)session
290                    task:(NSURLSessionTask *)task
291     didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
292       completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
293                                   NSURLCredential *credential))completionHandler
294     API_AVAILABLE(ios(7.0)) {
295   // The handling is modeled after GTMSessionFetcher.
296   if ([challenge.protectionSpace.authenticationMethod
297           isEqualToString:NSURLAuthenticationMethodServerTrust]) {
298     SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
299     if (serverTrust == NULL) {
300       [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
301                                    messageCode:kGULNetworkMessageCodeURLSession004
302                                        message:@"Received empty server trust for host. Host"
303                                        context:_request.URL];
304       completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
305       return;
306     }
307     NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
308     if (!credential) {
309       [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelWarning
310                                    messageCode:kGULNetworkMessageCodeURLSession005
311                                        message:@"Unable to verify server identity. Host"
312                                        context:_request.URL];
313       completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
314       return;
315     }
316
317     [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
318                                  messageCode:kGULNetworkMessageCodeURLSession006
319                                      message:@"Received SSL challenge for host. Host"
320                                      context:_request.URL];
321
322     void (^callback)(BOOL) = ^(BOOL allow) {
323       if (allow) {
324         completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
325       } else {
326         [self->_loggerDelegate
327             GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
328                         messageCode:kGULNetworkMessageCodeURLSession007
329                             message:@"Cancelling authentication challenge for host. Host"
330                             context:self->_request.URL];
331         completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
332       }
333     };
334
335     // Retain the trust object to avoid a SecTrustEvaluate() crash on iOS 7.
336     CFRetain(serverTrust);
337
338     // Evaluate the certificate chain.
339     //
340     // The delegate queue may be the main thread. Trust evaluation could cause some
341     // blocking network activity, so we must evaluate async, as documented at
342     // https://developer.apple.com/library/ios/technotes/tn2232/
343     dispatch_queue_t evaluateBackgroundQueue =
344         dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
345
346     dispatch_async(evaluateBackgroundQueue, ^{
347       SecTrustResultType trustEval = kSecTrustResultInvalid;
348       BOOL shouldAllow;
349       OSStatus trustError;
350
351       @synchronized([GULNetworkURLSession class]) {
352         trustError = SecTrustEvaluate(serverTrust, &trustEval);
353       }
354
355       if (trustError != errSecSuccess) {
356         [self->_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
357                                            messageCode:kGULNetworkMessageCodeURLSession008
358                                                message:@"Cannot evaluate server trust. Error, host"
359                                               contexts:@[ @(trustError), self->_request.URL ]];
360         shouldAllow = NO;
361       } else {
362         // Having a trust level "unspecified" by the user is the usual result, described at
363         // https://developer.apple.com/library/mac/qa/qa1360
364         shouldAllow =
365             (trustEval == kSecTrustResultUnspecified || trustEval == kSecTrustResultProceed);
366       }
367
368       // Call the call back with the permission.
369       callback(shouldAllow);
370
371       CFRelease(serverTrust);
372     });
373     return;
374   }
375
376   // Default handling for other Auth Challenges.
377   completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
378 }
379
380 #pragma mark - Internal Methods
381
382 /// Stores system completion handler with session ID as key.
383 - (void)addSystemCompletionHandler:(GULNetworkSystemCompletionHandler)handler
384                         forSession:(NSString *)identifier {
385   if (!handler) {
386     [_loggerDelegate
387         GULNetwork_logWithLevel:kGULNetworkLogLevelError
388                     messageCode:kGULNetworkMessageCodeURLSession009
389                         message:@"Cannot store nil system completion handler in network"];
390     return;
391   }
392
393   if (!identifier.length) {
394     [_loggerDelegate
395         GULNetwork_logWithLevel:kGULNetworkLogLevelError
396                     messageCode:kGULNetworkMessageCodeURLSession010
397                         message:
398                             @"Cannot store system completion handler with empty network "
399                              "session identifier"];
400     return;
401   }
402
403   GULMutableDictionary *systemCompletionHandlers =
404       [[self class] sessionIDToSystemCompletionHandlerDictionary];
405   if (systemCompletionHandlers[identifier]) {
406     [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelWarning
407                                  messageCode:kGULNetworkMessageCodeURLSession011
408                                      message:@"Got multiple system handlers for a single session ID"
409                                      context:identifier];
410   }
411
412   systemCompletionHandlers[identifier] = handler;
413 }
414
415 /// Calls the system provided completion handler with the session ID stored in the dictionary.
416 /// The handler will be removed from the dictionary after being called.
417 - (void)callSystemCompletionHandler:(NSString *)identifier {
418   GULMutableDictionary *systemCompletionHandlers =
419       [[self class] sessionIDToSystemCompletionHandlerDictionary];
420   GULNetworkSystemCompletionHandler handler = [systemCompletionHandlers objectForKey:identifier];
421
422   if (handler) {
423     [systemCompletionHandlers removeObjectForKey:identifier];
424
425     dispatch_async(dispatch_get_main_queue(), ^{
426       handler();
427     });
428   }
429 }
430
431 /// Sets or updates the session ID of this session.
432 - (void)setSessionID:(NSString *)sessionID {
433   _sessionID = [sessionID copy];
434 }
435
436 /// Creates a background session configuration with the session ID using the supported method.
437 - (NSURLSessionConfiguration *)backgroundSessionConfigWithSessionID:(NSString *)sessionID
438     API_AVAILABLE(ios(7.0)) {
439 #if (TARGET_OS_OSX && defined(MAC_OS_X_VERSION_10_10) &&         \
440      MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) || \
441     TARGET_OS_TV ||                                              \
442     (TARGET_OS_IOS && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)
443
444   // iOS 8/10.10 builds require the new backgroundSessionConfiguration method name.
445   return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID];
446
447 #elif (TARGET_OS_OSX && defined(MAC_OS_X_VERSION_10_10) &&        \
448        MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) || \
449     (TARGET_OS_IOS && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0)
450
451   // Do a runtime check to avoid a deprecation warning about using
452   // +backgroundSessionConfiguration: on iOS 8.
453   if ([NSURLSessionConfiguration
454           respondsToSelector:@selector(backgroundSessionConfigurationWithIdentifier:)]) {
455     // Running on iOS 8+/OS X 10.10+.
456 #pragma clang diagnostic push
457 #pragma clang diagnostic ignored "-Wunguarded-availability"
458     return [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionID];
459 #pragma clang diagnostic pop
460   } else {
461     // Running on iOS 7/OS X 10.9.
462     return [NSURLSessionConfiguration backgroundSessionConfiguration:sessionID];
463   }
464
465 #else
466   // Building with an SDK earlier than iOS 8/OS X 10.10.
467   return [NSURLSessionConfiguration backgroundSessionConfiguration:sessionID];
468 #endif
469 }
470
471 - (void)maybeRemoveTempFilesAtURL:(NSURL *)folderURL expiringTime:(NSTimeInterval)staleTime {
472   if (!folderURL.absoluteString.length) {
473     return;
474   }
475
476   NSFileManager *fileManager = [NSFileManager defaultManager];
477   NSError *error = nil;
478
479   NSArray *properties = @[ NSURLCreationDateKey ];
480   NSArray *directoryContent =
481       [fileManager contentsOfDirectoryAtURL:folderURL
482                  includingPropertiesForKeys:properties
483                                     options:NSDirectoryEnumerationSkipsSubdirectoryDescendants
484                                       error:&error];
485   if (error && error.code != NSFileReadNoSuchFileError) {
486     [_loggerDelegate
487         GULNetwork_logWithLevel:kGULNetworkLogLevelDebug
488                     messageCode:kGULNetworkMessageCodeURLSession012
489                         message:@"Cannot get files from the temporary network folder. Error"
490                         context:error];
491     return;
492   }
493
494   if (!directoryContent.count) {
495     return;
496   }
497
498   NSTimeInterval now = [NSDate date].timeIntervalSince1970;
499   for (NSURL *tempFile in directoryContent) {
500     NSDate *creationDate;
501     BOOL getCreationDate =
502         [tempFile getResourceValue:&creationDate forKey:NSURLCreationDateKey error:NULL];
503     if (!getCreationDate) {
504       continue;
505     }
506     NSTimeInterval creationTimeInterval = creationDate.timeIntervalSince1970;
507     if (fabs(now - creationTimeInterval) > staleTime) {
508       [self removeTempItemAtURL:tempFile];
509     }
510   }
511 }
512
513 /// Removes the temporary file written to disk for sending the request. It has to be cleaned up
514 /// after the session is done.
515 - (void)removeTempItemAtURL:(NSURL *)fileURL {
516   if (!fileURL.absoluteString.length) {
517     return;
518   }
519
520   NSFileManager *fileManager = [NSFileManager defaultManager];
521   NSError *error = nil;
522
523   if (![fileManager removeItemAtURL:fileURL error:&error] && error.code != NSFileNoSuchFileError) {
524     [_loggerDelegate
525         GULNetwork_logWithLevel:kGULNetworkLogLevelError
526                     messageCode:kGULNetworkMessageCodeURLSession013
527                         message:@"Failed to remove temporary uploading data file. Error"
528                         context:error.localizedDescription];
529   }
530 }
531
532 /// Gets the fetcher with the session ID.
533 + (instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
534   NSMapTable *sessionIdentifierToFetcherMap = [self sessionIDToFetcherMap];
535   GULNetworkURLSession *session = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
536   if (!session && [sessionIdentifier hasPrefix:kGULNetworkBackgroundSessionConfigIDPrefix]) {
537     session = [[GULNetworkURLSession alloc] initWithNetworkLoggerDelegate:nil];
538     [session setSessionID:sessionIdentifier];
539     [sessionIdentifierToFetcherMap setObject:session forKey:sessionIdentifier];
540   }
541   return session;
542 }
543
544 /// Returns a map of the fetcher by session ID. Creates a map if it is not created.
545 + (NSMapTable *)sessionIDToFetcherMap {
546   static NSMapTable *sessionIDToFetcherMap;
547
548   static dispatch_once_t sessionMapOnceToken;
549   dispatch_once(&sessionMapOnceToken, ^{
550     sessionIDToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
551   });
552   return sessionIDToFetcherMap;
553 }
554
555 /// Returns a map of system provided completion handler by session ID. Creates a map if it is not
556 /// created.
557 + (GULMutableDictionary *)sessionIDToSystemCompletionHandlerDictionary {
558   static GULMutableDictionary *systemCompletionHandlers;
559
560   static dispatch_once_t systemCompletionHandlerOnceToken;
561   dispatch_once(&systemCompletionHandlerOnceToken, ^{
562     systemCompletionHandlers = [[GULMutableDictionary alloc] init];
563   });
564   return systemCompletionHandlers;
565 }
566
567 - (NSURL *)temporaryFilePathWithSessionID:(NSString *)sessionID {
568   NSString *tempName = [NSString stringWithFormat:@"GULUpload_temp_%@", sessionID];
569   return [_networkDirectoryURL URLByAppendingPathComponent:tempName];
570 }
571
572 /// Makes sure that the directory to store temp files exists. If not, tries to create it and returns
573 /// YES. If there is anything wrong, returns NO.
574 - (BOOL)ensureTemporaryDirectoryExists {
575   NSFileManager *fileManager = [NSFileManager defaultManager];
576   NSError *error = nil;
577
578   // Create a temporary directory if it does not exist or was deleted.
579   if ([_networkDirectoryURL checkResourceIsReachableAndReturnError:&error]) {
580     return YES;
581   }
582
583   if (error && error.code != NSFileReadNoSuchFileError) {
584     [_loggerDelegate
585         GULNetwork_logWithLevel:kGULNetworkLogLevelWarning
586                     messageCode:kGULNetworkMessageCodeURLSession014
587                         message:@"Error while trying to access Network temp folder. Error"
588                         context:error];
589   }
590
591   NSError *writeError = nil;
592
593   [fileManager createDirectoryAtURL:_networkDirectoryURL
594         withIntermediateDirectories:YES
595                          attributes:nil
596                               error:&writeError];
597   if (writeError) {
598     [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
599                                  messageCode:kGULNetworkMessageCodeURLSession015
600                                      message:@"Cannot create temporary directory. Error"
601                                      context:writeError];
602     return NO;
603   }
604
605   // Set the iCloud exclusion attribute on the Documents URL.
606   [self excludeFromBackupForURL:_networkDirectoryURL];
607
608   return YES;
609 }
610
611 - (void)excludeFromBackupForURL:(NSURL *)url {
612   if (!url.path) {
613     return;
614   }
615
616   // Set the iCloud exclusion attribute on the Documents URL.
617   NSError *preventBackupError = nil;
618   [url setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&preventBackupError];
619   if (preventBackupError) {
620     [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
621                                  messageCode:kGULNetworkMessageCodeURLSession016
622                                      message:@"Cannot exclude temporary folder from iTunes backup"];
623   }
624 }
625
626 - (void)URLSession:(NSURLSession *)session
627                           task:(NSURLSessionTask *)task
628     willPerformHTTPRedirection:(NSHTTPURLResponse *)response
629                     newRequest:(NSURLRequest *)request
630              completionHandler:(void (^)(NSURLRequest *))completionHandler API_AVAILABLE(ios(7.0)) {
631   NSArray *nonAllowedRedirectionCodes = @[
632     @(kGULNetworkHTTPStatusCodeFound), @(kGULNetworkHTTPStatusCodeMovedPermanently),
633     @(kGULNetworkHTTPStatusCodeMovedTemporarily), @(kGULNetworkHTTPStatusCodeMultipleChoices)
634   ];
635
636   // Allow those not in the non allowed list to be followed.
637   if (![nonAllowedRedirectionCodes containsObject:@(response.statusCode)]) {
638     completionHandler(request);
639     return;
640   }
641
642   // Do not allow redirection if the response code is in the non-allowed list.
643   NSURLRequest *newRequest = request;
644
645   if (response) {
646     newRequest = nil;
647   }
648
649   completionHandler(newRequest);
650 }
651
652 #pragma mark - Helper Methods
653
654 - (void)callCompletionHandler:(GULNetworkURLSessionCompletionHandler)handler
655                  withResponse:(NSHTTPURLResponse *)response
656                          data:(NSData *)data
657                         error:(NSError *)error {
658   if (error) {
659     [_loggerDelegate GULNetwork_logWithLevel:kGULNetworkLogLevelError
660                                  messageCode:kGULNetworkMessageCodeURLSession017
661                                      message:@"Encounter network error. Code, error"
662                                     contexts:@[ @(error.code), error ]];
663   }
664
665   if (handler) {
666     dispatch_async(dispatch_get_main_queue(), ^{
667       handler(response, data, self->_sessionID, error);
668     });
669   }
670 }
671
672 - (void)populateSessionConfig:(NSURLSessionConfiguration *)sessionConfig
673                   withRequest:(NSURLRequest *)request API_AVAILABLE(ios(7.0)) {
674   sessionConfig.HTTPAdditionalHeaders = request.allHTTPHeaderFields;
675   sessionConfig.timeoutIntervalForRequest = request.timeoutInterval;
676   sessionConfig.timeoutIntervalForResource = request.timeoutInterval;
677   sessionConfig.requestCachePolicy = request.cachePolicy;
678 }
679
680 @end