Proyectos de Subversion Iphone Microlearning

Rev

| Ultima modificación | Ver Log |

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