summaryrefslogtreecommitdiff
path: root/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source
diff options
context:
space:
mode:
Diffstat (limited to 'StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source')
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.h52
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.m185
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.h148
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.m631
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.h49
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.m190
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.h1332
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m4670
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.h112
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m982
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.h196
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.m1381
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.h175
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.m1989
14 files changed, 12092 insertions, 0 deletions
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.h b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.h
new file mode 100644
index 00000000..ec3c0125
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.h
@@ -0,0 +1,52 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// The GTMGatherInput stream is an input stream implementation that is to be
+// instantiated with an NSArray of NSData objects. It works in the traditional
+// scatter/gather vector I/O model. Rather than allocating a big NSData object
+// to hold all of the data and performing a copy into that object, the
+// GTMGatherInputStream will maintain a reference to the NSArray and read from
+// each NSData in turn as the read method is called. You should not alter the
+// underlying set of NSData objects until all read operations on this input
+// stream have completed.
+
+#import <Foundation/Foundation.h>
+
+#ifndef GTM_NONNULL
+ #if defined(__has_attribute)
+ #if __has_attribute(nonnull)
+ #define GTM_NONNULL(x) __attribute__((nonnull x))
+ #else
+ #define GTM_NONNULL(x)
+ #endif
+ #else
+ #define GTM_NONNULL(x)
+ #endif
+#endif
+
+// Avoid multiple declaration of this class.
+//
+// Note: This should match the declaration of GTMGatherInputStream in GTMMIMEDocument.m
+
+#ifndef GTM_GATHERINPUTSTREAM_DECLARED
+#define GTM_GATHERINPUTSTREAM_DECLARED
+
+@interface GTMGatherInputStream : NSInputStream <NSStreamDelegate>
+
++ (NSInputStream *)streamWithArray:(NSArray *)dataArray GTM_NONNULL((1));
+
+@end
+
+#endif // GTM_GATHERINPUTSTREAM_DECLARED
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.m b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.m
new file mode 100644
index 00000000..0f65310f
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.m
@@ -0,0 +1,185 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "GTMGatherInputStream.h"
+
+@implementation GTMGatherInputStream {
+ NSArray *_dataArray; // NSDatas that should be "gathered" and streamed.
+ NSUInteger _arrayIndex; // Index in the array of the current NSData.
+ long long _dataOffset; // Offset in the current NSData we are processing.
+ NSStreamStatus _streamStatus;
+ id<NSStreamDelegate> __weak _delegate; // Stream delegate, defaults to self.
+}
+
++ (NSInputStream *)streamWithArray:(NSArray *)dataArray {
+ return [(GTMGatherInputStream *)[self alloc] initWithArray:dataArray];
+}
+
+- (instancetype)initWithArray:(NSArray *)dataArray {
+ self = [super init];
+ if (self) {
+ _dataArray = dataArray;
+ _delegate = self; // An NSStream's default delegate should be self.
+ }
+ return self;
+}
+
+#pragma mark - NSStream
+
+- (void)open {
+ _arrayIndex = 0;
+ _dataOffset = 0;
+ _streamStatus = NSStreamStatusOpen;
+}
+
+- (void)close {
+ _streamStatus = NSStreamStatusClosed;
+}
+
+- (id<NSStreamDelegate>)delegate {
+ return _delegate;
+}
+
+- (void)setDelegate:(id<NSStreamDelegate>)delegate {
+ if (delegate == nil) {
+ _delegate = self;
+ } else {
+ _delegate = delegate;
+ }
+}
+
+- (id)propertyForKey:(NSString *)key {
+ if ([key isEqual:NSStreamFileCurrentOffsetKey]) {
+ return @([self absoluteOffset]);
+ }
+ return nil;
+}
+
+- (BOOL)setProperty:(id)property forKey:(NSString *)key {
+ if ([key isEqual:NSStreamFileCurrentOffsetKey]) {
+ NSNumber *absoluteOffsetNumber = property;
+ [self setAbsoluteOffset:absoluteOffsetNumber.longLongValue];
+ return YES;
+ }
+ return NO;
+}
+
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode {
+}
+
+- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode {
+}
+
+- (NSStreamStatus)streamStatus {
+ return _streamStatus;
+}
+
+- (NSError *)streamError {
+ return nil;
+}
+
+#pragma mark - NSInputStream
+
+- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
+ NSInteger bytesRead = 0;
+ NSUInteger bytesRemaining = len;
+
+ // Read bytes from the currently-indexed array.
+ while ((bytesRemaining > 0) && (_arrayIndex < _dataArray.count)) {
+ NSData *data = [_dataArray objectAtIndex:_arrayIndex];
+
+ NSUInteger dataLen = data.length;
+ NSUInteger dataBytesLeft = dataLen - (NSUInteger)_dataOffset;
+
+ NSUInteger bytesToCopy = MIN(bytesRemaining, dataBytesLeft);
+ NSRange range = NSMakeRange((NSUInteger) _dataOffset, bytesToCopy);
+
+ [data getBytes:(buffer + bytesRead) range:range];
+
+ bytesRead += bytesToCopy;
+ _dataOffset += bytesToCopy;
+ bytesRemaining -= bytesToCopy;
+
+ if (_dataOffset == (long long)dataLen) {
+ _dataOffset = 0;
+ _arrayIndex++;
+ }
+ }
+ if (_arrayIndex >= _dataArray.count) {
+ _streamStatus = NSStreamStatusAtEnd;
+ }
+ return bytesRead;
+}
+
+- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len {
+ return NO; // We don't support this style of reading.
+}
+
+- (BOOL)hasBytesAvailable {
+ // If we return no, the read never finishes, even if we've already delivered all the bytes.
+ return YES;
+}
+
+#pragma mark - NSStreamDelegate
+
+- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {
+ id<NSStreamDelegate> delegate = _delegate;
+ if (delegate != self) {
+ [delegate stream:self handleEvent:streamEvent];
+ }
+}
+
+#pragma mark - Private
+
+- (long long)absoluteOffset {
+ long long absoluteOffset = 0;
+ NSUInteger index = 0;
+ for (NSData *data in _dataArray) {
+ if (index >= _arrayIndex) {
+ break;
+ }
+ absoluteOffset += data.length;
+ ++index;
+ }
+ absoluteOffset += _dataOffset;
+ return absoluteOffset;
+}
+
+- (void)setAbsoluteOffset:(long long)absoluteOffset {
+ if (absoluteOffset < 0) {
+ absoluteOffset = 0;
+ }
+ _arrayIndex = 0;
+ _dataOffset = absoluteOffset;
+ for (NSData *data in _dataArray) {
+ long long dataLen = (long long) data.length;
+ if (dataLen > _dataOffset) {
+ break;
+ }
+ _arrayIndex++;
+ _dataOffset -= dataLen;
+ }
+ if (_arrayIndex == _dataArray.count) {
+ if (_dataOffset > 0) {
+ _dataOffset = 0;
+ }
+ }
+}
+
+@end
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.h b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.h
new file mode 100644
index 00000000..451e1323
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.h
@@ -0,0 +1,148 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// This is a simple class to create or parse a MIME document.
+
+// To create a MIME document, allocate a new GTMMIMEDocument and start adding parts.
+// When you are done adding parts, call generateInputStream or generateDispatchData.
+//
+// A good reference for MIME is http://en.wikipedia.org/wiki/MIME
+
+#import <Foundation/Foundation.h>
+
+#ifndef GTM_NONNULL
+ #if defined(__has_attribute)
+ #if __has_attribute(nonnull)
+ #define GTM_NONNULL(x) __attribute__((nonnull x))
+ #else
+ #define GTM_NONNULL(x)
+ #endif
+ #else
+ #define GTM_NONNULL(x)
+ #endif
+#endif
+
+#ifndef GTM_DECLARE_GENERICS
+ #if __has_feature(objc_generics)
+ #define GTM_DECLARE_GENERICS 1
+ #else
+ #define GTM_DECLARE_GENERICS 0
+ #endif
+#endif
+
+#ifndef GTM_NSArrayOf
+ #if GTM_DECLARE_GENERICS
+ #define GTM_NSArrayOf(value) NSArray<value>
+ #define GTM_NSDictionaryOf(key, value) NSDictionary<key, value>
+ #else
+ #define GTM_NSArrayOf(value) NSArray
+ #define GTM_NSDictionaryOf(key, value) NSDictionary
+ #endif // GTM_DECLARE_GENERICS
+#endif // GTM_NSArrayOf
+
+
+// GTMMIMEDocumentPart represents a part of a MIME document.
+//
+// +[GTMMIMEDocument MIMEPartsWithBoundary:data:] returns an array of these.
+@interface GTMMIMEDocumentPart : NSObject
+
+@property(nonatomic, readonly) GTM_NSDictionaryOf(NSString *, NSString *) *headers;
+@property(nonatomic, readonly) NSData *headerData;
+@property(nonatomic, readonly) NSData *body;
+@property(nonatomic, readonly) NSUInteger length;
+
++ (instancetype)partWithHeaders:(NSDictionary *)headers body:(NSData *)body;
+
+@end
+
+@interface GTMMIMEDocument : NSObject
+
+// Get or set the unique boundary for the parts that have been added.
+//
+// When creating a MIME document from parts, this is typically calculated
+// automatically after all parts have been added.
+@property(nonatomic, copy) NSString *boundary;
+
+#pragma mark - Methods for Creating a MIME Document
+
++ (instancetype)MIMEDocument;
+
+// Adds a new part to this mime document with the given headers and body.
+// The headers keys and values should be NSStrings.
+// Adding a part may cause the boundary string to change.
+- (void)addPartWithHeaders:(GTM_NSDictionaryOf(NSString *, NSString *) *)headers
+ body:(NSData *)body GTM_NONNULL((1,2));
+
+// An inputstream that can be used to efficiently read the contents of the MIME document.
+//
+// Any parameter may be null if the result is not wanted.
+- (void)generateInputStream:(NSInputStream **)outStream
+ length:(unsigned long long *)outLength
+ boundary:(NSString **)outBoundary;
+
+// A dispatch_data_t with the contents of the MIME document.
+//
+// Note: dispatch_data_t is one-way toll-free bridged so the result
+// may be cast directly to NSData *.
+//
+// Any parameter may be null if the result is not wanted.
+- (void)generateDispatchData:(dispatch_data_t *)outDispatchData
+ length:(unsigned long long *)outLength
+ boundary:(NSString **)outBoundary;
+
+// Utility method for making a header section, including trailing newlines.
++ (NSData *)dataWithHeaders:(GTM_NSDictionaryOf(NSString *, NSString *) *)headers;
+
+#pragma mark - Methods for Parsing a MIME Document
+
+// Method for parsing out an array of MIME parts from a MIME document.
+//
+// Returns an array of GTMMIMEDocumentParts. Returns nil if no part can
+// be found.
++ (GTM_NSArrayOf(GTMMIMEDocumentPart *) *)MIMEPartsWithBoundary:(NSString *)boundary
+ data:(NSData *)fullDocumentData;
+
+// Utility method for efficiently searching possibly discontiguous NSData
+// for occurrences of target byte. This method does not "flatten" an NSData
+// that is composed of discontiguous blocks.
+//
+// The byte offsets of non-overlapping occurrences of the target are returned as
+// NSNumbers in the array.
++ (void)searchData:(NSData *)data
+ targetBytes:(const void *)targetBytes
+ targetLength:(NSUInteger)targetLength
+ foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets;
+
+// Utility method to parse header bytes into an NSDictionary.
++ (GTM_NSDictionaryOf(NSString *, NSString *) *)headersWithData:(NSData *)data;
+
+// ------ UNIT TESTING ONLY BELOW ------
+
+// Internal methods, exposed for unit testing only.
+- (void)seedRandomWith:(u_int32_t)seed;
+
++ (NSUInteger)findBytesWithNeedle:(const unsigned char *)needle
+ needleLength:(NSUInteger)needleLength
+ haystack:(const unsigned char *)haystack
+ haystackLength:(NSUInteger)haystackLength
+ foundOffset:(NSUInteger *)foundOffset;
+
++ (void)searchData:(NSData *)data
+ targetBytes:(const void *)targetBytes
+ targetLength:(NSUInteger)targetLength
+ foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets
+ foundBlockNumbers:(GTM_NSArrayOf(NSNumber *) **)outFoundBlockNumbers;
+
+@end
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.m b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.m
new file mode 100644
index 00000000..f4460c5d
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.m
@@ -0,0 +1,631 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "GTMMIMEDocument.h"
+
+// Avoid a hard dependency on GTMGatherInputStream.
+#ifndef GTM_GATHERINPUTSTREAM_DECLARED
+#define GTM_GATHERINPUTSTREAM_DECLARED
+
+@interface GTMGatherInputStream : NSInputStream <NSStreamDelegate>
+
++ (NSInputStream *)streamWithArray:(NSArray *)dataArray GTM_NONNULL((1));
+
+@end
+#endif // GTM_GATHERINPUTSTREAM_DECLARED
+
+// FindBytes
+//
+// Helper routine to search for the existence of a set of bytes (needle) within
+// a presumed larger set of bytes (haystack). Can find the first part of the
+// needle at the very end of the haystack.
+//
+// Returns the needle length on complete success, the number of bytes matched
+// if a partial needle was found at the end of the haystack, and 0 on failure.
+static NSUInteger FindBytes(const unsigned char *needle, NSUInteger needleLen,
+ const unsigned char *haystack, NSUInteger haystackLen,
+ NSUInteger *foundOffset);
+
+// SearchDataForBytes
+//
+// This implements the functionality of the +searchData: methods below. See the documentation
+// for those methods.
+static void SearchDataForBytes(NSData *data, const void *targetBytes, NSUInteger targetLength,
+ NSMutableArray *foundOffsets, NSMutableArray *foundBlockNumbers);
+
+@implementation GTMMIMEDocumentPart {
+ NSDictionary *_headers;
+ NSData *_headerData; // Header content including the ending "\r\n".
+ NSData *_bodyData;
+}
+
+@synthesize headers = _headers,
+ headerData = _headerData,
+ body = _bodyData;
+
+@dynamic length;
+
++ (instancetype)partWithHeaders:(NSDictionary *)headers body:(NSData *)body {
+ return [[self alloc] initWithHeaders:headers body:body];
+}
+
+- (instancetype)initWithHeaders:(NSDictionary *)headers body:(NSData *)body {
+ self = [super init];
+ if (self) {
+ _bodyData = body;
+ _headers = headers;
+ }
+ return self;
+}
+
+// Returns true if the part's header or data contain the given set of bytes.
+//
+// NOTE: We assume that the 'bytes' we are checking for do not contain "\r\n",
+// so we don't need to check the concatenation of the header and body bytes.
+- (BOOL)containsBytes:(const unsigned char *)bytes length:(NSUInteger)length {
+ // This uses custom search code rather than strcpy because the encoded data may contain
+ // null values.
+ NSData *headerData = self.headerData;
+ return (FindBytes(bytes, length, headerData.bytes, headerData.length, NULL) == length ||
+ FindBytes(bytes, length, _bodyData.bytes, _bodyData.length, NULL) == length);
+}
+
+- (NSData *)headerData {
+ if (!_headerData) {
+ _headerData = [GTMMIMEDocument dataWithHeaders:_headers];
+ }
+ return _headerData;
+}
+
+- (NSData *)body {
+ return _bodyData;
+}
+
+- (NSUInteger)length {
+ return _headerData.length + _bodyData.length;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %p (headers %lu keys, body %lu bytes)",
+ [self class], self, (unsigned long)_headers.count,
+ (unsigned long)_bodyData.length];
+}
+
+- (BOOL)isEqual:(GTMMIMEDocumentPart *)other {
+ if (self == other) return YES;
+ if (![other isKindOfClass:[GTMMIMEDocumentPart class]]) return NO;
+ return ((_bodyData == other->_bodyData || [_bodyData isEqual:other->_bodyData])
+ && (_headers == other->_headers || [_headers isEqual:other->_headers]));
+}
+
+- (NSUInteger)hash {
+ return _bodyData.hash | _headers.hash;
+}
+
+@end
+
+@implementation GTMMIMEDocument {
+ NSMutableArray *_parts; // Ordered array of GTMMIMEDocumentParts.
+ unsigned long long _length; // Length in bytes of the document.
+ NSString *_boundary;
+ u_int32_t _randomSeed; // For testing.
+}
+
++ (instancetype)MIMEDocument {
+ return [[self alloc] init];
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _parts = [[NSMutableArray alloc] init];
+ }
+ return self;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %p (%lu parts)",
+ [self class], self, (unsigned long)_parts.count];
+}
+
+#pragma mark - Joining Parts
+
+// Adds a new part to this mime document with the given headers and body.
+- (void)addPartWithHeaders:(NSDictionary *)headers body:(NSData *)body {
+ GTMMIMEDocumentPart *part = [GTMMIMEDocumentPart partWithHeaders:headers body:body];
+ [_parts addObject:part];
+ _boundary = nil;
+}
+
+// For unit testing only, seeds the random number generator so that we will
+// have reproducible boundary strings.
+- (void)seedRandomWith:(u_int32_t)seed {
+ _randomSeed = seed;
+ _boundary = nil;
+}
+
+- (u_int32_t)random {
+ if (_randomSeed) {
+ // For testing only.
+ return _randomSeed++;
+ } else {
+ return arc4random();
+ }
+}
+
+// Computes the mime boundary to use. This should only be called
+// after all the desired document parts have been added since it must compute
+// a boundary that does not exist in the document data.
+- (NSString *)boundary {
+ if (_boundary) {
+ return _boundary;
+ }
+
+ // Use an easily-readable boundary string.
+ NSString *const kBaseBoundary = @"END_OF_PART";
+
+ _boundary = kBaseBoundary;
+
+ // If the boundary isn't unique, append random numbers, up to 10 attempts;
+ // if that's still not unique, use a random number sequence instead, and call it good.
+ BOOL didCollide = NO;
+
+ const int maxTries = 10; // Arbitrarily chosen maximum attempts.
+ for (int tries = 0; tries < maxTries; ++tries) {
+
+ NSData *data = [_boundary dataUsingEncoding:NSUTF8StringEncoding];
+ const void *dataBytes = data.bytes;
+ NSUInteger dataLen = data.length;
+
+ for (GTMMIMEDocumentPart *part in _parts) {
+ didCollide = [part containsBytes:dataBytes length:dataLen];
+ if (didCollide) break;
+ }
+
+ if (!didCollide) break; // We're fine, no more attempts needed.
+
+ // Try again with a random number appended.
+ _boundary = [NSString stringWithFormat:@"%@_%08x", kBaseBoundary, [self random]];
+ }
+
+ if (didCollide) {
+ // Fallback... two random numbers.
+ _boundary = [NSString stringWithFormat:@"%08x_tedborg_%08x", [self random], [self random]];
+ }
+ return _boundary;
+}
+
+- (void)setBoundary:(NSString *)str {
+ _boundary = [str copy];
+}
+
+// Internal method.
+- (void)generateDataArray:(NSMutableArray *)dataArray
+ length:(unsigned long long *)outLength
+ boundary:(NSString **)outBoundary {
+
+ // The input stream is of the form:
+ // --boundary
+ // [part_1_headers]
+ // [part_1_data]
+ // --boundary
+ // [part_2_headers]
+ // [part_2_data]
+ // --boundary--
+
+ // First we set up our boundary NSData objects.
+ NSString *boundary = self.boundary;
+
+ NSString *mainBoundary = [NSString stringWithFormat:@"\r\n--%@\r\n", boundary];
+ NSString *endBoundary = [NSString stringWithFormat:@"\r\n--%@--\r\n", boundary];
+
+ NSData *mainBoundaryData = [mainBoundary dataUsingEncoding:NSUTF8StringEncoding];
+ NSData *endBoundaryData = [endBoundary dataUsingEncoding:NSUTF8StringEncoding];
+
+ // Now we add them all in proper order to our dataArray.
+ unsigned long long length = 0;
+
+ for (GTMMIMEDocumentPart *part in _parts) {
+ [dataArray addObject:mainBoundaryData];
+ [dataArray addObject:part.headerData];
+ [dataArray addObject:part.body];
+
+ length += part.length + mainBoundaryData.length;
+ }
+
+ [dataArray addObject:endBoundaryData];
+ length += endBoundaryData.length;
+
+ if (outLength) *outLength = length;
+ if (outBoundary) *outBoundary = boundary;
+}
+
+- (void)generateInputStream:(NSInputStream **)outStream
+ length:(unsigned long long *)outLength
+ boundary:(NSString **)outBoundary {
+ NSMutableArray *dataArray = outStream ? [NSMutableArray array] : nil;
+ [self generateDataArray:dataArray
+ length:outLength
+ boundary:outBoundary];
+
+ if (outStream) {
+ Class streamClass = NSClassFromString(@"GTMGatherInputStream");
+ NSAssert(streamClass != nil, @"GTMGatherInputStream not available.");
+
+ *outStream = [streamClass streamWithArray:dataArray];
+ }
+}
+
+- (void)generateDispatchData:(dispatch_data_t *)outDispatchData
+ length:(unsigned long long *)outLength
+ boundary:(NSString **)outBoundary {
+ NSMutableArray *dataArray = outDispatchData ? [NSMutableArray array] : nil;
+ [self generateDataArray:dataArray
+ length:outLength
+ boundary:outBoundary];
+
+ if (outDispatchData) {
+ // Create an empty data accumulator.
+ dispatch_data_t dataAccumulator;
+
+ dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
+
+ for (NSData *partData in dataArray) {
+ __block NSData *immutablePartData = [partData copy];
+ dispatch_data_t newDataPart =
+ dispatch_data_create(immutablePartData.bytes, immutablePartData.length, bgQueue, ^{
+ // We want the data retained until this block executes.
+ immutablePartData = nil;
+ });
+
+ if (dataAccumulator == nil) {
+ // First part.
+ dataAccumulator = newDataPart;
+ } else {
+ // Append the additional part.
+ dataAccumulator = dispatch_data_create_concat(dataAccumulator, newDataPart);
+ }
+ }
+ *outDispatchData = dataAccumulator;
+ }
+}
+
++ (NSData *)dataWithHeaders:(NSDictionary *)headers {
+ // Generate the header data by coalescing the dictionary as lines of "key: value\r\n".
+ NSMutableString* headerString = [NSMutableString string];
+
+ // Sort the header keys so we have a deterministic order for unit testing.
+ SEL sortSel = @selector(caseInsensitiveCompare:);
+ NSArray *sortedKeys = [headers.allKeys sortedArrayUsingSelector:sortSel];
+
+ for (NSString *key in sortedKeys) {
+ NSString *value = [headers objectForKey:key];
+
+#if DEBUG
+ // Look for troublesome characters in the header keys & values.
+ NSCharacterSet *badKeyChars = [NSCharacterSet characterSetWithCharactersInString:@":\r\n"];
+ NSCharacterSet *badValueChars = [NSCharacterSet characterSetWithCharactersInString:@"\r\n"];
+
+ NSRange badRange = [key rangeOfCharacterFromSet:badKeyChars];
+ NSAssert(badRange.location == NSNotFound, @"invalid key: %@", key);
+
+ badRange = [value rangeOfCharacterFromSet:badValueChars];
+ NSAssert(badRange.location == NSNotFound, @"invalid value: %@", value);
+#endif
+
+ [headerString appendFormat:@"%@: %@\r\n", key, value];
+ }
+ // Headers end with an extra blank line.
+ [headerString appendString:@"\r\n"];
+
+ NSData *result = [headerString dataUsingEncoding:NSUTF8StringEncoding];
+ return result;
+}
+
+#pragma mark - Separating Parts
+
++ (NSArray *)MIMEPartsWithBoundary:(NSString *)boundary
+ data:(NSData *)fullDocumentData {
+ // In MIME documents, the boundary is preceded by CRLF and two dashes, and followed
+ // at the end by two dashes.
+ NSData *boundaryData = [boundary dataUsingEncoding:NSUTF8StringEncoding];
+ NSUInteger boundaryLength = boundaryData.length;
+
+ NSMutableArray *foundBoundaryOffsets;
+ [self searchData:fullDocumentData
+ targetBytes:boundaryData.bytes
+ targetLength:boundaryLength
+ foundOffsets:&foundBoundaryOffsets];
+
+ // According to rfc1341, ignore anything before the first boundary, or after the last, though two
+ // dashes are expected to follow the last boundary.
+ if (foundBoundaryOffsets.count < 2) {
+ return nil;
+ }
+
+ // Wrap the full document data with a dispatch_data_t for more efficient slicing
+ // and dicing.
+ dispatch_data_t dataWrapper;
+ if ([fullDocumentData conformsToProtocol:@protocol(OS_dispatch_data)]) {
+ dataWrapper = (dispatch_data_t)fullDocumentData;
+ } else {
+ // A no-op self invocation on fullDocumentData will keep it retained until the block is invoked.
+ dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
+ dataWrapper = dispatch_data_create(fullDocumentData.bytes,
+ fullDocumentData.length,
+ bgQueue, ^{ [fullDocumentData self]; });
+ }
+ NSMutableArray *parts;
+ NSInteger previousBoundaryOffset = -1;
+ NSInteger partCounter = -1;
+ NSInteger numberOfPartsWithHeaders = 0;
+ for (NSNumber *currentBoundaryOffset in foundBoundaryOffsets) {
+ ++partCounter;
+ if (previousBoundaryOffset == -1) {
+ // This is the first boundary.
+ previousBoundaryOffset = currentBoundaryOffset.integerValue;
+ continue;
+ } else {
+ // Create a part data subrange between the previous boundary and this one.
+ //
+ // The last four bytes before a boundary are CRLF--.
+ // The first two bytes following a boundary are either CRLF or, for the last boundary, --.
+ NSInteger previousPartDataStartOffset =
+ previousBoundaryOffset + (NSInteger)boundaryLength + 2;
+ NSInteger previousPartDataEndOffset = currentBoundaryOffset.integerValue - 4;
+ NSInteger previousPartDataLength = previousPartDataEndOffset - previousPartDataStartOffset;
+
+ if (previousPartDataLength < 2) {
+ // The preceding part was too short to be useful.
+#if DEBUG
+ NSLog(@"MIME part %ld has %ld bytes", (long)partCounter - 1,
+ (long)previousPartDataLength);
+#endif
+ } else {
+ if (!parts) parts = [NSMutableArray array];
+
+ dispatch_data_t partData =
+ dispatch_data_create_subrange(dataWrapper,
+ (size_t)previousPartDataStartOffset, (size_t)previousPartDataLength);
+ // Scan the part data for the separator between headers and body. After the CRLF,
+ // either the headers start immediately, or there's another CRLF and there are no headers.
+ //
+ // We need to map the part data to get the first two bytes. (Or we could cast it to
+ // NSData and get the bytes pointer of that.) If we're concerned that a single part
+ // data may be expensive to map, we could make a subrange here for just the first two bytes,
+ // and map that two-byte subrange.
+ const void *partDataBuffer;
+ size_t partDataBufferSize;
+ dispatch_data_t mappedPartData NS_VALID_UNTIL_END_OF_SCOPE =
+ dispatch_data_create_map(partData, &partDataBuffer, &partDataBufferSize);
+ dispatch_data_t bodyData;
+ NSDictionary *headers;
+ BOOL hasAnotherCRLF = (((char *)partDataBuffer)[0] == '\r'
+ && ((char *)partDataBuffer)[1] == '\n');
+ mappedPartData = nil;
+
+ if (hasAnotherCRLF) {
+ // There are no headers; skip the CRLF to get to the body, and leave headers nil.
+ bodyData = dispatch_data_create_subrange(partData, 2, (size_t)previousPartDataLength - 2);
+ } else {
+ // There are part headers. They are separated from body data by CRLFCRLF.
+ NSArray *crlfOffsets;
+ [self searchData:(NSData *)partData
+ targetBytes:"\r\n\r\n"
+ targetLength:4
+ foundOffsets:&crlfOffsets];
+ if (crlfOffsets.count == 0) {
+#if DEBUG
+ // We could not distinguish body and headers.
+ NSLog(@"MIME part %ld lacks a header separator: %@", (long)partCounter - 1,
+ [[NSString alloc] initWithData:(NSData *)partData encoding:NSUTF8StringEncoding]);
+#endif
+ } else {
+ NSInteger headerSeparatorOffset = ((NSNumber *)crlfOffsets.firstObject).integerValue;
+ dispatch_data_t headerData =
+ dispatch_data_create_subrange(partData, 0, (size_t)headerSeparatorOffset);
+ headers = [self headersWithData:(NSData *)headerData];
+
+ bodyData = dispatch_data_create_subrange(partData, (size_t)headerSeparatorOffset + 4,
+ (size_t)(previousPartDataLength - (headerSeparatorOffset + 4)));
+
+ numberOfPartsWithHeaders++;
+ } // crlfOffsets.count == 0
+ } // hasAnotherCRLF
+ GTMMIMEDocumentPart *part = [GTMMIMEDocumentPart partWithHeaders:headers
+ body:(NSData *)bodyData];
+ [parts addObject:part];
+ } // previousPartDataLength < 2
+ previousBoundaryOffset = currentBoundaryOffset.integerValue;
+ }
+ }
+#if DEBUG
+ // In debug builds, warn if a reasonably long document lacks any CRLF characters.
+ if (numberOfPartsWithHeaders == 0) {
+ NSUInteger length = fullDocumentData.length;
+ if (length > 20) { // Reasonably long.
+ NSMutableArray *foundCRLFs;
+ [self searchData:fullDocumentData
+ targetBytes:"\r\n"
+ targetLength:2
+ foundOffsets:&foundCRLFs];
+ if (foundCRLFs.count == 0) {
+ // Parts were logged above (due to lacking header separators.)
+ NSLog(@"Warning: MIME document lacks any headers (may have wrong line endings)");
+ }
+ }
+ }
+#endif // DEBUG
+ return parts;
+}
+
+// Efficiently search the supplied data for the target bytes.
+//
+// This uses enumerateByteRangesUsingBlock: to scan for bytes. It can find
+// the target even if it spans multiple separate byte ranges.
+//
+// Returns an array of found byte offset values, as NSNumbers.
++ (void)searchData:(NSData *)data
+ targetBytes:(const void *)targetBytes
+ targetLength:(NSUInteger)targetLength
+ foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets {
+ NSMutableArray *foundOffsets = [NSMutableArray array];
+ SearchDataForBytes(data, targetBytes, targetLength, foundOffsets, NULL);
+ *outFoundOffsets = foundOffsets;
+}
+
+
+// This version of searchData: also returns the block numbers (0-based) where the
+// target was found, used for testing that the supplied dispatch_data buffer
+// has not been flattened.
++ (void)searchData:(NSData *)data
+ targetBytes:(const void *)targetBytes
+ targetLength:(NSUInteger)targetLength
+ foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets
+ foundBlockNumbers:(GTM_NSArrayOf(NSNumber *) **)outFoundBlockNumbers {
+ NSMutableArray *foundOffsets = [NSMutableArray array];
+ NSMutableArray *foundBlockNumbers = [NSMutableArray array];
+
+ SearchDataForBytes(data, targetBytes, targetLength, foundOffsets, foundBlockNumbers);
+ *outFoundOffsets = foundOffsets;
+ *outFoundBlockNumbers = foundBlockNumbers;
+}
+
+static void SearchDataForBytes(NSData *data, const void *targetBytes, NSUInteger targetLength,
+ NSMutableArray *foundOffsets, NSMutableArray *foundBlockNumbers) {
+ __block NSUInteger priorPartialMatchAmount = 0;
+ __block NSInteger priorPartialMatchStartingBlockNumber = -1;
+ __block NSInteger blockNumber = -1;
+
+ [data enumerateByteRangesUsingBlock:^(const void *bytes,
+ NSRange byteRange,
+ BOOL *stop) {
+ // Search for the first character in the current range.
+ const void *ptr = bytes;
+ NSInteger remainingInCurrentRange = (NSInteger)byteRange.length;
+ ++blockNumber;
+
+ if (priorPartialMatchAmount > 0) {
+ NSUInteger amountRemainingToBeMatched = targetLength - priorPartialMatchAmount;
+ NSUInteger remainingFoundOffset;
+ NSUInteger amountMatched = FindBytes(targetBytes + priorPartialMatchAmount,
+ amountRemainingToBeMatched,
+ ptr, (NSUInteger)remainingInCurrentRange, &remainingFoundOffset);
+ if (amountMatched == 0 || remainingFoundOffset > 0) {
+ // No match of the rest of the prior partial match in this range.
+ } else if (amountMatched < amountRemainingToBeMatched) {
+ // Another partial match; we're done with this range.
+ priorPartialMatchAmount = priorPartialMatchAmount + amountMatched;
+ return;
+ } else {
+ // The offset is in an earlier range.
+ NSUInteger offset = byteRange.location - priorPartialMatchAmount;
+ [foundOffsets addObject:@(offset)];
+ [foundBlockNumbers addObject:@(priorPartialMatchStartingBlockNumber)];
+ priorPartialMatchStartingBlockNumber = -1;
+ }
+ priorPartialMatchAmount = 0;
+ }
+
+ while (remainingInCurrentRange > 0) {
+ NSUInteger offsetFromPtr;
+ NSUInteger amountMatched = FindBytes(targetBytes, targetLength, ptr,
+ (NSUInteger)remainingInCurrentRange, &offsetFromPtr);
+ if (amountMatched == 0) {
+ // No match in this range.
+ return;
+ }
+ if (amountMatched < targetLength) {
+ // Found a partial target. If there's another range, we'll check for the rest.
+ priorPartialMatchAmount = amountMatched;
+ priorPartialMatchStartingBlockNumber = blockNumber;
+ return;
+ }
+ // Found the full target.
+ NSUInteger globalOffset = byteRange.location + (NSUInteger)(ptr - bytes) + offsetFromPtr;
+
+ [foundOffsets addObject:@(globalOffset)];
+ [foundBlockNumbers addObject:@(blockNumber)];
+
+ ptr += targetLength + offsetFromPtr;
+ remainingInCurrentRange -= (targetLength + offsetFromPtr);
+ }
+ }];
+}
+
+// Internal method only for testing; this calls through the static method.
++ (NSUInteger)findBytesWithNeedle:(const unsigned char *)needle
+ needleLength:(NSUInteger)needleLength
+ haystack:(const unsigned char *)haystack
+ haystackLength:(NSUInteger)haystackLength
+ foundOffset:(NSUInteger *)foundOffset {
+ return FindBytes(needle, needleLength, haystack, haystackLength, foundOffset);
+}
+
+// Utility method to parse header bytes into an NSDictionary.
++ (NSDictionary *)headersWithData:(NSData *)data {
+ NSString *headersString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ if (!headersString) return nil;
+
+ NSMutableDictionary *headers = [NSMutableDictionary dictionary];
+ NSScanner *scanner = [NSScanner scannerWithString:headersString];
+ // The scanner is skipping leading whitespace and newline characters by default.
+ NSCharacterSet *newlineCharacters = [NSCharacterSet newlineCharacterSet];
+ NSString *key;
+ NSString *value;
+ while ([scanner scanUpToString:@":" intoString:&key]
+ && [scanner scanString:@":" intoString:NULL]
+ && [scanner scanUpToCharactersFromSet:newlineCharacters intoString:&value]) {
+ [headers setObject:value forKey:key];
+ // Discard the trailing newline.
+ [scanner scanCharactersFromSet:newlineCharacters intoString:NULL];
+ }
+ return headers;
+}
+
+@end
+
+// Return how much of the needle was found in the haystack.
+//
+// If the result is less than needleLen, then the beginning of the needle
+// was found at the end of the haystack.
+static NSUInteger FindBytes(const unsigned char* needle, NSUInteger needleLen,
+ const unsigned char* haystack, NSUInteger haystackLen,
+ NSUInteger *foundOffset) {
+ const unsigned char *ptr = haystack;
+ NSInteger remain = (NSInteger)haystackLen;
+ // Assume memchr is an efficient way to find a match for the first
+ // byte of the needle, and memcmp is an efficient way to compare a
+ // range of bytes.
+ while (remain > 0 && (ptr = memchr(ptr, needle[0], (size_t)remain)) != 0) {
+ // The first character is present.
+ NSUInteger offset = (NSUInteger)(ptr - haystack);
+ remain = (NSInteger)(haystackLen - offset);
+
+ NSUInteger amountToCompare = MIN((NSUInteger)remain, needleLen);
+ if (memcmp(ptr, needle, amountToCompare) == 0) {
+ if (foundOffset) *foundOffset = offset;
+ return amountToCompare;
+ }
+ ptr++;
+ remain--;
+ }
+ if (foundOffset) *foundOffset = 0;
+ return 0;
+}
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.h b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.h
new file mode 100644
index 00000000..4e306428
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.h
@@ -0,0 +1,49 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#ifndef GTM_NONNULL
+ #if defined(__has_attribute)
+ #if __has_attribute(nonnull)
+ #define GTM_NONNULL(x) __attribute__((nonnull x))
+ #else
+ #define GTM_NONNULL(x)
+ #endif
+ #else
+ #define GTM_NONNULL(x)
+ #endif
+#endif
+
+
+@interface GTMReadMonitorInputStream : NSInputStream <NSStreamDelegate>
+
++ (instancetype)inputStreamWithStream:(NSInputStream *)input GTM_NONNULL((1));
+
+- (instancetype)initWithStream:(NSInputStream *)input GTM_NONNULL((1));
+
+// The read monitor selector is called when bytes have been read. It should have this signature:
+//
+// - (void)inputStream:(GTMReadMonitorInputStream *)stream
+// readIntoBuffer:(uint8_t *)buffer
+// length:(int64_t)length;
+
+@property(atomic, weak) id readDelegate;
+@property(atomic, assign) SEL readSelector;
+
+// Modes for invoking callbacks, when necessary.
+@property(atomic, strong) NSArray *runLoopModes;
+
+@end
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.m b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.m
new file mode 100644
index 00000000..6f95dd54
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.m
@@ -0,0 +1,190 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "GTMReadMonitorInputStream.h"
+
+@implementation GTMReadMonitorInputStream {
+ NSInputStream *_inputStream; // Encapsulated stream that does the work.
+
+ NSThread *_thread; // Thread in which this object was created.
+ NSArray *_runLoopModes; // Modes for calling callbacks, when necessary.
+}
+
+
+@synthesize readDelegate = _readDelegate;
+@synthesize readSelector = _readSelector;
+@synthesize runLoopModes = _runLoopModes;
+
+// We'll forward all unhandled messages to the NSInputStream class or to the encapsulated input
+// stream. This is needed for all messages sent to NSInputStream which aren't handled by our
+// superclass; that includes various private run loop calls.
++ (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
+ return [NSInputStream methodSignatureForSelector:selector];
+}
+
++ (void)forwardInvocation:(NSInvocation*)invocation {
+ [invocation invokeWithTarget:[NSInputStream class]];
+}
+
+- (BOOL)respondsToSelector:(SEL)selector {
+ return [_inputStream respondsToSelector:selector];
+}
+
+- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
+ return [_inputStream methodSignatureForSelector:selector];
+}
+
+- (void)forwardInvocation:(NSInvocation*)invocation {
+ [invocation invokeWithTarget:_inputStream];
+}
+
+#pragma mark -
+
++ (instancetype)inputStreamWithStream:(NSInputStream *)input {
+ return [[self alloc] initWithStream:input];
+}
+
+- (instancetype)initWithStream:(NSInputStream *)input {
+ self = [super init];
+ if (self) {
+ _inputStream = input;
+ _thread = [NSThread currentThread];
+ }
+ return self;
+}
+
+- (instancetype)init {
+ [self doesNotRecognizeSelector:_cmd];
+ return nil;
+}
+
+#pragma mark -
+
+- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
+ // Read from the encapsulated stream.
+ NSInteger numRead = [_inputStream read:buffer maxLength:len];
+ if (numRead > 0) {
+ if (_readDelegate && _readSelector) {
+ // Call the read selector with the buffer and number of bytes actually read into it.
+ BOOL isOnOriginalThread = [_thread isEqual:[NSThread currentThread]];
+ if (isOnOriginalThread) {
+ // Invoke immediately.
+ NSData *data = [NSData dataWithBytesNoCopy:buffer
+ length:(NSUInteger)numRead
+ freeWhenDone:NO];
+ [self invokeReadSelectorWithBuffer:data];
+ } else {
+ // Copy the buffer into an NSData to be retained by the performSelector,
+ // and invoke on the proper thread.
+ SEL sel = @selector(invokeReadSelectorWithBuffer:);
+ NSData *data = [NSData dataWithBytes:buffer length:(NSUInteger)numRead];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ if (_runLoopModes) {
+ [self performSelector:sel
+ onThread:_thread
+ withObject:data
+ waitUntilDone:NO
+ modes:_runLoopModes];
+ } else {
+ [self performSelector:sel
+ onThread:_thread
+ withObject:data
+ waitUntilDone:NO];
+ }
+#pragma clang diagnostic pop
+ }
+ }
+ }
+ return numRead;
+}
+
+- (void)invokeReadSelectorWithBuffer:(NSData *)data {
+ const void *buffer = data.bytes;
+ int64_t length = (int64_t)data.length;
+
+ id argSelf = self;
+ id readDelegate = _readDelegate;
+ if (readDelegate) {
+ NSMethodSignature *signature = [readDelegate methodSignatureForSelector:_readSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
+ [invocation setSelector:_readSelector];
+ [invocation setTarget:readDelegate];
+ [invocation setArgument:&argSelf atIndex:2];
+ [invocation setArgument:&buffer atIndex:3];
+ [invocation setArgument:&length atIndex:4];
+ [invocation invoke];
+ }
+}
+
+- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len {
+ return [_inputStream getBuffer:buffer length:len];
+}
+
+- (BOOL)hasBytesAvailable {
+ return [_inputStream hasBytesAvailable];
+}
+
+#pragma mark Standard messages
+
+// Pass expected messages to our encapsulated stream.
+//
+// We want our encapsulated NSInputStream to handle the standard messages;
+// we don't want the superclass to handle them.
+- (void)open {
+ [_inputStream open];
+}
+
+- (void)close {
+ [_inputStream close];
+}
+
+- (id)delegate {
+ return [_inputStream delegate];
+}
+
+- (void)setDelegate:(id)delegate {
+ [_inputStream setDelegate:delegate];
+}
+
+- (id)propertyForKey:(NSString *)key {
+ return [_inputStream propertyForKey:key];
+}
+
+- (BOOL)setProperty:(id)property forKey:(NSString *)key {
+ return [_inputStream setProperty:property forKey:key];
+}
+
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode {
+ [_inputStream scheduleInRunLoop:aRunLoop forMode:mode];
+}
+
+- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode {
+ [_inputStream removeFromRunLoop:aRunLoop forMode:mode];
+}
+
+- (NSStreamStatus)streamStatus {
+ return [_inputStream streamStatus];
+}
+
+- (NSError *)streamError {
+ return [_inputStream streamError];
+}
+
+@end
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.h b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.h
new file mode 100644
index 00000000..0504aa75
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.h
@@ -0,0 +1,1332 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// GTMSessionFetcher is a wrapper around NSURLSession for http operations.
+//
+// What does this offer on top of of NSURLSession?
+//
+// - Block-style callbacks for useful functionality like progress rather
+// than delegate methods.
+// - Out-of-process uploads and downloads using NSURLSession, including
+// management of fetches after relaunch.
+// - Integration with GTMAppAuth for invisible management and refresh of
+// authorization tokens.
+// - Pretty-printed http logging.
+// - Cookies handling that does not interfere with or get interfered with
+// by WebKit cookies or on Mac by Safari and other apps.
+// - Credentials handling for the http operation.
+// - Rate-limiting and cookie grouping when fetchers are created with
+// GTMSessionFetcherService.
+//
+// If the bodyData or bodyFileURL property is set, then a POST request is assumed.
+//
+// Each fetcher is assumed to be for a one-shot fetch request; don't reuse the object
+// for a second fetch.
+//
+// The fetcher will be self-retained as long as a connection is pending.
+//
+// To keep user activity private, URLs must have an https scheme (unless the property
+// allowedInsecureSchemes is set to permit the scheme.)
+//
+// Callbacks will be released when the fetch completes or is stopped, so there is no need
+// to use weak self references in the callback blocks.
+//
+// Sample usage:
+//
+// _fetcherService = [[GTMSessionFetcherService alloc] init];
+//
+// GTMSessionFetcher *myFetcher = [_fetcherService fetcherWithURLString:myURLString];
+// myFetcher.retryEnabled = YES;
+// myFetcher.comment = @"First profile image";
+//
+// // Optionally specify a file URL or NSData for the request body to upload.
+// myFetcher.bodyData = [postString dataUsingEncoding:NSUTF8StringEncoding];
+//
+// [myFetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+// if (error != nil) {
+// // Server status code or network error.
+// //
+// // If the domain is kGTMSessionFetcherStatusDomain then the error code
+// // is a failure status from the server.
+// } else {
+// // Fetch succeeded.
+// }
+// }];
+//
+// There is also a beginFetch call that takes a pointer and selector for the completion handler;
+// a pointer and selector is a better style when the callback is a substantial, separate method.
+//
+// NOTE: Fetches may retrieve data from the server even though the server
+// returned an error, so the criteria for success is a non-nil error.
+// The completion handler is called when the server status is >= 300 with an NSError
+// having domain kGTMSessionFetcherStatusDomain and code set to the server status.
+//
+// Status codes are at <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>
+//
+//
+// Background session support:
+//
+// Out-of-process uploads and downloads may be created by setting the fetcher's
+// useBackgroundSession property. Data to be uploaded should be provided via
+// the uploadFileURL property; the download destination should be specified with
+// the destinationFileURL. NOTE: Background upload files should be in a location
+// that will be valid even after the device is restarted, so the file should not
+// be uploaded from a system temporary or cache directory.
+//
+// Background session transfers are slower, and should typically be used only
+// for very large downloads or uploads (hundreds of megabytes).
+//
+// When background sessions are used in iOS apps, the application delegate must
+// pass through the parameters from UIApplicationDelegate's
+// application:handleEventsForBackgroundURLSession:completionHandler: to the
+// fetcher class.
+//
+// When the application has been relaunched, it may also create a new fetcher
+// instance to handle completion of the transfers.
+//
+// - (void)application:(UIApplication *)application
+// handleEventsForBackgroundURLSession:(NSString *)identifier
+// completionHandler:(void (^)())completionHandler {
+// // Application was re-launched on completing an out-of-process download.
+//
+// // Pass the URLSession info related to this re-launch to the fetcher class.
+// [GTMSessionFetcher application:application
+// handleEventsForBackgroundURLSession:identifier
+// completionHandler:completionHandler];
+//
+// // Get a fetcher related to this re-launch and re-hook up a completionHandler to it.
+// GTMSessionFetcher *fetcher = [GTMSessionFetcher fetcherWithSessionIdentifier:identifier];
+// NSURL *destinationFileURL = fetcher.destinationFileURL;
+// fetcher.completionHandler = ^(NSData *data, NSError *error) {
+// [self downloadCompletedToFile:destinationFileURL error:error];
+// };
+// }
+//
+//
+// Threading and queue support:
+//
+// Networking always happens on a background thread; there is no advantage to
+// changing thread or queue to create or start a fetcher.
+//
+// Callbacks are run on the main thread; alternatively, the app may set the
+// fetcher's callbackQueue to a dispatch queue.
+//
+// Once the fetcher's beginFetch method has been called, the fetcher's methods and
+// properties may be accessed from any thread.
+//
+// Downloading to disk:
+//
+// To have downloaded data saved directly to disk, specify a file URL for the
+// destinationFileURL property.
+//
+// HTTP methods and headers:
+//
+// Alternative HTTP methods, like PUT, and custom headers can be specified by
+// creating the fetcher with an appropriate NSMutableURLRequest.
+//
+//
+// Caching:
+//
+// The fetcher avoids caching. That is best for API requests, but may hurt
+// repeat fetches of static data. Apps may enable a persistent disk cache by
+// customizing the config:
+//
+// fetcher.configurationBlock = ^(GTMSessionFetcher *configFetcher,
+// NSURLSessionConfiguration *config) {
+// config.URLCache = [NSURLCache sharedURLCache];
+// };
+//
+// Or use the standard system config to share cookie storage with web views
+// and to enable disk caching:
+//
+// fetcher.configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
+//
+//
+// Cookies:
+//
+// There are three supported mechanisms for remembering cookies between fetches.
+//
+// By default, a standalone GTMSessionFetcher uses a mutable array held
+// statically to track cookies for all instantiated fetchers. This avoids
+// cookies being set by servers for the application from interfering with
+// Safari and WebKit cookie settings, and vice versa.
+// The fetcher cookies are lost when the application quits.
+//
+// To rely instead on WebKit's global NSHTTPCookieStorage, set the fetcher's
+// cookieStorage property:
+// myFetcher.cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
+//
+// To share cookies with other apps, use the method introduced in iOS 9/OS X 10.11:
+// myFetcher.cookieStorage =
+// [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:kMyCompanyContainedID];
+//
+// To ignore existing cookies and only have cookies related to the single fetch
+// be applied, make a temporary cookie storage object:
+// myFetcher.cookieStorage = [[GTMSessionCookieStorage alloc] init];
+//
+// Note: cookies set while following redirects will be sent to the server, as
+// the redirects are followed by the fetcher.
+//
+// To completely disable cookies, similar to setting cookieStorageMethod to
+// kGTMHTTPFetcherCookieStorageMethodNone, adjust the session configuration
+// appropriately in the fetcher or fetcher service:
+// fetcher.configurationBlock = ^(GTMSessionFetcher *configFetcher,
+// NSURLSessionConfiguration *config) {
+// config.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever;
+// config.HTTPShouldSetCookies = NO;
+// };
+//
+// If the fetcher is created from a GTMSessionFetcherService object
+// then the cookie storage mechanism is set to use the cookie storage in the
+// service object rather than the static storage. Disabling cookies in the
+// session configuration set on a service object will disable cookies for all
+// fetchers created from that GTMSessionFetcherService object, since the session
+// configuration is propagated to the fetcher.
+//
+//
+// Monitoring data transfers.
+//
+// The fetcher supports a variety of properties for progress monitoring
+// progress with callback blocks.
+// GTMSessionFetcherSendProgressBlock sendProgressBlock
+// GTMSessionFetcherReceivedProgressBlock receivedProgressBlock
+// GTMSessionFetcherDownloadProgressBlock downloadProgressBlock
+//
+// If supplied by the server, the anticipated total download size is available
+// as [[myFetcher response] expectedContentLength] (and may be -1 for unknown
+// download sizes.)
+//
+//
+// Automatic retrying of fetches
+//
+// The fetcher can optionally create a timer and reattempt certain kinds of
+// fetch failures (status codes 408, request timeout; 502, gateway failure;
+// 503, service unavailable; 504, gateway timeout; networking errors
+// NSURLErrorTimedOut and NSURLErrorNetworkConnectionLost.) The user may
+// set a retry selector to customize the type of errors which will be retried.
+//
+// Retries are done in an exponential-backoff fashion (that is, after 1 second,
+// 2, 4, 8, and so on.)
+//
+// Enabling automatic retries looks like this:
+// myFetcher.retryEnabled = YES;
+//
+// With retries enabled, the completion callbacks are called only
+// when no more retries will be attempted. Calling the fetcher's stopFetching
+// method will terminate the retry timer, without the finished or failure
+// selectors being invoked.
+//
+// Optionally, the client may set the maximum retry interval:
+// myFetcher.maxRetryInterval = 60.0; // in seconds; default is 60 seconds
+// // for downloads, 600 for uploads
+//
+// Servers should never send a 400 or 500 status for errors that are retryable
+// by clients, as those values indicate permanent failures. In nearly all
+// cases, the default standard retry behavior is correct for clients, and no
+// custom client retry behavior is needed or appropriate. Servers that send
+// non-retryable status codes and expect the client to retry the request are
+// faulty.
+//
+// Still, the client may provide a block to determine if a status code or other
+// error should be retried. The block returns YES to set the retry timer or NO
+// to fail without additional fetch attempts.
+//
+// The retry method may return the |suggestedWillRetry| argument to get the
+// default retry behavior. Server status codes are present in the
+// error argument, and have the domain kGTMSessionFetcherStatusDomain. The
+// user's method may look something like this:
+//
+// myFetcher.retryBlock = ^(BOOL suggestedWillRetry, NSError *error,
+// GTMSessionFetcherRetryResponse response) {
+// // Perhaps examine error.domain and error.code, or fetcher.retryCount
+// //
+// // Respond with YES to start the retry timer, NO to proceed to the failure
+// // callback, or suggestedWillRetry to get default behavior for the
+// // current error domain and code values.
+// response(suggestedWillRetry);
+// };
+
+
+#import <Foundation/Foundation.h>
+
+#if TARGET_OS_IPHONE
+#import <UIKit/UIKit.h>
+#endif
+#if TARGET_OS_WATCH
+#import <WatchKit/WatchKit.h>
+#endif
+
+// By default it is stripped from non DEBUG builds. Developers can override
+// this in their project settings.
+#ifndef STRIP_GTM_FETCH_LOGGING
+ #if !DEBUG
+ #define STRIP_GTM_FETCH_LOGGING 1
+ #else
+ #define STRIP_GTM_FETCH_LOGGING 0
+ #endif
+#endif
+
+// Logs in debug builds.
+#ifndef GTMSESSION_LOG_DEBUG
+ #if DEBUG
+ #define GTMSESSION_LOG_DEBUG(...) NSLog(__VA_ARGS__)
+ #else
+ #define GTMSESSION_LOG_DEBUG(...) do { } while (0)
+ #endif
+#endif
+
+// Asserts in debug builds (or logs in debug builds if GTMSESSION_ASSERT_AS_LOG
+// or NS_BLOCK_ASSERTIONS are defined.)
+#ifndef GTMSESSION_ASSERT_DEBUG
+ #if DEBUG && !defined(NS_BLOCK_ASSERTIONS) && !GTMSESSION_ASSERT_AS_LOG
+ #undef GTMSESSION_ASSERT_AS_LOG
+ #define GTMSESSION_ASSERT_AS_LOG 1
+ #endif
+
+ #if DEBUG && !GTMSESSION_ASSERT_AS_LOG
+ #define GTMSESSION_ASSERT_DEBUG(...) NSAssert(__VA_ARGS__)
+ #elif DEBUG
+ #define GTMSESSION_ASSERT_DEBUG(pred, ...) if (!(pred)) { NSLog(__VA_ARGS__); }
+ #else
+ #define GTMSESSION_ASSERT_DEBUG(pred, ...) do { } while (0)
+ #endif
+#endif
+
+// Asserts in debug builds, logs in release builds (or logs in debug builds if
+// GTMSESSION_ASSERT_AS_LOG is defined.)
+#ifndef GTMSESSION_ASSERT_DEBUG_OR_LOG
+ #if DEBUG && !GTMSESSION_ASSERT_AS_LOG
+ #define GTMSESSION_ASSERT_DEBUG_OR_LOG(...) NSAssert(__VA_ARGS__)
+ #else
+ #define GTMSESSION_ASSERT_DEBUG_OR_LOG(pred, ...) if (!(pred)) { NSLog(__VA_ARGS__); }
+ #endif
+#endif
+
+// Macro useful for examining messages from NSURLSession during debugging.
+#if 0
+#define GTM_LOG_SESSION_DELEGATE(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__)
+#else
+#define GTM_LOG_SESSION_DELEGATE(...)
+#endif
+
+#ifndef GTM_NULLABLE
+ #if __has_feature(nullability) // Available starting in Xcode 6.3
+ #define GTM_NULLABLE_TYPE __nullable
+ #define GTM_NONNULL_TYPE __nonnull
+ #define GTM_NULLABLE nullable
+ #define GTM_NONNULL_DECL nonnull // GTM_NONNULL is used by GTMDefines.h
+ #define GTM_NULL_RESETTABLE null_resettable
+
+ #define GTM_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
+ #define GTM_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END
+ #else
+ #define GTM_NULLABLE_TYPE
+ #define GTM_NONNULL_TYPE
+ #define GTM_NULLABLE
+ #define GTM_NONNULL_DECL
+ #define GTM_NULL_RESETTABLE
+ #define GTM_ASSUME_NONNULL_BEGIN
+ #define GTM_ASSUME_NONNULL_END
+ #endif // __has_feature(nullability)
+#endif // GTM_NULLABLE
+
+#ifndef GTM_DECLARE_GENERICS
+ #if __has_feature(objc_generics)
+ #define GTM_DECLARE_GENERICS 1
+ #else
+ #define GTM_DECLARE_GENERICS 0
+ #endif
+#endif
+
+#ifndef GTM_NSArrayOf
+ #if GTM_DECLARE_GENERICS
+ #define GTM_NSArrayOf(value) NSArray<value>
+ #define GTM_NSDictionaryOf(key, value) NSDictionary<key, value>
+ #else
+ #define GTM_NSArrayOf(value) NSArray
+ #define GTM_NSDictionaryOf(key, value) NSDictionary
+ #endif // __has_feature(objc_generics)
+#endif // GTM_NSArrayOf
+
+// For iOS, the fetcher can declare itself a background task to allow fetches
+// to finish when the app leaves the foreground.
+//
+// (This is unrelated to providing a background configuration, which allows
+// out-of-process uploads and downloads.)
+//
+// To disallow use of background tasks during fetches, the target should define
+// GTM_BACKGROUND_TASK_FETCHING to 0, or alternatively may set the
+// skipBackgroundTask property to YES.
+#if TARGET_OS_IPHONE && !TARGET_OS_WATCH && !defined(GTM_BACKGROUND_TASK_FETCHING)
+ #define GTM_BACKGROUND_TASK_FETCHING 1
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if (TARGET_OS_TV \
+ || TARGET_OS_WATCH \
+ || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \
+ || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0))
+ #ifndef GTM_USE_SESSION_FETCHER
+ #define GTM_USE_SESSION_FETCHER 1
+ #endif
+#endif
+
+#if !defined(GTMBridgeFetcher)
+ // These bridge macros should be identical in GTMHTTPFetcher.h and GTMSessionFetcher.h
+ #if GTM_USE_SESSION_FETCHER
+ // Macros to new fetcher class.
+ #define GTMBridgeFetcher GTMSessionFetcher
+ #define GTMBridgeFetcherService GTMSessionFetcherService
+ #define GTMBridgeFetcherServiceProtocol GTMSessionFetcherServiceProtocol
+ #define GTMBridgeAssertValidSelector GTMSessionFetcherAssertValidSelector
+ #define GTMBridgeCookieStorage GTMSessionCookieStorage
+ #define GTMBridgeCleanedUserAgentString GTMFetcherCleanedUserAgentString
+ #define GTMBridgeSystemVersionString GTMFetcherSystemVersionString
+ #define GTMBridgeApplicationIdentifier GTMFetcherApplicationIdentifier
+ #define kGTMBridgeFetcherStatusDomain kGTMSessionFetcherStatusDomain
+ #define kGTMBridgeFetcherStatusBadRequest GTMSessionFetcherStatusBadRequest
+ #else
+ // Macros to old fetcher class.
+ #define GTMBridgeFetcher GTMHTTPFetcher
+ #define GTMBridgeFetcherService GTMHTTPFetcherService
+ #define GTMBridgeFetcherServiceProtocol GTMHTTPFetcherServiceProtocol
+ #define GTMBridgeAssertValidSelector GTMAssertSelectorNilOrImplementedWithArgs
+ #define GTMBridgeCookieStorage GTMCookieStorage
+ #define GTMBridgeCleanedUserAgentString GTMCleanedUserAgentString
+ #define GTMBridgeSystemVersionString GTMSystemVersionString
+ #define GTMBridgeApplicationIdentifier GTMApplicationIdentifier
+ #define kGTMBridgeFetcherStatusDomain kGTMHTTPFetcherStatusDomain
+ #define kGTMBridgeFetcherStatusBadRequest kGTMHTTPFetcherStatusBadRequest
+ #endif // GTM_USE_SESSION_FETCHER
+#endif
+
+// When creating background sessions to perform out-of-process uploads and
+// downloads, on app launch any background sessions must be reconnected in
+// order to receive events that occurred while the app was not running.
+//
+// The fetcher will automatically attempt to recreate the sessions on app
+// start, but doing so reads from NSUserDefaults. This may have launch-time
+// performance impacts.
+//
+// To avoid launch performance impacts, on iPhone/iPad with iOS 13+ the
+// GTMSessionFetcher class will register for the app launch notification and
+// perform the reconnect then.
+//
+// Apps targeting Mac or older iOS SDKs can opt into the new behavior by defining
+// GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH=1.
+//
+// Apps targeting new SDKs can force the old behavior by defining
+// GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH = 0.
+#ifndef GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH
+ // Default to the on-launch behavior for iOS 13+.
+ #if TARGET_OS_IOS && defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
+ #define GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH 1
+ #else
+ #define GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH 0
+ #endif
+#endif
+
+GTM_ASSUME_NONNULL_BEGIN
+
+// Notifications
+//
+// Fetch started and stopped, and fetch retry delay started and stopped.
+extern NSString *const kGTMSessionFetcherStartedNotification;
+extern NSString *const kGTMSessionFetcherStoppedNotification;
+extern NSString *const kGTMSessionFetcherRetryDelayStartedNotification;
+extern NSString *const kGTMSessionFetcherRetryDelayStoppedNotification;
+
+// Completion handler notification. This is intended for use by code capturing
+// and replaying fetch requests and results for testing. For fetches where
+// destinationFileURL or accumulateDataBlock is set for the fetcher, the data
+// will be nil for successful fetches.
+//
+// This notification is posted on the main thread.
+extern NSString *const kGTMSessionFetcherCompletionInvokedNotification;
+extern NSString *const kGTMSessionFetcherCompletionDataKey;
+extern NSString *const kGTMSessionFetcherCompletionErrorKey;
+
+// Constants for NSErrors created by the fetcher (excluding server status errors,
+// and error objects originating in the OS.)
+extern NSString *const kGTMSessionFetcherErrorDomain;
+
+// The fetcher turns server error status values (3XX, 4XX, 5XX) into NSErrors
+// with domain kGTMSessionFetcherStatusDomain.
+//
+// Any server response body data accompanying the status error is added to the
+// userInfo dictionary with key kGTMSessionFetcherStatusDataKey.
+extern NSString *const kGTMSessionFetcherStatusDomain;
+extern NSString *const kGTMSessionFetcherStatusDataKey;
+extern NSString *const kGTMSessionFetcherStatusDataContentTypeKey;
+
+// When a fetch fails with an error, these keys are included in the error userInfo
+// dictionary if retries were attempted.
+extern NSString *const kGTMSessionFetcherNumberOfRetriesDoneKey;
+extern NSString *const kGTMSessionFetcherElapsedIntervalWithRetriesKey;
+
+// Background session support requires access to NSUserDefaults.
+// If [NSUserDefaults standardUserDefaults] doesn't yield the correct NSUserDefaults for your usage,
+// ie for an App Extension, then implement this class/method to return the correct NSUserDefaults.
+// https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW6
+@interface GTMSessionFetcherUserDefaultsFactory : NSObject
+
++ (NSUserDefaults *)fetcherUserDefaults;
+
+@end
+
+#ifdef __cplusplus
+}
+#endif
+
+typedef NS_ENUM(NSInteger, GTMSessionFetcherError) {
+ GTMSessionFetcherErrorDownloadFailed = -1,
+ GTMSessionFetcherErrorUploadChunkUnavailable = -2,
+ GTMSessionFetcherErrorBackgroundExpiration = -3,
+ GTMSessionFetcherErrorBackgroundFetchFailed = -4,
+ GTMSessionFetcherErrorInsecureRequest = -5,
+ GTMSessionFetcherErrorTaskCreationFailed = -6,
+};
+
+typedef NS_ENUM(NSInteger, GTMSessionFetcherStatus) {
+ // Standard http status codes.
+ GTMSessionFetcherStatusNotModified = 304,
+ GTMSessionFetcherStatusBadRequest = 400,
+ GTMSessionFetcherStatusUnauthorized = 401,
+ GTMSessionFetcherStatusForbidden = 403,
+ GTMSessionFetcherStatusPreconditionFailed = 412
+};
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+@class GTMSessionCookieStorage;
+@class GTMSessionFetcher;
+
+// The configuration block is for modifying the NSURLSessionConfiguration only.
+// DO NOT change any fetcher properties in the configuration block.
+typedef void (^GTMSessionFetcherConfigurationBlock)(GTMSessionFetcher *fetcher,
+ NSURLSessionConfiguration *configuration);
+typedef void (^GTMSessionFetcherSystemCompletionHandler)(void);
+typedef void (^GTMSessionFetcherCompletionHandler)(NSData * GTM_NULLABLE_TYPE data,
+ NSError * GTM_NULLABLE_TYPE error);
+typedef void (^GTMSessionFetcherBodyStreamProviderResponse)(NSInputStream *bodyStream);
+typedef void (^GTMSessionFetcherBodyStreamProvider)(GTMSessionFetcherBodyStreamProviderResponse response);
+typedef void (^GTMSessionFetcherDidReceiveResponseDispositionBlock)(NSURLSessionResponseDisposition disposition);
+typedef void (^GTMSessionFetcherDidReceiveResponseBlock)(NSURLResponse *response,
+ GTMSessionFetcherDidReceiveResponseDispositionBlock dispositionBlock);
+typedef void (^GTMSessionFetcherChallengeDispositionBlock)(NSURLSessionAuthChallengeDisposition disposition,
+ NSURLCredential * GTM_NULLABLE_TYPE credential);
+typedef void (^GTMSessionFetcherChallengeBlock)(GTMSessionFetcher *fetcher,
+ NSURLAuthenticationChallenge *challenge,
+ GTMSessionFetcherChallengeDispositionBlock dispositionBlock);
+typedef void (^GTMSessionFetcherWillRedirectResponse)(NSURLRequest * GTM_NULLABLE_TYPE redirectedRequest);
+typedef void (^GTMSessionFetcherWillRedirectBlock)(NSHTTPURLResponse *redirectResponse,
+ NSURLRequest *redirectRequest,
+ GTMSessionFetcherWillRedirectResponse response);
+typedef void (^GTMSessionFetcherAccumulateDataBlock)(NSData * GTM_NULLABLE_TYPE buffer);
+typedef void (^GTMSessionFetcherSimulateByteTransferBlock)(NSData * GTM_NULLABLE_TYPE buffer,
+ int64_t bytesWritten,
+ int64_t totalBytesWritten,
+ int64_t totalBytesExpectedToWrite);
+typedef void (^GTMSessionFetcherReceivedProgressBlock)(int64_t bytesWritten,
+ int64_t totalBytesWritten);
+typedef void (^GTMSessionFetcherDownloadProgressBlock)(int64_t bytesWritten,
+ int64_t totalBytesWritten,
+ int64_t totalBytesExpectedToWrite);
+typedef void (^GTMSessionFetcherSendProgressBlock)(int64_t bytesSent,
+ int64_t totalBytesSent,
+ int64_t totalBytesExpectedToSend);
+typedef void (^GTMSessionFetcherWillCacheURLResponseResponse)(NSCachedURLResponse * GTM_NULLABLE_TYPE cachedResponse);
+typedef void (^GTMSessionFetcherWillCacheURLResponseBlock)(NSCachedURLResponse *proposedResponse,
+ GTMSessionFetcherWillCacheURLResponseResponse responseBlock);
+typedef void (^GTMSessionFetcherRetryResponse)(BOOL shouldRetry);
+typedef void (^GTMSessionFetcherRetryBlock)(BOOL suggestedWillRetry,
+ NSError * GTM_NULLABLE_TYPE error,
+ GTMSessionFetcherRetryResponse response);
+
+API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0))
+typedef void (^GTMSessionFetcherMetricsCollectionBlock)(NSURLSessionTaskMetrics *metrics);
+
+typedef void (^GTMSessionFetcherTestResponse)(NSHTTPURLResponse * GTM_NULLABLE_TYPE response,
+ NSData * GTM_NULLABLE_TYPE data,
+ NSError * GTM_NULLABLE_TYPE error);
+typedef void (^GTMSessionFetcherTestBlock)(GTMSessionFetcher *fetcherToTest,
+ GTMSessionFetcherTestResponse testResponse);
+
+void GTMSessionFetcherAssertValidSelector(id GTM_NULLABLE_TYPE obj, SEL GTM_NULLABLE_TYPE sel, ...);
+
+// Utility functions for applications self-identifying to servers via a
+// user-agent header
+
+// The "standard" user agent includes the application identifier, taken from the bundle,
+// followed by a space and the system version string. Pass nil to use +mainBundle as the source
+// of the bundle identifier.
+//
+// Applications may use this as a starting point for their own user agent strings, perhaps
+// with additional sections appended. Use GTMFetcherCleanedUserAgentString() below to
+// clean up any string being added to the user agent.
+NSString *GTMFetcherStandardUserAgentString(NSBundle * GTM_NULLABLE_TYPE bundle);
+
+// Make a generic name and version for the current application, like
+// com.example.MyApp/1.2.3 relying on the bundle identifier and the
+// CFBundleShortVersionString or CFBundleVersion.
+//
+// The bundle ID may be overridden as the base identifier string by
+// adding to the bundle's Info.plist a "GTMUserAgentID" key.
+//
+// If no bundle ID or override is available, the process name preceded
+// by "proc_" is used.
+NSString *GTMFetcherApplicationIdentifier(NSBundle * GTM_NULLABLE_TYPE bundle);
+
+// Make an identifier like "MacOSX/10.7.1" or "iPod_Touch/4.1 hw/iPod1_1"
+NSString *GTMFetcherSystemVersionString(void);
+
+// Make a parseable user-agent identifier from the given string, replacing whitespace
+// and commas with underscores, and removing other characters that may interfere
+// with parsing of the full user-agent string.
+//
+// For example, @"[My App]" would become @"My_App"
+NSString *GTMFetcherCleanedUserAgentString(NSString *str);
+
+// Grab the data from an input stream. Since streams cannot be assumed to be rewindable,
+// this may be destructive; the caller can try to rewind the stream (by setting the
+// NSStreamFileCurrentOffsetKey property) or can just use the NSData to make a new
+// NSInputStream. This function is intended to facilitate testing rather than be used in
+// production.
+//
+// This function operates synchronously on the current thread. Depending on how the
+// input stream is implemented, it may be appropriate to dispatch to a different
+// queue before calling this function.
+//
+// Failure is indicated by a returned data value of nil.
+NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+
+#if !GTM_USE_SESSION_FETCHER
+@protocol GTMHTTPFetcherServiceProtocol;
+#endif
+
+// This protocol allows abstract references to the fetcher service, primarily for
+// fetchers (which may be compiled without the fetcher service class present.)
+//
+// Apps should not need to use this protocol.
+@protocol GTMSessionFetcherServiceProtocol <NSObject>
+// This protocol allows us to call into the service without requiring
+// GTMSessionFetcherService sources in this project
+
+@property(atomic, strong) dispatch_queue_t callbackQueue;
+
+- (BOOL)fetcherShouldBeginFetching:(GTMSessionFetcher *)fetcher;
+- (void)fetcherDidCreateSession:(GTMSessionFetcher *)fetcher;
+- (void)fetcherDidBeginFetching:(GTMSessionFetcher *)fetcher;
+- (void)fetcherDidStop:(GTMSessionFetcher *)fetcher;
+
+- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request;
+- (BOOL)isDelayingFetcher:(GTMSessionFetcher *)fetcher;
+
+@property(atomic, assign) BOOL reuseSession;
+- (GTM_NULLABLE NSURLSession *)session;
+- (GTM_NULLABLE NSURLSession *)sessionForFetcherCreation;
+- (GTM_NULLABLE id<NSURLSessionDelegate>)sessionDelegate;
+- (GTM_NULLABLE NSDate *)stoppedAllFetchersDate;
+
+// Methods for compatibility with the old GTMHTTPFetcher.
+@property(atomic, readonly, strong, GTM_NULLABLE) NSOperationQueue *delegateQueue;
+
+@end // @protocol GTMSessionFetcherServiceProtocol
+
+#ifndef GTM_FETCHER_AUTHORIZATION_PROTOCOL
+#define GTM_FETCHER_AUTHORIZATION_PROTOCOL 1
+@protocol GTMFetcherAuthorizationProtocol <NSObject>
+@required
+// This protocol allows us to call the authorizer without requiring its sources
+// in this project.
+- (void)authorizeRequest:(GTM_NULLABLE NSMutableURLRequest *)request
+ delegate:(id)delegate
+ didFinishSelector:(SEL)sel;
+
+- (void)stopAuthorization;
+
+- (void)stopAuthorizationForRequest:(NSURLRequest *)request;
+
+- (BOOL)isAuthorizingRequest:(NSURLRequest *)request;
+
+- (BOOL)isAuthorizedRequest:(NSURLRequest *)request;
+
+@property(atomic, strong, readonly, GTM_NULLABLE) NSString *userEmail;
+
+@optional
+
+// Indicate if authorization may be attempted. Even if this succeeds,
+// authorization may fail if the user's permissions have been revoked.
+@property(atomic, readonly) BOOL canAuthorize;
+
+// For development only, allow authorization of non-SSL requests, allowing
+// transmission of the bearer token unencrypted.
+@property(atomic, assign) BOOL shouldAuthorizeAllRequests;
+
+- (void)authorizeRequest:(GTM_NULLABLE NSMutableURLRequest *)request
+ completionHandler:(void (^)(NSError * GTM_NULLABLE_TYPE error))handler;
+
+#if GTM_USE_SESSION_FETCHER
+@property(atomic, weak, GTM_NULLABLE) id<GTMSessionFetcherServiceProtocol> fetcherService;
+#else
+@property(atomic, weak, GTM_NULLABLE) id<GTMHTTPFetcherServiceProtocol> fetcherService;
+#endif
+
+- (BOOL)primeForRefresh;
+
+@end
+#endif // GTM_FETCHER_AUTHORIZATION_PROTOCOL
+
+#if GTM_BACKGROUND_TASK_FETCHING
+// A protocol for an alternative target for messages from GTMSessionFetcher to UIApplication.
+// Set the target using +[GTMSessionFetcher setSubstituteUIApplication:]
+@protocol GTMUIApplicationProtocol <NSObject>
+- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithName:(nullable NSString *)taskName
+ expirationHandler:(void(^ __nullable)(void))handler;
+- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier;
+@end
+#endif
+
+#pragma mark -
+
+// GTMSessionFetcher objects are used for async retrieval of an http get or post
+//
+// See additional comments at the beginning of this file
+@interface GTMSessionFetcher : NSObject <NSURLSessionDelegate>
+
+// Create a fetcher
+//
+// fetcherWithRequest will return an autoreleased fetcher, but if
+// the connection is successfully created, the connection should retain the
+// fetcher for the life of the connection as well. So the caller doesn't have
+// to retain the fetcher explicitly unless they want to be able to cancel it.
++ (instancetype)fetcherWithRequest:(GTM_NULLABLE NSURLRequest *)request;
+
+// Convenience methods that make a request, like +fetcherWithRequest
++ (instancetype)fetcherWithURL:(NSURL *)requestURL;
++ (instancetype)fetcherWithURLString:(NSString *)requestURLString;
+
+// Methods for creating fetchers to continue previous fetches.
++ (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData;
++ (GTM_NULLABLE instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier;
+
+// Returns an array of currently active fetchers for background sessions,
+// both restarted and newly created ones.
++ (GTM_NSArrayOf(GTMSessionFetcher *) *)fetchersForBackgroundSessions;
+
+// Designated initializer.
+//
+// Applications should create fetchers with a "fetcherWith..." method on a fetcher
+// service or a class method, not with this initializer.
+//
+// The configuration should typically be nil. Applications needing to customize
+// the configuration may do so by setting the configurationBlock property.
+- (instancetype)initWithRequest:(GTM_NULLABLE NSURLRequest *)request
+ configuration:(GTM_NULLABLE NSURLSessionConfiguration *)configuration;
+
+// The fetcher's request. This may not be set after beginFetch has been invoked. The request
+// may change due to redirects.
+@property(atomic, strong, GTM_NULLABLE) NSURLRequest *request;
+
+// Set a header field value on the request. Header field value changes will not
+// affect a fetch after the fetch has begun.
+- (void)setRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field;
+
+// Data used for resuming a download task.
+@property(atomic, readonly, GTM_NULLABLE) NSData *downloadResumeData;
+
+// The configuration; this must be set before the fetch begins. If no configuration is
+// set or inherited from the fetcher service, then the fetcher uses an ephemeral config.
+//
+// NOTE: This property should typically be nil. Applications needing to customize
+// the configuration should do so by setting the configurationBlock property.
+// That allows the fetcher to pick an appropriate base configuration, with the
+// application setting only the configuration properties it needs to customize.
+@property(atomic, strong, GTM_NULLABLE) NSURLSessionConfiguration *configuration;
+
+// A block the client may use to customize the configuration used to create the session.
+//
+// This is called synchronously, either on the thread that begins the fetch or, during a retry,
+// on the main thread. The configuration block may be called repeatedly if multiple fetchers are
+// created.
+//
+// The configuration block is for modifying the NSURLSessionConfiguration only.
+// DO NOT change any fetcher properties in the configuration block. Fetcher properties
+// may be set in the fetcher service prior to fetcher creation, or on the fetcher prior
+// to invoking beginFetch.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherConfigurationBlock configurationBlock;
+
+// A session is created as needed by the fetcher. A fetcher service object
+// may maintain sessions for multiple fetches to the same host.
+@property(atomic, strong, GTM_NULLABLE) NSURLSession *session;
+
+// The task in flight.
+@property(atomic, readonly, GTM_NULLABLE) NSURLSessionTask *sessionTask;
+
+// The background session identifier.
+@property(atomic, readonly, GTM_NULLABLE) NSString *sessionIdentifier;
+
+// Indicates a fetcher created to finish a background session task.
+@property(atomic, readonly) BOOL wasCreatedFromBackgroundSession;
+
+// Additional user-supplied data to encode into the session identifier. Since session identifier
+// length limits are unspecified, this should be kept small. Key names beginning with an underscore
+// are reserved for use by the fetcher.
+@property(atomic, strong, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, NSString *) *sessionUserInfo;
+
+// The human-readable description to be assigned to the task.
+@property(atomic, copy, GTM_NULLABLE) NSString *taskDescription;
+
+// The priority assigned to the task, if any. Use NSURLSessionTaskPriorityLow,
+// NSURLSessionTaskPriorityDefault, or NSURLSessionTaskPriorityHigh.
+@property(atomic, assign) float taskPriority;
+
+// The fetcher encodes information used to resume a session in the session identifier.
+// This method, intended for internal use returns the encoded information. The sessionUserInfo
+// dictionary is stored as identifier metadata.
+- (GTM_NULLABLE GTM_NSDictionaryOf(NSString *, NSString *) *)sessionIdentifierMetadata;
+
+#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
+// The app should pass to this method the completion handler passed in the app delegate method
+// application:handleEventsForBackgroundURLSession:completionHandler:
++ (void)application:(UIApplication *)application
+ handleEventsForBackgroundURLSession:(NSString *)identifier
+ completionHandler:(GTMSessionFetcherSystemCompletionHandler)completionHandler;
+#endif
+
+// Indicate that a newly created session should be a background session.
+// A new session identifier will be created by the fetcher.
+//
+// Warning: The only thing background sessions are for is rare download
+// of huge, batched files of data. And even just for those, there's a lot
+// of pain and hackery needed to get transfers to actually happen reliably
+// with background sessions.
+//
+// Don't try to upload or download in many background sessions, since the system
+// will impose an exponentially increasing time penalty to prevent the app from
+// getting too much background execution time.
+//
+// References:
+//
+// "Moving to Fewer, Larger Transfers"
+// https://forums.developer.apple.com/thread/14853
+//
+// "NSURLSession’s Resume Rate Limiter"
+// https://forums.developer.apple.com/thread/14854
+//
+// "Background Session Task state persistence"
+// https://forums.developer.apple.com/thread/11554
+//
+@property(atomic, assign) BOOL useBackgroundSession;
+
+// Indicates if the fetcher was started using a background session.
+@property(atomic, readonly, getter=isUsingBackgroundSession) BOOL usingBackgroundSession;
+
+// Indicates if uploads should use an upload task. This is always set for file or stream-provider
+// bodies, but may be set explicitly for NSData bodies.
+@property(atomic, assign) BOOL useUploadTask;
+
+// Indicates that the fetcher is using a session that may be shared with other fetchers.
+@property(atomic, readonly) BOOL canShareSession;
+
+// By default, the fetcher allows only secure (https) schemes unless this
+// property is set, or the GTM_ALLOW_INSECURE_REQUESTS build flag is set.
+//
+// For example, during debugging when fetching from a development server that lacks SSL support,
+// this may be set to @[ @"http" ], or when the fetcher is used to retrieve local files,
+// this may be set to @[ @"file" ].
+//
+// This should be left as nil for release builds to avoid creating the opportunity for
+// leaking private user behavior and data. If a server is providing insecure URLs
+// for fetching by the client app, report the problem as server security & privacy bug.
+//
+// For builds with the iOS 9/OS X 10.11 and later SDKs, this property is required only when
+// the app specifies NSAppTransportSecurity/NSAllowsArbitraryLoads in the main bundle's Info.plist.
+@property(atomic, copy, GTM_NULLABLE) GTM_NSArrayOf(NSString *) *allowedInsecureSchemes;
+
+// By default, the fetcher prohibits localhost requests unless this property is set,
+// or the GTM_ALLOW_INSECURE_REQUESTS build flag is set.
+//
+// For localhost requests, the URL scheme is not checked when this property is set.
+//
+// For builds with the iOS 9/OS X 10.11 and later SDKs, this property is required only when
+// the app specifies NSAppTransportSecurity/NSAllowsArbitraryLoads in the main bundle's Info.plist.
+@property(atomic, assign) BOOL allowLocalhostRequest;
+
+// By default, the fetcher requires valid server certs. This may be bypassed
+// temporarily for development against a test server with an invalid cert.
+@property(atomic, assign) BOOL allowInvalidServerCertificates;
+
+// Cookie storage object for this fetcher. If nil, the fetcher will use a static cookie
+// storage instance shared among fetchers. If this fetcher was created by a fetcher service
+// object, it will be set to use the service object's cookie storage. See Cookies section above for
+// the full discussion.
+//
+// Because as of Jan 2014 standalone instances of NSHTTPCookieStorage do not actually
+// store any cookies (Radar 15735276) we use our own subclass, GTMSessionCookieStorage,
+// to hold cookies in memory.
+@property(atomic, strong, GTM_NULLABLE) NSHTTPCookieStorage *cookieStorage;
+
+// Setting the credential is optional; it is used if the connection receives
+// an authentication challenge.
+@property(atomic, strong, GTM_NULLABLE) NSURLCredential *credential;
+
+// Setting the proxy credential is optional; it is used if the connection
+// receives an authentication challenge from a proxy.
+@property(atomic, strong, GTM_NULLABLE) NSURLCredential *proxyCredential;
+
+// If body data, body file URL, or body stream provider is not set, then a GET request
+// method is assumed.
+@property(atomic, strong, GTM_NULLABLE) NSData *bodyData;
+
+// File to use as the request body. This forces use of an upload task.
+@property(atomic, strong, GTM_NULLABLE) NSURL *bodyFileURL;
+
+// Length of body to send, expected or actual.
+@property(atomic, readonly) int64_t bodyLength;
+
+// The body stream provider may be called repeatedly to provide a body.
+// Setting a body stream provider forces use of an upload task.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherBodyStreamProvider bodyStreamProvider;
+
+// Object to add authorization to the request, if needed.
+//
+// This may not be changed once beginFetch has been invoked.
+@property(atomic, strong, GTM_NULLABLE) id<GTMFetcherAuthorizationProtocol> authorizer;
+
+// The service object that created and monitors this fetcher, if any.
+@property(atomic, strong) id<GTMSessionFetcherServiceProtocol> service;
+
+// The host, if any, used to classify this fetcher in the fetcher service.
+@property(atomic, copy, GTM_NULLABLE) NSString *serviceHost;
+
+// The priority, if any, used for starting fetchers in the fetcher service.
+//
+// Lower values are higher priority; the default is 0, and values may
+// be negative or positive. This priority affects only the start order of
+// fetchers that are being delayed by a fetcher service when the running fetchers
+// exceeds the service's maxRunningFetchersPerHost. A priority of NSIntegerMin will
+// exempt this fetcher from delay.
+@property(atomic, assign) NSInteger servicePriority;
+
+// The delegate's optional didReceiveResponse block may be used to inspect or alter
+// the session task response.
+//
+// This is called on the callback queue.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock;
+
+// The delegate's optional challenge block may be used to inspect or alter
+// the session task challenge.
+//
+// If this block is not set, the fetcher's default behavior for the NSURLSessionTask
+// didReceiveChallenge: delegate method is to use the fetcher's respondToChallenge: method
+// which relies on the fetcher's credential and proxyCredential properties.
+//
+// Warning: This may be called repeatedly if the challenge fails. Check
+// challenge.previousFailureCount to identify repeated invocations.
+//
+// This is called on the callback queue.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherChallengeBlock challengeBlock;
+
+// The delegate's optional willRedirect block may be used to inspect or alter
+// the redirection.
+//
+// This is called on the callback queue.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherWillRedirectBlock willRedirectBlock;
+
+// The optional send progress block reports body bytes uploaded.
+//
+// This is called on the callback queue.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherSendProgressBlock sendProgressBlock;
+
+// The optional accumulate block may be set by clients wishing to accumulate data
+// themselves rather than let the fetcher append each buffer to an NSData.
+//
+// When this is called with nil data (such as on redirect) the client
+// should empty its accumulation buffer.
+//
+// This is called on the callback queue.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherAccumulateDataBlock accumulateDataBlock;
+
+// The optional received progress block may be used to monitor data
+// received from a data task.
+//
+// This is called on the callback queue.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherReceivedProgressBlock receivedProgressBlock;
+
+// The delegate's optional downloadProgress block may be used to monitor download
+// progress in writing to disk.
+//
+// This is called on the callback queue.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherDownloadProgressBlock downloadProgressBlock;
+
+// The delegate's optional willCacheURLResponse block may be used to alter the cached
+// NSURLResponse. The user may prevent caching by passing nil to the block's response.
+//
+// This is called on the callback queue.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock;
+
+// Enable retrying; see comments at the top of this file. Setting
+// retryEnabled=YES resets the min and max retry intervals.
+@property(atomic, assign, getter=isRetryEnabled) BOOL retryEnabled;
+
+// Retry block is optional for retries.
+//
+// If present, this block should call the response block with YES to cause a retry or NO to end the
+// fetch.
+// See comments at the top of this file.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherRetryBlock retryBlock;
+
+// The optional block for collecting the metrics of the present session.
+//
+// This is called on the callback queue.
+@property(atomic, copy, GTM_NULLABLE)
+ GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock API_AVAILABLE(
+ ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0));
+
+// Retry intervals must be strictly less than maxRetryInterval, else
+// they will be limited to maxRetryInterval and no further retries will
+// be attempted. Setting maxRetryInterval to 0.0 will reset it to the
+// default value, 60 seconds for downloads and 600 seconds for uploads.
+@property(atomic, assign) NSTimeInterval maxRetryInterval;
+
+// Starting retry interval. Setting minRetryInterval to 0.0 will reset it
+// to a random value between 1.0 and 2.0 seconds. Clients should normally not
+// set this except for unit testing.
+@property(atomic, assign) NSTimeInterval minRetryInterval;
+
+// Multiplier used to increase the interval between retries, typically 2.0.
+// Clients should not need to set this.
+@property(atomic, assign) double retryFactor;
+
+// Number of retries attempted.
+@property(atomic, readonly) NSUInteger retryCount;
+
+// Interval delay to precede next retry.
+@property(atomic, readonly) NSTimeInterval nextRetryInterval;
+
+#if GTM_BACKGROUND_TASK_FETCHING
+// Skip use of a UIBackgroundTask, thus requiring fetches to complete when the app is in the
+// foreground.
+//
+// Targets should define GTM_BACKGROUND_TASK_FETCHING to 0 to avoid use of a UIBackgroundTask
+// on iOS to allow fetches to complete in the background. This property is available when
+// it's not practical to set the preprocessor define.
+@property(atomic, assign) BOOL skipBackgroundTask;
+#endif // GTM_BACKGROUND_TASK_FETCHING
+
+// Begin fetching the request
+//
+// The delegate may optionally implement the callback or pass nil for the selector or handler.
+//
+// The delegate and all callback blocks are retained between the beginFetch call until after the
+// finish callback, or until the fetch is stopped.
+//
+// An error is passed to the callback for server statuses 300 or
+// higher, with the status stored as the error object's code.
+//
+// finishedSEL has a signature like:
+// - (void)fetcher:(GTMSessionFetcher *)fetcher
+// finishedWithData:(NSData *)data
+// error:(NSError *)error;
+//
+// If the application has specified a destinationFileURL or an accumulateDataBlock
+// for the fetcher, the data parameter passed to the callback will be nil.
+
+- (void)beginFetchWithDelegate:(GTM_NULLABLE id)delegate
+ didFinishSelector:(GTM_NULLABLE SEL)finishedSEL;
+
+- (void)beginFetchWithCompletionHandler:(GTM_NULLABLE GTMSessionFetcherCompletionHandler)handler;
+
+// Returns YES if this fetcher is in the process of fetching a URL.
+@property(atomic, readonly, getter=isFetching) BOOL fetching;
+
+// Cancel the fetch of the request that's currently in progress. The completion handler
+// will not be called.
+- (void)stopFetching;
+
+// A block to be called when the fetch completes.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherCompletionHandler completionHandler;
+
+// A block to be called if download resume data becomes available.
+@property(atomic, strong, GTM_NULLABLE) void (^resumeDataBlock)(NSData *);
+
+// Return the status code from the server response.
+@property(atomic, readonly) NSInteger statusCode;
+
+// Return the http headers from the response.
+@property(atomic, strong, readonly, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, NSString *) *responseHeaders;
+
+// The response, once it's been received.
+@property(atomic, strong, readonly, GTM_NULLABLE) NSURLResponse *response;
+
+// Bytes downloaded so far.
+@property(atomic, readonly) int64_t downloadedLength;
+
+// Buffer of currently-downloaded data, if available.
+@property(atomic, readonly, strong, GTM_NULLABLE) NSData *downloadedData;
+
+// Local path to which the downloaded file will be moved.
+//
+// If a file already exists at the path, it will be overwritten.
+// Will create the enclosing folders if they are not present.
+@property(atomic, strong, GTM_NULLABLE) NSURL *destinationFileURL;
+
+// The time this fetcher originally began fetching. This is useful as a time
+// barrier for ignoring irrelevant fetch notifications or callbacks.
+@property(atomic, strong, readonly, GTM_NULLABLE) NSDate *initialBeginFetchDate;
+
+// userData is retained solely for the convenience of the client.
+@property(atomic, strong, GTM_NULLABLE) id userData;
+
+// Stored property values are retained solely for the convenience of the client.
+@property(atomic, copy, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, id) *properties;
+
+- (void)setProperty:(GTM_NULLABLE id)obj forKey:(NSString *)key; // Pass nil for obj to remove the property.
+- (GTM_NULLABLE id)propertyForKey:(NSString *)key;
+
+- (void)addPropertiesFromDictionary:(GTM_NSDictionaryOf(NSString *, id) *)dict;
+
+// Comments are useful for logging, so are strongly recommended for each fetcher.
+@property(atomic, copy, GTM_NULLABLE) NSString *comment;
+
+- (void)setCommentWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2);
+
+// Log of request and response, if logging is enabled
+@property(atomic, copy, GTM_NULLABLE) NSString *log;
+
+// Callbacks are run on this queue. If none is supplied, the main queue is used.
+@property(atomic, strong, GTM_NULL_RESETTABLE) dispatch_queue_t callbackQueue;
+
+// The queue used internally by the session to invoke its delegate methods in the fetcher.
+//
+// Application callbacks are always called by the fetcher on the callbackQueue above,
+// not on this queue. Apps should generally not change this queue.
+//
+// The default delegate queue is the main queue.
+//
+// This value is ignored after the session has been created, so this
+// property should be set in the fetcher service rather in the fetcher as it applies
+// to a shared session.
+@property(atomic, strong, GTM_NULL_RESETTABLE) NSOperationQueue *sessionDelegateQueue;
+
+// Spin the run loop or sleep the thread, discarding events, until the fetch has completed.
+//
+// This is only for use in testing or in tools without a user interface.
+//
+// Note: Synchronous fetches should never be used by shipping apps; they are
+// sufficient reason for rejection from the app store.
+//
+// Returns NO if timed out.
+- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds;
+
+// Test block is optional for testing.
+//
+// If present, this block will cause the fetcher to skip starting the session, and instead
+// use the test block response values when calling the completion handler and delegate code.
+//
+// Test code can set this on the fetcher or on the fetcher service. For testing libraries
+// that use a fetcher without exposing either the fetcher or the fetcher service, the global
+// method setGlobalTestBlock: will set the block for all fetchers that do not have a test
+// block set.
+//
+// The test code can pass nil for all response parameters to indicate that the fetch
+// should proceed.
+//
+// Applications can exclude test block support by setting GTM_DISABLE_FETCHER_TEST_BLOCK.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherTestBlock testBlock;
+
++ (void)setGlobalTestBlock:(GTM_NULLABLE GTMSessionFetcherTestBlock)block;
+
+// When using the testBlock, |testBlockAccumulateDataChunkCount| is the desired number of chunks to
+// divide the response data into if the client has streaming enabled. The data will be divided up to
+// |testBlockAccumulateDataChunkCount| chunks; however, the exact amount may vary depending on the
+// size of the response data (e.g. a 1-byte response can only be divided into one chunk).
+@property(atomic, readwrite) NSUInteger testBlockAccumulateDataChunkCount;
+
+#if GTM_BACKGROUND_TASK_FETCHING
+// For testing or to override UIApplication invocations, apps may specify an alternative
+// target for messages to UIApplication.
++ (void)setSubstituteUIApplication:(nullable id<GTMUIApplicationProtocol>)substituteUIApplication;
++ (nullable id<GTMUIApplicationProtocol>)substituteUIApplication;
+#endif // GTM_BACKGROUND_TASK_FETCHING
+
+// Exposed for testing.
++ (GTMSessionCookieStorage *)staticCookieStorage;
++ (BOOL)appAllowsInsecureRequests;
+
+#if STRIP_GTM_FETCH_LOGGING
+// If logging is stripped, provide a stub for the main method
+// for controlling logging.
++ (void)setLoggingEnabled:(BOOL)flag;
++ (BOOL)isLoggingEnabled;
+
+#else
+
+// These methods let an application log specific body text, such as the text description of a binary
+// request or response. The application should set the fetcher to defer response body logging until
+// the response has been received and the log response body has been set by the app. For example:
+//
+// fetcher.logRequestBody = [binaryObject stringDescription];
+// fetcher.deferResponseBodyLogging = YES;
+// [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+// if (error == nil) {
+// fetcher.logResponseBody = [[[MyThing alloc] initWithData:data] stringDescription];
+// }
+// fetcher.deferResponseBodyLogging = NO;
+// }];
+
+@property(atomic, copy, GTM_NULLABLE) NSString *logRequestBody;
+@property(atomic, assign) BOOL deferResponseBodyLogging;
+@property(atomic, copy, GTM_NULLABLE) NSString *logResponseBody;
+
+// Internal logging support.
+@property(atomic, readonly) NSData *loggedStreamData;
+@property(atomic, assign) BOOL hasLoggedError;
+@property(atomic, strong, GTM_NULLABLE) NSURL *redirectedFromURL;
+- (void)appendLoggedStreamData:(NSData *)dataToAdd;
+- (void)clearLoggedStreamData;
+
+#endif // STRIP_GTM_FETCH_LOGGING
+
+@end
+
+@interface GTMSessionFetcher (BackwardsCompatibilityOnly)
+// Clients using GTMSessionFetcher should set the cookie storage explicitly themselves.
+// This method is just for compatibility with the old GTMHTTPFetcher class.
+- (void)setCookieStorageMethod:(NSInteger)method;
+@end
+
+// Until we can just instantiate NSHTTPCookieStorage for local use, we'll
+// implement all the public methods ourselves. This stores cookies only in
+// memory. Additional methods are provided for testing.
+//
+// iOS 9/OS X 10.11 added +[NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:]
+// which may also be used to create cookie storage.
+@interface GTMSessionCookieStorage : NSHTTPCookieStorage
+
+// Add the array off cookies to the storage, replacing duplicates.
+// Also removes expired cookies from the storage.
+- (void)setCookies:(GTM_NULLABLE GTM_NSArrayOf(NSHTTPCookie *) *)cookies;
+
+- (void)removeAllCookies;
+
+@end
+
+// Macros to monitor synchronization blocks in debug builds.
+// These report problems using GTMSessionCheckDebug.
+//
+// GTMSessionMonitorSynchronized Start monitoring a top-level-only
+// @sync scope.
+// GTMSessionMonitorRecursiveSynchronized Start monitoring a top-level or
+// recursive @sync scope.
+// GTMSessionCheckSynchronized Verify that the current execution
+// is inside a @sync scope.
+// GTMSessionCheckNotSynchronized Verify that the current execution
+// is not inside a @sync scope.
+//
+// Example usage:
+//
+// - (void)myExternalMethod {
+// @synchronized(self) {
+// GTMSessionMonitorSynchronized(self)
+//
+// - (void)myInternalMethod {
+// GTMSessionCheckSynchronized(self);
+//
+// - (void)callMyCallbacks {
+// GTMSessionCheckNotSynchronized(self);
+//
+// GTMSessionCheckNotSynchronized is available for verifying the code isn't
+// in a deadlockable @sync state when posting notifications and invoking
+// callbacks. Don't use GTMSessionCheckNotSynchronized immediately before a
+// @sync scope; the normal recursiveness check of GTMSessionMonitorSynchronized
+// can catch those.
+
+#ifdef __OBJC__
+// If asserts are entirely no-ops, the synchronization monitor is just a bunch
+// of counting code that doesn't report exceptional circumstances in any way.
+// Only build the synchronization monitor code if NS_BLOCK_ASSERTIONS is not
+// defined or asserts are being logged instead.
+#if DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG)
+ #define __GTMSessionMonitorSynchronizedVariableInner(varname, counter) \
+ varname ## counter
+ #define __GTMSessionMonitorSynchronizedVariable(varname, counter) \
+ __GTMSessionMonitorSynchronizedVariableInner(varname, counter)
+
+ #define GTMSessionMonitorSynchronized(obj) \
+ NS_VALID_UNTIL_END_OF_SCOPE id \
+ __GTMSessionMonitorSynchronizedVariable(__monitor, __COUNTER__) = \
+ [[GTMSessionSyncMonitorInternal alloc] initWithSynchronizationObject:obj \
+ allowRecursive:NO \
+ functionName:__func__]
+
+ #define GTMSessionMonitorRecursiveSynchronized(obj) \
+ NS_VALID_UNTIL_END_OF_SCOPE id \
+ __GTMSessionMonitorSynchronizedVariable(__monitor, __COUNTER__) = \
+ [[GTMSessionSyncMonitorInternal alloc] initWithSynchronizationObject:obj \
+ allowRecursive:YES \
+ functionName:__func__]
+
+ #define GTMSessionCheckSynchronized(obj) { \
+ GTMSESSION_ASSERT_DEBUG( \
+ [GTMSessionSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \
+ @"GTMSessionCheckSynchronized(" #obj ") failed: not sync'd" \
+ @" on " #obj " in %s. Call stack:\n%@", \
+ __func__, [NSThread callStackSymbols]); \
+ }
+
+ #define GTMSessionCheckNotSynchronized(obj) { \
+ GTMSESSION_ASSERT_DEBUG( \
+ ![GTMSessionSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \
+ @"GTMSessionCheckNotSynchronized(" #obj ") failed: was sync'd" \
+ @" on " #obj " in %s by %@. Call stack:\n%@", __func__, \
+ [GTMSessionSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \
+ [NSThread callStackSymbols]); \
+ }
+
+// GTMSessionSyncMonitorInternal is a private class that keeps track of the
+// beginning and end of synchronized scopes.
+//
+// This class should not be used directly, but only via the
+// GTMSessionMonitorSynchronized macro.
+@interface GTMSessionSyncMonitorInternal : NSObject
+- (instancetype)initWithSynchronizationObject:(id)object
+ allowRecursive:(BOOL)allowRecursive
+ functionName:(const char *)functionName;
+// Return the names of the functions that hold sync on the object, or nil if none.
++ (NSArray *)functionsHoldingSynchronizationOnObject:(id)object;
+@end
+
+#else
+ #define GTMSessionMonitorSynchronized(obj) do { } while (0)
+ #define GTMSessionMonitorRecursiveSynchronized(obj) do { } while (0)
+ #define GTMSessionCheckSynchronized(obj) do { } while (0)
+ #define GTMSessionCheckNotSynchronized(obj) do { } while (0)
+#endif // !DEBUG
+#endif // __OBJC__
+
+
+GTM_ASSUME_NONNULL_END
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m
new file mode 100644
index 00000000..3dc64597
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m
@@ -0,0 +1,4670 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "GTMSessionFetcher.h"
+#if TARGET_OS_OSX && GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH
+// To reconnect background sessions on Mac outside +load requires importing and linking
+// AppKit to access the NSApplicationDidFinishLaunching symbol.
+#import <AppKit/AppKit.h>
+#endif
+
+#import <sys/utsname.h>
+
+#ifndef STRIP_GTM_FETCH_LOGGING
+ #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
+#endif
+
+GTM_ASSUME_NONNULL_BEGIN
+
+NSString *const kGTMSessionFetcherStartedNotification = @"kGTMSessionFetcherStartedNotification";
+NSString *const kGTMSessionFetcherStoppedNotification = @"kGTMSessionFetcherStoppedNotification";
+NSString *const kGTMSessionFetcherRetryDelayStartedNotification = @"kGTMSessionFetcherRetryDelayStartedNotification";
+NSString *const kGTMSessionFetcherRetryDelayStoppedNotification = @"kGTMSessionFetcherRetryDelayStoppedNotification";
+
+NSString *const kGTMSessionFetcherCompletionInvokedNotification = @"kGTMSessionFetcherCompletionInvokedNotification";
+NSString *const kGTMSessionFetcherCompletionDataKey = @"data";
+NSString *const kGTMSessionFetcherCompletionErrorKey = @"error";
+
+NSString *const kGTMSessionFetcherErrorDomain = @"com.google.GTMSessionFetcher";
+NSString *const kGTMSessionFetcherStatusDomain = @"com.google.HTTPStatus";
+NSString *const kGTMSessionFetcherStatusDataKey = @"data"; // data returned with a kGTMSessionFetcherStatusDomain error
+NSString *const kGTMSessionFetcherStatusDataContentTypeKey = @"data_content_type";
+
+NSString *const kGTMSessionFetcherNumberOfRetriesDoneKey = @"kGTMSessionFetcherNumberOfRetriesDoneKey";
+NSString *const kGTMSessionFetcherElapsedIntervalWithRetriesKey = @"kGTMSessionFetcherElapsedIntervalWithRetriesKey";
+
+static NSString *const kGTMSessionIdentifierPrefix = @"com.google.GTMSessionFetcher";
+static NSString *const kGTMSessionIdentifierDestinationFileURLMetadataKey = @"_destURL";
+static NSString *const kGTMSessionIdentifierBodyFileURLMetadataKey = @"_bodyURL";
+
+// The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH),
+// 1 minute for downloads.
+static const NSTimeInterval kUnsetMaxRetryInterval = -1.0;
+static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0;
+static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.;
+
+// The maximum data length that can be loaded to the error userInfo
+static const int64_t kMaximumDownloadErrorDataLength = 20000;
+
+#ifdef GTMSESSION_PERSISTED_DESTINATION_KEY
+// Projects using unique class names should also define a unique persisted destination key.
+static NSString * const kGTMSessionFetcherPersistedDestinationKey =
+ GTMSESSION_PERSISTED_DESTINATION_KEY;
+#else
+static NSString * const kGTMSessionFetcherPersistedDestinationKey =
+ @"com.google.GTMSessionFetcher.downloads";
+#endif
+
+GTM_ASSUME_NONNULL_END
+
+//
+// GTMSessionFetcher
+//
+
+#if 0
+#define GTM_LOG_BACKGROUND_SESSION(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__)
+#else
+#define GTM_LOG_BACKGROUND_SESSION(...)
+#endif
+
+#ifndef GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
+ #if (TARGET_OS_TV \
+ || TARGET_OS_WATCH \
+ || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \
+ || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0))
+ #define GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY 1
+ #endif
+#endif
+
+#if ((defined(TARGET_OS_MACCATALYST) && TARGET_OS_MACCATALYST) || \
+ (TARGET_OS_OSX && defined(__MAC_10_15) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_15) || \
+ (TARGET_OS_IOS && defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_13_0) || \
+ (TARGET_OS_WATCH && defined(__WATCHOS_6_0) && __WATCHOS_VERSION_MIN_REQUIRED >= __WATCHOS_6_0) || \
+ (TARGET_OS_TV && defined(__TVOS_13_0) && __TVOS_VERSION_MIN_REQUIRED >= __TVOS_13_0))
+#define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1
+#define GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1
+#elif ((TARGET_OS_OSX && defined(__MAC_10_15) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_15) || \
+ (TARGET_OS_IOS && defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0) || \
+ (TARGET_OS_WATCH && defined(__WATCHOS_6_0) && __WATCHOS_VERSION_MAX_ALLOWED >= __WATCHOS_6_0) || \
+ (TARGET_OS_TV && defined(__TVOS_13_0) && __TVOS_VERSION_MAX_ALLOWED >= __TVOS_13_0))
+#define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 0
+#define GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1
+#else
+#define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 0
+#define GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 0
+#endif
+
+#if ((defined(TARGET_OS_MACCATALYST) && TARGET_OS_MACCATALYST) || \
+ (TARGET_OS_OSX && defined(__MAC_10_15) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_15) || \
+ (TARGET_OS_IOS && defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_13_0) || \
+ (TARGET_OS_WATCH && defined(__WATCHOS_6_0) && __WATCHOS_VERSION_MIN_REQUIRED >= __WATCHOS_6_0) || \
+ (TARGET_OS_TV && defined(__TVOS_13_0) && __TVOS_VERSION_MIN_REQUIRED >= __TVOS_13_0))
+#define GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR 1
+#else
+#define GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR 0
+#endif
+
+@interface GTMSessionFetcher ()
+
+@property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadedData;
+@property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadResumeData;
+
+#if GTM_BACKGROUND_TASK_FETCHING
+// Should always be accessed within an @synchronized(self).
+@property(assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
+#endif
+
+@property(atomic, readwrite, getter=isUsingBackgroundSession) BOOL usingBackgroundSession;
+
+@end
+
+#if !GTMSESSION_BUILD_COMBINED_SOURCES
+@interface GTMSessionFetcher (GTMSessionFetcherLoggingInternal)
+- (void)logFetchWithError:(NSError *)error;
+- (void)logNowWithError:(GTM_NULLABLE NSError *)error;
+- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream;
+- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
+ (GTMSessionFetcherBodyStreamProvider)streamProvider;
+@end
+#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
+
+GTM_ASSUME_NONNULL_BEGIN
+
+static NSTimeInterval InitialMinRetryInterval(void) {
+ return 1.0 + ((double)(arc4random_uniform(0x0FFFF)) / (double) 0x0FFFF);
+}
+
+static BOOL IsLocalhost(NSString * GTM_NULLABLE_TYPE host) {
+ // We check if there's host, and then make the comparisons.
+ if (host == nil) return NO;
+ return ([host caseInsensitiveCompare:@"localhost"] == NSOrderedSame
+ || [host isEqual:@"::1"]
+ || [host isEqual:@"127.0.0.1"]);
+}
+
+static NSDictionary *GTM_NULLABLE_TYPE GTMErrorUserInfoForData(
+ NSData *GTM_NULLABLE_TYPE data, NSDictionary *GTM_NULLABLE_TYPE responseHeaders) {
+ NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
+
+ if (data.length > 0) {
+ userInfo[kGTMSessionFetcherStatusDataKey] = data;
+
+ NSString *contentType = responseHeaders[@"Content-Type"];
+ if (contentType) {
+ userInfo[kGTMSessionFetcherStatusDataContentTypeKey] = contentType;
+ }
+ }
+
+ return userInfo.count > 0 ? userInfo : nil;
+}
+
+static GTMSessionFetcherTestBlock GTM_NULLABLE_TYPE gGlobalTestBlock;
+
+@implementation GTMSessionFetcher {
+ NSMutableURLRequest *_request; // after beginFetch, changed only in delegate callbacks
+ BOOL _useUploadTask; // immutable after beginFetch
+ NSURL *_bodyFileURL; // immutable after beginFetch
+ GTMSessionFetcherBodyStreamProvider _bodyStreamProvider; // immutable after beginFetch
+ NSURLSession *_session;
+ BOOL _shouldInvalidateSession; // immutable after beginFetch
+ NSURLSession *_sessionNeedingInvalidation;
+ NSURLSessionConfiguration *_configuration;
+ NSURLSessionTask *_sessionTask;
+ NSString *_taskDescription;
+ float _taskPriority;
+ NSURLResponse *_response;
+ NSString *_sessionIdentifier;
+ BOOL _wasCreatedFromBackgroundSession;
+ BOOL _didCreateSessionIdentifier;
+ NSString *_sessionIdentifierUUID;
+ BOOL _userRequestedBackgroundSession;
+ BOOL _usingBackgroundSession;
+ NSMutableData * GTM_NULLABLE_TYPE _downloadedData;
+ NSError *_downloadFinishedError;
+ NSData *_downloadResumeData; // immutable after construction
+ NSData * GTM_NULLABLE_TYPE _downloadTaskErrorData; // Data for when download task fails
+ NSURL *_destinationFileURL;
+ int64_t _downloadedLength;
+ NSURLCredential *_credential; // username & password
+ NSURLCredential *_proxyCredential; // credential supplied to proxy servers
+ BOOL _isStopNotificationNeeded; // set when start notification has been sent
+ BOOL _isUsingTestBlock; // set when a test block was provided (remains set when the block is released)
+ id _userData; // retained, if set by caller
+ NSMutableDictionary *_properties; // more data retained for caller
+ dispatch_queue_t _callbackQueue;
+ dispatch_group_t _callbackGroup; // read-only after creation
+ NSOperationQueue *_delegateQueue; // immutable after beginFetch
+
+ id<GTMFetcherAuthorizationProtocol> _authorizer; // immutable after beginFetch
+
+ // The service object that created and monitors this fetcher, if any.
+ id<GTMSessionFetcherServiceProtocol> _service; // immutable; set by the fetcher service upon creation
+ NSString *_serviceHost;
+ NSInteger _servicePriority; // immutable after beginFetch
+ BOOL _hasStoppedFetching; // counterpart to _initialBeginFetchDate
+ BOOL _userStoppedFetching;
+
+ BOOL _isRetryEnabled; // user wants auto-retry
+ NSTimer *_retryTimer;
+ NSUInteger _retryCount;
+ NSTimeInterval _maxRetryInterval; // default 60 (download) or 600 (upload) seconds
+ NSTimeInterval _minRetryInterval; // random between 1 and 2 seconds
+ NSTimeInterval _retryFactor; // default interval multiplier is 2
+ NSTimeInterval _lastRetryInterval;
+ NSDate *_initialBeginFetchDate; // date that beginFetch was first invoked; immutable after initial beginFetch
+ NSDate *_initialRequestDate; // date of first request to the target server (ignoring auth)
+ BOOL _hasAttemptedAuthRefresh; // accessed only in shouldRetryNowForStatus:
+
+ NSString *_comment; // comment for log
+ NSString *_log;
+#if !STRIP_GTM_FETCH_LOGGING
+ NSMutableData *_loggedStreamData;
+ NSURL *_redirectedFromURL;
+ NSString *_logRequestBody;
+ NSString *_logResponseBody;
+ BOOL _hasLoggedError;
+ BOOL _deferResponseBodyLogging;
+#endif
+}
+
+#if !GTMSESSION_UNIT_TESTING
++ (void)load {
+#if GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_IPHONE
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc addObserver:self
+ selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:)
+ name:UIApplicationDidFinishLaunchingNotification
+ object:nil];
+#elif GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_OSX
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc addObserver:self
+ selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:)
+ name:NSApplicationDidFinishLaunchingNotification
+ object:nil];
+#else
+ [self fetchersForBackgroundSessions];
+#endif
+}
+
++ (void)reconnectFetchersForBackgroundSessionsOnAppLaunch:(NSNotification *)notification {
+ // Give all other app-did-launch handlers a chance to complete before
+ // reconnecting the fetchers. Not doing this may lead to reconnecting
+ // before the app delegate has a chance to run.
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self fetchersForBackgroundSessions];
+ });
+}
+#endif // !GTMSESSION_UNIT_TESTING
+
++ (instancetype)fetcherWithRequest:(GTM_NULLABLE NSURLRequest *)request {
+ return [[self alloc] initWithRequest:request configuration:nil];
+}
+
++ (instancetype)fetcherWithURL:(NSURL *)requestURL {
+ return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
+}
+
++ (instancetype)fetcherWithURLString:(NSString *)requestURLString {
+ return [self fetcherWithURL:(NSURL *)[NSURL URLWithString:requestURLString]];
+}
+
++ (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData {
+ GTMSessionFetcher *fetcher = [self fetcherWithRequest:nil];
+ fetcher.comment = @"Resuming download";
+ fetcher.downloadResumeData = resumeData;
+ return fetcher;
+}
+
++ (GTM_NULLABLE instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
+ GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
+ NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
+ GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
+ if (!fetcher && [sessionIdentifier hasPrefix:kGTMSessionIdentifierPrefix]) {
+ fetcher = [self fetcherWithRequest:nil];
+ [fetcher setSessionIdentifier:sessionIdentifier];
+ [sessionIdentifierToFetcherMap setObject:fetcher forKey:sessionIdentifier];
+ fetcher->_wasCreatedFromBackgroundSession = YES;
+ [fetcher setCommentWithFormat:@"Resuming %@",
+ fetcher && fetcher->_sessionIdentifierUUID ? fetcher->_sessionIdentifierUUID : @"?"];
+ }
+ return fetcher;
+}
+
++ (NSMapTable *)sessionIdentifierToFetcherMap {
+ // TODO: What if a service is involved in creating the fetcher? Currently, when re-creating
+ // fetchers, if a service was involved, it is not re-created. Should the service maintain a map?
+ static NSMapTable *gSessionIdentifierToFetcherMap = nil;
+
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ gSessionIdentifierToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
+ });
+ return gSessionIdentifierToFetcherMap;
+}
+
+#if !GTM_ALLOW_INSECURE_REQUESTS
++ (BOOL)appAllowsInsecureRequests {
+ // If the main bundle Info.plist key NSAppTransportSecurity is present, and it specifies
+ // NSAllowsArbitraryLoads, then we need to explicitly enforce secure schemes.
+#if GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
+ static BOOL allowsInsecureRequests;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSBundle *mainBundle = [NSBundle mainBundle];
+ NSDictionary *appTransportSecurity =
+ [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"];
+ allowsInsecureRequests =
+ [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue];
+ });
+ return allowsInsecureRequests;
+#else
+ // For builds targeting iOS 8 or 10.10 and earlier, we want to require fetcher
+ // security checks.
+ return YES;
+#endif // GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
+}
+#else // GTM_ALLOW_INSECURE_REQUESTS
++ (BOOL)appAllowsInsecureRequests {
+ return YES;
+}
+#endif // !GTM_ALLOW_INSECURE_REQUESTS
+
+
+- (instancetype)init {
+ return [self initWithRequest:nil configuration:nil];
+}
+
+- (instancetype)initWithRequest:(NSURLRequest *)request {
+ return [self initWithRequest:request configuration:nil];
+}
+
+- (instancetype)initWithRequest:(GTM_NULLABLE NSURLRequest *)request
+ configuration:(GTM_NULLABLE NSURLSessionConfiguration *)configuration {
+ self = [super init];
+ if (self) {
+#if GTM_BACKGROUND_TASK_FETCHING
+ _backgroundTaskIdentifier = UIBackgroundTaskInvalid;
+#endif
+ _request = [request mutableCopy];
+ _configuration = configuration;
+
+ NSData *bodyData = request.HTTPBody;
+ if (bodyData) {
+ _bodyLength = (int64_t)bodyData.length;
+ } else {
+ _bodyLength = NSURLSessionTransferSizeUnknown;
+ }
+
+ _callbackQueue = dispatch_get_main_queue();
+ _callbackGroup = dispatch_group_create();
+ _delegateQueue = [NSOperationQueue mainQueue];
+
+ _minRetryInterval = InitialMinRetryInterval();
+ _maxRetryInterval = kUnsetMaxRetryInterval;
+
+ _taskPriority = -1.0f; // Valid values if set are 0.0...1.0.
+
+ _testBlockAccumulateDataChunkCount = 1;
+
+#if !STRIP_GTM_FETCH_LOGGING
+ // Encourage developers to set the comment property or use
+ // setCommentWithFormat: by providing a default string.
+ _comment = @"(No fetcher comment set)";
+#endif
+ }
+ return self;
+}
+
+- (id)copyWithZone:(NSZone *)zone {
+ // disallow use of fetchers in a copy property
+ [self doesNotRecognizeSelector:_cmd];
+ return nil;
+}
+
+- (NSString *)description {
+ NSString *requestStr = self.request.URL.description;
+ if (requestStr.length == 0) {
+ if (self.downloadResumeData.length > 0) {
+ requestStr = @"<download resume data>";
+ } else if (_wasCreatedFromBackgroundSession) {
+ requestStr = @"<from bg session>";
+ } else {
+ requestStr = @"<no request>";
+ }
+ }
+ return [NSString stringWithFormat:@"%@ %p (%@)", [self class], self, requestStr];
+}
+
+- (void)dealloc {
+ GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded,
+ @"unbalanced fetcher notification for %@", _request.URL);
+ [self forgetSessionIdentifierForFetcherWithoutSyncCheck];
+
+ // Note: if a session task or a retry timer was pending, then this instance
+ // would be retained by those so it wouldn't be getting dealloc'd,
+ // hence we don't need to stopFetch here
+}
+
+#pragma mark -
+
+// Begin fetching the URL (or begin a retry fetch). The delegate is retained
+// for the duration of the fetch connection.
+
+- (void)beginFetchWithCompletionHandler:(GTM_NULLABLE GTMSessionFetcherCompletionHandler)handler {
+ GTMSessionCheckNotSynchronized(self);
+
+ _completionHandler = [handler copy];
+
+ // The user may have called setDelegate: earlier if they want to use other
+ // delegate-style callbacks during the fetch; otherwise, the delegate is nil,
+ // which is fine.
+ [self beginFetchMayDelay:YES mayAuthorize:YES];
+}
+
+// Begin fetching the URL for a retry fetch. The delegate and completion handler
+// are already provided, and do not need to be copied.
+- (void)beginFetchForRetry {
+ GTMSessionCheckNotSynchronized(self);
+
+ [self beginFetchMayDelay:YES mayAuthorize:YES];
+}
+
+- (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(GTM_NULLABLE_TYPE id)target
+ didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector {
+ GTMSessionFetcherAssertValidSelector(target, finishedSelector, @encode(GTMSessionFetcher *),
+ @encode(NSData *), @encode(NSError *), 0);
+ GTMSessionFetcherCompletionHandler completionHandler = ^(NSData *data, NSError *error) {
+ if (target && finishedSelector) {
+ id selfArg = self; // Placate ARC.
+ NSMethodSignature *sig = [target methodSignatureForSelector:finishedSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:(SEL)finishedSelector];
+ [invocation setTarget:target];
+ [invocation setArgument:&selfArg atIndex:2];
+ [invocation setArgument:&data atIndex:3];
+ [invocation setArgument:&error atIndex:4];
+ [invocation invoke];
+ }
+ };
+ return completionHandler;
+}
+
+- (void)beginFetchWithDelegate:(GTM_NULLABLE_TYPE id)target
+ didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector {
+ GTMSessionCheckNotSynchronized(self);
+
+ GTMSessionFetcherCompletionHandler handler = [self completionHandlerWithTarget:target
+ didFinishSelector:finishedSelector];
+ [self beginFetchWithCompletionHandler:handler];
+}
+
+- (void)beginFetchMayDelay:(BOOL)mayDelay
+ mayAuthorize:(BOOL)mayAuthorize {
+ // This is the internal entry point for re-starting fetches.
+ GTMSessionCheckNotSynchronized(self);
+
+ NSMutableURLRequest *fetchRequest = _request; // The request property is now externally immutable.
+ NSURL *fetchRequestURL = fetchRequest.URL;
+ NSString *priorSessionIdentifier = self.sessionIdentifier;
+
+ // A utility block for creating error objects when we fail to start the fetch.
+ NSError *(^beginFailureError)(NSInteger) = ^(NSInteger code){
+ NSString *urlString = fetchRequestURL.absoluteString;
+ NSDictionary *userInfo = @{
+ NSURLErrorFailingURLStringErrorKey : (urlString ? urlString : @"(missing URL)")
+ };
+ return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
+ code:code
+ userInfo:userInfo];
+ };
+
+ // Catch delegate queue maxConcurrentOperationCount values other than 1, particularly
+ // NSOperationQueueDefaultMaxConcurrentOperationCount (-1), to avoid the additional complexity
+ // of simultaneous or out-of-order delegate callbacks.
+ GTMSESSION_ASSERT_DEBUG(_delegateQueue.maxConcurrentOperationCount == 1,
+ @"delegate queue %@ should support one concurrent operation, not %ld",
+ _delegateQueue.name,
+ (long)_delegateQueue.maxConcurrentOperationCount);
+
+ if (!_initialBeginFetchDate) {
+ // This ivar is set only here on the initial beginFetch so need not be synchronized.
+ _initialBeginFetchDate = [[NSDate alloc] init];
+ }
+
+ if (self.sessionTask != nil) {
+ // If cached fetcher returned through fetcherWithSessionIdentifier:, then it's
+ // already begun, but don't consider this a failure, since the user need not know this.
+ if (self.sessionIdentifier != nil) {
+ return;
+ }
+ GTMSESSION_ASSERT_DEBUG(NO, @"Fetch object %@ being reused; this should never happen", self);
+ [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
+ return;
+ }
+
+ if (fetchRequestURL == nil && !_downloadResumeData && !priorSessionIdentifier) {
+ GTMSESSION_ASSERT_DEBUG(NO, @"Beginning a fetch requires a request with a URL");
+ [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
+ return;
+ }
+
+ // We'll respect the user's request for a background session (unless this is
+ // an upload fetcher, which does its initial request foreground.)
+ self.usingBackgroundSession = self.useBackgroundSession && [self canFetchWithBackgroundSession];
+
+ NSURL *bodyFileURL = self.bodyFileURL;
+ if (bodyFileURL) {
+ NSError *fileCheckError;
+ if (![bodyFileURL checkResourceIsReachableAndReturnError:&fileCheckError]) {
+ // This assert fires when the file being uploaded no longer exists once
+ // the fetcher is ready to start the upload.
+ GTMSESSION_ASSERT_DEBUG_OR_LOG(0, @"Body file is unreachable: %@\n %@",
+ bodyFileURL.path, fileCheckError);
+ [self failToBeginFetchWithError:fileCheckError];
+ return;
+ }
+ }
+
+ NSString *requestScheme = fetchRequestURL.scheme;
+ BOOL isDataRequest = [requestScheme isEqual:@"data"];
+ if (isDataRequest) {
+ // NSURLSession does not support data URLs in background sessions.
+#if DEBUG
+ if (priorSessionIdentifier || self.sessionIdentifier) {
+ GTMSESSION_LOG_DEBUG(@"Converting background to foreground session for %@",
+ fetchRequest);
+ }
+#endif
+ // If priorSessionIdentifier is allowed to stay non-nil, a background session can
+ // still be created.
+ priorSessionIdentifier = nil;
+ [self setSessionIdentifierInternal:nil];
+ self.usingBackgroundSession = NO;
+ }
+
+#if GTM_ALLOW_INSECURE_REQUESTS
+ BOOL shouldCheckSecurity = NO;
+#else
+ BOOL shouldCheckSecurity = (fetchRequestURL != nil
+ && !isDataRequest
+ && [[self class] appAllowsInsecureRequests]);
+#endif
+
+ if (shouldCheckSecurity) {
+ // Allow https only for requests, unless overridden by the client.
+ //
+ // Non-https requests may too easily be snooped, so we disallow them by default.
+ //
+ // file: and data: schemes are usually safe if they are hardcoded in the client or provided
+ // by a trusted source, but since it's fairly rare to need them, it's safest to make clients
+ // explicitly whitelist them.
+ BOOL isSecure =
+ requestScheme != nil && [requestScheme caseInsensitiveCompare:@"https"] == NSOrderedSame;
+ if (!isSecure) {
+ BOOL allowRequest = NO;
+ NSString *host = fetchRequestURL.host;
+
+ // Check schemes first. A file scheme request may be allowed here, or as a localhost request.
+ for (NSString *allowedScheme in _allowedInsecureSchemes) {
+ if (requestScheme != nil &&
+ [requestScheme caseInsensitiveCompare:allowedScheme] == NSOrderedSame) {
+ allowRequest = YES;
+ break;
+ }
+ }
+ if (!allowRequest) {
+ // Check for localhost requests. Security checks only occur for non-https requests, so
+ // this check won't happen for an https request to localhost.
+ BOOL isLocalhostRequest = (host.length == 0 && [fetchRequestURL isFileURL]) || IsLocalhost(host);
+ if (isLocalhostRequest) {
+ if (self.allowLocalhostRequest) {
+ allowRequest = YES;
+ } else {
+ GTMSESSION_ASSERT_DEBUG(NO, @"Fetch request for localhost but fetcher"
+ @" allowLocalhostRequest is not set: %@", fetchRequestURL);
+ }
+ } else {
+ GTMSESSION_ASSERT_DEBUG(NO, @"Insecure fetch request has a scheme (%@)"
+ @" not found in fetcher allowedInsecureSchemes (%@): %@",
+ requestScheme, _allowedInsecureSchemes ?: @" @[] ", fetchRequestURL);
+ }
+ }
+
+ if (!allowRequest) {
+#if !DEBUG
+ NSLog(@"Insecure fetch disallowed for %@", fetchRequestURL.description ?: @"nil request URL");
+#endif
+ [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorInsecureRequest)];
+ return;
+ }
+ } // !isSecure
+ } // (requestURL != nil) && !isDataRequest
+
+ if (self.cookieStorage == nil) {
+ self.cookieStorage = [[self class] staticCookieStorage];
+ }
+
+ BOOL isRecreatingSession = (self.sessionIdentifier != nil) && (fetchRequest == nil);
+
+ self.canShareSession = !isRecreatingSession && !self.usingBackgroundSession;
+
+ if (!self.session && self.canShareSession) {
+ self.session = [_service sessionForFetcherCreation];
+ // If _session is nil, then the service's session creation semaphore will block
+ // until this fetcher invokes fetcherDidCreateSession: below, so this *must* invoke
+ // that method, even if the session fails to be created.
+ }
+
+ if (!self.session) {
+ // Create a session.
+ if (!_configuration) {
+ if (priorSessionIdentifier || self.usingBackgroundSession) {
+ NSString *sessionIdentifier = priorSessionIdentifier;
+ if (!sessionIdentifier) {
+ sessionIdentifier = [self createSessionIdentifierWithMetadata:nil];
+ }
+ NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap];
+ [sessionIdentifierToFetcherMap setObject:self forKey:self.sessionIdentifier];
+
+ if (@available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.10, *)) {
+ _configuration =
+ [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier];
+ } else {
+#if ((!TARGET_OS_IPHONE && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) \
+ || (TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0))
+ // If building with support for iOS 7 or < macOS 10.10, allow using the older
+ // -backgroundSessionConfiguration: method, otherwise leave it out to avoid deprecated
+ // API warnings/errors.
+ _configuration =
+ [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier];
+#endif
+ }
+ self.usingBackgroundSession = YES;
+ self.canShareSession = NO;
+ } else {
+ _configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
+ }
+#if !GTM_ALLOW_INSECURE_REQUESTS
+#if GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION
+ _configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
+#elif GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION
+ if (@available(iOS 13, tvOS 13, watchOS 6, macOS 10.15, *)) {
+#if TARGET_OS_IOS
+ // Early seeds of iOS 13 don't actually support the selector and several
+ // months later, those seeds are still in use, so validate if the selector
+ // is supported.
+ if ([_configuration respondsToSelector:@selector(setTLSMinimumSupportedProtocolVersion:)]) {
+ _configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
+ } else {
+ _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12;
+ }
+#else
+ _configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
+#endif // TARGET_OS_IOS
+ } else {
+ _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12;
+ }
+#else
+ _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12;
+#endif // GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION
+#endif
+ } // !_configuration
+ _configuration.HTTPCookieStorage = self.cookieStorage;
+
+ if (_configurationBlock) {
+ _configurationBlock(self, _configuration);
+ }
+
+ id<NSURLSessionDelegate> delegate = [_service sessionDelegate];
+ if (!delegate || !self.canShareSession) {
+ delegate = self;
+ }
+ self.session = [NSURLSession sessionWithConfiguration:_configuration
+ delegate:delegate
+ delegateQueue:self.sessionDelegateQueue];
+ GTMSESSION_ASSERT_DEBUG(self.session, @"Couldn't create session");
+
+ // Tell the service about the session created by this fetcher. This also signals the
+ // service's semaphore to allow other fetchers to request this session.
+ [_service fetcherDidCreateSession:self];
+
+ // If this assertion fires, the client probably tried to use a session identifier that was
+ // already used. The solution is to make the client use a unique identifier (or better yet let
+ // the session fetcher assign the identifier).
+ GTMSESSION_ASSERT_DEBUG(self.session.delegate == delegate, @"Couldn't assign delegate.");
+
+ if (self.session) {
+ BOOL isUsingSharedDelegate = (delegate != self);
+ if (!isUsingSharedDelegate) {
+ _shouldInvalidateSession = YES;
+ }
+ }
+ }
+
+ if (isRecreatingSession) {
+ _shouldInvalidateSession = YES;
+
+ // Let's make sure there are tasks still running or if not that we get a callback from a
+ // completed one; otherwise, we assume the tasks failed.
+ // This is the observed behavior perhaps 25% of the time within the Simulator running 7.0.3 on
+ // exiting the app after starting an upload and relaunching the app if we manage to relaunch
+ // after the task has completed, but before the system relaunches us in the background.
+ [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks,
+ NSArray *downloadTasks) {
+ if (dataTasks.count == 0 && uploadTasks.count == 0 && downloadTasks.count == 0) {
+ double const kDelayInSeconds = 1.0; // We should get progress indication or completion soon
+ dispatch_time_t checkForFeedbackDelay =
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDelayInSeconds * NSEC_PER_SEC));
+ dispatch_after(checkForFeedbackDelay, dispatch_get_main_queue(), ^{
+ if (!self.sessionTask && !fetchRequest) {
+ // If our task and/or request haven't been restored, then we assume task feedback lost.
+ [self removePersistedBackgroundSessionFromDefaults];
+ NSError *sessionError =
+ [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
+ code:GTMSessionFetcherErrorBackgroundFetchFailed
+ userInfo:nil];
+ [self failToBeginFetchWithError:sessionError];
+ }
+ });
+ }
+ }];
+ return;
+ }
+
+ self.downloadedData = nil;
+ self.downloadedLength = 0;
+
+ if (_servicePriority == NSIntegerMin) {
+ mayDelay = NO;
+ }
+ if (mayDelay && _service) {
+ BOOL shouldFetchNow = [_service fetcherShouldBeginFetching:self];
+ if (!shouldFetchNow) {
+ // The fetch is deferred, but will happen later.
+ //
+ // If this session is held by the fetcher service, clear the session now so that we don't
+ // assume it's still valid after the fetcher is restarted.
+ if (self.canShareSession) {
+ self.session = nil;
+ }
+ return;
+ }
+ }
+
+ NSString *effectiveHTTPMethod = [fetchRequest valueForHTTPHeaderField:@"X-HTTP-Method-Override"];
+ if (effectiveHTTPMethod == nil) {
+ effectiveHTTPMethod = fetchRequest.HTTPMethod;
+ }
+ BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil
+ || [effectiveHTTPMethod isEqual:@"GET"]);
+
+ BOOL needsUploadTask = (self.useUploadTask || self.bodyFileURL || self.bodyStreamProvider);
+ if (_bodyData || self.bodyStreamProvider || fetchRequest.HTTPBodyStream) {
+ if (isEffectiveHTTPGet) {
+ fetchRequest.HTTPMethod = @"POST";
+ isEffectiveHTTPGet = NO;
+ }
+
+ if (_bodyData) {
+ if (!needsUploadTask) {
+ fetchRequest.HTTPBody = _bodyData;
+ }
+#if !STRIP_GTM_FETCH_LOGGING
+ } else if (fetchRequest.HTTPBodyStream) {
+ if ([self respondsToSelector:@selector(loggedInputStreamForInputStream:)]) {
+ fetchRequest.HTTPBodyStream =
+ [self performSelector:@selector(loggedInputStreamForInputStream:)
+ withObject:fetchRequest.HTTPBodyStream];
+ }
+#endif
+ }
+ }
+
+ // We authorize after setting up the http method and body in the request
+ // because OAuth 1 may need to sign the request body
+ if (mayAuthorize && _authorizer && !isDataRequest) {
+ BOOL isAuthorized = [_authorizer isAuthorizedRequest:fetchRequest];
+ if (!isAuthorized) {
+ // Authorization needed.
+ //
+ // If this session is held by the fetcher service, clear the session now so that we don't
+ // assume it's still valid after authorization completes.
+ if (self.canShareSession) {
+ self.session = nil;
+ }
+
+ // Authorizing the request will recursively call this beginFetch:mayDelay:
+ // or failToBeginFetchWithError:.
+ [self authorizeRequest];
+ return;
+ }
+ }
+
+ // set the default upload or download retry interval, if necessary
+ if ([self isRetryEnabled] && self.maxRetryInterval <= 0) {
+ if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) {
+ [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval];
+ } else {
+ [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval];
+ }
+ }
+
+ // finally, start the connection
+ NSURLSessionTask *newSessionTask;
+ BOOL needsDataAccumulator = NO;
+ if (_downloadResumeData) {
+ newSessionTask = [_session downloadTaskWithResumeData:_downloadResumeData];
+ GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
+ @"Failed downloadTaskWithResumeData for %@, resume data %lu bytes",
+ _session, (unsigned long)_downloadResumeData.length);
+ } else if (_destinationFileURL && !isDataRequest) {
+ newSessionTask = [_session downloadTaskWithRequest:fetchRequest];
+ GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed downloadTaskWithRequest for %@, %@",
+ _session, fetchRequest);
+ } else if (needsUploadTask) {
+ if (bodyFileURL) {
+ newSessionTask = [_session uploadTaskWithRequest:fetchRequest
+ fromFile:bodyFileURL];
+ GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
+ @"Failed uploadTaskWithRequest for %@, %@, file %@",
+ _session, fetchRequest, bodyFileURL.path);
+ } else if (self.bodyStreamProvider) {
+ newSessionTask = [_session uploadTaskWithStreamedRequest:fetchRequest];
+ GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
+ @"Failed uploadTaskWithStreamedRequest for %@, %@",
+ _session, fetchRequest);
+ } else {
+ GTMSESSION_ASSERT_DEBUG_OR_LOG(_bodyData != nil,
+ @"Upload task needs body data, %@", fetchRequest);
+ newSessionTask = [_session uploadTaskWithRequest:fetchRequest
+ fromData:(NSData * GTM_NONNULL_TYPE)_bodyData];
+ GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
+ @"Failed uploadTaskWithRequest for %@, %@, body data %lu bytes",
+ _session, fetchRequest, (unsigned long)_bodyData.length);
+ }
+ needsDataAccumulator = YES;
+ } else {
+ newSessionTask = [_session dataTaskWithRequest:fetchRequest];
+ needsDataAccumulator = YES;
+ GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed dataTaskWithRequest for %@, %@",
+ _session, fetchRequest);
+ }
+ self.sessionTask = newSessionTask;
+
+ if (!newSessionTask) {
+ // We shouldn't get here; if we're here, an earlier assertion should have fired to explain
+ // which session task creation failed.
+ [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorTaskCreationFailed)];
+ return;
+ }
+
+ if (needsDataAccumulator && _accumulateDataBlock == nil) {
+ self.downloadedData = [NSMutableData data];
+ }
+ if (_taskDescription) {
+ newSessionTask.taskDescription = _taskDescription;
+ }
+ if (_taskPriority >= 0) {
+ if (@available(iOS 8.0, macOS 10.10, *)) {
+ newSessionTask.priority = _taskPriority;
+ }
+ }
+
+#if GTM_DISABLE_FETCHER_TEST_BLOCK
+ GTMSESSION_ASSERT_DEBUG(_testBlock == nil && gGlobalTestBlock == nil, @"test blocks disabled");
+ _testBlock = nil;
+#else
+ if (!_testBlock) {
+ if (gGlobalTestBlock) {
+ // Note that the test block may pass nil for all of its response parameters,
+ // indicating that the fetch should actually proceed. This is useful when the
+ // global test block has been set, and the app is only testing a specific
+ // fetcher. The block simulation code will then resume the task.
+ _testBlock = gGlobalTestBlock;
+ }
+ }
+ _isUsingTestBlock = (_testBlock != nil);
+#endif // GTM_DISABLE_FETCHER_TEST_BLOCK
+
+#if GTM_BACKGROUND_TASK_FETCHING
+ id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication];
+ // Background tasks seem to interfere with out-of-process uploads and downloads.
+ if (app && !self.skipBackgroundTask && !self.usingBackgroundSession) {
+ // Tell UIApplication that we want to continue even when the app is in the
+ // background.
+#if DEBUG
+ NSString *bgTaskName = [NSString stringWithFormat:@"%@-%@",
+ [self class], fetchRequest.URL.host];
+#else
+ NSString *bgTaskName = @"GTMSessionFetcher";
+#endif
+ __block UIBackgroundTaskIdentifier bgTaskID = [app beginBackgroundTaskWithName:bgTaskName
+ expirationHandler:^{
+ // Background task expiration callback - this block is always invoked by
+ // UIApplication on the main thread.
+ if (bgTaskID != UIBackgroundTaskInvalid) {
+ @synchronized(self) {
+ if (bgTaskID == self.backgroundTaskIdentifier) {
+ self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
+ }
+ }
+ [app endBackgroundTask:bgTaskID];
+ }
+ }];
+ @synchronized(self) {
+ self.backgroundTaskIdentifier = bgTaskID;
+ }
+ }
+#endif
+
+ if (!_initialRequestDate) {
+ _initialRequestDate = [[NSDate alloc] init];
+ }
+
+ // We don't expect to reach here even on retry or auth until a stop notification has been sent
+ // for the previous task, but we should ensure that we don't unbalance that.
+ GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"Start notification without a prior stop");
+ [self sendStopNotificationIfNeeded];
+
+ [self addPersistedBackgroundSessionToDefaults];
+
+ [self setStopNotificationNeeded:YES];
+
+ [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStartedNotification
+ userInfo:nil
+ requireAsync:NO];
+
+ // The service needs to know our task if it is serving as NSURLSession delegate.
+ [_service fetcherDidBeginFetching:self];
+
+ if (_testBlock) {
+#if !GTM_DISABLE_FETCHER_TEST_BLOCK
+ [self simulateFetchForTestBlock];
+#endif
+ } else {
+ // We resume the session task after posting the notification since the
+ // delegate callbacks may happen immediately if the fetch is started off
+ // the main thread or the session delegate queue is on a background thread,
+ // and we don't want to post a start notification after a premature finish
+ // of the session task.
+ [newSessionTask resume];
+ }
+}
+
+NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError) {
+ NSMutableData *data = [NSMutableData data];
+
+ [inputStream open];
+ NSInteger numberOfBytesRead = 0;
+ while ([inputStream hasBytesAvailable]) {
+ uint8_t buffer[512];
+ numberOfBytesRead = [inputStream read:buffer maxLength:sizeof(buffer)];
+ if (numberOfBytesRead > 0) {
+ [data appendBytes:buffer length:(NSUInteger)numberOfBytesRead];
+ } else {
+ break;
+ }
+ }
+ [inputStream close];
+ NSError *streamError = inputStream.streamError;
+
+ if (streamError) {
+ data = nil;
+ }
+ if (outError) {
+ *outError = streamError;
+ }
+ return data;
+}
+
+#if !GTM_DISABLE_FETCHER_TEST_BLOCK
+
+- (void)simulateFetchForTestBlock {
+ // This is invoked on the same thread as the beginFetch method was.
+ //
+ // Callbacks will all occur on the callback queue.
+ _testBlock(self, ^(NSURLResponse *response, NSData *responseData, NSError *error) {
+ // Callback from test block.
+ if (response == nil && responseData == nil && error == nil) {
+ // Assume the fetcher should execute rather than be tested.
+ self->_testBlock = nil;
+ self->_isUsingTestBlock = NO;
+ [self->_sessionTask resume];
+ return;
+ }
+
+ GTMSessionFetcherBodyStreamProvider bodyStreamProvider = self.bodyStreamProvider;
+ if (bodyStreamProvider) {
+ bodyStreamProvider(^(NSInputStream *bodyStream){
+ // Read from the input stream into an NSData buffer. We'll drain the stream
+ // explicitly on a background queue.
+ [self invokeOnCallbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
+ afterUserStopped:NO
+ block:^{
+ NSError *streamError;
+ NSData *streamedData = GTMDataFromInputStream(bodyStream, &streamError);
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ // Continue callbacks on the main thread, since serial behavior
+ // is more reliable for tests.
+ [self simulateDataCallbacksForTestBlockWithBodyData:streamedData
+ response:response
+ responseData:responseData
+ error:(error ?: streamError)];
+ });
+ }];
+ });
+ } else {
+ // No input stream; use the supplied data or file URL.
+ NSURL *bodyFileURL = self.bodyFileURL;
+ if (bodyFileURL) {
+ NSError *readError;
+ self->_bodyData = [NSData dataWithContentsOfURL:bodyFileURL
+ options:NSDataReadingMappedIfSafe
+ error:&readError];
+ error = readError;
+ }
+
+ // No stream provider.
+
+ // In real fetches, nothing happens until the run loop spins, so apps have leeway to
+ // set callbacks after they call beginFetch. We'll mirror that fetcher behavior by
+ // delaying callbacks here at least to the next spin of the run loop. That keeps
+ // immediate, synchronous setting of callback blocks after beginFetch working in tests.
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self simulateDataCallbacksForTestBlockWithBodyData:self->_bodyData
+ response:response
+ responseData:responseData
+ error:error];
+ });
+ }
+ });
+}
+
+- (void)simulateByteTransferReportWithDataLength:(int64_t)totalDataLength
+ block:(GTMSessionFetcherSendProgressBlock)block {
+ // This utility method simulates transfer progress with up to three callbacks.
+ // It is used to call back to any of the progress blocks.
+ int64_t sendReportSize = totalDataLength / 3 + 1;
+ int64_t totalSent = 0;
+ while (totalSent < totalDataLength) {
+ int64_t bytesRemaining = totalDataLength - totalSent;
+ sendReportSize = MIN(sendReportSize, bytesRemaining);
+ totalSent += sendReportSize;
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ block(sendReportSize, totalSent, totalDataLength);
+ }];
+ }
+}
+
+- (void)simulateDataCallbacksForTestBlockWithBodyData:(NSData * GTM_NULLABLE_TYPE)bodyData
+ response:(NSURLResponse *)response
+ responseData:(NSData *)suppliedData
+ error:(NSError *)suppliedError {
+ __block NSData *responseData = suppliedData;
+ __block NSError *responseError = suppliedError;
+
+ // This method does the test simulation of callbacks once the upload
+ // and download data are known.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // Get copies of ivars we'll access in async invocations. This simulation assumes
+ // they won't change during fetcher execution.
+ NSURL *destinationFileURL = _destinationFileURL;
+ GTMSessionFetcherWillRedirectBlock willRedirectBlock = _willRedirectBlock;
+ GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock = _didReceiveResponseBlock;
+ GTMSessionFetcherSendProgressBlock sendProgressBlock = _sendProgressBlock;
+ GTMSessionFetcherDownloadProgressBlock downloadProgressBlock = _downloadProgressBlock;
+ GTMSessionFetcherAccumulateDataBlock accumulateDataBlock = _accumulateDataBlock;
+ GTMSessionFetcherReceivedProgressBlock receivedProgressBlock = _receivedProgressBlock;
+ GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock =
+ _willCacheURLResponseBlock;
+ GTMSessionFetcherChallengeBlock challengeBlock = _challengeBlock;
+
+ // Simulate receipt of redirection.
+ if (willRedirectBlock) {
+ [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
+ block:^{
+ willRedirectBlock((NSHTTPURLResponse *)response, self->_request,
+ ^(NSURLRequest *redirectRequest) {
+ // For simulation, we'll assume the app will just continue.
+ });
+ }];
+ }
+
+ // If the fetcher has a challenge block, simulate a challenge.
+ //
+ // It might be nice to eventually let the user determine which testBlock
+ // fetches get challenged rather than always executing the supplied
+ // challenge block.
+ if (challengeBlock) {
+ [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
+ block:^{
+ NSURL *requestURL = self->_request.URL;
+ NSString *host = requestURL.host;
+ NSURLProtectionSpace *pspace =
+ [[NSURLProtectionSpace alloc] initWithHost:host
+ port:requestURL.port.integerValue
+ protocol:requestURL.scheme
+ realm:nil
+ authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
+ id<NSURLAuthenticationChallengeSender> unusedSender =
+ (id<NSURLAuthenticationChallengeSender>)[NSNull null];
+ NSURLAuthenticationChallenge *challenge =
+ [[NSURLAuthenticationChallenge alloc] initWithProtectionSpace:pspace
+ proposedCredential:nil
+ previousFailureCount:0
+ failureResponse:nil
+ error:nil
+ sender:unusedSender];
+ challengeBlock(self, challenge, ^(NSURLSessionAuthChallengeDisposition disposition,
+ NSURLCredential * GTM_NULLABLE_TYPE credential){
+ // We could change the responseData and responseError based on the disposition,
+ // but it's easier for apps to just supply the expected data and error
+ // directly to the test block. So this simulation ignores the disposition.
+ });
+ }];
+ }
+
+ // Simulate receipt of an initial response.
+ if (response && didReceiveResponseBlock) {
+ [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
+ block:^{
+ didReceiveResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) {
+ // For simulation, we'll assume the disposition is to continue.
+ });
+ }];
+ }
+
+ // Simulate reporting send progress.
+ if (sendProgressBlock) {
+ [self simulateByteTransferReportWithDataLength:(int64_t)bodyData.length
+ block:^(int64_t bytesSent,
+ int64_t totalBytesSent,
+ int64_t totalBytesExpectedToSend) {
+ // This is invoked on the callback queue unless stopped.
+ sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
+ }];
+ }
+
+ if (destinationFileURL) {
+ // Simulate download to file progress.
+ if (downloadProgressBlock) {
+ [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length
+ block:^(int64_t bytesDownloaded,
+ int64_t totalBytesDownloaded,
+ int64_t totalBytesExpectedToDownload) {
+ // This is invoked on the callback queue unless stopped.
+ downloadProgressBlock(bytesDownloaded, totalBytesDownloaded,
+ totalBytesExpectedToDownload);
+ }];
+ }
+
+ NSError *writeError;
+ [responseData writeToURL:destinationFileURL
+ options:NSDataWritingAtomic
+ error:&writeError];
+ if (writeError) {
+ // Tell the test code that writing failed.
+ responseError = writeError;
+ }
+ } else {
+ // Simulate download to NSData progress.
+ if ((accumulateDataBlock || receivedProgressBlock) && responseData) {
+ [self simulateByteTransferWithData:responseData
+ block:^(NSData *data,
+ int64_t bytesReceived,
+ int64_t totalBytesReceived,
+ int64_t totalBytesExpectedToReceive) {
+ // This is invoked on the callback queue unless stopped.
+ if (accumulateDataBlock) {
+ accumulateDataBlock(data);
+ }
+
+ if (receivedProgressBlock) {
+ receivedProgressBlock(bytesReceived, totalBytesReceived);
+ }
+ }];
+ }
+
+ if (!accumulateDataBlock) {
+ _downloadedData = [responseData mutableCopy];
+ }
+
+ if (willCacheURLResponseBlock) {
+ // Simulate letting the client inspect and alter the cached response.
+ NSData *cachedData = responseData ?: [[NSData alloc] init]; // Always have non-nil data.
+ NSCachedURLResponse *cachedResponse =
+ [[NSCachedURLResponse alloc] initWithResponse:response
+ data:cachedData];
+ [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
+ block:^{
+ willCacheURLResponseBlock(cachedResponse, ^(NSCachedURLResponse *responseToCache){
+ // The app may provide an alternative response, or nil to defeat caching.
+ });
+ }];
+ }
+ }
+ _response = response;
+ } // @synchronized(self)
+
+ NSOperationQueue *queue = self.sessionDelegateQueue;
+ [queue addOperationWithBlock:^{
+ // Rather than invoke failToBeginFetchWithError: we want to simulate completion of
+ // a connection that started and ended, so we'll call down to finishWithError:
+ NSInteger status = responseError ? responseError.code : 200;
+ if (status >= 200 && status <= 399) {
+ [self finishWithError:nil shouldRetry:NO];
+ } else {
+ [self shouldRetryNowForStatus:status
+ error:responseError
+ forceAssumeRetry:NO
+ response:^(BOOL shouldRetry) {
+ [self finishWithError:responseError shouldRetry:shouldRetry];
+ }];
+ }
+ }];
+}
+
+- (void)simulateByteTransferWithData:(NSData *)responseData
+ block:(GTMSessionFetcherSimulateByteTransferBlock)transferBlock {
+ // This utility method simulates transfering data to the client. It divides the data into at most
+ // "chunkCount" chunks and then passes each chunk along with a progress update to transferBlock.
+ // This function can be used with accumulateDataBlock or receivedProgressBlock.
+
+ NSUInteger chunkCount = MAX(self.testBlockAccumulateDataChunkCount, (NSUInteger) 1);
+ NSUInteger totalDataLength = responseData.length;
+ NSUInteger sendDataSize = totalDataLength / chunkCount + 1;
+ NSUInteger totalSent = 0;
+ while (totalSent < totalDataLength) {
+ NSUInteger bytesRemaining = totalDataLength - totalSent;
+ sendDataSize = MIN(sendDataSize, bytesRemaining);
+ NSData *chunkData = [responseData subdataWithRange:NSMakeRange(totalSent, sendDataSize)];
+ totalSent += sendDataSize;
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ transferBlock(chunkData,
+ (int64_t)sendDataSize,
+ (int64_t)totalSent,
+ (int64_t)totalDataLength);
+ }];
+ }
+}
+
+#endif // !GTM_DISABLE_FETCHER_TEST_BLOCK
+
+- (void)setSessionTask:(NSURLSessionTask *)sessionTask {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_sessionTask != sessionTask) {
+ _sessionTask = sessionTask;
+ if (_sessionTask) {
+ // Request could be nil on restoring this fetcher from a background session.
+ if (!_request) {
+ _request = [_sessionTask.originalRequest mutableCopy];
+ }
+ }
+ }
+ } // @synchronized(self)
+}
+
+- (NSURLSessionTask * GTM_NULLABLE_TYPE)sessionTask {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _sessionTask;
+ } // @synchronized(self)
+}
+
++ (NSUserDefaults *)fetcherUserDefaults {
+ static NSUserDefaults *gFetcherUserDefaults = nil;
+
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ Class fetcherUserDefaultsClass = NSClassFromString(@"GTMSessionFetcherUserDefaultsFactory");
+ if (fetcherUserDefaultsClass) {
+ gFetcherUserDefaults = [fetcherUserDefaultsClass fetcherUserDefaults];
+ } else {
+ gFetcherUserDefaults = [NSUserDefaults standardUserDefaults];
+ }
+ });
+ return gFetcherUserDefaults;
+}
+
+- (void)addPersistedBackgroundSessionToDefaults {
+ NSString *sessionIdentifier = self.sessionIdentifier;
+ if (!sessionIdentifier) {
+ return;
+ }
+ NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions];
+ if ([oldBackgroundSessions containsObject:_sessionIdentifier]) {
+ return;
+ }
+ NSMutableArray *newBackgroundSessions =
+ [NSMutableArray arrayWithArray:oldBackgroundSessions];
+ [newBackgroundSessions addObject:sessionIdentifier];
+ GTM_LOG_BACKGROUND_SESSION(@"Add to background sessions: %@", newBackgroundSessions);
+
+ NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
+ [userDefaults setObject:newBackgroundSessions
+ forKey:kGTMSessionFetcherPersistedDestinationKey];
+ [userDefaults synchronize];
+}
+
+- (void)removePersistedBackgroundSessionFromDefaults {
+ NSString *sessionIdentifier = self.sessionIdentifier;
+ if (!sessionIdentifier) return;
+
+ NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions];
+ if (!oldBackgroundSessions) {
+ return;
+ }
+ NSMutableArray *newBackgroundSessions =
+ [NSMutableArray arrayWithArray:oldBackgroundSessions];
+ NSUInteger sessionIndex = [newBackgroundSessions indexOfObject:sessionIdentifier];
+ if (sessionIndex == NSNotFound) {
+ return;
+ }
+ [newBackgroundSessions removeObjectAtIndex:sessionIndex];
+ GTM_LOG_BACKGROUND_SESSION(@"Remove from background sessions: %@", newBackgroundSessions);
+
+ NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
+ if (newBackgroundSessions.count == 0) {
+ [userDefaults removeObjectForKey:kGTMSessionFetcherPersistedDestinationKey];
+ } else {
+ [userDefaults setObject:newBackgroundSessions
+ forKey:kGTMSessionFetcherPersistedDestinationKey];
+ }
+ [userDefaults synchronize];
+}
+
++ (GTM_NULLABLE NSArray *)activePersistedBackgroundSessions {
+ NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
+ NSArray *oldBackgroundSessions =
+ [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey];
+ if (oldBackgroundSessions.count == 0) {
+ return nil;
+ }
+ NSMutableArray *activeBackgroundSessions = nil;
+ NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
+ for (NSString *sessionIdentifier in oldBackgroundSessions) {
+ GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
+ if (fetcher) {
+ if (!activeBackgroundSessions) {
+ activeBackgroundSessions = [[NSMutableArray alloc] init];
+ }
+ [activeBackgroundSessions addObject:sessionIdentifier];
+ }
+ }
+ return activeBackgroundSessions;
+}
+
++ (NSArray *)fetchersForBackgroundSessions {
+ NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
+ NSArray *backgroundSessions =
+ [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey];
+ NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
+ NSMutableArray *fetchers = [NSMutableArray array];
+ for (NSString *sessionIdentifier in backgroundSessions) {
+ GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
+ if (!fetcher) {
+ fetcher = [self fetcherWithSessionIdentifier:sessionIdentifier];
+ GTMSESSION_ASSERT_DEBUG(fetcher != nil,
+ @"Unexpected invalid session identifier: %@", sessionIdentifier);
+ [fetcher beginFetchWithCompletionHandler:nil];
+ }
+ GTM_LOG_BACKGROUND_SESSION(@"%@ restoring session %@ by creating fetcher %@ %p",
+ [self class], sessionIdentifier, fetcher, fetcher);
+ if (fetcher != nil) {
+ [fetchers addObject:fetcher];
+ }
+ }
+ return fetchers;
+}
+
+#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
++ (void)application:(UIApplication *)application
+ handleEventsForBackgroundURLSession:(NSString *)identifier
+ completionHandler:(GTMSessionFetcherSystemCompletionHandler)completionHandler {
+ GTMSessionFetcher *fetcher = [self fetcherWithSessionIdentifier:identifier];
+ if (fetcher != nil) {
+ fetcher.systemCompletionHandler = completionHandler;
+ } else {
+ GTM_LOG_BACKGROUND_SESSION(@"%@ did not create background session identifier: %@",
+ [self class], identifier);
+ }
+}
+#endif
+
+- (NSString * GTM_NULLABLE_TYPE)sessionIdentifier {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _sessionIdentifier;
+ } // @synchronized(self)
+}
+
+- (void)setSessionIdentifier:(NSString *)sessionIdentifier {
+ GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSESSION_ASSERT_DEBUG(!_session, @"Unable to set session identifier after session created");
+ _sessionIdentifier = [sessionIdentifier copy];
+ _usingBackgroundSession = YES;
+ _canShareSession = NO;
+ [self restoreDefaultStateForSessionIdentifierMetadata];
+ } // @synchronized(self)
+}
+
+- (void)setSessionIdentifierInternal:(GTM_NULLABLE NSString *)sessionIdentifier {
+ // This internal method only does a synchronized set of the session identifier.
+ // It does not have side effects on the background session, shared session, or
+ // session identifier metadata.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _sessionIdentifier = [sessionIdentifier copy];
+ } // @synchronized(self)
+}
+
+- (NSDictionary * GTM_NULLABLE_TYPE)sessionUserInfo {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_sessionUserInfo == nil) {
+ // We'll return the metadata dictionary with internal keys removed. This avoids the user
+ // re-using the userInfo dictionary later and accidentally including the internal keys.
+ NSMutableDictionary *metadata = [[self sessionIdentifierMetadataUnsynchronized] mutableCopy];
+ NSSet *keysToRemove = [metadata keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
+ return [key hasPrefix:@"_"];
+ }];
+ [metadata removeObjectsForKeys:[keysToRemove allObjects]];
+ if (metadata.count > 0) {
+ _sessionUserInfo = metadata;
+ }
+ }
+ return _sessionUserInfo;
+ } // @synchronized(self)
+}
+
+- (void)setSessionUserInfo:(NSDictionary * GTM_NULLABLE_TYPE)dictionary {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSESSION_ASSERT_DEBUG(_sessionIdentifier == nil, @"Too late to assign userInfo");
+ _sessionUserInfo = dictionary;
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSDictionary *)sessionIdentifierDefaultMetadata {
+ GTMSessionCheckSynchronized(self);
+
+ NSMutableDictionary *defaultUserInfo = [[NSMutableDictionary alloc] init];
+ if (_destinationFileURL) {
+ defaultUserInfo[kGTMSessionIdentifierDestinationFileURLMetadataKey] =
+ [_destinationFileURL absoluteString];
+ }
+ if (_bodyFileURL) {
+ defaultUserInfo[kGTMSessionIdentifierBodyFileURLMetadataKey] = [_bodyFileURL absoluteString];
+ }
+ return (defaultUserInfo.count > 0) ? defaultUserInfo : nil;
+}
+
+- (void)restoreDefaultStateForSessionIdentifierMetadata {
+ GTMSessionCheckSynchronized(self);
+
+ NSDictionary *metadata = [self sessionIdentifierMetadataUnsynchronized];
+ NSString *destinationFileURLString = metadata[kGTMSessionIdentifierDestinationFileURLMetadataKey];
+ if (destinationFileURLString) {
+ _destinationFileURL = [NSURL URLWithString:destinationFileURLString];
+ GTM_LOG_BACKGROUND_SESSION(@"Restoring destination file URL: %@", _destinationFileURL);
+ }
+ NSString *bodyFileURLString = metadata[kGTMSessionIdentifierBodyFileURLMetadataKey];
+ if (bodyFileURLString) {
+ _bodyFileURL = [NSURL URLWithString:bodyFileURLString];
+ GTM_LOG_BACKGROUND_SESSION(@"Restoring body file URL: %@", _bodyFileURL);
+ }
+}
+
+- (NSDictionary * GTM_NULLABLE_TYPE)sessionIdentifierMetadata {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [self sessionIdentifierMetadataUnsynchronized];
+ }
+}
+
+- (NSDictionary * GTM_NULLABLE_TYPE)sessionIdentifierMetadataUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ // Session Identifier format: "com.google.<ClassName>_<UUID>_<Metadata in JSON format>
+ if (!_sessionIdentifier) {
+ return nil;
+ }
+ NSScanner *metadataScanner = [NSScanner scannerWithString:_sessionIdentifier];
+ [metadataScanner setCharactersToBeSkipped:nil];
+ NSString *metadataString;
+ NSString *uuid;
+ if ([metadataScanner scanUpToString:@"_" intoString:NULL] &&
+ [metadataScanner scanString:@"_" intoString:NULL] &&
+ [metadataScanner scanUpToString:@"_" intoString:&uuid] &&
+ [metadataScanner scanString:@"_" intoString:NULL] &&
+ [metadataScanner scanUpToString:@"\n" intoString:&metadataString]) {
+ _sessionIdentifierUUID = uuid;
+ NSData *metadataData = [metadataString dataUsingEncoding:NSUTF8StringEncoding];
+ NSError *error;
+ NSDictionary *metadataDict =
+ [NSJSONSerialization JSONObjectWithData:metadataData
+ options:0
+ error:&error];
+ GTM_LOG_BACKGROUND_SESSION(@"User Info from session identifier: %@ %@",
+ metadataDict, error ? error : @"");
+ return metadataDict;
+ }
+ return nil;
+}
+
+- (NSString *)createSessionIdentifierWithMetadata:(NSDictionary * GTM_NULLABLE_TYPE)metadataToInclude {
+ NSString *result;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // Session Identifier format: "com.google.<ClassName>_<UUID>_<Metadata in JSON format>
+ GTMSESSION_ASSERT_DEBUG(!_sessionIdentifier, @"Session identifier already created");
+ _sessionIdentifierUUID = [[NSUUID UUID] UUIDString];
+ _sessionIdentifier =
+ [NSString stringWithFormat:@"%@_%@", kGTMSessionIdentifierPrefix, _sessionIdentifierUUID];
+ // Start with user-supplied keys so they cannot accidentally override the fetcher's keys.
+ NSMutableDictionary *metadataDict =
+ [NSMutableDictionary dictionaryWithDictionary:(NSDictionary * GTM_NONNULL_TYPE)_sessionUserInfo];
+
+ if (metadataToInclude) {
+ [metadataDict addEntriesFromDictionary:(NSDictionary *)metadataToInclude];
+ }
+ NSDictionary *defaultMetadataDict = [self sessionIdentifierDefaultMetadata];
+ if (defaultMetadataDict) {
+ [metadataDict addEntriesFromDictionary:defaultMetadataDict];
+ }
+ if (metadataDict.count > 0) {
+ NSData *metadataData = [NSJSONSerialization dataWithJSONObject:metadataDict
+ options:0
+ error:NULL];
+ GTMSESSION_ASSERT_DEBUG(metadataData != nil,
+ @"Session identifier user info failed to convert to JSON");
+ if (metadataData.length > 0) {
+ NSString *metadataString = [[NSString alloc] initWithData:metadataData
+ encoding:NSUTF8StringEncoding];
+ _sessionIdentifier =
+ [_sessionIdentifier stringByAppendingFormat:@"_%@", metadataString];
+ }
+ }
+ _didCreateSessionIdentifier = YES;
+ result = _sessionIdentifier;
+ } // @synchronized(self)
+ return result;
+}
+
+- (void)failToBeginFetchWithError:(NSError *)error {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _hasStoppedFetching = YES;
+ }
+
+ if (error == nil) {
+ error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
+ code:GTMSessionFetcherErrorDownloadFailed
+ userInfo:nil];
+ }
+
+ [self invokeFetchCallbacksOnCallbackQueueWithData:nil
+ error:error];
+ [self releaseCallbacks];
+
+ [_service fetcherDidStop:self];
+
+ self.authorizer = nil;
+}
+
++ (GTMSessionCookieStorage *)staticCookieStorage {
+ static GTMSessionCookieStorage *gCookieStorage = nil;
+
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ gCookieStorage = [[GTMSessionCookieStorage alloc] init];
+ });
+ return gCookieStorage;
+}
+
+#if GTM_BACKGROUND_TASK_FETCHING
+
+- (void)endBackgroundTask {
+ // Whenever the connection stops or background execution expires,
+ // we need to tell UIApplication we're done.
+ UIBackgroundTaskIdentifier bgTaskID;
+ @synchronized(self) {
+ bgTaskID = self.backgroundTaskIdentifier;
+ if (bgTaskID != UIBackgroundTaskInvalid) {
+ self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
+ }
+ }
+
+ if (bgTaskID != UIBackgroundTaskInvalid) {
+ id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication];
+ [app endBackgroundTask:bgTaskID];
+ }
+}
+
+#endif // GTM_BACKGROUND_TASK_FETCHING
+
+- (void)authorizeRequest {
+ GTMSessionCheckNotSynchronized(self);
+
+ id authorizer = self.authorizer;
+ SEL asyncAuthSel = @selector(authorizeRequest:delegate:didFinishSelector:);
+ if ([authorizer respondsToSelector:asyncAuthSel]) {
+ SEL callbackSel = @selector(authorizer:request:finishedWithError:);
+ NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
+ [authorizer authorizeRequest:mutableRequest
+ delegate:self
+ didFinishSelector:callbackSel];
+ } else {
+ GTMSESSION_ASSERT_DEBUG(authorizer == nil, @"invalid authorizer for fetch");
+
+ // No authorizing possible, and authorizing happens only after any delay;
+ // just begin fetching
+ [self beginFetchMayDelay:NO
+ mayAuthorize:NO];
+ }
+}
+
+- (void)authorizer:(id<GTMFetcherAuthorizationProtocol>)auth
+ request:(NSMutableURLRequest *)authorizedRequest
+ finishedWithError:(NSError *)error {
+ GTMSessionCheckNotSynchronized(self);
+
+ if (error != nil) {
+ // We can't fetch without authorization
+ [self failToBeginFetchWithError:error];
+ } else {
+ @synchronized(self) {
+ _request = authorizedRequest;
+ }
+ [self beginFetchMayDelay:NO
+ mayAuthorize:NO];
+ }
+}
+
+
+- (BOOL)canFetchWithBackgroundSession {
+ // Subclasses may override.
+ return YES;
+}
+
+// Returns YES if the fetcher has been started and has not yet stopped.
+//
+// Fetching includes waiting for authorization or for retry, waiting to be allowed by the
+// service object to start the request, and actually fetching the request.
+- (BOOL)isFetching {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [self isFetchingUnsynchronized];
+ }
+}
+
+- (BOOL)isFetchingUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ BOOL hasBegun = (_initialBeginFetchDate != nil);
+ return hasBegun && !_hasStoppedFetching;
+}
+
+- (NSURLResponse * GTM_NULLABLE_TYPE)response {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSURLResponse *response = [self responseUnsynchronized];
+ return response;
+ } // @synchronized(self)
+}
+
+- (NSURLResponse * GTM_NULLABLE_TYPE)responseUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ NSURLResponse *response = _sessionTask.response;
+ if (!response) response = _response;
+ return response;
+}
+
+- (NSInteger)statusCode {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSInteger statusCode = [self statusCodeUnsynchronized];
+ return statusCode;
+ } // @synchronized(self)
+}
+
+- (NSInteger)statusCodeUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ NSURLResponse *response = [self responseUnsynchronized];
+ NSInteger statusCode;
+
+ if ([response respondsToSelector:@selector(statusCode)]) {
+ statusCode = [(NSHTTPURLResponse *)response statusCode];
+ } else {
+ // Default to zero, in hopes of hinting "Unknown" (we can't be
+ // sure that things are OK enough to use 200).
+ statusCode = 0;
+ }
+ return statusCode;
+}
+
+- (NSDictionary * GTM_NULLABLE_TYPE)responseHeaders {
+ GTMSessionCheckNotSynchronized(self);
+
+ NSURLResponse *response = self.response;
+ if ([response respondsToSelector:@selector(allHeaderFields)]) {
+ NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
+ return headers;
+ }
+ return nil;
+}
+
+- (NSDictionary * GTM_NULLABLE_TYPE)responseHeadersUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ NSURLResponse *response = [self responseUnsynchronized];
+ if ([response respondsToSelector:@selector(allHeaderFields)]) {
+ NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
+ return headers;
+ }
+ return nil;
+}
+
+- (void)releaseCallbacks {
+ // Avoid releasing blocks in the sync section since objects dealloc'd by
+ // the blocks being released may call back into the fetcher or fetcher
+ // service.
+ dispatch_queue_t NS_VALID_UNTIL_END_OF_SCOPE holdCallbackQueue;
+ GTMSessionFetcherCompletionHandler NS_VALID_UNTIL_END_OF_SCOPE holdCompletionHandler;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ holdCallbackQueue = _callbackQueue;
+ holdCompletionHandler = _completionHandler;
+
+ _callbackQueue = nil;
+ _completionHandler = nil; // Setter overridden in upload. Setter assumed to be used externally.
+ }
+
+ // Set local callback pointers to nil here rather than let them release at the end of the scope
+ // to make any problems due to the blocks being released be a bit more obvious in a stack trace.
+ holdCallbackQueue = nil;
+ holdCompletionHandler = nil;
+
+ self.configurationBlock = nil;
+ self.didReceiveResponseBlock = nil;
+ self.challengeBlock = nil;
+ self.willRedirectBlock = nil;
+ self.sendProgressBlock = nil;
+ self.receivedProgressBlock = nil;
+ self.downloadProgressBlock = nil;
+ self.accumulateDataBlock = nil;
+ self.willCacheURLResponseBlock = nil;
+ self.retryBlock = nil;
+ self.testBlock = nil;
+ self.resumeDataBlock = nil;
+ if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *)) {
+ self.metricsCollectionBlock = nil;
+ }
+}
+
+- (void)forgetSessionIdentifierForFetcher {
+ GTMSessionCheckSynchronized(self);
+ [self forgetSessionIdentifierForFetcherWithoutSyncCheck];
+}
+
+- (void)forgetSessionIdentifierForFetcherWithoutSyncCheck {
+ // This should be called inside a @synchronized block (except during dealloc.)
+ if (_sessionIdentifier) {
+ NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap];
+ [sessionIdentifierToFetcherMap removeObjectForKey:_sessionIdentifier];
+ _sessionIdentifier = nil;
+ _didCreateSessionIdentifier = NO;
+ }
+}
+
+// External stop method
+- (void)stopFetching {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // Prevent enqueued callbacks from executing.
+ _userStoppedFetching = YES;
+ } // @synchronized(self)
+ [self stopFetchReleasingCallbacks:YES];
+}
+
+// Cancel the fetch of the URL that's currently in progress.
+//
+// If shouldReleaseCallbacks is NO then the fetch will be retried so the callbacks
+// need to still be retained.
+- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
+ [self removePersistedBackgroundSessionFromDefaults];
+
+ id<GTMSessionFetcherServiceProtocol> service;
+ NSMutableURLRequest *request;
+
+ // If the task or the retry timer is all that's retaining the fetcher,
+ // we want to be sure this instance survives stopping at least long enough for
+ // the stack to unwind.
+ __autoreleasing GTMSessionFetcher *holdSelf = self;
+
+ BOOL hasCanceledTask = NO;
+
+ [holdSelf destroyRetryTimer];
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _hasStoppedFetching = YES;
+
+ service = _service;
+ request = _request;
+
+ if (_sessionTask) {
+ // In case cancelling the task or session calls this recursively, we want
+ // to ensure that we'll only release the task and delegate once,
+ // so first set _sessionTask to nil
+ //
+ // This may be called in a callback from the task, so use autorelease to avoid
+ // releasing the task in its own callback.
+ __autoreleasing NSURLSessionTask *oldTask = _sessionTask;
+ if (!_isUsingTestBlock) {
+ _response = _sessionTask.response;
+ }
+ _sessionTask = nil;
+
+ if ([oldTask state] != NSURLSessionTaskStateCompleted) {
+ // For download tasks, when the fetch is stopped, we may provide resume data that can
+ // be used to create a new session.
+ BOOL mayResume = (_resumeDataBlock
+ && [oldTask respondsToSelector:@selector(cancelByProducingResumeData:)]);
+ if (!mayResume) {
+ [oldTask cancel];
+ // A side effect of stopping the task is that URLSession:task:didCompleteWithError:
+ // will be invoked asynchronously on the delegate queue.
+ } else {
+ void (^resumeBlock)(NSData *) = _resumeDataBlock;
+ _resumeDataBlock = nil;
+
+ // Save callbackQueue since releaseCallbacks clears it.
+ dispatch_queue_t callbackQueue = _callbackQueue;
+ dispatch_group_enter(_callbackGroup);
+ [(NSURLSessionDownloadTask *)oldTask cancelByProducingResumeData:^(NSData *resumeData) {
+ [self invokeOnCallbackQueue:callbackQueue
+ afterUserStopped:YES
+ block:^{
+ resumeBlock(resumeData);
+ dispatch_group_leave(self->_callbackGroup);
+ }];
+ }];
+ }
+ hasCanceledTask = YES;
+ }
+ }
+
+ // If the task was canceled, wait until the URLSession:task:didCompleteWithError: to call
+ // finishTasksAndInvalidate, since calling it immediately tends to crash, see radar 18471901.
+ if (_session) {
+ BOOL shouldInvalidate = _shouldInvalidateSession;
+#if TARGET_OS_IPHONE
+ // Don't invalidate if we've got a systemCompletionHandler, since
+ // URLSessionDidFinishEventsForBackgroundURLSession: won't be called if invalidated.
+ shouldInvalidate = shouldInvalidate && !self.systemCompletionHandler;
+#endif
+ if (shouldInvalidate) {
+ __autoreleasing NSURLSession *oldSession = _session;
+ _session = nil;
+
+ if (!hasCanceledTask) {
+ [oldSession finishTasksAndInvalidate];
+ } else {
+ _sessionNeedingInvalidation = oldSession;
+ }
+ }
+ }
+ } // @synchronized(self)
+
+ // send the stopped notification
+ [self sendStopNotificationIfNeeded];
+
+ [_authorizer stopAuthorizationForRequest:request];
+
+ if (shouldReleaseCallbacks) {
+ [self releaseCallbacks];
+
+ self.authorizer = nil;
+ }
+
+ [service fetcherDidStop:self];
+
+#if GTM_BACKGROUND_TASK_FETCHING
+ [self endBackgroundTask];
+#endif
+}
+
+- (void)setStopNotificationNeeded:(BOOL)flag {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _isStopNotificationNeeded = flag;
+ } // @synchronized(self)
+}
+
+- (void)sendStopNotificationIfNeeded {
+ BOOL sendNow = NO;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_isStopNotificationNeeded) {
+ _isStopNotificationNeeded = NO;
+ sendNow = YES;
+ }
+ } // @synchronized(self)
+
+ if (sendNow) {
+ [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStoppedNotification
+ userInfo:nil
+ requireAsync:NO];
+ }
+}
+
+- (void)retryFetch {
+ [self stopFetchReleasingCallbacks:NO];
+
+ // A retry will need a configuration with a fresh session identifier.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_sessionIdentifier && _didCreateSessionIdentifier) {
+ [self forgetSessionIdentifierForFetcher];
+ _configuration = nil;
+ }
+
+ if (_canShareSession) {
+ // Force a grab of the current session from the fetcher service in case
+ // the service's old one has become invalid.
+ _session = nil;
+ }
+ } // @synchronized(self)
+
+ [self beginFetchForRetry];
+}
+
+- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
+ // Uncovered in upload fetcher testing, because the chunk fetcher is being waited on, and gets
+ // released by the upload code. The uploader just holds onto it with an ivar, and that gets
+ // nilled in the chunk fetcher callback.
+ // Used once in while loop just to avoid unused variable compiler warning.
+ __autoreleasing GTMSessionFetcher *holdSelf = self;
+
+ NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
+
+ BOOL shouldSpinRunLoop = ([NSThread isMainThread] &&
+ (!self.callbackQueue
+ || self.callbackQueue == dispatch_get_main_queue()));
+ BOOL expired = NO;
+
+ // Loop until the callbacks have been called and released, and until
+ // the connection is no longer pending, until there are no callback dispatches
+ // in flight, or until the timeout has expired.
+ int64_t delta = (int64_t)(100 * NSEC_PER_MSEC); // 100 ms
+ while (1) {
+ BOOL isTaskInProgress = (holdSelf->_sessionTask
+ && [_sessionTask state] != NSURLSessionTaskStateCompleted);
+ BOOL needsToCallCompletion = (_completionHandler != nil);
+ BOOL isCallbackInProgress = (_callbackGroup
+ && dispatch_group_wait(_callbackGroup, dispatch_time(DISPATCH_TIME_NOW, delta)));
+
+ if (!isTaskInProgress && !needsToCallCompletion && !isCallbackInProgress) break;
+
+ expired = ([giveUpDate timeIntervalSinceNow] < 0);
+ if (expired) {
+ GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher waitForCompletionWithTimeout:%0.1f expired -- "
+ @"%@%@%@", timeoutInSeconds,
+ isTaskInProgress ? @"taskInProgress " : @"",
+ needsToCallCompletion ? @"needsToCallCompletion " : @"",
+ isCallbackInProgress ? @"isCallbackInProgress" : @"");
+ break;
+ }
+
+ // Run the current run loop 1/1000 of a second to give the networking
+ // code a chance to work
+ const NSTimeInterval kSpinInterval = 0.001;
+ if (shouldSpinRunLoop) {
+ NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:kSpinInterval];
+ [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
+ } else {
+ [NSThread sleepForTimeInterval:kSpinInterval];
+ }
+ }
+ return !expired;
+}
+
++ (void)setGlobalTestBlock:(GTMSessionFetcherTestBlock GTM_NULLABLE_TYPE)block {
+#if GTM_DISABLE_FETCHER_TEST_BLOCK
+ GTMSESSION_ASSERT_DEBUG(block == nil, @"test blocks disabled");
+#endif
+ gGlobalTestBlock = [block copy];
+}
+
+#if GTM_BACKGROUND_TASK_FETCHING
+
+static GTM_NULLABLE_TYPE id<GTMUIApplicationProtocol> gSubstituteUIApp;
+
++ (void)setSubstituteUIApplication:(nullable id<GTMUIApplicationProtocol>)app {
+ gSubstituteUIApp = app;
+}
+
++ (nullable id<GTMUIApplicationProtocol>)substituteUIApplication {
+ return gSubstituteUIApp;
+}
+
++ (nullable id<GTMUIApplicationProtocol>)fetcherUIApplication {
+ id<GTMUIApplicationProtocol> app = gSubstituteUIApp;
+ if (app) return app;
+
+ // iOS App extensions should not call [UIApplication sharedApplication], even
+ // if UIApplication responds to it.
+
+ static Class applicationClass = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ BOOL isAppExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"];
+ if (!isAppExtension) {
+ Class cls = NSClassFromString(@"UIApplication");
+ if (cls && [cls respondsToSelector:NSSelectorFromString(@"sharedApplication")]) {
+ applicationClass = cls;
+ }
+ }
+ });
+
+ if (applicationClass) {
+ app = (id<GTMUIApplicationProtocol>)[applicationClass sharedApplication];
+ }
+ return app;
+}
+#endif // GTM_BACKGROUND_TASK_FETCHING
+
+#pragma mark NSURLSession Delegate Methods
+
+// NSURLSession documentation indicates that redirectRequest can be passed to the handler
+// but empirically redirectRequest lacks the HTTP body, so passing it will break POSTs.
+// Instead, we construct a new request, a copy of the original, with overrides from the
+// redirect.
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+willPerformHTTPRedirection:(NSHTTPURLResponse *)redirectResponse
+ newRequest:(NSURLRequest *)redirectRequest
+ completionHandler:(void (^)(NSURLRequest * GTM_NULLABLE_TYPE))handler {
+ [self setSessionTask:task];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ willPerformHTTPRedirection:%@ newRequest:%@",
+ [self class], self, session, task, redirectResponse, redirectRequest);
+
+ if ([self userStoppedFetching]) {
+ handler(nil);
+ return;
+ }
+ if (redirectRequest && redirectResponse) {
+ // Copy the original request, including the body.
+ NSURLRequest *originalRequest = self.request;
+ NSMutableURLRequest *newRequest = [originalRequest mutableCopy];
+
+ // The new requests's URL overrides the original's URL.
+ [newRequest setURL:[GTMSessionFetcher redirectURLWithOriginalRequestURL:originalRequest.URL
+ redirectRequestURL:redirectRequest.URL]];
+
+ // Any headers in the redirect override headers in the original.
+ NSDictionary *redirectHeaders = redirectRequest.allHTTPHeaderFields;
+ for (NSString *key in redirectHeaders) {
+ NSString *value = [redirectHeaders objectForKey:key];
+ [newRequest setValue:value forHTTPHeaderField:key];
+ }
+
+ redirectRequest = newRequest;
+
+ // Log the response we just received
+ [self setResponse:redirectResponse];
+ [self logNowWithError:nil];
+
+ GTMSessionFetcherWillRedirectBlock willRedirectBlock = self.willRedirectBlock;
+ if (willRedirectBlock) {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+ [self invokeOnCallbackQueueAfterUserStopped:YES
+ block:^{
+ willRedirectBlock(redirectResponse, redirectRequest, ^(NSURLRequest *clientRequest) {
+
+ // Update the request for future logging.
+ [self updateMutableRequest:[clientRequest mutableCopy]];
+
+ handler(clientRequest);
+ });
+ }];
+ } // @synchronized(self)
+ return;
+ }
+ // Continues here if the client did not provide a redirect block.
+
+ // Update the request for future logging.
+ [self updateMutableRequest:[redirectRequest mutableCopy]];
+ }
+ handler(redirectRequest);
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+didReceiveResponse:(NSURLResponse *)response
+ completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))handler {
+ [self setSessionTask:dataTask];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveResponse:%@",
+ [self class], self, session, dataTask, response);
+ void (^accumulateAndFinish)(NSURLSessionResponseDisposition) =
+ ^(NSURLSessionResponseDisposition dispositionValue) {
+ // This method is called when the server has determined that it
+ // has enough information to create the NSURLResponse
+ // it can be called multiple times, for example in the case of a
+ // redirect, so each time we reset the data.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ BOOL hadPreviousData = self->_downloadedLength > 0;
+
+ [self->_downloadedData setLength:0];
+ self->_downloadedLength = 0;
+
+ if (hadPreviousData && (dispositionValue != NSURLSessionResponseCancel)) {
+ // Tell the accumulate block to discard prior data.
+ GTMSessionFetcherAccumulateDataBlock accumulateBlock = self->_accumulateDataBlock;
+ if (accumulateBlock) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ accumulateBlock(nil);
+ }];
+ }
+ }
+ } // @synchronized(self)
+ handler(dispositionValue);
+ };
+
+ GTMSessionFetcherDidReceiveResponseBlock receivedResponseBlock;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ receivedResponseBlock = _didReceiveResponseBlock;
+ if (receivedResponseBlock) {
+ // We will ultimately need to call back to NSURLSession's handler with the disposition value
+ // for this delegate method even if the user has stopped the fetcher.
+ [self invokeOnCallbackQueueAfterUserStopped:YES
+ block:^{
+ receivedResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) {
+ accumulateAndFinish(desiredDisposition);
+ });
+ }];
+ }
+ } // @synchronized(self)
+
+ if (receivedResponseBlock == nil) {
+ accumulateAndFinish(NSURLSessionResponseAllow);
+ }
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didBecomeDownloadTask:%@",
+ [self class], self, session, dataTask, downloadTask);
+ [self setSessionTask:downloadTask];
+}
+
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
+ NSURLCredential * GTM_NULLABLE_TYPE credential))handler {
+ [self setSessionTask:task];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didReceiveChallenge:%@",
+ [self class], self, session, task, challenge);
+
+ GTMSessionFetcherChallengeBlock challengeBlock = self.challengeBlock;
+ if (challengeBlock) {
+ // The fetcher user has provided custom challenge handling.
+ //
+ // We will ultimately need to call back to NSURLSession's handler with the disposition value
+ // for this delegate method even if the user has stopped the fetcher.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [self invokeOnCallbackQueueAfterUserStopped:YES
+ block:^{
+ challengeBlock(self, challenge, handler);
+ }];
+ }
+ } else {
+ // No challenge block was provided by the client.
+ [self respondToChallenge:challenge
+ completionHandler:handler];
+ }
+}
+
+- (void)respondToChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
+ NSURLCredential * GTM_NULLABLE_TYPE credential))handler {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSInteger previousFailureCount = [challenge previousFailureCount];
+ if (previousFailureCount <= 2) {
+ NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
+ NSString *authenticationMethod = [protectionSpace authenticationMethod];
+ if ([authenticationMethod isEqual:NSURLAuthenticationMethodServerTrust]) {
+ // SSL.
+ //
+ // Background sessions seem to require an explicit check of the server trust object
+ // rather than default handling.
+ SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
+ if (serverTrust == NULL) {
+ // No server trust information is available.
+ handler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+ } else {
+ // Server trust information is available.
+ void (^callback)(SecTrustRef, BOOL) = ^(SecTrustRef trustRef, BOOL allow){
+ if (allow) {
+ NSURLCredential *trustCredential = [NSURLCredential credentialForTrust:trustRef];
+ handler(NSURLSessionAuthChallengeUseCredential, trustCredential);
+ } else {
+ GTMSESSION_LOG_DEBUG(@"Cancelling authentication challenge for %@", self->_request.URL);
+ handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
+ }
+ };
+ if (_allowInvalidServerCertificates) {
+ callback(serverTrust, YES);
+ } else {
+ [[self class] evaluateServerTrust:serverTrust
+ forRequest:_request
+ completionHandler:callback];
+ }
+ }
+ return;
+ }
+
+ NSURLCredential *credential = _credential;
+
+ if ([[challenge protectionSpace] isProxy] && _proxyCredential != nil) {
+ credential = _proxyCredential;
+ }
+
+ if (credential) {
+ handler(NSURLSessionAuthChallengeUseCredential, credential);
+ } else {
+ // The credential is still nil; tell the OS to use the default handling. This is needed
+ // for things that can come out of the keychain (proxies, client certificates, etc.).
+ //
+ // Note: Looking up a credential with NSURLCredentialStorage's
+ // defaultCredentialForProtectionSpace: is *not* the same invoking the handler with
+ // NSURLSessionAuthChallengePerformDefaultHandling. In the case of
+ // NSURLAuthenticationMethodClientCertificate, you can get nil back from
+ // NSURLCredentialStorage, while using this code path instead works.
+ handler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+ }
+
+ } else {
+ // We've failed auth 3 times. The completion handler will be called with code
+ // NSURLErrorCancelled.
+ handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
+ }
+ } // @synchronized(self)
+}
+
+// Return redirect URL based on the original request URL and redirect request URL.
+//
+// Method disallows any scheme changes between the original request URL and redirect request URL
+// aside from "http" to "https". If a change in scheme is detected the redirect URL inherits the
+// scheme from the original request URL.
++ (GTM_NULLABLE NSURL *)redirectURLWithOriginalRequestURL:(GTM_NULLABLE NSURL *)originalRequestURL
+ redirectRequestURL:(GTM_NULLABLE NSURL *)redirectRequestURL {
+ // In the case of an NSURLSession redirect, neither URL should ever be nil; as a sanity check
+ // if either is nil return the other URL.
+ if (!redirectRequestURL) return originalRequestURL;
+ if (!originalRequestURL) return redirectRequestURL;
+
+ NSString *originalScheme = originalRequestURL.scheme;
+ NSString *redirectScheme = redirectRequestURL.scheme;
+ BOOL insecureToSecureRedirect =
+ (originalScheme != nil && [originalScheme caseInsensitiveCompare:@"http"] == NSOrderedSame &&
+ redirectScheme != nil && [redirectScheme caseInsensitiveCompare:@"https"] == NSOrderedSame);
+
+ // This can't really be nil for the inputs, but to keep the analyzer happy
+ // for the -caseInsensitiveCompare: call below, give it a value if it were.
+ if (!originalScheme) originalScheme = @"https";
+
+ // Check for changes to the scheme and disallow any changes except for http to https.
+ if (!insecureToSecureRedirect &&
+ (redirectScheme.length != originalScheme.length ||
+ [redirectScheme caseInsensitiveCompare:originalScheme] != NSOrderedSame)) {
+ NSURLComponents *components =
+ [NSURLComponents componentsWithURL:(NSURL * _Nonnull)redirectRequestURL
+ resolvingAgainstBaseURL:NO];
+ components.scheme = originalScheme;
+ return components.URL;
+ }
+
+ return redirectRequestURL;
+}
+
+// Validate the certificate chain.
+//
+// This may become a public method if it appears to be useful to users.
++ (void)evaluateServerTrust:(SecTrustRef)serverTrust
+ forRequest:(NSURLRequest *)request
+ completionHandler:(void (^)(SecTrustRef trustRef, BOOL allow))handler {
+ // Retain the trust object to avoid a SecTrustEvaluate() crash on iOS 7.
+ CFRetain(serverTrust);
+
+ // Evaluate the certificate chain.
+ //
+ // The delegate queue may be the main thread. Trust evaluation could cause some
+ // blocking network activity, so we must evaluate async, as documented at
+ // https://developer.apple.com/library/ios/technotes/tn2232/
+ //
+ // We must also avoid multiple uses of the trust object, per docs:
+ // "It is not safe to call this function concurrently with any other function that uses
+ // the same trust management object, or to re-enter this function for the same trust
+ // management object."
+ //
+ // SecTrustEvaluateAsync both does sync execution of Evaluate and calls back on the
+ // queue passed to it, according to at sources in
+ // http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-55050.9/lib/SecTrust.cpp
+ // It would require a global serial queue to ensure the evaluate happens only on a
+ // single thread at a time, so we'll stick with using SecTrustEvaluate on a background
+ // thread.
+ dispatch_queue_t evaluateBackgroundQueue =
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
+ dispatch_async(evaluateBackgroundQueue, ^{
+ // It looks like the implementation of SecTrustEvaluate() on Mac grabs a global lock,
+ // so it may be redundant for us to also lock, but it's easy to synchronize here
+ // anyway.
+ BOOL shouldAllow;
+#if GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR
+ CFErrorRef errorRef = NULL;
+ @synchronized ([GTMSessionFetcher class]) {
+ GTMSessionMonitorSynchronized([GTMSessionFetcher class]);
+
+ // SecTrustEvaluateWithError handles both the "proceed" and "unspecified" cases,
+ // so it is not necessary to check the trust result the evaluation returns true.
+ shouldAllow = SecTrustEvaluateWithError(serverTrust, &errorRef);
+ }
+
+ if (errorRef) {
+ GTMSESSION_LOG_DEBUG(@"Error %d evaluating trust for %@",
+ (int)CFErrorGetCode(errorRef), request);
+ CFRelease(errorRef);
+ }
+#else
+ SecTrustResultType trustEval = kSecTrustResultInvalid;
+ OSStatus trustError;
+ @synchronized([GTMSessionFetcher class]) {
+ GTMSessionMonitorSynchronized([GTMSessionFetcher class]);
+
+ trustError = SecTrustEvaluate(serverTrust, &trustEval);
+ }
+ if (trustError != errSecSuccess) {
+ GTMSESSION_LOG_DEBUG(@"Error %d evaluating trust for %@",
+ (int)trustError, request);
+ shouldAllow = NO;
+ } else {
+ // Having a trust level "unspecified" by the user is the usual result, described at
+ // https://developer.apple.com/library/mac/qa/qa1360
+ if (trustEval == kSecTrustResultUnspecified
+ || trustEval == kSecTrustResultProceed) {
+ shouldAllow = YES;
+ } else {
+ shouldAllow = NO;
+ GTMSESSION_LOG_DEBUG(@"Challenge SecTrustResultType %u for %@, properties: %@",
+ trustEval, request.URL.host,
+ CFBridgingRelease(SecTrustCopyProperties(serverTrust)));
+ }
+ }
+#endif // GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR
+ handler(serverTrust, shouldAllow);
+
+ CFRelease(serverTrust);
+ });
+}
+
+- (void)invokeOnCallbackQueueUnlessStopped:(void (^)(void))block {
+ [self invokeOnCallbackQueueAfterUserStopped:NO
+ block:block];
+}
+
+- (void)invokeOnCallbackQueueAfterUserStopped:(BOOL)afterStopped
+ block:(void (^)(void))block {
+ GTMSessionCheckSynchronized(self);
+
+ [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:afterStopped
+ block:block];
+}
+
+- (void)invokeOnCallbackUnsynchronizedQueueAfterUserStopped:(BOOL)afterStopped
+ block:(void (^)(void))block {
+ // testBlock simulation code may not be synchronizing when this is invoked.
+ [self invokeOnCallbackQueue:_callbackQueue
+ afterUserStopped:afterStopped
+ block:block];
+}
+
+- (void)invokeOnCallbackQueue:(dispatch_queue_t)callbackQueue
+ afterUserStopped:(BOOL)afterStopped
+ block:(void (^)(void))block {
+ if (callbackQueue) {
+ dispatch_group_async(_callbackGroup, callbackQueue, ^{
+ if (!afterStopped) {
+ NSDate *serviceStoppedAllDate = [self->_service stoppedAllFetchersDate];
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // Avoid a race between stopFetching and the callback.
+ if (self->_userStoppedFetching) {
+ return;
+ }
+
+ // Also avoid calling back if the service has stopped all fetchers
+ // since this one was created. The fetcher may have stopped before
+ // stopAllFetchers was invoked, so _userStoppedFetching wasn't set,
+ // but the app still won't expect the callback to fire after
+ // the service's stopAllFetchers was invoked.
+ if (serviceStoppedAllDate
+ && [self->_initialBeginFetchDate compare:serviceStoppedAllDate] != NSOrderedDescending) {
+ // stopAllFetchers was called after this fetcher began.
+ return;
+ }
+ } // @synchronized(self)
+ }
+ block();
+ });
+ }
+}
+
+- (void)invokeFetchCallbacksOnCallbackQueueWithData:(GTM_NULLABLE NSData *)data
+ error:(GTM_NULLABLE NSError *)error {
+ // Callbacks will be released in the method stopFetchReleasingCallbacks:
+ GTMSessionFetcherCompletionHandler handler;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ handler = _completionHandler;
+
+ if (handler) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ handler(data, error);
+
+ // Post a notification, primarily to allow code to collect responses for
+ // testing.
+ //
+ // The observing code is not likely on the fetcher's callback
+ // queue, so this posts explicitly to the main queue.
+ NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
+ if (data) {
+ userInfo[kGTMSessionFetcherCompletionDataKey] = data;
+ }
+ if (error) {
+ userInfo[kGTMSessionFetcherCompletionErrorKey] = error;
+ }
+ [self postNotificationOnMainThreadWithName:kGTMSessionFetcherCompletionInvokedNotification
+ userInfo:userInfo
+ requireAsync:NO];
+ }];
+ }
+ } // @synchronized(self)
+}
+
+- (void)postNotificationOnMainThreadWithName:(NSString *)noteName
+ userInfo:(GTM_NULLABLE NSDictionary *)userInfo
+ requireAsync:(BOOL)requireAsync {
+ dispatch_block_t postBlock = ^{
+ [[NSNotificationCenter defaultCenter] postNotificationName:noteName
+ object:self
+ userInfo:userInfo];
+ };
+
+ if ([NSThread isMainThread] && !requireAsync) {
+ // Post synchronously for compatibility with older code using the fetcher.
+
+ // Avoid calling out to other code from inside a sync block to avoid risk
+ // of a deadlock or of recursive sync.
+ GTMSessionCheckNotSynchronized(self);
+
+ postBlock();
+ } else {
+ dispatch_async(dispatch_get_main_queue(), postBlock);
+ }
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)uploadTask
+ needNewBodyStream:(void (^)(NSInputStream * GTM_NULLABLE_TYPE bodyStream))completionHandler {
+ [self setSessionTask:uploadTask];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ needNewBodyStream:",
+ [self class], self, session, uploadTask);
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSessionFetcherBodyStreamProvider provider = _bodyStreamProvider;
+#if !STRIP_GTM_FETCH_LOGGING
+ if ([self respondsToSelector:@selector(loggedStreamProviderForStreamProvider:)]) {
+ provider = [self performSelector:@selector(loggedStreamProviderForStreamProvider:)
+ withObject:provider];
+ }
+#endif
+ if (provider) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ provider(completionHandler);
+ }];
+ } else {
+ GTMSESSION_ASSERT_DEBUG(NO, @"NSURLSession expects a stream provider");
+
+ completionHandler(nil);
+ }
+ } // @synchronized(self)
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didSendBodyData:(int64_t)bytesSent
+ totalBytesSent:(int64_t)totalBytesSent
+totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
+ [self setSessionTask:task];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didSendBodyData:%lld"
+ @" totalBytesSent:%lld totalBytesExpectedToSend:%lld",
+ [self class], self, session, task, bytesSent, totalBytesSent,
+ totalBytesExpectedToSend);
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (!_sendProgressBlock) {
+ return;
+ }
+ // We won't hold on to send progress block; it's ok to not send it if the upload finishes.
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ GTMSessionFetcherSendProgressBlock progressBlock;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ progressBlock = self->_sendProgressBlock;
+ }
+ if (progressBlock) {
+ progressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
+ }
+ }];
+ } // @synchronized(self)
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+ didReceiveData:(NSData *)data {
+ [self setSessionTask:dataTask];
+ NSUInteger bufferLength = data.length;
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveData:%p (%llu bytes)",
+ [self class], self, session, dataTask, data,
+ (unsigned long long)bufferLength);
+ if (bufferLength == 0) {
+ // Observed on completing an out-of-process upload.
+ return;
+ }
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSessionFetcherAccumulateDataBlock accumulateBlock = _accumulateDataBlock;
+ if (accumulateBlock) {
+ // Let the client accumulate the data.
+ _downloadedLength += bufferLength;
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ accumulateBlock(data);
+ }];
+ } else if (!_userStoppedFetching) {
+ // Append to the mutable data buffer unless the fetch has been cancelled.
+
+ // Resumed upload tasks may not yet have a data buffer.
+ if (_downloadedData == nil) {
+ // Using NSClassFromString for iOS 6 compatibility.
+ GTMSESSION_ASSERT_DEBUG(
+ ![dataTask isKindOfClass:NSClassFromString(@"NSURLSessionDownloadTask")],
+ @"Resumed download tasks should not receive data bytes");
+ _downloadedData = [[NSMutableData alloc] init];
+ }
+
+ [_downloadedData appendData:data];
+ _downloadedLength = (int64_t)_downloadedData.length;
+
+ // We won't hold on to receivedProgressBlock here; it's ok to not send
+ // it if the transfer finishes.
+ if (_receivedProgressBlock) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ GTMSessionFetcherReceivedProgressBlock progressBlock;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ progressBlock = self->_receivedProgressBlock;
+ }
+ if (progressBlock) {
+ progressBlock((int64_t)bufferLength, self->_downloadedLength);
+ }
+ }];
+ }
+ }
+ } // @synchronized(self)
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+ willCacheResponse:(NSCachedURLResponse *)proposedResponse
+ completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ willCacheResponse:%@ %@",
+ [self class], self, session, dataTask,
+ proposedResponse, proposedResponse.response);
+ GTMSessionFetcherWillCacheURLResponseBlock callback;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ callback = _willCacheURLResponseBlock;
+
+ if (callback) {
+ [self invokeOnCallbackQueueAfterUserStopped:YES
+ block:^{
+ callback(proposedResponse, completionHandler);
+ }];
+ }
+ } // @synchronized(self)
+ if (!callback) {
+ completionHandler(proposedResponse);
+ }
+}
+
+
+- (void)URLSession:(NSURLSession *)session
+ downloadTask:(NSURLSessionDownloadTask *)downloadTask
+ didWriteData:(int64_t)bytesWritten
+ totalBytesWritten:(int64_t)totalBytesWritten
+totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didWriteData:%lld"
+ @" bytesWritten:%lld totalBytesExpectedToWrite:%lld",
+ [self class], self, session, downloadTask, bytesWritten,
+ totalBytesWritten, totalBytesExpectedToWrite);
+ [self setSessionTask:downloadTask];
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if ((totalBytesExpectedToWrite != NSURLSessionTransferSizeUnknown) &&
+ (totalBytesExpectedToWrite < totalBytesWritten)) {
+ // Have observed cases were bytesWritten == totalBytesExpectedToWrite,
+ // but totalBytesWritten > totalBytesExpectedToWrite, so setting to unkown in these cases.
+ totalBytesExpectedToWrite = NSURLSessionTransferSizeUnknown;
+ }
+
+ GTMSessionFetcherDownloadProgressBlock progressBlock;
+ progressBlock = self->_downloadProgressBlock;
+ if (progressBlock) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ progressBlock(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
+ }];
+ }
+ } // @synchronized(self)
+}
+
+- (void)URLSession:(NSURLSession *)session
+ downloadTask:(NSURLSessionDownloadTask *)downloadTask
+ didResumeAtOffset:(int64_t)fileOffset
+expectedTotalBytes:(int64_t)expectedTotalBytes {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didResumeAtOffset:%lld"
+ @" expectedTotalBytes:%lld",
+ [self class], self, session, downloadTask, fileOffset,
+ expectedTotalBytes);
+ [self setSessionTask:downloadTask];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ downloadTask:(NSURLSessionDownloadTask *)downloadTask
+didFinishDownloadingToURL:(NSURL *)downloadLocationURL {
+ // Download may have relaunched app, so update _sessionTask.
+ [self setSessionTask:downloadTask];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didFinishDownloadingToURL:%@",
+ [self class], self, session, downloadTask, downloadLocationURL);
+ NSNumber *fileSizeNum;
+ [downloadLocationURL getResourceValue:&fileSizeNum
+ forKey:NSURLFileSizeKey
+ error:NULL];
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSURL *destinationURL = _destinationFileURL;
+
+ _downloadedLength = fileSizeNum.longLongValue;
+
+ // Overwrite any previous file at the destination URL.
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ NSError *removeError;
+ if (![fileMgr removeItemAtURL:destinationURL error:&removeError]
+ && removeError.code != NSFileNoSuchFileError) {
+ GTMSESSION_LOG_DEBUG(@"Could not remove previous file at %@ due to %@",
+ downloadLocationURL.path, removeError);
+ }
+
+ NSInteger statusCode = [self statusCodeUnsynchronized];
+ if (statusCode < 200 || statusCode > 399) {
+ // In OS X 10.11, the response body is written to a file even on a server
+ // status error. For convenience of the fetcher client, we'll skip saving the
+ // downloaded body to the destination URL so that clients do not need to know
+ // to delete the file following fetch errors.
+ GTMSESSION_LOG_DEBUG(@"Abandoning download due to status %ld, file %@",
+ (long)statusCode, downloadLocationURL.path);
+
+ // On error code, add the contents of the temporary file to _downloadTaskErrorData
+ // This way fetcher clients have access to error details possibly passed by the server.
+ if (_downloadedLength > 0 && _downloadedLength <= kMaximumDownloadErrorDataLength) {
+ _downloadTaskErrorData = [NSData dataWithContentsOfURL:downloadLocationURL];
+ } else if (_downloadedLength > kMaximumDownloadErrorDataLength) {
+ GTMSESSION_LOG_DEBUG(@"Download error data for file %@ not passed to userInfo due to size "
+ @"%lld", downloadLocationURL.path, _downloadedLength);
+ }
+ } else {
+ NSError *moveError;
+ NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent];
+ BOOL didMoveDownload = NO;
+ if ([fileMgr createDirectoryAtURL:destinationFolderURL
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&moveError]) {
+ didMoveDownload = [fileMgr moveItemAtURL:downloadLocationURL
+ toURL:destinationURL
+ error:&moveError];
+ }
+ if (!didMoveDownload) {
+ _downloadFinishedError = moveError;
+ }
+ GTM_LOG_BACKGROUND_SESSION(@"%@ %p Moved download from \"%@\" to \"%@\" %@",
+ [self class], self,
+ downloadLocationURL.path, destinationURL.path,
+ error ? error : @"");
+ }
+ } // @synchronized(self)
+}
+
+/* Sent as the last message related to a specific task. Error may be
+ * nil, which implies that no error occurred and this task is complete.
+ */
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+didCompleteWithError:(NSError *)error {
+ [self setSessionTask:task];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didCompleteWithError:%@",
+ [self class], self, session, task, error);
+
+ NSInteger status = self.statusCode;
+ BOOL forceAssumeRetry = NO;
+ BOOL succeeded = NO;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+#if !GTM_DISABLE_FETCHER_TEST_BLOCK
+ // The task is never resumed when a testBlock is used. When the session is destroyed,
+ // we should ignore the callback, since the testBlock support code itself invokes
+ // shouldRetryNowForStatus: and finishWithError:shouldRetry:
+ if (_isUsingTestBlock) return;
+#endif
+
+ if (error == nil) {
+ error = _downloadFinishedError;
+ }
+ succeeded = (error == nil && status >= 0 && status < 300);
+ if (succeeded) {
+ // Succeeded.
+ _bodyLength = task.countOfBytesSent;
+ }
+ } // @synchronized(self)
+
+ if (succeeded) {
+ [self finishWithError:nil shouldRetry:NO];
+ return;
+ }
+ // For background redirects, no delegate method is called, so we cannot restore a stripped
+ // Authorization header, so if a 403 ("Forbidden") was generated due to a missing OAuth 2 header,
+ // set the current request's URL to the redirected URL, so we in effect restore the Authorization
+ // header.
+ if ((status == 403) && self.usingBackgroundSession) {
+ NSURL *redirectURL = self.response.URL;
+ NSURLRequest *request = self.request;
+ if (![request.URL isEqual:redirectURL]) {
+ NSString *authorizationHeader = [request.allHTTPHeaderFields objectForKey:@"Authorization"];
+ if (authorizationHeader != nil) {
+ NSMutableURLRequest *mutableRequest = [request mutableCopy];
+ mutableRequest.URL = redirectURL;
+ [self updateMutableRequest:mutableRequest];
+ // Avoid assuming the session is still valid.
+ self.session = nil;
+ forceAssumeRetry = YES;
+ }
+ }
+ }
+
+ // If invalidating the session was deferred in stopFetchReleasingCallbacks: then do it now.
+ NSURLSession *oldSession = self.sessionNeedingInvalidation;
+ if (oldSession) {
+ [self setSessionNeedingInvalidation:NULL];
+ [oldSession finishTasksAndInvalidate];
+ }
+
+ // Failed.
+ [self shouldRetryNowForStatus:status
+ error:error
+ forceAssumeRetry:forceAssumeRetry
+ response:^(BOOL shouldRetry) {
+ [self finishWithError:error shouldRetry:shouldRetry];
+ }];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
+ API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)) {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+ GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock = _metricsCollectionBlock;
+ if (metricsCollectionBlock) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ metricsCollectionBlock(metrics);
+ }];
+ }
+ }
+}
+
+#if TARGET_OS_IPHONE
+- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSessionDidFinishEventsForBackgroundURLSession:%@",
+ [self class], self, session);
+ [self removePersistedBackgroundSessionFromDefaults];
+
+ GTMSessionFetcherSystemCompletionHandler handler;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ handler = self.systemCompletionHandler;
+ self.systemCompletionHandler = nil;
+ } // @synchronized(self)
+ if (handler) {
+ GTM_LOG_BACKGROUND_SESSION(@"%@ %p Calling system completionHandler", [self class], self);
+ handler();
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSURLSession *oldSession = _session;
+ _session = nil;
+ if (_shouldInvalidateSession) {
+ [oldSession finishTasksAndInvalidate];
+ }
+ } // @synchronized(self)
+ }
+}
+#endif
+
+- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(GTM_NULLABLE NSError *)error {
+ // This may happen repeatedly for retries. On authentication callbacks, the retry
+ // may begin before the prior session sends the didBecomeInvalid delegate message.
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@",
+ [self class], self, session, error);
+ if (session == (NSURLSession *)self.session) {
+ GTM_LOG_SESSION_DELEGATE(@" Unexpected retained invalid session: %@", session);
+ self.session = nil;
+ }
+}
+
+- (void)finishWithError:(GTM_NULLABLE NSError *)error shouldRetry:(BOOL)shouldRetry {
+ [self removePersistedBackgroundSessionFromDefaults];
+
+ BOOL shouldStopFetching = YES;
+ NSData *downloadedData = nil;
+#if !STRIP_GTM_FETCH_LOGGING
+ BOOL shouldDeferLogging = NO;
+#endif
+ BOOL shouldBeginRetryTimer = NO;
+ NSInteger status = [self statusCode];
+ NSURL *destinationURL = self.destinationFileURL;
+
+ BOOL fetchSucceeded = (error == nil && status >= 0 && status < 300);
+
+#if !STRIP_GTM_FETCH_LOGGING
+ if (!fetchSucceeded) {
+ if (!shouldDeferLogging && !self.hasLoggedError) {
+ [self logNowWithError:error];
+ self.hasLoggedError = YES;
+ }
+ }
+#endif // !STRIP_GTM_FETCH_LOGGING
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+#if !STRIP_GTM_FETCH_LOGGING
+ shouldDeferLogging = _deferResponseBodyLogging;
+#endif
+ if (fetchSucceeded) {
+ // Success
+ if ((_downloadedData.length > 0) && (destinationURL != nil)) {
+ // Overwrite any previous file at the destination URL.
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ [fileMgr removeItemAtURL:destinationURL
+ error:NULL];
+ NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent];
+ BOOL didMoveDownload = NO;
+ if ([fileMgr createDirectoryAtURL:destinationFolderURL
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&error]) {
+ didMoveDownload = [_downloadedData writeToURL:destinationURL
+ options:NSDataWritingAtomic
+ error:&error];
+ }
+ if (didMoveDownload) {
+ _downloadedData = nil;
+ } else {
+ _downloadFinishedError = error;
+ }
+ }
+ downloadedData = _downloadedData;
+ } else {
+ // Unsuccessful with error or status over 300. Retry or notify the delegate of failure
+ if (shouldRetry) {
+ // Retrying.
+ shouldBeginRetryTimer = YES;
+ shouldStopFetching = NO;
+ } else {
+ if (error == nil) {
+ // Create an error.
+ NSDictionary *userInfo = GTMErrorUserInfoForData(
+ _downloadedData.length > 0 ? _downloadedData : _downloadTaskErrorData,
+ [self responseHeadersUnsynchronized]);
+
+ error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
+ code:status
+ userInfo:userInfo];
+ } else {
+ // If the error had resume data, and the client supplied a resume block, pass the
+ // data to the client.
+ void (^resumeBlock)(NSData *) = _resumeDataBlock;
+ _resumeDataBlock = nil;
+ if (resumeBlock) {
+ NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
+ if (resumeData) {
+ [self invokeOnCallbackQueueAfterUserStopped:YES block:^{
+ resumeBlock(resumeData);
+ }];
+ }
+ }
+ }
+ if (_downloadedData.length > 0) {
+ downloadedData = _downloadedData;
+ }
+ // If the error occurred after retries, report the number and duration of the
+ // retries. This provides a clue to a developer looking at the error description
+ // that the fetcher did retry before failing with this error.
+ if (_retryCount > 0) {
+ NSMutableDictionary *userInfoWithRetries =
+ [NSMutableDictionary dictionaryWithDictionary:(NSDictionary *)error.userInfo];
+ NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow];
+ [userInfoWithRetries setObject:@(timeSinceInitialRequest)
+ forKey:kGTMSessionFetcherElapsedIntervalWithRetriesKey];
+ [userInfoWithRetries setObject:@(_retryCount)
+ forKey:kGTMSessionFetcherNumberOfRetriesDoneKey];
+ error = [NSError errorWithDomain:(NSString *)error.domain
+ code:error.code
+ userInfo:userInfoWithRetries];
+ }
+ }
+ }
+ } // @synchronized(self)
+
+ if (shouldBeginRetryTimer) {
+ [self beginRetryTimer];
+ }
+
+ // We want to send the stop notification before calling the delegate's
+ // callback selector, since the callback selector may release all of
+ // the fetcher properties that the client is using to track the fetches.
+ //
+ // We'll also stop now so that, to any observers watching the notifications,
+ // it doesn't look like our wait for a retry (which may be long,
+ // 30 seconds or more) is part of the network activity.
+ [self sendStopNotificationIfNeeded];
+
+ if (shouldStopFetching) {
+ [self invokeFetchCallbacksOnCallbackQueueWithData:downloadedData
+ error:error];
+ // The upload subclass doesn't want to release callbacks until upload chunks have completed.
+ BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion];
+ [self stopFetchReleasingCallbacks:shouldRelease];
+ }
+
+#if !STRIP_GTM_FETCH_LOGGING
+ // _hasLoggedError is only set by this method
+ if (!shouldDeferLogging && !_hasLoggedError) {
+ [self logNowWithError:error];
+ }
+#endif
+}
+
+- (BOOL)shouldReleaseCallbacksUponCompletion {
+ // A subclass can override this to keep callbacks around after the
+ // connection has finished successfully
+ return YES;
+}
+
+- (void)logNowWithError:(GTM_NULLABLE NSError *)error {
+ GTMSessionCheckNotSynchronized(self);
+
+ // If the logging category is available, then log the current request,
+ // response, data, and error
+ if ([self respondsToSelector:@selector(logFetchWithError:)]) {
+ [self performSelector:@selector(logFetchWithError:) withObject:error];
+ }
+}
+
+#pragma mark Retries
+
+- (BOOL)isRetryError:(NSError *)error {
+ struct RetryRecord {
+ __unsafe_unretained NSString *const domain;
+ NSInteger code;
+ };
+
+ struct RetryRecord retries[] = {
+ { kGTMSessionFetcherStatusDomain, 408 }, // request timeout
+ { kGTMSessionFetcherStatusDomain, 502 }, // failure gatewaying to another server
+ { kGTMSessionFetcherStatusDomain, 503 }, // service unavailable
+ { kGTMSessionFetcherStatusDomain, 504 }, // request timeout
+ { NSURLErrorDomain, NSURLErrorTimedOut },
+ { NSURLErrorDomain, NSURLErrorNetworkConnectionLost },
+ { nil, 0 }
+ };
+
+ // NSError's isEqual always returns false for equal but distinct instances
+ // of NSError, so we have to compare the domain and code values explicitly
+ NSString *domain = error.domain;
+ NSInteger code = error.code;
+ for (int idx = 0; retries[idx].domain != nil; idx++) {
+ if (code == retries[idx].code && [domain isEqual:retries[idx].domain]) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+// shouldRetryNowForStatus:error: responds with YES if the user has enabled retries
+// and the status or error is one that is suitable for retrying. "Suitable"
+// means either the isRetryError:'s list contains the status or error, or the
+// user's retry block is present and returns YES when called, or the
+// authorizer may be able to fix.
+- (void)shouldRetryNowForStatus:(NSInteger)status
+ error:(NSError *)error
+ forceAssumeRetry:(BOOL)forceAssumeRetry
+ response:(GTMSessionFetcherRetryResponse)response {
+ // Determine if a refreshed authorizer may avoid an authorization error
+ BOOL willRetry = NO;
+
+ // We assume _authorizer is immutable after beginFetch, and _hasAttemptedAuthRefresh is modified
+ // only in this method, and this method is invoked on the serial delegate queue.
+ //
+ // We want to avoid calling the authorizer from inside a sync block.
+ BOOL isFirstAuthError = (_authorizer != nil
+ && !_hasAttemptedAuthRefresh
+ && status == GTMSessionFetcherStatusUnauthorized); // 401
+
+ BOOL hasPrimed = NO;
+ if (isFirstAuthError) {
+ if ([_authorizer respondsToSelector:@selector(primeForRefresh)]) {
+ hasPrimed = [_authorizer primeForRefresh];
+ }
+ }
+
+ BOOL shouldRetryForAuthRefresh = NO;
+ if (hasPrimed) {
+ shouldRetryForAuthRefresh = YES;
+ _hasAttemptedAuthRefresh = YES;
+ [self updateRequestValue:nil forHTTPHeaderField:@"Authorization"];
+ }
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ BOOL shouldDoRetry = [self isRetryEnabledUnsynchronized];
+ if (shouldDoRetry && ![self hasRetryAfterInterval]) {
+
+ // Determine if we're doing exponential backoff retries
+ shouldDoRetry = [self nextRetryIntervalUnsynchronized] < _maxRetryInterval;
+
+ if (shouldDoRetry) {
+ // If an explicit max retry interval was set, we expect repeated backoffs to take
+ // up to roughly twice that for repeated fast failures. If the initial attempt is
+ // already more than 3 times the max retry interval, then failures have taken a long time
+ // (such as from network timeouts) so don't retry again to avoid the app becoming
+ // unexpectedly unresponsive.
+ if (_maxRetryInterval > 0) {
+ NSTimeInterval maxAllowedIntervalBeforeRetry = _maxRetryInterval * 3;
+ NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow];
+ if (timeSinceInitialRequest > maxAllowedIntervalBeforeRetry) {
+ shouldDoRetry = NO;
+ }
+ }
+ }
+ }
+ BOOL canRetry = shouldRetryForAuthRefresh || forceAssumeRetry || shouldDoRetry;
+ if (canRetry) {
+ NSDictionary *userInfo =
+ GTMErrorUserInfoForData(_downloadedData, [self responseHeadersUnsynchronized]);
+ NSError *statusError = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
+ code:status
+ userInfo:userInfo];
+ if (error == nil) {
+ error = statusError;
+ }
+ willRetry = shouldRetryForAuthRefresh ||
+ forceAssumeRetry ||
+ [self isRetryError:error] ||
+ ((error != statusError) && [self isRetryError:statusError]);
+
+ // If the user has installed a retry callback, consult that.
+ GTMSessionFetcherRetryBlock retryBlock = _retryBlock;
+ if (retryBlock) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ retryBlock(willRetry, error, response);
+ }];
+ return;
+ }
+ }
+ } // @synchronized(self)
+ response(willRetry);
+}
+
+- (BOOL)hasRetryAfterInterval {
+ GTMSessionCheckSynchronized(self);
+
+ NSDictionary *responseHeaders = [self responseHeadersUnsynchronized];
+ NSString *retryAfterValue = [responseHeaders valueForKey:@"Retry-After"];
+ return (retryAfterValue != nil);
+}
+
+- (NSTimeInterval)retryAfterInterval {
+ GTMSessionCheckSynchronized(self);
+
+ NSDictionary *responseHeaders = [self responseHeadersUnsynchronized];
+ NSString *retryAfterValue = [responseHeaders valueForKey:@"Retry-After"];
+ if (retryAfterValue == nil) {
+ return 0;
+ }
+ // Retry-After formatted as HTTP-date | delta-seconds
+ // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+ NSDateFormatter *rfc1123DateFormatter = [[NSDateFormatter alloc] init];
+ rfc1123DateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
+ rfc1123DateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
+ rfc1123DateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss z";
+ NSDate *retryAfterDate = [rfc1123DateFormatter dateFromString:retryAfterValue];
+ NSTimeInterval retryAfterInterval = (retryAfterDate != nil) ?
+ retryAfterDate.timeIntervalSinceNow : retryAfterValue.intValue;
+ retryAfterInterval = MAX(0, retryAfterInterval);
+ return retryAfterInterval;
+}
+
+- (void)beginRetryTimer {
+ if (![NSThread isMainThread]) {
+ // Defer creating and starting the timer until we're on the main thread to ensure it has
+ // a run loop.
+ dispatch_group_async(_callbackGroup, dispatch_get_main_queue(), ^{
+ [self beginRetryTimer];
+ });
+ return;
+ }
+
+ [self destroyRetryTimer];
+
+#if GTM_BACKGROUND_TASK_FETCHING
+ // Don't keep a background task active while awaiting retry, which can lead to the
+ // app exceeding the allotted time for keeping the background task open, causing the
+ // system to terminate the app. When the retry starts, a new background task will
+ // be created.
+ [self endBackgroundTask];
+#endif // GTM_BACKGROUND_TASK_FETCHING
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSTimeInterval nextInterval = [self nextRetryIntervalUnsynchronized];
+ NSTimeInterval maxInterval = _maxRetryInterval;
+ NSTimeInterval newInterval = MIN(nextInterval, (maxInterval > 0 ? maxInterval : DBL_MAX));
+ NSTimeInterval newIntervalTolerance = (newInterval / 10) > 1.0 ?: 1.0;
+
+ _lastRetryInterval = newInterval;
+
+ _retryTimer = [NSTimer timerWithTimeInterval:newInterval
+ target:self
+ selector:@selector(retryTimerFired:)
+ userInfo:nil
+ repeats:NO];
+ _retryTimer.tolerance = newIntervalTolerance;
+ [[NSRunLoop mainRunLoop] addTimer:_retryTimer
+ forMode:NSDefaultRunLoopMode];
+ } // @synchronized(self)
+
+ [self postNotificationOnMainThreadWithName:kGTMSessionFetcherRetryDelayStartedNotification
+ userInfo:nil
+ requireAsync:NO];
+}
+
+- (void)retryTimerFired:(NSTimer *)timer {
+ [self destroyRetryTimer];
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _retryCount++;
+ } // @synchronized(self)
+
+ NSOperationQueue *queue = self.sessionDelegateQueue;
+ [queue addOperationWithBlock:^{
+ [self retryFetch];
+ }];
+}
+
+- (void)destroyRetryTimer {
+ BOOL shouldNotify = NO;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_retryTimer) {
+ [_retryTimer invalidate];
+ _retryTimer = nil;
+ shouldNotify = YES;
+ }
+ }
+
+ if (shouldNotify) {
+ [self postNotificationOnMainThreadWithName:kGTMSessionFetcherRetryDelayStoppedNotification
+ userInfo:nil
+ requireAsync:NO];
+ }
+}
+
+- (NSUInteger)retryCount {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _retryCount;
+ } // @synchronized(self)
+}
+
+- (NSTimeInterval)nextRetryInterval {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSTimeInterval interval = [self nextRetryIntervalUnsynchronized];
+ return interval;
+ } // @synchronized(self)
+}
+
+- (NSTimeInterval)nextRetryIntervalUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ NSInteger statusCode = [self statusCodeUnsynchronized];
+ if ((statusCode == 503) && [self hasRetryAfterInterval]) {
+ NSTimeInterval secs = [self retryAfterInterval];
+ return secs;
+ }
+ // The next wait interval is the factor (2.0) times the last interval,
+ // but never less than the minimum interval.
+ NSTimeInterval secs = _lastRetryInterval * _retryFactor;
+ if (_maxRetryInterval > 0) {
+ secs = MIN(secs, _maxRetryInterval);
+ }
+ secs = MAX(secs, _minRetryInterval);
+
+ return secs;
+}
+
+- (NSTimer *)retryTimer {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _retryTimer;
+ } // @synchronized(self)
+}
+
+- (BOOL)isRetryEnabled {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _isRetryEnabled;
+ } // @synchronized(self)
+}
+
+- (BOOL)isRetryEnabledUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ return _isRetryEnabled;
+}
+
+- (void)setRetryEnabled:(BOOL)flag {
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (flag && !_isRetryEnabled) {
+ // We defer initializing these until the user calls setRetryEnabled
+ // to avoid using the random number generator if it's not needed.
+ // However, this means min and max intervals for this fetcher are reset
+ // as a side effect of calling setRetryEnabled.
+ //
+ // Make an initial retry interval random between 1.0 and 2.0 seconds
+ _minRetryInterval = InitialMinRetryInterval();
+ _maxRetryInterval = kUnsetMaxRetryInterval;
+ _retryFactor = 2.0;
+ _lastRetryInterval = 0.0;
+ }
+ _isRetryEnabled = flag;
+ } // @synchronized(self)
+};
+
+- (NSTimeInterval)maxRetryInterval {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _maxRetryInterval;
+ } // @synchronized(self)
+}
+
+- (void)setMaxRetryInterval:(NSTimeInterval)secs {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (secs > 0) {
+ _maxRetryInterval = secs;
+ } else {
+ _maxRetryInterval = kUnsetMaxRetryInterval;
+ }
+ } // @synchronized(self)
+}
+
+- (double)minRetryInterval {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _minRetryInterval;
+ } // @synchronized(self)
+}
+
+- (void)setMinRetryInterval:(NSTimeInterval)secs {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (secs > 0) {
+ _minRetryInterval = secs;
+ } else {
+ // Set min interval to a random value between 1.0 and 2.0 seconds
+ // so that if multiple clients start retrying at the same time, they'll
+ // repeat at different times and avoid overloading the server
+ _minRetryInterval = InitialMinRetryInterval();
+ }
+ } // @synchronized(self)
+
+}
+
+#pragma mark iOS System Completion Handlers
+
+#if TARGET_OS_IPHONE
+static NSMutableDictionary *gSystemCompletionHandlers = nil;
+
+- (GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler {
+ return [[self class] systemCompletionHandlerForSessionIdentifier:_sessionIdentifier];
+}
+
+- (void)setSystemCompletionHandler:(GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler {
+ [[self class] setSystemCompletionHandler:systemCompletionHandler
+ forSessionIdentifier:_sessionIdentifier];
+}
+
++ (void)setSystemCompletionHandler:(GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler
+ forSessionIdentifier:(NSString *)sessionIdentifier {
+ if (!sessionIdentifier) {
+ NSLog(@"%s with nil identifier", __PRETTY_FUNCTION__);
+ return;
+ }
+
+ @synchronized([GTMSessionFetcher class]) {
+ if (gSystemCompletionHandlers == nil && systemCompletionHandler != nil) {
+ gSystemCompletionHandlers = [[NSMutableDictionary alloc] init];
+ }
+ // Use setValue: to remove the object if completionHandler is nil.
+ [gSystemCompletionHandlers setValue:systemCompletionHandler
+ forKey:sessionIdentifier];
+ }
+}
+
++ (GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandlerForSessionIdentifier:(NSString *)sessionIdentifier {
+ if (!sessionIdentifier) {
+ return nil;
+ }
+ @synchronized([GTMSessionFetcher class]) {
+ return [gSystemCompletionHandlers objectForKey:sessionIdentifier];
+ }
+}
+#endif // TARGET_OS_IPHONE
+
+#pragma mark Getters and Setters
+
+@synthesize downloadResumeData = _downloadResumeData,
+ configuration = _configuration,
+ configurationBlock = _configurationBlock,
+ sessionTask = _sessionTask,
+ wasCreatedFromBackgroundSession = _wasCreatedFromBackgroundSession,
+ sessionUserInfo = _sessionUserInfo,
+ taskDescription = _taskDescription,
+ taskPriority = _taskPriority,
+ usingBackgroundSession = _usingBackgroundSession,
+ canShareSession = _canShareSession,
+ completionHandler = _completionHandler,
+ credential = _credential,
+ proxyCredential = _proxyCredential,
+ bodyData = _bodyData,
+ bodyLength = _bodyLength,
+ service = _service,
+ serviceHost = _serviceHost,
+ accumulateDataBlock = _accumulateDataBlock,
+ receivedProgressBlock = _receivedProgressBlock,
+ downloadProgressBlock = _downloadProgressBlock,
+ resumeDataBlock = _resumeDataBlock,
+ didReceiveResponseBlock = _didReceiveResponseBlock,
+ challengeBlock = _challengeBlock,
+ willRedirectBlock = _willRedirectBlock,
+ sendProgressBlock = _sendProgressBlock,
+ willCacheURLResponseBlock = _willCacheURLResponseBlock,
+ retryBlock = _retryBlock,
+ metricsCollectionBlock = _metricsCollectionBlock,
+ retryFactor = _retryFactor,
+ allowedInsecureSchemes = _allowedInsecureSchemes,
+ allowLocalhostRequest = _allowLocalhostRequest,
+ allowInvalidServerCertificates = _allowInvalidServerCertificates,
+ cookieStorage = _cookieStorage,
+ callbackQueue = _callbackQueue,
+ initialBeginFetchDate = _initialBeginFetchDate,
+ testBlock = _testBlock,
+ testBlockAccumulateDataChunkCount = _testBlockAccumulateDataChunkCount,
+ comment = _comment,
+ log = _log;
+
+#if !STRIP_GTM_FETCH_LOGGING
+@synthesize redirectedFromURL = _redirectedFromURL,
+ logRequestBody = _logRequestBody,
+ logResponseBody = _logResponseBody,
+ hasLoggedError = _hasLoggedError;
+#endif
+
+#if GTM_BACKGROUND_TASK_FETCHING
+@synthesize backgroundTaskIdentifier = _backgroundTaskIdentifier,
+ skipBackgroundTask = _skipBackgroundTask;
+#endif
+
+- (GTM_NULLABLE NSURLRequest *)request {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [_request copy];
+ } // @synchronized(self)
+}
+
+- (void)setRequest:(GTM_NULLABLE NSURLRequest *)request {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (![self isFetchingUnsynchronized]) {
+ _request = [request mutableCopy];
+ } else {
+ GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked");
+ }
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSMutableURLRequest *)mutableRequestForTesting {
+ // Allow tests only to modify the request, useful during retries.
+ return _request;
+}
+
+// Internal method for updating the request property such as on redirects.
+- (void)updateMutableRequest:(GTM_NULLABLE NSMutableURLRequest *)request {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _request = request;
+ } // @synchronized(self)
+}
+
+// Set a header field value on the request. Header field value changes will not
+// affect a fetch after the fetch has begun.
+- (void)setRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field {
+ if (![self isFetching]) {
+ [self updateRequestValue:value forHTTPHeaderField:field];
+ } else {
+ GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked");
+ }
+}
+
+// Internal method for updating request headers.
+- (void)updateRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [_request setValue:value forHTTPHeaderField:field];
+ } // @synchronized(self)
+}
+
+- (void)setResponse:(GTM_NULLABLE NSURLResponse *)response {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _response = response;
+ } // @synchronized(self)
+}
+
+- (int64_t)bodyLength {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_bodyLength == NSURLSessionTransferSizeUnknown) {
+ if (_bodyData) {
+ _bodyLength = (int64_t)_bodyData.length;
+ } else if (_bodyFileURL) {
+ NSNumber *fileSizeNum = nil;
+ NSError *fileSizeError = nil;
+ if ([_bodyFileURL getResourceValue:&fileSizeNum
+ forKey:NSURLFileSizeKey
+ error:&fileSizeError]) {
+ _bodyLength = [fileSizeNum longLongValue];
+ }
+ }
+ }
+ return _bodyLength;
+ } // @synchronized(self)
+}
+
+- (BOOL)useUploadTask {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _useUploadTask;
+ } // @synchronized(self)
+}
+
+- (void)setUseUploadTask:(BOOL)flag {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (flag != _useUploadTask) {
+ GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
+ @"useUploadTask should not change after beginFetch has been invoked");
+ _useUploadTask = flag;
+ }
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSURL *)bodyFileURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _bodyFileURL;
+ } // @synchronized(self)
+}
+
+- (void)setBodyFileURL:(GTM_NULLABLE NSURL *)fileURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // The comparison here is a trivial optimization and forgiveness for any client that
+ // repeatedly sets the property, so it just uses pointer comparison rather than isEqual:.
+ if (fileURL != _bodyFileURL) {
+ GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
+ @"fileURL should not change after beginFetch has been invoked");
+
+ _bodyFileURL = fileURL;
+ }
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE GTMSessionFetcherBodyStreamProvider)bodyStreamProvider {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _bodyStreamProvider;
+ } // @synchronized(self)
+}
+
+- (void)setBodyStreamProvider:(GTM_NULLABLE GTMSessionFetcherBodyStreamProvider)block {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
+ @"stream provider should not change after beginFetch has been invoked");
+
+ _bodyStreamProvider = [block copy];
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE id<GTMFetcherAuthorizationProtocol>)authorizer {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _authorizer;
+ } // @synchronized(self)
+}
+
+- (void)setAuthorizer:(GTM_NULLABLE id<GTMFetcherAuthorizationProtocol>)authorizer {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (authorizer != _authorizer) {
+ if ([self isFetchingUnsynchronized]) {
+ GTMSESSION_ASSERT_DEBUG(0, @"authorizer should not change after beginFetch has been invoked");
+ } else {
+ _authorizer = authorizer;
+ }
+ }
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSData *)downloadedData {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _downloadedData;
+ } // @synchronized(self)
+}
+
+- (void)setDownloadedData:(GTM_NULLABLE NSData *)data {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _downloadedData = [data mutableCopy];
+ } // @synchronized(self)
+}
+
+- (int64_t)downloadedLength {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _downloadedLength;
+ } // @synchronized(self)
+}
+
+- (void)setDownloadedLength:(int64_t)length {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _downloadedLength = length;
+ } // @synchronized(self)
+}
+
+- (dispatch_queue_t GTM_NONNULL_TYPE)callbackQueue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _callbackQueue;
+ } // @synchronized(self)
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _callbackQueue = queue ?: dispatch_get_main_queue();
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSURLSession *)session {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _session;
+ } // @synchronized(self)
+}
+
+- (NSInteger)servicePriority {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _servicePriority;
+ } // @synchronized(self)
+}
+
+- (void)setServicePriority:(NSInteger)value {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (value != _servicePriority) {
+ GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
+ @"servicePriority should not change after beginFetch has been invoked");
+
+ _servicePriority = value;
+ }
+ } // @synchronized(self)
+}
+
+
+- (void)setSession:(GTM_NULLABLE NSURLSession *)session {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _session = session;
+ } // @synchronized(self)
+}
+
+- (BOOL)canShareSession {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _canShareSession;
+ } // @synchronized(self)
+}
+
+- (void)setCanShareSession:(BOOL)flag {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _canShareSession = flag;
+ } // @synchronized(self)
+}
+
+- (BOOL)useBackgroundSession {
+ // This reflects if the user requested a background session, not necessarily
+ // if one was created. That is tracked with _usingBackgroundSession.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _userRequestedBackgroundSession;
+ } // @synchronized(self)
+}
+
+- (void)setUseBackgroundSession:(BOOL)flag {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (flag != _userRequestedBackgroundSession) {
+ GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
+ @"useBackgroundSession should not change after beginFetch has been invoked");
+
+ _userRequestedBackgroundSession = flag;
+ }
+ } // @synchronized(self)
+}
+
+- (BOOL)isUsingBackgroundSession {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _usingBackgroundSession;
+ } // @synchronized(self)
+}
+
+- (void)setUsingBackgroundSession:(BOOL)flag {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _usingBackgroundSession = flag;
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSURLSession *)sessionNeedingInvalidation {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _sessionNeedingInvalidation;
+ } // @synchronized(self)
+}
+
+- (void)setSessionNeedingInvalidation:(GTM_NULLABLE NSURLSession *)session {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _sessionNeedingInvalidation = session;
+ } // @synchronized(self)
+}
+
+- (NSOperationQueue * GTM_NONNULL_TYPE)sessionDelegateQueue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _delegateQueue;
+ } // @synchronized(self)
+}
+
+- (void)setSessionDelegateQueue:(NSOperationQueue * GTM_NULLABLE_TYPE)queue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (queue != _delegateQueue) {
+ if ([self isFetchingUnsynchronized]) {
+ GTMSESSION_ASSERT_DEBUG(0, @"sessionDelegateQueue should not change after fetch begins");
+ } else {
+ _delegateQueue = queue ?: [NSOperationQueue mainQueue];
+ }
+ }
+ } // @synchronized(self)
+}
+
+- (BOOL)userStoppedFetching {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _userStoppedFetching;
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE id)userData {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _userData;
+ } // @synchronized(self)
+}
+
+- (void)setUserData:(GTM_NULLABLE id)theObj {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _userData = theObj;
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSURL *)destinationFileURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _destinationFileURL;
+ } // @synchronized(self)
+}
+
+- (void)setDestinationFileURL:(GTM_NULLABLE NSURL *)destinationFileURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (((_destinationFileURL == nil) && (destinationFileURL == nil)) ||
+ [_destinationFileURL isEqual:destinationFileURL]) {
+ return;
+ }
+ if (_sessionIdentifier) {
+ // This is something we don't expect to happen in production.
+ // However if it ever happen, leave a system log.
+ NSLog(@"%@: Destination File URL changed from (%@) to (%@) after session identifier has "
+ @"been created.",
+ [self class], _destinationFileURL, destinationFileURL);
+#if DEBUG
+ // On both the simulator and devices, the path can change to the download file, but the name
+ // shouldn't change. Technically, this isn't supported in the fetcher, but the change of
+ // URL is expected to happen only across development runs through Xcode.
+ NSString *oldFilename = [_destinationFileURL lastPathComponent];
+ NSString *newFilename = [destinationFileURL lastPathComponent];
+ #pragma unused(oldFilename)
+ #pragma unused(newFilename)
+ GTMSESSION_ASSERT_DEBUG([oldFilename isEqualToString:newFilename],
+ @"Destination File URL cannot be changed after session identifier has been created");
+#endif
+ }
+ _destinationFileURL = destinationFileURL;
+ } // @synchronized(self)
+}
+
+- (void)setProperties:(GTM_NULLABLE NSDictionary *)dict {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _properties = [dict mutableCopy];
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSDictionary *)properties {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _properties;
+ } // @synchronized(self)
+}
+
+- (void)setProperty:(GTM_NULLABLE id)obj forKey:(NSString *)key {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_properties == nil && obj != nil) {
+ _properties = [[NSMutableDictionary alloc] init];
+ }
+ [_properties setValue:obj forKey:key];
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE id)propertyForKey:(NSString *)key {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [_properties objectForKey:key];
+ } // @synchronized(self)
+}
+
+- (void)addPropertiesFromDictionary:(NSDictionary *)dict {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_properties == nil && dict != nil) {
+ [self setProperties:[dict mutableCopy]];
+ } else {
+ [_properties addEntriesFromDictionary:dict];
+ }
+ } // @synchronized(self)
+}
+
+- (void)setCommentWithFormat:(id)format, ... {
+#if !STRIP_GTM_FETCH_LOGGING
+ NSString *result = format;
+ if (format) {
+ va_list argList;
+ va_start(argList, format);
+
+ result = [[NSString alloc] initWithFormat:format
+ arguments:argList];
+ va_end(argList);
+ }
+ [self setComment:result];
+#endif
+}
+
+#if !STRIP_GTM_FETCH_LOGGING
+- (NSData *)loggedStreamData {
+ return _loggedStreamData;
+}
+
+- (void)appendLoggedStreamData:dataToAdd {
+ if (!_loggedStreamData) {
+ _loggedStreamData = [NSMutableData data];
+ }
+ [_loggedStreamData appendData:dataToAdd];
+}
+
+- (void)clearLoggedStreamData {
+ _loggedStreamData = nil;
+}
+
+- (void)setDeferResponseBodyLogging:(BOOL)deferResponseBodyLogging {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (deferResponseBodyLogging != _deferResponseBodyLogging) {
+ _deferResponseBodyLogging = deferResponseBodyLogging;
+ if (!deferResponseBodyLogging && !self.hasLoggedError) {
+ [_delegateQueue addOperationWithBlock:^{
+ [self logNowWithError:nil];
+ }];
+ }
+ }
+ } // @synchronized(self)
+}
+
+- (BOOL)deferResponseBodyLogging {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _deferResponseBodyLogging;
+ } // @synchronized(self)
+}
+
+#else
++ (void)setLoggingEnabled:(BOOL)flag {
+}
+
++ (BOOL)isLoggingEnabled {
+ return NO;
+}
+#endif // STRIP_GTM_FETCH_LOGGING
+
+@end
+
+@implementation GTMSessionFetcher (BackwardsCompatibilityOnly)
+
+- (void)setCookieStorageMethod:(NSInteger)method {
+ // For backwards compatibility with the old fetcher, we'll support the old constants.
+ //
+ // Clients using the GTMSessionFetcher class should set the cookie storage explicitly
+ // themselves.
+ NSHTTPCookieStorage *storage = nil;
+ switch(method) {
+ case 0: // kGTMHTTPFetcherCookieStorageMethodStatic
+ // nil storage will use [[self class] staticCookieStorage] when the fetch begins.
+ break;
+ case 1: // kGTMHTTPFetcherCookieStorageMethodFetchHistory
+ // Do nothing; use whatever was set by the fetcher service.
+ return;
+ case 2: // kGTMHTTPFetcherCookieStorageMethodSystemDefault
+ storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
+ break;
+ case 3: // kGTMHTTPFetcherCookieStorageMethodNone
+ // Create temporary storage for this fetcher only.
+ storage = [[GTMSessionCookieStorage alloc] init];
+ break;
+ default:
+ GTMSESSION_ASSERT_DEBUG(0, @"Invalid cookie storage method: %d", (int)method);
+ }
+ self.cookieStorage = storage;
+}
+
+@end
+
+@implementation GTMSessionCookieStorage {
+ NSMutableArray *_cookies;
+ NSHTTPCookieAcceptPolicy _policy;
+}
+
+- (id)init {
+ self = [super init];
+ if (self != nil) {
+ _cookies = [[NSMutableArray alloc] init];
+ }
+ return self;
+}
+
+- (GTM_NULLABLE NSArray *)cookies {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [_cookies copy];
+ } // @synchronized(self)
+}
+
+- (void)setCookie:(NSHTTPCookie *)cookie {
+ if (!cookie) return;
+ if (_policy == NSHTTPCookieAcceptPolicyNever) return;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [self internalSetCookie:cookie];
+ } // @synchronized(self)
+}
+
+// Note: this should only be called from inside a @synchronized(self) block.
+- (void)internalSetCookie:(NSHTTPCookie *)newCookie {
+ GTMSessionCheckSynchronized(self);
+
+ if (_policy == NSHTTPCookieAcceptPolicyNever) return;
+
+ BOOL isValidCookie = (newCookie.name.length > 0
+ && newCookie.domain.length > 0
+ && newCookie.path.length > 0);
+ GTMSESSION_ASSERT_DEBUG(isValidCookie, @"invalid cookie: %@", newCookie);
+
+ if (isValidCookie) {
+ // Remove the cookie if it's currently in the array.
+ NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
+ if (oldCookie) {
+ [_cookies removeObjectIdenticalTo:oldCookie];
+ }
+
+ if (![[self class] hasCookieExpired:newCookie]) {
+ [_cookies addObject:newCookie];
+ }
+ }
+}
+
+// Add all cookies in the new cookie array to the storage,
+// replacing stored cookies as appropriate.
+//
+// Side effect: removes expired cookies from the storage array.
+- (void)setCookies:(GTM_NULLABLE NSArray *)newCookies {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [self removeExpiredCookies];
+
+ for (NSHTTPCookie *newCookie in newCookies) {
+ [self internalSetCookie:newCookie];
+ }
+ } // @synchronized(self)
+}
+
+- (void)setCookies:(NSArray *)cookies forURL:(GTM_NULLABLE NSURL *)URL mainDocumentURL:(GTM_NULLABLE NSURL *)mainDocumentURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_policy == NSHTTPCookieAcceptPolicyNever) {
+ return;
+ }
+
+ if (_policy == NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain) {
+ NSString *mainHost = mainDocumentURL.host;
+ NSString *associatedHost = URL.host;
+ if (!mainHost || ![associatedHost hasSuffix:mainHost]) {
+ return;
+ }
+ }
+ } // @synchronized(self)
+ [self setCookies:cookies];
+}
+
+- (void)deleteCookie:(NSHTTPCookie *)cookie {
+ if (!cookie) return;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
+ if (foundCookie) {
+ [_cookies removeObjectIdenticalTo:foundCookie];
+ }
+ } // @synchronized(self)
+}
+
+// Retrieve all cookies appropriate for the given URL, considering
+// domain, path, cookie name, expiration, security setting.
+// Side effect: removed expired cookies from the storage array.
+- (GTM_NULLABLE NSArray *)cookiesForURL:(NSURL *)theURL {
+ NSMutableArray *foundCookies = nil;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [self removeExpiredCookies];
+
+ // We'll prepend "." to the desired domain, since we want the
+ // actual domain "nytimes.com" to still match the cookie domain
+ // ".nytimes.com" when we check it below with hasSuffix.
+ NSString *host = theURL.host.lowercaseString;
+ NSString *path = theURL.path;
+ NSString *scheme = [theURL scheme];
+
+ NSString *requestingDomain = nil;
+ BOOL isLocalhostRetrieval = NO;
+
+ if (IsLocalhost(host)) {
+ isLocalhostRetrieval = YES;
+ } else {
+ if (host.length > 0) {
+ requestingDomain = [@"." stringByAppendingString:host];
+ }
+ }
+
+ for (NSHTTPCookie *storedCookie in _cookies) {
+ NSString *cookieDomain = storedCookie.domain.lowercaseString;
+ NSString *cookiePath = storedCookie.path;
+ BOOL cookieIsSecure = [storedCookie isSecure];
+
+ BOOL isDomainOK;
+
+ if (isLocalhostRetrieval) {
+ // Prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
+ // is "localhost.local"
+ isDomainOK = (IsLocalhost(cookieDomain)
+ || [cookieDomain isEqual:@"localhost.local"]);
+ } else {
+ // Ensure we're matching exact domain names. We prepended a dot to the
+ // requesting domain, so we can also prepend one here if needed before
+ // checking if the request contains the cookie domain.
+ if (![cookieDomain hasPrefix:@"."]) {
+ cookieDomain = [@"." stringByAppendingString:cookieDomain];
+ }
+ isDomainOK = [requestingDomain hasSuffix:cookieDomain];
+ }
+
+ BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
+ BOOL isSecureOK = (!cookieIsSecure
+ || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame);
+
+ if (isDomainOK && isPathOK && isSecureOK) {
+ if (foundCookies == nil) {
+ foundCookies = [NSMutableArray array];
+ }
+ [foundCookies addObject:storedCookie];
+ }
+ }
+ } // @synchronized(self)
+ return foundCookies;
+}
+
+// Override methods from the NSHTTPCookieStorage (NSURLSessionTaskAdditions) category.
+- (void)storeCookies:(NSArray *)cookies forTask:(NSURLSessionTask *)task {
+ NSURLRequest *currentRequest = task.currentRequest;
+ [self setCookies:cookies forURL:currentRequest.URL mainDocumentURL:nil];
+}
+
+- (void)getCookiesForTask:(NSURLSessionTask *)task
+ completionHandler:(void (^)(GTM_NSArrayOf(NSHTTPCookie *) *))completionHandler {
+ if (completionHandler) {
+ NSURLRequest *currentRequest = task.currentRequest;
+ NSURL *currentRequestURL = currentRequest.URL;
+ NSArray *cookies = [self cookiesForURL:currentRequestURL];
+ completionHandler(cookies);
+ }
+}
+
+// Return a cookie from the array with the same name, domain, and path as the
+// given cookie, or else return nil if none found.
+//
+// Both the cookie being tested and all cookies in the storage array should
+// be valid (non-nil name, domains, paths).
+//
+// Note: this should only be called from inside a @synchronized(self) block
+- (GTM_NULLABLE NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
+ GTMSessionCheckSynchronized(self);
+
+ NSString *name = cookie.name;
+ NSString *domain = cookie.domain;
+ NSString *path = cookie.path;
+
+ GTMSESSION_ASSERT_DEBUG(name && domain && path,
+ @"Invalid stored cookie (name:%@ domain:%@ path:%@)", name, domain, path);
+
+ for (NSHTTPCookie *storedCookie in _cookies) {
+ if ([storedCookie.name isEqual:name]
+ && [storedCookie.domain isEqual:domain]
+ && [storedCookie.path isEqual:path]) {
+ return storedCookie;
+ }
+ }
+ return nil;
+}
+
+// Internal routine to remove any expired cookies from the array, excluding
+// cookies with nil expirations.
+//
+// Note: this should only be called from inside a @synchronized(self) block
+- (void)removeExpiredCookies {
+ GTMSessionCheckSynchronized(self);
+
+ // Count backwards since we're deleting items from the array
+ for (NSInteger idx = (NSInteger)_cookies.count - 1; idx >= 0; idx--) {
+ NSHTTPCookie *storedCookie = [_cookies objectAtIndex:(NSUInteger)idx];
+ if ([[self class] hasCookieExpired:storedCookie]) {
+ [_cookies removeObjectAtIndex:(NSUInteger)idx];
+ }
+ }
+}
+
++ (BOOL)hasCookieExpired:(NSHTTPCookie *)cookie {
+ NSDate *expiresDate = [cookie expiresDate];
+ if (expiresDate == nil) {
+ // Cookies seem to have a Expires property even when the expiresDate method returns nil.
+ id expiresVal = [[cookie properties] objectForKey:NSHTTPCookieExpires];
+ if ([expiresVal isKindOfClass:[NSDate class]]) {
+ expiresDate = expiresVal;
+ }
+ }
+ BOOL hasExpired = (expiresDate != nil && [expiresDate timeIntervalSinceNow] < 0);
+ return hasExpired;
+}
+
+- (void)removeAllCookies {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [_cookies removeAllObjects];
+ } // @synchronized(self)
+}
+
+- (NSHTTPCookieAcceptPolicy)cookieAcceptPolicy {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _policy;
+ } // @synchronized(self)
+}
+
+- (void)setCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy)cookieAcceptPolicy {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _policy = cookieAcceptPolicy;
+ } // @synchronized(self)
+}
+
+@end
+
+void GTMSessionFetcherAssertValidSelector(id GTM_NULLABLE_TYPE obj, SEL GTM_NULLABLE_TYPE sel, ...) {
+ // Verify that the object's selector is implemented with the proper
+ // number and type of arguments
+#if DEBUG
+ va_list argList;
+ va_start(argList, sel);
+
+ if (obj && sel) {
+ // Check that the selector is implemented
+ if (![obj respondsToSelector:sel]) {
+ NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed",
+ NSStringFromClass([(id)obj class]),
+ NSStringFromSelector((SEL)sel));
+ NSCAssert(0, @"callback selector unimplemented or misnamed");
+ } else {
+ const char *expectedArgType;
+ unsigned int argCount = 2; // skip self and _cmd
+ NSMethodSignature *sig = [obj methodSignatureForSelector:sel];
+
+ // Check that each expected argument is present and of the correct type
+ while ((expectedArgType = va_arg(argList, const char*)) != 0) {
+
+ if ([sig numberOfArguments] > argCount) {
+ const char *foundArgType = [sig getArgumentTypeAtIndex:argCount];
+
+ if (0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) {
+ NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s",
+ NSStringFromClass([(id)obj class]),
+ NSStringFromSelector((SEL)sel), (argCount - 2), expectedArgType);
+ NSCAssert(0, @"callback selector argument type mistake");
+ }
+ }
+ argCount++;
+ }
+
+ // Check that the proper number of arguments are present in the selector
+ if (argCount != [sig numberOfArguments]) {
+ NSLog(@"\"%@\" selector \"%@\" should have %d arguments",
+ NSStringFromClass([(id)obj class]),
+ NSStringFromSelector((SEL)sel), (argCount - 2));
+ NSCAssert(0, @"callback selector arguments incorrect");
+ }
+ }
+ }
+
+ va_end(argList);
+#endif
+}
+
+NSString *GTMFetcherCleanedUserAgentString(NSString *str) {
+ // Reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html
+ // and http://www-archive.mozilla.org/build/user-agent-strings.html
+
+ if (str == nil) return @"";
+
+ NSMutableString *result = [NSMutableString stringWithString:str];
+
+ // Replace spaces and commas with underscores
+ [result replaceOccurrencesOfString:@" "
+ withString:@"_"
+ options:0
+ range:NSMakeRange(0, result.length)];
+ [result replaceOccurrencesOfString:@","
+ withString:@"_"
+ options:0
+ range:NSMakeRange(0, result.length)];
+
+ // Delete http token separators and remaining whitespace
+ static NSCharacterSet *charsToDelete = nil;
+ if (charsToDelete == nil) {
+ // Make a set of unwanted characters
+ NSString *const kSeparators = @"()<>@;:\\\"/[]?={}";
+
+ NSMutableCharacterSet *mutableChars =
+ [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
+ [mutableChars addCharactersInString:kSeparators];
+ charsToDelete = [mutableChars copy]; // hang on to an immutable copy
+ }
+
+ while (1) {
+ NSRange separatorRange = [result rangeOfCharacterFromSet:charsToDelete];
+ if (separatorRange.location == NSNotFound) break;
+
+ [result deleteCharactersInRange:separatorRange];
+ };
+
+ return result;
+}
+
+NSString *GTMFetcherSystemVersionString(void) {
+ static NSString *sSavedSystemString;
+
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ // The Xcode 8 SDKs finally cleaned up this mess by providing TARGET_OS_OSX
+ // and TARGET_OS_IOS, but to build with older SDKs, those don't exist and
+ // instead one has to rely on TARGET_OS_MAC (which is true for iOS, watchOS,
+ // and tvOS) and TARGET_OS_IPHONE (which is true for iOS, watchOS, tvOS). So
+ // one has to order these carefully so you pick off the specific things
+ // first.
+ // If the code can ever assume Xcode 8 or higher (even when building for
+ // older OSes), then
+ // TARGET_OS_MAC -> TARGET_OS_OSX
+ // TARGET_OS_IPHONE -> TARGET_OS_IOS
+ // TARGET_IPHONE_SIMULATOR -> TARGET_OS_SIMULATOR
+#if TARGET_OS_WATCH
+ // watchOS - WKInterfaceDevice
+
+ WKInterfaceDevice *currentDevice = [WKInterfaceDevice currentDevice];
+
+ NSString *rawModel = [currentDevice model];
+ NSString *model = GTMFetcherCleanedUserAgentString(rawModel);
+
+ NSString *systemVersion = [currentDevice systemVersion];
+
+#if TARGET_OS_SIMULATOR
+ NSString *hardwareModel = @"sim";
+#else
+ NSString *hardwareModel;
+ struct utsname unameRecord;
+ if (uname(&unameRecord) == 0) {
+ NSString *machineName = @(unameRecord.machine);
+ hardwareModel = GTMFetcherCleanedUserAgentString(machineName);
+ }
+ if (hardwareModel.length == 0) {
+ hardwareModel = @"unk";
+ }
+#endif
+
+ sSavedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@",
+ model, systemVersion, hardwareModel];
+ // Example: Apple_Watch/3.0 hw/Watch1_2
+#elif TARGET_OS_TV || TARGET_OS_IPHONE
+ // iOS and tvOS have UIDevice, use that.
+ UIDevice *currentDevice = [UIDevice currentDevice];
+
+ NSString *rawModel = [currentDevice model];
+ NSString *model = GTMFetcherCleanedUserAgentString(rawModel);
+
+ NSString *systemVersion = [currentDevice systemVersion];
+
+#if TARGET_IPHONE_SIMULATOR || TARGET_OS_SIMULATOR
+ NSString *hardwareModel = @"sim";
+#else
+ NSString *hardwareModel;
+ struct utsname unameRecord;
+ if (uname(&unameRecord) == 0) {
+ NSString *machineName = @(unameRecord.machine);
+ hardwareModel = GTMFetcherCleanedUserAgentString(machineName);
+ }
+ if (hardwareModel.length == 0) {
+ hardwareModel = @"unk";
+ }
+#endif
+
+ sSavedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@",
+ model, systemVersion, hardwareModel];
+ // Example: iPod_Touch/2.2 hw/iPod1_1
+ // Example: Apple_TV/9.2 hw/AppleTV5,3
+#elif TARGET_OS_MAC
+ // Mac build
+ NSProcessInfo *procInfo = [NSProcessInfo processInfo];
+#if !defined(MAC_OS_X_VERSION_10_10)
+ BOOL hasOperatingSystemVersion = NO;
+#elif MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10
+ BOOL hasOperatingSystemVersion =
+ [procInfo respondsToSelector:@selector(operatingSystemVersion)];
+#else
+ BOOL hasOperatingSystemVersion = YES;
+#endif
+ NSString *versString;
+ if (hasOperatingSystemVersion) {
+#if defined(MAC_OS_X_VERSION_10_10)
+ // A reference to NSOperatingSystemVersion requires the 10.10 SDK.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunguarded-availability"
+// Disable unguarded availability warning as we can't use the @availability macro until we require
+// all clients to build with Xcode 9 or above.
+ NSOperatingSystemVersion version = procInfo.operatingSystemVersion;
+#pragma clang diagnostic pop
+ versString = [NSString stringWithFormat:@"%ld.%ld.%ld",
+ (long)version.majorVersion, (long)version.minorVersion,
+ (long)version.patchVersion];
+#else
+#pragma unused(procInfo)
+#endif
+ } else {
+ // With Gestalt inexplicably deprecated in 10.8, we're reduced to reading
+ // the system plist file.
+ NSString *const kPath = @"/System/Library/CoreServices/SystemVersion.plist";
+ NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:kPath];
+ versString = [plist objectForKey:@"ProductVersion"];
+ if (versString.length == 0) {
+ versString = @"10.?.?";
+ }
+ }
+
+ sSavedSystemString = [[NSString alloc] initWithFormat:@"MacOSX/%@", versString];
+#elif defined(_SYS_UTSNAME_H)
+ // Foundation-only build
+ struct utsname unameRecord;
+ uname(&unameRecord);
+
+ sSavedSystemString = [NSString stringWithFormat:@"%s/%s",
+ unameRecord.sysname, unameRecord.release]; // "Darwin/8.11.1"
+#else
+#error No branch taken for a default user agent
+#endif
+ });
+ return sSavedSystemString;
+}
+
+NSString *GTMFetcherStandardUserAgentString(NSBundle * GTM_NULLABLE_TYPE bundle) {
+ NSString *result = [NSString stringWithFormat:@"%@ %@",
+ GTMFetcherApplicationIdentifier(bundle),
+ GTMFetcherSystemVersionString()];
+ return result;
+}
+
+NSString *GTMFetcherApplicationIdentifier(NSBundle * GTM_NULLABLE_TYPE bundle) {
+ @synchronized([GTMSessionFetcher class]) {
+ static NSMutableDictionary *sAppIDMap = nil;
+
+ // If there's a bundle ID, use that; otherwise, use the process name
+ if (bundle == nil) {
+ bundle = [NSBundle mainBundle];
+ }
+ NSString *bundleID = [bundle bundleIdentifier];
+ if (bundleID == nil) {
+ bundleID = @"";
+ }
+
+ NSString *identifier = [sAppIDMap objectForKey:bundleID];
+ if (identifier) return identifier;
+
+ // Apps may add a string to the info.plist to uniquely identify different builds.
+ identifier = [bundle objectForInfoDictionaryKey:@"GTMUserAgentID"];
+ if (identifier.length == 0) {
+ if (bundleID.length > 0) {
+ identifier = bundleID;
+ } else {
+ // Fall back on the procname, prefixed by "proc" to flag that it's
+ // autogenerated and perhaps unreliable
+ NSString *procName = [[NSProcessInfo processInfo] processName];
+ identifier = [NSString stringWithFormat:@"proc_%@", procName];
+ }
+ }
+
+ // Clean up whitespace and special characters
+ identifier = GTMFetcherCleanedUserAgentString(identifier);
+
+ // If there's a version number, append that
+ NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
+ if (version.length == 0) {
+ version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"];
+ }
+
+ // Clean up whitespace and special characters
+ version = GTMFetcherCleanedUserAgentString(version);
+
+ // Glue the two together (cleanup done above or else cleanup would strip the
+ // slash)
+ if (version.length > 0) {
+ identifier = [identifier stringByAppendingFormat:@"/%@", version];
+ }
+
+ if (sAppIDMap == nil) {
+ sAppIDMap = [[NSMutableDictionary alloc] init];
+ }
+ [sAppIDMap setObject:identifier forKey:bundleID];
+ return identifier;
+ }
+}
+
+#if DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG)
+@implementation GTMSessionSyncMonitorInternal {
+ NSValue *_objectKey; // The synchronize target object.
+ const char *_functionName; // The function containing the monitored sync block.
+}
+
+- (instancetype)initWithSynchronizationObject:(id)object
+ allowRecursive:(BOOL)allowRecursive
+ functionName:(const char *)functionName {
+ self = [super init];
+ if (self) {
+ Class threadKey = [GTMSessionSyncMonitorInternal class];
+ _objectKey = [NSValue valueWithNonretainedObject:object];
+ _functionName = functionName;
+
+ NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary;
+ NSMutableDictionary *counters = threadDict[threadKey];
+ if (counters == nil) {
+ counters = [NSMutableDictionary dictionary];
+ threadDict[(id)threadKey] = counters;
+ }
+ NSCountedSet *functionNamesCounter = counters[_objectKey];
+ NSUInteger numberOfSyncingFunctions = functionNamesCounter.count;
+
+ if (!allowRecursive) {
+ BOOL isTopLevelSyncScope = (numberOfSyncingFunctions == 0);
+ NSArray *stack = [NSThread callStackSymbols];
+ GTMSESSION_ASSERT_DEBUG(isTopLevelSyncScope,
+ @"*** Recursive sync on %@ at %s; previous sync at %@\n%@",
+ [object class], functionName, functionNamesCounter.allObjects,
+ [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]);
+ }
+
+ if (!functionNamesCounter) {
+ functionNamesCounter = [NSCountedSet set];
+ counters[_objectKey] = functionNamesCounter;
+ }
+ [functionNamesCounter addObject:(id _Nonnull)@(functionName)];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ Class threadKey = [GTMSessionSyncMonitorInternal class];
+
+ NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary;
+ NSMutableDictionary *counters = threadDict[threadKey];
+ NSCountedSet *functionNamesCounter = counters[_objectKey];
+ NSString *functionNameStr = @(_functionName);
+ NSUInteger numberOfSyncsByThisFunction = [functionNamesCounter countForObject:functionNameStr];
+ NSArray *stack = [NSThread callStackSymbols];
+ GTMSESSION_ASSERT_DEBUG(numberOfSyncsByThisFunction > 0, @"Sync not found on %@ at %s\n%@",
+ [_objectKey.nonretainedObjectValue class], _functionName,
+ [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]);
+ [functionNamesCounter removeObject:functionNameStr];
+ if (functionNamesCounter.count == 0) {
+ [counters removeObjectForKey:_objectKey];
+ }
+}
+
++ (NSArray *)functionsHoldingSynchronizationOnObject:(id)object {
+ Class threadKey = [GTMSessionSyncMonitorInternal class];
+ NSValue *localObjectKey = [NSValue valueWithNonretainedObject:object];
+
+ NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary;
+ NSMutableDictionary *counters = threadDict[threadKey];
+ NSCountedSet *functionNamesCounter = counters[localObjectKey];
+ return functionNamesCounter.count > 0 ? functionNamesCounter.allObjects : nil;
+}
+@end
+#endif // DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG)
+GTM_ASSUME_NONNULL_END
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.h b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.h
new file mode 100644
index 00000000..5ccea78e
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.h
@@ -0,0 +1,112 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "GTMSessionFetcher.h"
+
+// GTM HTTP Logging
+//
+// All traffic using GTMSessionFetcher can be easily logged. Call
+//
+// [GTMSessionFetcher setLoggingEnabled:YES];
+//
+// to begin generating log files.
+//
+// Unless explicitly set by the application using +setLoggingDirectory:,
+// logs are put into a default directory, located at:
+// * macOS: ~/Desktop/GTMHTTPDebugLogs
+// * iOS simulator: ~/GTMHTTPDebugLogs (in application sandbox)
+// * iOS device: ~/Documents/GTMHTTPDebugLogs (in application sandbox)
+//
+// Tip: use the Finder's "Sort By Date" to find the most recent logs.
+//
+// Each run of an application gets a separate set of log files. An html
+// file is generated to simplify browsing the run's http transactions.
+// The html file includes javascript links for inline viewing of uploaded
+// and downloaded data.
+//
+// A symlink is created in the logs folder to simplify finding the html file
+// for the latest run of the application; the symlink is called
+//
+// AppName_http_log_newest.html
+//
+// For better viewing of XML logs, use Camino or Firefox rather than Safari.
+//
+// Each fetcher may be given a comment to be inserted as a label in the logs,
+// such as
+// [fetcher setCommentWithFormat:@"retrieve item %@", itemName];
+//
+// Projects may define STRIP_GTM_FETCH_LOGGING to remove logging code.
+
+#if !STRIP_GTM_FETCH_LOGGING
+
+@interface GTMSessionFetcher (GTMSessionFetcherLogging)
+
+// Note: on macOS the default logs directory is ~/Desktop/GTMHTTPDebugLogs; on
+// iOS simulators it will be the ~/GTMHTTPDebugLogs (in the app sandbox); on
+// iOS devices it will be in ~/Documents/GTMHTTPDebugLogs (in the app sandbox).
+// These directories will be created as needed, and are excluded from backups
+// to iCloud and iTunes.
+//
+// If a custom directory is set, the directory should already exist. It is
+// the application's responsibility to exclude any custom directory from
+// backups, if desired.
++ (void)setLoggingDirectory:(NSString *)path;
++ (NSString *)loggingDirectory;
+
+// client apps can turn logging on and off
++ (void)setLoggingEnabled:(BOOL)isLoggingEnabled;
++ (BOOL)isLoggingEnabled;
+
+// client apps can turn off logging to a file if they want to only check
+// the fetcher's log property
++ (void)setLoggingToFileEnabled:(BOOL)isLoggingToFileEnabled;
++ (BOOL)isLoggingToFileEnabled;
+
+// client apps can optionally specify process name and date string used in
+// log file names
++ (void)setLoggingProcessName:(NSString *)processName;
++ (NSString *)loggingProcessName;
+
++ (void)setLoggingDateStamp:(NSString *)dateStamp;
++ (NSString *)loggingDateStamp;
+
+// client apps can specify the directory for the log for this specific run,
+// typically to match the directory used by another fetcher class, like:
+//
+// [GTMSessionFetcher setLogDirectoryForCurrentRun:[GTMHTTPFetcher logDirectoryForCurrentRun]];
+//
+// Setting this overrides the logging directory, process name, and date stamp when writing
+// the log file.
++ (void)setLogDirectoryForCurrentRun:(NSString *)logDirectoryForCurrentRun;
++ (NSString *)logDirectoryForCurrentRun;
+
+// Prunes old log directories that have not been modified since the provided date.
+// This will not delete the current run's log directory.
++ (void)deleteLogDirectoriesOlderThanDate:(NSDate *)date;
+
+// internal; called by fetcher
+- (void)logFetchWithError:(NSError *)error;
+- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream;
+- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
+ (GTMSessionFetcherBodyStreamProvider)streamProvider;
+
+// internal; accessors useful for viewing logs
++ (NSString *)processNameLogPrefix;
++ (NSString *)symlinkNameSuffix;
++ (NSString *)htmlFileName;
+
+@end
+
+#endif // !STRIP_GTM_FETCH_LOGGING
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m
new file mode 100644
index 00000000..cdf5c179
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m
@@ -0,0 +1,982 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#include <sys/stat.h>
+#include <unistd.h>
+
+#import "GTMSessionFetcherLogging.h"
+
+#ifndef STRIP_GTM_FETCH_LOGGING
+ #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
+#endif
+
+#if !STRIP_GTM_FETCH_LOGGING
+
+// Sensitive credential strings are replaced in logs with _snip_
+//
+// Apps that must see the contents of sensitive tokens can set this to 1
+#ifndef SKIP_GTM_FETCH_LOGGING_SNIPPING
+#define SKIP_GTM_FETCH_LOGGING_SNIPPING 0
+#endif
+
+// If GTMReadMonitorInputStream is available, it can be used for
+// capturing uploaded streams of data
+//
+// We locally declare methods of GTMReadMonitorInputStream so we
+// do not need to import the header, as some projects may not have it available
+#if !GTMSESSION_BUILD_COMBINED_SOURCES
+@interface GTMReadMonitorInputStream : NSInputStream
+
++ (instancetype)inputStreamWithStream:(NSInputStream *)input;
+
+@property (assign) id readDelegate;
+@property (assign) SEL readSelector;
+
+@end
+#else
+@class GTMReadMonitorInputStream;
+#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
+
+@interface GTMSessionFetcher (GTMHTTPFetcherLoggingUtilities)
+
++ (NSString *)headersStringForDictionary:(NSDictionary *)dict;
++ (NSString *)snipSubstringOfString:(NSString *)originalStr
+ betweenStartString:(NSString *)startStr
+ endString:(NSString *)endStr;
+- (void)inputStream:(GTMReadMonitorInputStream *)stream
+ readIntoBuffer:(void *)buffer
+ length:(int64_t)length;
+
+@end
+
+@implementation GTMSessionFetcher (GTMSessionFetcherLogging)
+
+// fetchers come and fetchers go, but statics are forever
+static BOOL gIsLoggingEnabled = NO;
+static BOOL gIsLoggingToFile = YES;
+static NSString *gLoggingDirectoryPath = nil;
+static NSString *gLogDirectoryForCurrentRun = nil;
+static NSString *gLoggingDateStamp = nil;
+static NSString *gLoggingProcessName = nil;
+
++ (void)setLoggingDirectory:(NSString *)path {
+ gLoggingDirectoryPath = [path copy];
+}
+
++ (NSString *)loggingDirectory {
+ if (!gLoggingDirectoryPath) {
+ NSArray *paths = nil;
+#if TARGET_IPHONE_SIMULATOR
+ // default to a directory called GTMHTTPDebugLogs into a sandbox-safe
+ // directory that a developer can find easily, the application home
+ paths = @[ NSHomeDirectory() ];
+#elif TARGET_OS_IPHONE
+ // Neither ~/Desktop nor ~/Home is writable on an actual iOS, watchOS, or tvOS device.
+ // Put it in ~/Documents.
+ paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+#else
+ // default to a directory called GTMHTTPDebugLogs in the desktop folder
+ paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES);
+#endif
+
+ NSString *desktopPath = paths.firstObject;
+ if (desktopPath) {
+ NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs";
+ NSString *logsFolderPath = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName];
+
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ BOOL isDir;
+ BOOL doesFolderExist = [fileMgr fileExistsAtPath:logsFolderPath isDirectory:&isDir];
+ if (!doesFolderExist) {
+ // make the directory
+ doesFolderExist = [fileMgr createDirectoryAtPath:logsFolderPath
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:NULL];
+ if (doesFolderExist) {
+ // The directory has been created. Exclude it from backups.
+ NSURL *pathURL = [NSURL fileURLWithPath:logsFolderPath isDirectory:YES];
+ [pathURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:NULL];
+ }
+ }
+
+ if (doesFolderExist) {
+ // it's there; store it in the global
+ gLoggingDirectoryPath = [logsFolderPath copy];
+ }
+ }
+ }
+ return gLoggingDirectoryPath;
+}
+
++ (void)setLogDirectoryForCurrentRun:(NSString *)logDirectoryForCurrentRun {
+ // Set the path for this run's logs.
+ gLogDirectoryForCurrentRun = [logDirectoryForCurrentRun copy];
+}
+
++ (NSString *)logDirectoryForCurrentRun {
+ // make a directory for this run's logs, like SyncProto_logs_10-16_01-56-58PM
+ if (gLogDirectoryForCurrentRun) return gLogDirectoryForCurrentRun;
+
+ NSString *parentDir = [self loggingDirectory];
+ NSString *logNamePrefix = [self processNameLogPrefix];
+ NSString *dateStamp = [self loggingDateStamp];
+ NSString *dirName = [NSString stringWithFormat:@"%@%@", logNamePrefix, dateStamp];
+ NSString *logDirectory = [parentDir stringByAppendingPathComponent:dirName];
+
+ if (gIsLoggingToFile) {
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ // Be sure that the first time this app runs, it's not writing to a preexisting folder
+ static BOOL gShouldReuseFolder = NO;
+ if (!gShouldReuseFolder) {
+ gShouldReuseFolder = YES;
+ NSString *origLogDir = logDirectory;
+ for (int ctr = 2; ctr < 20; ++ctr) {
+ if (![fileMgr fileExistsAtPath:logDirectory]) break;
+
+ // append a digit
+ logDirectory = [origLogDir stringByAppendingFormat:@"_%d", ctr];
+ }
+ }
+ if (![fileMgr createDirectoryAtPath:logDirectory
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:NULL]) return nil;
+ }
+ gLogDirectoryForCurrentRun = logDirectory;
+
+ return gLogDirectoryForCurrentRun;
+}
+
++ (void)setLoggingEnabled:(BOOL)isLoggingEnabled {
+ gIsLoggingEnabled = isLoggingEnabled;
+}
+
++ (BOOL)isLoggingEnabled {
+ return gIsLoggingEnabled;
+}
+
++ (void)setLoggingToFileEnabled:(BOOL)isLoggingToFileEnabled {
+ gIsLoggingToFile = isLoggingToFileEnabled;
+}
+
++ (BOOL)isLoggingToFileEnabled {
+ return gIsLoggingToFile;
+}
+
++ (void)setLoggingProcessName:(NSString *)processName {
+ gLoggingProcessName = [processName copy];
+}
+
++ (NSString *)loggingProcessName {
+ // get the process name (once per run) replacing spaces with underscores
+ if (!gLoggingProcessName) {
+ NSString *procName = [[NSProcessInfo processInfo] processName];
+ gLoggingProcessName = [procName stringByReplacingOccurrencesOfString:@" " withString:@"_"];
+ }
+ return gLoggingProcessName;
+}
+
++ (void)setLoggingDateStamp:(NSString *)dateStamp {
+ gLoggingDateStamp = [dateStamp copy];
+}
+
++ (NSString *)loggingDateStamp {
+ // We'll pick one date stamp per run, so a run that starts at a later second
+ // will get a unique results html file
+ if (!gLoggingDateStamp) {
+ // produce a string like 08-21_01-41-23PM
+
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
+ [formatter setFormatterBehavior:NSDateFormatterBehavior10_4];
+ [formatter setDateFormat:@"M-dd_hh-mm-ssa"];
+
+ gLoggingDateStamp = [formatter stringFromDate:[NSDate date]];
+ }
+ return gLoggingDateStamp;
+}
+
++ (NSString *)processNameLogPrefix {
+ static NSString *gPrefix = nil;
+ if (!gPrefix) {
+ NSString *processName = [self loggingProcessName];
+ gPrefix = [[NSString alloc] initWithFormat:@"%@_log_", processName];
+ }
+ return gPrefix;
+}
+
++ (NSString *)symlinkNameSuffix {
+ return @"_log_newest.html";
+}
+
++ (NSString *)htmlFileName {
+ return @"aperçu_http_log.html";
+}
+
++ (void)deleteLogDirectoriesOlderThanDate:(NSDate *)cutoffDate {
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ NSURL *parentDir = [NSURL fileURLWithPath:[[self class] loggingDirectory]];
+ NSURL *logDirectoryForCurrentRun =
+ [NSURL fileURLWithPath:[[self class] logDirectoryForCurrentRun]];
+ NSError *error;
+ NSArray *contents = [fileMgr contentsOfDirectoryAtURL:parentDir
+ includingPropertiesForKeys:@[ NSURLContentModificationDateKey ]
+ options:0
+ error:&error];
+ for (NSURL *itemURL in contents) {
+ if ([itemURL isEqual:logDirectoryForCurrentRun]) continue;
+
+ NSDate *modDate;
+ if ([itemURL getResourceValue:&modDate
+ forKey:NSURLContentModificationDateKey
+ error:&error]) {
+ if ([modDate compare:cutoffDate] == NSOrderedAscending) {
+ if (![fileMgr removeItemAtURL:itemURL error:&error]) {
+ NSLog(@"deleteLogDirectoriesOlderThanDate failed to delete %@: %@",
+ itemURL.path, error);
+ }
+ }
+ } else {
+ NSLog(@"deleteLogDirectoriesOlderThanDate failed to get mod date of %@: %@",
+ itemURL.path, error);
+ }
+ }
+}
+
+// formattedStringFromData returns a prettyprinted string for XML or JSON input,
+// and a plain string for other input data
+- (NSString *)formattedStringFromData:(NSData *)inputData
+ contentType:(NSString *)contentType
+ JSON:(NSDictionary **)outJSON {
+ if (!inputData) return nil;
+
+ // if the content type is JSON and we have the parsing class available, use that
+ if ([contentType hasPrefix:@"application/json"] && inputData.length > 5) {
+ // convert from JSON string to NSObjects and back to a formatted string
+ NSMutableDictionary *obj = [NSJSONSerialization JSONObjectWithData:inputData
+ options:NSJSONReadingMutableContainers
+ error:NULL];
+ if (obj) {
+ if (outJSON) *outJSON = obj;
+ if ([obj isKindOfClass:[NSMutableDictionary class]]) {
+ // for security and privacy, omit OAuth 2 response access and refresh tokens
+ if ([obj valueForKey:@"refresh_token"] != nil) {
+ [obj setObject:@"_snip_" forKey:@"refresh_token"];
+ }
+ if ([obj valueForKey:@"access_token"] != nil) {
+ [obj setObject:@"_snip_" forKey:@"access_token"];
+ }
+ }
+ NSData *data = [NSJSONSerialization dataWithJSONObject:obj
+ options:NSJSONWritingPrettyPrinted
+ error:NULL];
+ if (data) {
+ NSString *jsonStr = [[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding];
+ return jsonStr;
+ }
+ }
+ }
+
+#if !TARGET_OS_IPHONE && !GTM_SKIP_LOG_XMLFORMAT
+ // verify that this data starts with the bytes indicating XML
+
+ NSString *const kXMLLintPath = @"/usr/bin/xmllint";
+ static BOOL gHasCheckedAvailability = NO;
+ static BOOL gIsXMLLintAvailable = NO;
+
+ if (!gHasCheckedAvailability) {
+ gIsXMLLintAvailable = [[NSFileManager defaultManager] fileExistsAtPath:kXMLLintPath];
+ gHasCheckedAvailability = YES;
+ }
+ if (gIsXMLLintAvailable
+ && inputData.length > 5
+ && strncmp(inputData.bytes, "<?xml", 5) == 0) {
+
+ // call xmllint to format the data
+ NSTask *task = [[NSTask alloc] init];
+ [task setLaunchPath:kXMLLintPath];
+
+ // use the dash argument to specify stdin as the source file
+ [task setArguments:@[ @"--format", @"-" ]];
+ [task setEnvironment:@{}];
+
+ NSPipe *inputPipe = [NSPipe pipe];
+ NSPipe *outputPipe = [NSPipe pipe];
+ [task setStandardInput:inputPipe];
+ [task setStandardOutput:outputPipe];
+
+ [task launch];
+
+ [[inputPipe fileHandleForWriting] writeData:inputData];
+ [[inputPipe fileHandleForWriting] closeFile];
+
+ // drain the stdout before waiting for the task to exit
+ NSData *formattedData = [[outputPipe fileHandleForReading] readDataToEndOfFile];
+
+ [task waitUntilExit];
+
+ int status = [task terminationStatus];
+ if (status == 0 && formattedData.length > 0) {
+ // success
+ inputData = formattedData;
+ }
+ }
+#else
+ // we can't call external tasks on the iPhone; leave the XML unformatted
+#endif
+
+ NSString *dataStr = [[NSString alloc] initWithData:inputData
+ encoding:NSUTF8StringEncoding];
+ return dataStr;
+}
+
+// stringFromStreamData creates a string given the supplied data
+//
+// If NSString can create a UTF-8 string from the data, then that is returned.
+//
+// Otherwise, this routine tries to find a MIME boundary at the beginning of the data block, and
+// uses that to break up the data into parts. Each part will be used to try to make a UTF-8 string.
+// For parts that fail, a replacement string showing the part header and <<n bytes>> is supplied
+// in place of the binary data.
+
+- (NSString *)stringFromStreamData:(NSData *)data
+ contentType:(NSString *)contentType {
+
+ if (!data) return nil;
+
+ // optimistically, see if the whole data block is UTF-8
+ NSString *streamDataStr = [self formattedStringFromData:data
+ contentType:contentType
+ JSON:NULL];
+ if (streamDataStr) return streamDataStr;
+
+ // Munge a buffer by replacing non-ASCII bytes with underscores, and turn that munged buffer an
+ // NSString. That gives us a string we can use with NSScanner.
+ NSMutableData *mutableData = [NSMutableData dataWithData:data];
+ unsigned char *bytes = (unsigned char *)mutableData.mutableBytes;
+
+ for (unsigned int idx = 0; idx < mutableData.length; ++idx) {
+ if (bytes[idx] > 0x7F || bytes[idx] == 0) {
+ bytes[idx] = '_';
+ }
+ }
+
+ NSString *mungedStr = [[NSString alloc] initWithData:mutableData
+ encoding:NSUTF8StringEncoding];
+ if (mungedStr) {
+
+ // scan for the boundary string
+ NSString *boundary = nil;
+ NSScanner *scanner = [NSScanner scannerWithString:mungedStr];
+
+ if ([scanner scanUpToString:@"\r\n" intoString:&boundary]
+ && [boundary hasPrefix:@"--"]) {
+
+ // we found a boundary string; use it to divide the string into parts
+ NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary];
+
+ // look at each munged part in the original string, and try to convert those into UTF-8
+ NSMutableArray *origParts = [NSMutableArray array];
+ NSUInteger offset = 0;
+ for (NSString *mungedPart in mungedParts) {
+ NSUInteger partSize = mungedPart.length;
+ NSData *origPartData = [data subdataWithRange:NSMakeRange(offset, partSize)];
+ NSString *origPartStr = [[NSString alloc] initWithData:origPartData
+ encoding:NSUTF8StringEncoding];
+ if (origPartStr) {
+ // we could make this original part into UTF-8; use the string
+ [origParts addObject:origPartStr];
+ } else {
+ // this part can't be made into UTF-8; scan the header, if we can
+ NSString *header = nil;
+ NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart];
+ if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) {
+ // we couldn't find a header
+ header = @"";
+ }
+ // make a part string with the header and <<n bytes>>
+ NSString *binStr = [NSString stringWithFormat:@"\r%@\r<<%lu bytes>>\r",
+ header, (long)(partSize - header.length)];
+ [origParts addObject:binStr];
+ }
+ offset += partSize + boundary.length;
+ }
+ // rejoin the original parts
+ streamDataStr = [origParts componentsJoinedByString:boundary];
+ }
+ }
+ if (!streamDataStr) {
+ // give up; just make a string showing the uploaded bytes
+ streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", (unsigned int)data.length];
+ }
+ return streamDataStr;
+}
+
+// logFetchWithError is called following a successful or failed fetch attempt
+//
+// This method does all the work for appending to and creating log files
+
+- (void)logFetchWithError:(NSError *)error {
+ if (![[self class] isLoggingEnabled]) return;
+ NSString *logDirectory = [[self class] logDirectoryForCurrentRun];
+ if (!logDirectory) return;
+ NSString *processName = [[self class] loggingProcessName];
+
+ // TODO: add Javascript to display response data formatted in hex
+
+ // each response's NSData goes into its own xml or txt file, though all responses for this run of
+ // the app share a main html file. This counter tracks all fetch responses for this app run.
+ //
+ // we'll use a local variable since this routine may be reentered while waiting for XML formatting
+ // to be completed by an external task
+ static int gResponseCounter = 0;
+ int responseCounter = ++gResponseCounter;
+
+ NSURLResponse *response = [self response];
+ NSDictionary *responseHeaders = [self responseHeaders];
+ NSString *responseDataStr = nil;
+ NSDictionary *responseJSON = nil;
+
+ // if there's response data, decide what kind of file to put it in based on the first bytes of the
+ // file or on the mime type supplied by the server
+ NSString *responseMIMEType = [response MIMEType];
+ BOOL isResponseImage = NO;
+
+ // file name for an image data file
+ NSString *responseDataFileName = nil;
+
+ int64_t responseDataLength = self.downloadedLength;
+ if (responseDataLength > 0) {
+ NSData *downloadedData = self.downloadedData;
+ if (downloadedData == nil
+ && responseDataLength > 0
+ && responseDataLength < 20000
+ && self.destinationFileURL) {
+ // There's a download file that's not too big, so get the data to display from the downloaded
+ // file.
+ NSURL *destinationURL = self.destinationFileURL;
+ downloadedData = [NSData dataWithContentsOfURL:destinationURL];
+ }
+ NSString *responseType = [responseHeaders valueForKey:@"Content-Type"];
+ responseDataStr = [self formattedStringFromData:downloadedData
+ contentType:responseType
+ JSON:&responseJSON];
+ NSString *responseDataExtn = nil;
+ NSData *dataToWrite = nil;
+ if (responseDataStr) {
+ // we were able to make a UTF-8 string from the response data
+ if ([responseMIMEType isEqual:@"application/atom+xml"]
+ || [responseMIMEType hasSuffix:@"/xml"]) {
+ responseDataExtn = @"xml";
+ dataToWrite = [responseDataStr dataUsingEncoding:NSUTF8StringEncoding];
+ }
+ } else if ([responseMIMEType isEqual:@"image/jpeg"]) {
+ responseDataExtn = @"jpg";
+ dataToWrite = downloadedData;
+ isResponseImage = YES;
+ } else if ([responseMIMEType isEqual:@"image/gif"]) {
+ responseDataExtn = @"gif";
+ dataToWrite = downloadedData;
+ isResponseImage = YES;
+ } else if ([responseMIMEType isEqual:@"image/png"]) {
+ responseDataExtn = @"png";
+ dataToWrite = downloadedData;
+ isResponseImage = YES;
+ } else {
+ // add more non-text types here
+ }
+ // if we have an extension, save the raw data in a file with that extension
+ if (responseDataExtn && dataToWrite) {
+ // generate a response file base name like
+ NSString *responseBaseName = [NSString stringWithFormat:@"fetch_%d_response", responseCounter];
+ responseDataFileName = [responseBaseName stringByAppendingPathExtension:responseDataExtn];
+ NSString *responseDataFilePath = [logDirectory stringByAppendingPathComponent:responseDataFileName];
+
+ NSError *downloadedError = nil;
+ if (gIsLoggingToFile && ![dataToWrite writeToFile:responseDataFilePath
+ options:0
+ error:&downloadedError]) {
+ NSLog(@"%@ logging write error:%@ (%@)", [self class], downloadedError, responseDataFileName);
+ }
+ }
+ }
+ // we'll have one main html file per run of the app
+ NSString *htmlName = [[self class] htmlFileName];
+ NSString *htmlPath =[logDirectory stringByAppendingPathComponent:htmlName];
+
+ // if the html file exists (from logging previous fetches) we don't need
+ // to re-write the header or the scripts
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ BOOL didFileExist = [fileMgr fileExistsAtPath:htmlPath];
+
+ NSMutableString* outputHTML = [NSMutableString string];
+
+ // we need a header to say we'll have UTF-8 text
+ if (!didFileExist) {
+ [outputHTML appendFormat:@"<html><head><meta http-equiv=\"content-type\" "
+ "content=\"text/html; charset=UTF-8\"><title>%@ HTTP fetch log %@</title>",
+ processName, [[self class] loggingDateStamp]];
+ }
+ // now write the visible html elements
+ NSString *copyableFileName = [NSString stringWithFormat:@"fetch_%d.txt", responseCounter];
+
+ NSDate *now = [NSDate date];
+ // write the date & time, the comment, and the link to the plain-text (copyable) log
+ [outputHTML appendFormat:@"<b>%@ &nbsp;&nbsp;&nbsp;&nbsp; ", now];
+
+ NSString *comment = [self comment];
+ if (comment.length > 0) {
+ [outputHTML appendFormat:@"%@ &nbsp;&nbsp;&nbsp;&nbsp; ", comment];
+ }
+ [outputHTML appendFormat:@"</b><a href='%@'><i>request/response log</i></a><br>", copyableFileName];
+ NSTimeInterval elapsed = -self.initialBeginFetchDate.timeIntervalSinceNow;
+ [outputHTML appendFormat:@"elapsed: %5.3fsec<br>", elapsed];
+
+ // write the request URL
+ NSURLRequest *request = self.request;
+ NSString *requestMethod = request.HTTPMethod;
+ NSURL *requestURL = request.URL;
+
+ // Save the request URL for next time in case this redirects.
+ NSString *redirectedFromURLString = [self.redirectedFromURL absoluteString];
+ self.redirectedFromURL = [requestURL copy];
+ if (redirectedFromURLString) {
+ [outputHTML appendFormat:@"<FONT COLOR='#990066'><i>redirected from %@</i></FONT><br>",
+ redirectedFromURLString];
+ }
+ [outputHTML appendFormat:@"<b>request:</b> %@ <code>%@</code><br>\n", requestMethod, requestURL];
+
+ // write the request headers
+ NSDictionary *requestHeaders = request.allHTTPHeaderFields;
+ NSUInteger numberOfRequestHeaders = requestHeaders.count;
+ if (numberOfRequestHeaders > 0) {
+ // Indicate if the request is authorized; warn if the request is authorized but non-SSL
+ NSString *auth = [requestHeaders objectForKey:@"Authorization"];
+ NSString *headerDetails = @"";
+ if (auth) {
+ BOOL isInsecure = [[requestURL scheme] isEqual:@"http"];
+ if (isInsecure) {
+ // 26A0 = âš 
+ headerDetails =
+ @"&nbsp;&nbsp;&nbsp;<i>authorized, non-SSL</i><FONT COLOR='#FF00FF'> &#x26A0;</FONT> ";
+ } else {
+ headerDetails = @"&nbsp;&nbsp;&nbsp;<i>authorized</i>";
+ }
+ }
+ NSString *cookiesHdr = [requestHeaders objectForKey:@"Cookie"];
+ if (cookiesHdr) {
+ headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>cookies</i>"];
+ }
+ NSString *matchHdr = [requestHeaders objectForKey:@"If-Match"];
+ if (matchHdr) {
+ headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>if-match</i>"];
+ }
+ matchHdr = [requestHeaders objectForKey:@"If-None-Match"];
+ if (matchHdr) {
+ headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>if-none-match</i>"];
+ }
+ [outputHTML appendFormat:@"&nbsp;&nbsp; headers: %d %@<br>",
+ (int)numberOfRequestHeaders, headerDetails];
+ } else {
+ [outputHTML appendFormat:@"&nbsp;&nbsp; headers: none<br>"];
+ }
+ // write the request post data
+ NSData *bodyData = nil;
+ NSData *loggedStreamData = self.loggedStreamData;
+ if (loggedStreamData) {
+ bodyData = loggedStreamData;
+ } else {
+ bodyData = self.bodyData;
+ if (bodyData == nil) {
+ bodyData = self.request.HTTPBody;
+ }
+ }
+ uint64_t bodyDataLength = bodyData.length;
+
+ if (bodyData.length == 0) {
+ // If the data is in a body upload file URL, read that in if it's not huge.
+ NSURL *bodyFileURL = self.bodyFileURL;
+ if (bodyFileURL) {
+ NSNumber *fileSizeNum = nil;
+ NSError *fileSizeError = nil;
+ if ([bodyFileURL getResourceValue:&fileSizeNum
+ forKey:NSURLFileSizeKey
+ error:&fileSizeError]) {
+ bodyDataLength = [fileSizeNum unsignedLongLongValue];
+ if (bodyDataLength > 0 && bodyDataLength < 50000) {
+ bodyData = [NSData dataWithContentsOfURL:bodyFileURL
+ options:NSDataReadingUncached
+ error:&fileSizeError];
+ }
+ }
+ }
+ }
+ NSString *bodyDataStr = nil;
+ NSString *postType = [requestHeaders valueForKey:@"Content-Type"];
+
+ if (bodyDataLength > 0) {
+ [outputHTML appendFormat:@"&nbsp;&nbsp; data: %llu bytes, <code>%@</code><br>\n",
+ bodyDataLength, postType ? postType : @"(no type)"];
+ NSString *logRequestBody = self.logRequestBody;
+ if (logRequestBody) {
+ bodyDataStr = [logRequestBody copy];
+ self.logRequestBody = nil;
+ } else {
+ bodyDataStr = [self stringFromStreamData:bodyData
+ contentType:postType];
+ if (bodyDataStr) {
+ // remove OAuth 2 client secret and refresh token
+ bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
+ betweenStartString:@"client_secret="
+ endString:@"&"];
+ bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
+ betweenStartString:@"refresh_token="
+ endString:@"&"];
+ // remove ClientLogin password
+ bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
+ betweenStartString:@"&Passwd="
+ endString:@"&"];
+ }
+ }
+ } else {
+ // no post data
+ }
+ // write the response status, MIME type, URL
+ NSInteger status = [self statusCode];
+ if (response) {
+ NSString *statusString = @"";
+ if (status != 0) {
+ if (status == 200 || status == 201) {
+ statusString = [NSString stringWithFormat:@"%ld", (long)status];
+
+ // report any JSON-RPC error
+ if ([responseJSON isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *jsonError = [responseJSON objectForKey:@"error"];
+ if ([jsonError isKindOfClass:[NSDictionary class]]) {
+ NSString *jsonCode = [[jsonError valueForKey:@"code"] description];
+ NSString *jsonMessage = [jsonError valueForKey:@"message"];
+ if (jsonCode || jsonMessage) {
+ // 2691 = âš‘
+ NSString *const jsonErrFmt =
+ @"&nbsp;&nbsp;&nbsp;<i>JSON error:</i> <FONT COLOR='#FF00FF'>%@ %@ &nbsp;&#x2691;</FONT>";
+ statusString = [statusString stringByAppendingFormat:jsonErrFmt,
+ jsonCode ? jsonCode : @"",
+ jsonMessage ? jsonMessage : @""];
+ }
+ }
+ }
+ } else {
+ // purple for anything other than 200 or 201
+ NSString *flag = status >= 400 ? @"&nbsp;&#x2691;" : @""; // 2691 = âš‘
+ NSString *explanation = [NSHTTPURLResponse localizedStringForStatusCode:status];
+ NSString *const statusFormat = @"<FONT COLOR='#FF00FF'>%ld %@ %@</FONT>";
+ statusString = [NSString stringWithFormat:statusFormat, (long)status, explanation, flag];
+ }
+ }
+ // show the response URL only if it's different from the request URL
+ NSString *responseURLStr = @"";
+ NSURL *responseURL = response.URL;
+
+ if (responseURL && ![responseURL isEqual:request.URL]) {
+ NSString *const responseURLFormat =
+ @"<FONT COLOR='#FF00FF'>response URL:</FONT> <code>%@</code><br>\n";
+ responseURLStr = [NSString stringWithFormat:responseURLFormat, [responseURL absoluteString]];
+ }
+ [outputHTML appendFormat:@"<b>response:</b>&nbsp;&nbsp;status %@<br>\n%@",
+ statusString, responseURLStr];
+ // Write the response headers
+ NSUInteger numberOfResponseHeaders = responseHeaders.count;
+ if (numberOfResponseHeaders > 0) {
+ // Indicate if the server is setting cookies
+ NSString *cookiesSet = [responseHeaders valueForKey:@"Set-Cookie"];
+ NSString *cookiesStr =
+ cookiesSet ? @"&nbsp;&nbsp;<FONT COLOR='#990066'><i>sets cookies</i></FONT>" : @"";
+ // Indicate if the server is redirecting
+ NSString *location = [responseHeaders valueForKey:@"Location"];
+ BOOL isRedirect = status >= 300 && status <= 399 && location != nil;
+ NSString *redirectsStr =
+ isRedirect ? @"&nbsp;&nbsp;<FONT COLOR='#990066'><i>redirects</i></FONT>" : @"";
+ [outputHTML appendFormat:@"&nbsp;&nbsp; headers: %d %@ %@<br>\n",
+ (int)numberOfResponseHeaders, cookiesStr, redirectsStr];
+ } else {
+ [outputHTML appendString:@"&nbsp;&nbsp; headers: none<br>\n"];
+ }
+ }
+ // error
+ if (error) {
+ [outputHTML appendFormat:@"<b>Error:</b> %@ <br>\n", error.description];
+ }
+ // Write the response data
+ if (responseDataFileName) {
+ if (isResponseImage) {
+ // Make a small inline image that links to the full image file
+ [outputHTML appendFormat:@"&nbsp;&nbsp; data: %lld bytes, <code>%@</code><br>",
+ responseDataLength, responseMIMEType];
+ NSString *const fmt =
+ @"<a href=\"%@\"><img src='%@' alt='image' style='border:solid thin;max-height:32'></a>\n";
+ [outputHTML appendFormat:fmt, responseDataFileName, responseDataFileName];
+ } else {
+ // The response data was XML; link to the xml file
+ NSString *const fmt =
+ @"&nbsp;&nbsp; data: %lld bytes, <code>%@</code>&nbsp;&nbsp;&nbsp;<i><a href=\"%@\">%@</a></i>\n";
+ [outputHTML appendFormat:fmt, responseDataLength, responseMIMEType,
+ responseDataFileName, [responseDataFileName pathExtension]];
+ }
+ } else {
+ // The response data was not an image; just show the length and MIME type
+ [outputHTML appendFormat:@"&nbsp;&nbsp; data: %lld bytes, <code>%@</code>\n",
+ responseDataLength, responseMIMEType ? responseMIMEType : @"(no response type)"];
+ }
+ // Make a single string of the request and response, suitable for copying
+ // to the clipboard and pasting into a bug report
+ NSMutableString *copyable = [NSMutableString string];
+ if (comment) {
+ [copyable appendFormat:@"%@\n\n", comment];
+ }
+ [copyable appendFormat:@"%@ elapsed: %5.3fsec\n", now, elapsed];
+ if (redirectedFromURLString) {
+ [copyable appendFormat:@"Redirected from %@\n", redirectedFromURLString];
+ }
+ [copyable appendFormat:@"Request: %@ %@\n", requestMethod, requestURL];
+ if (requestHeaders.count > 0) {
+ [copyable appendFormat:@"Request headers:\n%@\n",
+ [[self class] headersStringForDictionary:requestHeaders]];
+ }
+ if (bodyDataLength > 0) {
+ [copyable appendFormat:@"Request body: (%llu bytes)\n", bodyDataLength];
+ if (bodyDataStr) {
+ [copyable appendFormat:@"%@\n", bodyDataStr];
+ }
+ [copyable appendString:@"\n"];
+ }
+ if (response) {
+ [copyable appendFormat:@"Response: status %d\n", (int) status];
+ [copyable appendFormat:@"Response headers:\n%@\n",
+ [[self class] headersStringForDictionary:responseHeaders]];
+ [copyable appendFormat:@"Response body: (%lld bytes)\n", responseDataLength];
+ if (responseDataLength > 0) {
+ NSString *logResponseBody = self.logResponseBody;
+ if (logResponseBody) {
+ // The user has provided the response body text.
+ responseDataStr = [logResponseBody copy];
+ self.logResponseBody = nil;
+ }
+ if (responseDataStr != nil) {
+ [copyable appendFormat:@"%@\n", responseDataStr];
+ } else {
+ // Even though it's redundant, we'll put in text to indicate that all the bytes are binary.
+ if (self.destinationFileURL) {
+ [copyable appendFormat:@"<<%lld bytes>> to file %@\n",
+ responseDataLength, self.destinationFileURL.path];
+ } else {
+ [copyable appendFormat:@"<<%lld bytes>>\n", responseDataLength];
+ }
+ }
+ }
+ }
+ if (error) {
+ [copyable appendFormat:@"Error: %@\n", error];
+ }
+ // Save to log property before adding the separator
+ self.log = copyable;
+
+ [copyable appendString:@"-----------------------------------------------------------\n"];
+
+ // Write the copyable version to another file (linked to at the top of the html file, above)
+ //
+ // Ideally, something to just copy this to the clipboard like
+ // <span onCopy='window.event.clipboardData.setData(\"Text\",
+ // \"copyable stuff\");return false;'>Copy here.</span>"
+ // would work everywhere, but it only works in Safari as of 8/2010
+ if (gIsLoggingToFile) {
+ NSString *parentDir = [[self class] loggingDirectory];
+ NSString *copyablePath = [logDirectory stringByAppendingPathComponent:copyableFileName];
+ NSError *copyableError = nil;
+ if (![copyable writeToFile:copyablePath
+ atomically:NO
+ encoding:NSUTF8StringEncoding
+ error:&copyableError]) {
+ // Error writing to file
+ NSLog(@"%@ logging write error:%@ (%@)", [self class], copyableError, copyablePath);
+ }
+ [outputHTML appendString:@"<br><hr><p>"];
+
+ // Append the HTML to the main output file
+ const char* htmlBytes = outputHTML.UTF8String;
+ NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath
+ append:YES];
+ [stream open];
+ [stream write:(const uint8_t *) htmlBytes maxLength:strlen(htmlBytes)];
+ [stream close];
+
+ // Make a symlink to the latest html
+ NSString *const symlinkNameSuffix = [[self class] symlinkNameSuffix];
+ NSString *symlinkName = [processName stringByAppendingString:symlinkNameSuffix];
+ NSString *symlinkPath = [parentDir stringByAppendingPathComponent:symlinkName];
+
+ [fileMgr removeItemAtPath:symlinkPath error:NULL];
+ [fileMgr createSymbolicLinkAtPath:symlinkPath
+ withDestinationPath:htmlPath
+ error:NULL];
+#if TARGET_OS_IPHONE
+ static BOOL gReportedLoggingPath = NO;
+ if (!gReportedLoggingPath) {
+ gReportedLoggingPath = YES;
+ NSLog(@"GTMSessionFetcher logging to \"%@\"", parentDir);
+ }
+#endif
+ }
+}
+
+- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream {
+ if (!inputStream) return nil;
+ if (![GTMSessionFetcher isLoggingEnabled]) return inputStream;
+
+ [self clearLoggedStreamData]; // Clear any previous data.
+ Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream");
+ if (!monitorClass) {
+ NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>";
+ NSData *stringData = [str dataUsingEncoding:NSUTF8StringEncoding];
+ [self appendLoggedStreamData:stringData];
+ return inputStream;
+ }
+ inputStream = [monitorClass inputStreamWithStream:inputStream];
+
+ GTMReadMonitorInputStream *readMonitorInputStream = (GTMReadMonitorInputStream *)inputStream;
+ [readMonitorInputStream setReadDelegate:self];
+ SEL readSel = @selector(inputStream:readIntoBuffer:length:);
+ [readMonitorInputStream setReadSelector:readSel];
+
+ return inputStream;
+}
+
+- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
+ (GTMSessionFetcherBodyStreamProvider)streamProvider {
+ if (!streamProvider) return nil;
+ if (![GTMSessionFetcher isLoggingEnabled]) return streamProvider;
+
+ [self clearLoggedStreamData]; // Clear any previous data.
+ Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream");
+ if (!monitorClass) {
+ NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>";
+ NSData *stringData = [str dataUsingEncoding:NSUTF8StringEncoding];
+ [self appendLoggedStreamData:stringData];
+ return streamProvider;
+ }
+ GTMSessionFetcherBodyStreamProvider loggedStreamProvider =
+ ^(GTMSessionFetcherBodyStreamProviderResponse response) {
+ streamProvider(^(NSInputStream *bodyStream) {
+ bodyStream = [self loggedInputStreamForInputStream:bodyStream];
+ response(bodyStream);
+ });
+ };
+ return loggedStreamProvider;
+}
+
+@end
+
+@implementation GTMSessionFetcher (GTMSessionFetcherLoggingUtilities)
+
+- (void)inputStream:(GTMReadMonitorInputStream *)stream
+ readIntoBuffer:(void *)buffer
+ length:(int64_t)length {
+ // append the captured data
+ NSData *data = [NSData dataWithBytesNoCopy:buffer
+ length:(NSUInteger)length
+ freeWhenDone:NO];
+ [self appendLoggedStreamData:data];
+}
+
+#pragma mark Fomatting Utilities
+
++ (NSString *)snipSubstringOfString:(NSString *)originalStr
+ betweenStartString:(NSString *)startStr
+ endString:(NSString *)endStr {
+#if SKIP_GTM_FETCH_LOGGING_SNIPPING
+ return originalStr;
+#else
+ if (!originalStr) return nil;
+
+ // Find the start string, and replace everything between it
+ // and the end string (or the end of the original string) with "_snip_"
+ NSRange startRange = [originalStr rangeOfString:startStr];
+ if (startRange.location == NSNotFound) return originalStr;
+
+ // We found the start string
+ NSUInteger originalLength = originalStr.length;
+ NSUInteger startOfTarget = NSMaxRange(startRange);
+ NSRange targetAndRest = NSMakeRange(startOfTarget, originalLength - startOfTarget);
+ NSRange endRange = [originalStr rangeOfString:endStr
+ options:0
+ range:targetAndRest];
+ NSRange replaceRange;
+ if (endRange.location == NSNotFound) {
+ // Found no end marker so replace to end of string
+ replaceRange = targetAndRest;
+ } else {
+ // Replace up to the endStr
+ replaceRange = NSMakeRange(startOfTarget, endRange.location - startOfTarget);
+ }
+ NSString *result = [originalStr stringByReplacingCharactersInRange:replaceRange
+ withString:@"_snip_"];
+ return result;
+#endif // SKIP_GTM_FETCH_LOGGING_SNIPPING
+}
+
++ (NSString *)headersStringForDictionary:(NSDictionary *)dict {
+ // Format the dictionary in http header style, like
+ // Accept: application/json
+ // Cache-Control: no-cache
+ // Content-Type: application/json; charset=utf-8
+ //
+ // Pad the key names, but not beyond 16 chars, since long custom header
+ // keys just create too much whitespace
+ NSArray *keys = [dict.allKeys sortedArrayUsingSelector:@selector(compare:)];
+
+ NSMutableString *str = [NSMutableString string];
+ for (NSString *key in keys) {
+ NSString *value = [dict valueForKey:key];
+ if ([key isEqual:@"Authorization"]) {
+ // Remove OAuth 1 token
+ value = [[self class] snipSubstringOfString:value
+ betweenStartString:@"oauth_token=\""
+ endString:@"\""];
+
+ // Remove OAuth 2 bearer token (draft 16, and older form)
+ value = [[self class] snipSubstringOfString:value
+ betweenStartString:@"Bearer "
+ endString:@"\n"];
+ value = [[self class] snipSubstringOfString:value
+ betweenStartString:@"OAuth "
+ endString:@"\n"];
+
+ // Remove Google ClientLogin
+ value = [[self class] snipSubstringOfString:value
+ betweenStartString:@"GoogleLogin auth="
+ endString:@"\n"];
+ }
+ [str appendFormat:@" %@: %@\n", key, value];
+ }
+ return str;
+}
+
+@end
+
+#endif // !STRIP_GTM_FETCH_LOGGING
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.h b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.h
new file mode 100644
index 00000000..312abaa7
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.h
@@ -0,0 +1,196 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// For best performance and convenient usage, fetchers should be generated by a common
+// GTMSessionFetcherService instance, like
+//
+// _fetcherService = [[GTMSessionFetcherService alloc] init];
+// GTMSessionFetcher* myFirstFetcher = [_fetcherService fetcherWithRequest:request1];
+// GTMSessionFetcher* mySecondFetcher = [_fetcherService fetcherWithRequest:request2];
+
+#import "GTMSessionFetcher.h"
+
+GTM_ASSUME_NONNULL_BEGIN
+
+// Notifications.
+
+// This notification indicates a reusable session has become invalid. It is intended mainly for the
+// service's unit tests.
+//
+// The notification object is the fetcher service.
+// The invalid session is provided via the userInfo kGTMSessionFetcherServiceSessionKey key.
+extern NSString *const kGTMSessionFetcherServiceSessionBecameInvalidNotification;
+extern NSString *const kGTMSessionFetcherServiceSessionKey;
+
+@interface GTMSessionFetcherService : NSObject<GTMSessionFetcherServiceProtocol>
+
+// Queues of delayed and running fetchers. Each dictionary contains arrays
+// of GTMSessionFetcher *fetchers, keyed by NSString *host
+@property(atomic, strong, readonly, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, NSArray *) *delayedFetchersByHost;
+@property(atomic, strong, readonly, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, NSArray *) *runningFetchersByHost;
+
+// A max value of 0 means no fetchers should be delayed.
+// The default limit is 10 simultaneous fetchers targeting each host.
+// This does not apply to fetchers whose useBackgroundSession property is YES. Since services are
+// not resurrected on an app relaunch, delayed fetchers would effectively be abandoned.
+@property(atomic, assign) NSUInteger maxRunningFetchersPerHost;
+
+// Properties to be applied to each fetcher; see GTMSessionFetcher.h for descriptions
+@property(atomic, strong, GTM_NULLABLE) NSURLSessionConfiguration *configuration;
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherConfigurationBlock configurationBlock;
+@property(atomic, strong, GTM_NULLABLE) NSHTTPCookieStorage *cookieStorage;
+@property(atomic, strong, GTM_NULL_RESETTABLE) dispatch_queue_t callbackQueue;
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherChallengeBlock challengeBlock;
+@property(atomic, strong, GTM_NULLABLE) NSURLCredential *credential;
+@property(atomic, strong) NSURLCredential *proxyCredential;
+@property(atomic, copy, GTM_NULLABLE) GTM_NSArrayOf(NSString *) *allowedInsecureSchemes;
+@property(atomic, assign) BOOL allowLocalhostRequest;
+@property(atomic, assign) BOOL allowInvalidServerCertificates;
+@property(atomic, assign, getter=isRetryEnabled) BOOL retryEnabled;
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherRetryBlock retryBlock;
+@property(atomic, assign) NSTimeInterval maxRetryInterval;
+@property(atomic, assign) NSTimeInterval minRetryInterval;
+@property(atomic, copy, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, id) *properties;
+@property(atomic, copy, GTM_NULLABLE)
+ GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock API_AVAILABLE(
+ ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0));
+
+#if GTM_BACKGROUND_TASK_FETCHING
+@property(atomic, assign) BOOL skipBackgroundTask;
+#endif
+
+// A default useragent of GTMFetcherStandardUserAgentString(nil) will be given to each fetcher
+// created by this service unless the request already has a user-agent header set.
+// This default will be added starting with builds with the SDKs for OS X 10.11 and iOS 9.
+//
+// To use the configuration's default user agent, set this property to nil.
+@property(atomic, copy, GTM_NULLABLE) NSString *userAgent;
+
+// The authorizer to attach to the created fetchers. If a specific fetcher should
+// not authorize its requests, the fetcher's authorizer property may be set to nil
+// before the fetch begins.
+@property(atomic, strong, GTM_NULLABLE) id<GTMFetcherAuthorizationProtocol> authorizer;
+
+// Delegate queue used by the session when calling back to the fetcher. The default
+// is the main queue. Changing this does not affect the queue used to call back to the
+// application; that is specified by the callbackQueue property above.
+@property(atomic, strong, GTM_NULL_RESETTABLE) NSOperationQueue *sessionDelegateQueue;
+
+// When enabled, indicates the same session should be used by subsequent fetchers.
+//
+// This is enabled by default.
+@property(atomic, assign) BOOL reuseSession;
+
+// Sets the delay until an unused session is invalidated.
+// The default interval is 60 seconds.
+//
+// If the interval is set to 0, then any reused session is not invalidated except by
+// explicitly invoking -resetSession. Be aware that setting the interval to 0 thus
+// causes the session's delegate to be retained until the session is explicitly reset.
+@property(atomic, assign) NSTimeInterval unusedSessionTimeout;
+
+// If shouldReuseSession is enabled, this will force creation of a new session when future
+// fetchers begin.
+- (void)resetSession;
+
+// Create a fetcher
+//
+// These methods will return a fetcher. If successfully created, the connection
+// will hold a strong reference to it for the life of the connection as well.
+// So the caller doesn't have to hold onto the fetcher explicitly unless they
+// want to be able to monitor or cancel it.
+- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request;
+- (GTMSessionFetcher *)fetcherWithURL:(NSURL *)requestURL;
+- (GTMSessionFetcher *)fetcherWithURLString:(NSString *)requestURLString;
+
+// Common method for fetcher creation.
+//
+// -fetcherWithRequest:fetcherClass: may be overridden to customize creation of
+// fetchers. This is the ONLY method in the GTMSessionFetcher library intended to
+// be overridden.
+- (id)fetcherWithRequest:(NSURLRequest *)request
+ fetcherClass:(Class)fetcherClass;
+
+- (BOOL)isDelayingFetcher:(GTMSessionFetcher *)fetcher;
+
+- (NSUInteger)numberOfFetchers; // running + delayed fetchers
+- (NSUInteger)numberOfRunningFetchers;
+- (NSUInteger)numberOfDelayedFetchers;
+
+// Return a list of all running or delayed fetchers. This includes fetchers created
+// by the service which have been started and have not yet stopped.
+//
+// Returns an array of fetcher objects, or nil if none.
+- (GTM_NULLABLE GTM_NSArrayOf(GTMSessionFetcher *) *)issuedFetchers;
+
+// Search for running or delayed fetchers with the specified URL.
+//
+// Returns an array of fetcher objects found, or nil if none found.
+- (GTM_NULLABLE GTM_NSArrayOf(GTMSessionFetcher *) *)issuedFetchersWithRequestURL:(NSURL *)requestURL;
+
+- (void)stopAllFetchers;
+
+// Methods for use by the fetcher class only.
+- (GTM_NULLABLE NSURLSession *)session;
+- (GTM_NULLABLE NSURLSession *)sessionForFetcherCreation;
+- (GTM_NULLABLE id<NSURLSessionDelegate>)sessionDelegate;
+- (GTM_NULLABLE NSDate *)stoppedAllFetchersDate;
+
+// The testBlock can inspect its fetcher parameter's request property to
+// determine which fetcher is being faked.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherTestBlock testBlock;
+
+@end
+
+@interface GTMSessionFetcherService (TestingSupport)
+
+// Convenience methods to create a fetcher service for testing.
+//
+// Fetchers generated by this mock fetcher service will not perform any
+// network operation, but will invoke callbacks and provide the supplied data
+// or error to the completion handler.
+//
+// You can make more customized mocks by setting the test block property of the service
+// or fetcher; the test block can inspect the fetcher's request or other properties.
+//
+// See the description of the testBlock property below.
++ (instancetype)mockFetcherServiceWithFakedData:(GTM_NULLABLE NSData *)fakedDataOrNil
+ fakedError:(GTM_NULLABLE NSError *)fakedErrorOrNil;
++ (instancetype)mockFetcherServiceWithFakedData:(GTM_NULLABLE NSData *)fakedDataOrNil
+ fakedResponse:(NSHTTPURLResponse *)fakedResponse
+ fakedError:(GTM_NULLABLE NSError *)fakedErrorOrNil;
+
+// Spin the run loop and discard events (or, if not on the main thread, just sleep the thread)
+// until all running and delayed fetchers have completed.
+//
+// This is only for use in testing or in tools without a user interface.
+//
+// Synchronous fetches should never be done by shipping apps; they are
+// sufficient reason for rejection from the app store.
+//
+// Returns NO if timed out.
+- (BOOL)waitForCompletionOfAllFetchersWithTimeout:(NSTimeInterval)timeoutInSeconds;
+
+@end
+
+@interface GTMSessionFetcherService (BackwardsCompatibilityOnly)
+
+// Clients using GTMSessionFetcher should set the cookie storage explicitly themselves.
+// This method is just for compatibility with the old fetcher.
+@property(atomic, assign) NSInteger cookieStorageMethod;
+
+@end
+
+GTM_ASSUME_NONNULL_END
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.m b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.m
new file mode 100644
index 00000000..f9942c01
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.m
@@ -0,0 +1,1381 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "GTMSessionFetcherService.h"
+
+NSString *const kGTMSessionFetcherServiceSessionBecameInvalidNotification
+ = @"kGTMSessionFetcherServiceSessionBecameInvalidNotification";
+NSString *const kGTMSessionFetcherServiceSessionKey
+ = @"kGTMSessionFetcherServiceSessionKey";
+
+#if !GTMSESSION_BUILD_COMBINED_SOURCES
+@interface GTMSessionFetcher (ServiceMethods)
+- (BOOL)beginFetchMayDelay:(BOOL)mayDelay
+ mayAuthorize:(BOOL)mayAuthorize;
+@end
+#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
+
+@interface GTMSessionFetcherService ()
+
+@property(atomic, strong, readwrite) NSDictionary *delayedFetchersByHost;
+@property(atomic, strong, readwrite) NSDictionary *runningFetchersByHost;
+
+@end
+
+// Since NSURLSession doesn't support a separate delegate per task (!), instances of this
+// class serve as a session delegate trampoline.
+//
+// This class maps a session's tasks to fetchers, and resends delegate messages to the task's
+// fetcher.
+@interface GTMSessionFetcherSessionDelegateDispatcher : NSObject<NSURLSessionDelegate>
+
+// The session for the tasks in this dispatcher's task-to-fetcher map.
+@property(atomic) NSURLSession *session;
+
+// The timer interval for invalidating a session that has no active tasks.
+@property(atomic) NSTimeInterval discardInterval;
+
+// The current discard timer.
+@property(atomic, readonly) NSTimer *discardTimer;
+
+
+- (instancetype)initWithParentService:(GTMSessionFetcherService *)parentService
+ sessionDiscardInterval:(NSTimeInterval)discardInterval;
+
+- (void)setFetcher:(GTMSessionFetcher *)fetcher
+ forTask:(NSURLSessionTask *)task;
+- (void)removeFetcher:(GTMSessionFetcher *)fetcher;
+
+// Before using a session, tells the delegate dispatcher to stop the discard timer.
+- (void)startSessionUsage;
+
+// When abandoning a delegate dispatcher, we want to avoid the session retaining
+// the delegate after tasks complete.
+- (void)abandon;
+
+@end
+
+
+@implementation GTMSessionFetcherService {
+ NSMutableDictionary *_delayedFetchersByHost;
+ NSMutableDictionary *_runningFetchersByHost;
+ NSUInteger _maxRunningFetchersPerHost;
+
+ // When this ivar is nil, the service will not reuse sessions.
+ GTMSessionFetcherSessionDelegateDispatcher *_delegateDispatcher;
+
+ // Fetchers will wait on this if another fetcher is creating the shared NSURLSession.
+ dispatch_semaphore_t _sessionCreationSemaphore;
+
+ dispatch_queue_t _callbackQueue;
+ NSOperationQueue *_delegateQueue;
+ NSHTTPCookieStorage *_cookieStorage;
+ NSString *_userAgent;
+ NSTimeInterval _timeout;
+
+ NSURLCredential *_credential; // Username & password.
+ NSURLCredential *_proxyCredential; // Credential supplied to proxy servers.
+
+ NSInteger _cookieStorageMethod;
+
+ id<GTMFetcherAuthorizationProtocol> _authorizer;
+
+ // For waitForCompletionOfAllFetchersWithTimeout: we need to wait on stopped fetchers since
+ // they've not yet finished invoking their queued callbacks. This array is nil except when
+ // waiting on fetchers.
+ NSMutableArray *_stoppedFetchersToWaitFor;
+
+ // For fetchers that enqueued their callbacks before stopAllFetchers was called on the service,
+ // set a barrier so the callbacks know to bail out.
+ NSDate *_stoppedAllFetchersDate;
+}
+
+@synthesize maxRunningFetchersPerHost = _maxRunningFetchersPerHost,
+ configuration = _configuration,
+ configurationBlock = _configurationBlock,
+ cookieStorage = _cookieStorage,
+ userAgent = _userAgent,
+ challengeBlock = _challengeBlock,
+ credential = _credential,
+ proxyCredential = _proxyCredential,
+ allowedInsecureSchemes = _allowedInsecureSchemes,
+ allowLocalhostRequest = _allowLocalhostRequest,
+ allowInvalidServerCertificates = _allowInvalidServerCertificates,
+ retryEnabled = _retryEnabled,
+ retryBlock = _retryBlock,
+ maxRetryInterval = _maxRetryInterval,
+ minRetryInterval = _minRetryInterval,
+ metricsCollectionBlock = _metricsCollectionBlock,
+ properties = _properties,
+ unusedSessionTimeout = _unusedSessionTimeout,
+ testBlock = _testBlock;
+
+#if GTM_BACKGROUND_TASK_FETCHING
+@synthesize skipBackgroundTask = _skipBackgroundTask;
+#endif
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _delayedFetchersByHost = [[NSMutableDictionary alloc] init];
+ _runningFetchersByHost = [[NSMutableDictionary alloc] init];
+ _maxRunningFetchersPerHost = 10;
+ _cookieStorageMethod = -1;
+ _unusedSessionTimeout = 60.0;
+ _delegateDispatcher =
+ [[GTMSessionFetcherSessionDelegateDispatcher alloc] initWithParentService:self
+ sessionDiscardInterval:_unusedSessionTimeout];
+ _callbackQueue = dispatch_get_main_queue();
+
+ _delegateQueue = [[NSOperationQueue alloc] init];
+ _delegateQueue.maxConcurrentOperationCount = 1;
+ _delegateQueue.name = @"com.google.GTMSessionFetcher.NSURLSessionDelegateQueue";
+
+ _sessionCreationSemaphore = dispatch_semaphore_create(1);
+
+ // Starting with the SDKs for OS X 10.11/iOS 9, the service has a default useragent.
+ // Apps can remove this and get the default system "CFNetwork" useragent by setting the
+ // fetcher service's userAgent property to nil.
+#if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \
+ || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0)
+ _userAgent = GTMFetcherStandardUserAgentString(nil);
+#endif
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self detachAuthorizer];
+ [_delegateDispatcher abandon];
+}
+
+#pragma mark Generate a new fetcher
+
+// Clients may override this method. Clients should not override any other library methods.
+- (id)fetcherWithRequest:(NSURLRequest *)request
+ fetcherClass:(Class)fetcherClass {
+ GTMSessionFetcher *fetcher = [[fetcherClass alloc] initWithRequest:request
+ configuration:self.configuration];
+ fetcher.callbackQueue = self.callbackQueue;
+ fetcher.sessionDelegateQueue = self.sessionDelegateQueue;
+ fetcher.challengeBlock = self.challengeBlock;
+ fetcher.credential = self.credential;
+ fetcher.proxyCredential = self.proxyCredential;
+ fetcher.authorizer = self.authorizer;
+ fetcher.cookieStorage = self.cookieStorage;
+ fetcher.allowedInsecureSchemes = self.allowedInsecureSchemes;
+ fetcher.allowLocalhostRequest = self.allowLocalhostRequest;
+ fetcher.allowInvalidServerCertificates = self.allowInvalidServerCertificates;
+ fetcher.configurationBlock = self.configurationBlock;
+ fetcher.retryEnabled = self.retryEnabled;
+ fetcher.retryBlock = self.retryBlock;
+ fetcher.maxRetryInterval = self.maxRetryInterval;
+ fetcher.minRetryInterval = self.minRetryInterval;
+ if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *)) {
+ fetcher.metricsCollectionBlock = self.metricsCollectionBlock;
+ }
+ fetcher.properties = self.properties;
+ fetcher.service = self;
+ if (self.cookieStorageMethod >= 0) {
+ [fetcher setCookieStorageMethod:self.cookieStorageMethod];
+ }
+
+#if GTM_BACKGROUND_TASK_FETCHING
+ fetcher.skipBackgroundTask = self.skipBackgroundTask;
+#endif
+
+ NSString *userAgent = self.userAgent;
+ if (userAgent.length > 0
+ && [request valueForHTTPHeaderField:@"User-Agent"] == nil) {
+ [fetcher setRequestValue:userAgent
+ forHTTPHeaderField:@"User-Agent"];
+ }
+ fetcher.testBlock = self.testBlock;
+
+ return fetcher;
+}
+
+- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request {
+ return [self fetcherWithRequest:request
+ fetcherClass:[GTMSessionFetcher class]];
+}
+
+- (GTMSessionFetcher *)fetcherWithURL:(NSURL *)requestURL {
+ return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
+}
+
+- (GTMSessionFetcher *)fetcherWithURLString:(NSString *)requestURLString {
+ NSURL *url = [NSURL URLWithString:requestURLString];
+ return [self fetcherWithURL:url];
+}
+
+// Returns a session for the fetcher's host, or nil.
+- (NSURLSession *)session {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSURLSession *session = _delegateDispatcher.session;
+ return session;
+ }
+}
+
+// Returns a session for the fetcher's host, or nil. For shared sessions, this
+// waits on a semaphore, blocking other fetchers while the caller creates the
+// session if needed.
+- (NSURLSession *)sessionForFetcherCreation {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+ if (!_delegateDispatcher) {
+ // This fetcher is creating a non-shared session, so skip the semaphore usage.
+ return nil;
+ }
+ }
+
+ // Wait if another fetcher is currently creating a session; avoid waiting
+ // inside the @synchronized block, as that can deadlock.
+ dispatch_semaphore_wait(_sessionCreationSemaphore, DISPATCH_TIME_FOREVER);
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // Before getting the NSURLSession for task creation, it is
+ // important to invalidate and nil out the session discard timer; otherwise
+ // the session can be invalidated between when it is returned to the
+ // fetcher, and when the fetcher attempts to create its NSURLSessionTask.
+ [_delegateDispatcher startSessionUsage];
+
+ NSURLSession *session = _delegateDispatcher.session;
+ if (session) {
+ // The calling fetcher will receive a preexisting session, so
+ // we can allow other fetchers to create a session.
+ dispatch_semaphore_signal(_sessionCreationSemaphore);
+ } else {
+ // No existing session was obtained, so the calling fetcher will create the session;
+ // it *must* invoke fetcherDidCreateSession: to signal the dispatcher's semaphore after
+ // the session has been created (or fails to be created) to avoid a hang.
+ }
+ return session;
+ }
+}
+
+- (id<NSURLSessionDelegate>)sessionDelegate {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _delegateDispatcher;
+ }
+}
+
+#pragma mark Queue Management
+
+- (void)addRunningFetcher:(GTMSessionFetcher *)fetcher
+ forHost:(NSString *)host {
+ // Add to the array of running fetchers for this host, creating the array if needed.
+ NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host];
+ if (runningForHost == nil) {
+ runningForHost = [NSMutableArray arrayWithObject:fetcher];
+ [_runningFetchersByHost setObject:runningForHost forKey:host];
+ } else {
+ [runningForHost addObject:fetcher];
+ }
+}
+
+- (void)addDelayedFetcher:(GTMSessionFetcher *)fetcher
+ forHost:(NSString *)host {
+ // Add to the array of delayed fetchers for this host, creating the array if needed.
+ NSMutableArray *delayedForHost = [_delayedFetchersByHost objectForKey:host];
+ if (delayedForHost == nil) {
+ delayedForHost = [NSMutableArray arrayWithObject:fetcher];
+ [_delayedFetchersByHost setObject:delayedForHost forKey:host];
+ } else {
+ [delayedForHost addObject:fetcher];
+ }
+}
+
+- (BOOL)isDelayingFetcher:(GTMSessionFetcher *)fetcher {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSString *host = fetcher.request.URL.host;
+ if (host == nil) {
+ return NO;
+ }
+ NSArray *delayedForHost = [_delayedFetchersByHost objectForKey:host];
+ NSUInteger idx = [delayedForHost indexOfObjectIdenticalTo:fetcher];
+ BOOL isDelayed = (delayedForHost != nil) && (idx != NSNotFound);
+ return isDelayed;
+ }
+}
+
+- (BOOL)fetcherShouldBeginFetching:(GTMSessionFetcher *)fetcher {
+ // Entry point from the fetcher
+ NSURL *requestURL = fetcher.request.URL;
+ NSString *host = requestURL.host;
+
+ // Addresses "file:///path" case where localhost is the implicit host.
+ if (host.length == 0 && [requestURL isFileURL]) {
+ host = @"localhost";
+ }
+
+ if (host.length == 0) {
+ // Data URIs legitimately have no host, reject other hostless URLs.
+ GTMSESSION_ASSERT_DEBUG([[requestURL scheme] isEqual:@"data"], @"%@ lacks host", fetcher);
+ return YES;
+ }
+
+ BOOL shouldBeginResult;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host];
+ if (runningForHost != nil
+ && [runningForHost indexOfObjectIdenticalTo:fetcher] != NSNotFound) {
+ GTMSESSION_ASSERT_DEBUG(NO, @"%@ was already running", fetcher);
+ return YES;
+ }
+
+ BOOL shouldRunNow = (fetcher.usingBackgroundSession
+ || _maxRunningFetchersPerHost == 0
+ || _maxRunningFetchersPerHost >
+ [[self class] numberOfNonBackgroundSessionFetchers:runningForHost]);
+ if (shouldRunNow) {
+ [self addRunningFetcher:fetcher forHost:host];
+ shouldBeginResult = YES;
+ } else {
+ [self addDelayedFetcher:fetcher forHost:host];
+ shouldBeginResult = NO;
+ }
+ } // @synchronized(self)
+
+ // We'll save the host that serves as the key for this fetcher's array
+ // to avoid any chance of the underlying request changing, stranding
+ // the fetcher in the wrong array
+ fetcher.serviceHost = host;
+
+ return shouldBeginResult;
+}
+
+- (void)startFetcher:(GTMSessionFetcher *)fetcher {
+ [fetcher beginFetchMayDelay:NO
+ mayAuthorize:YES];
+}
+
+// Internal utility. Returns a fetcher's delegate if it's a dispatcher, or nil if the fetcher
+// is its own delegate (possibly via proxy) and has no dispatcher.
+- (GTMSessionFetcherSessionDelegateDispatcher *)delegateDispatcherForFetcher:(GTMSessionFetcher *)fetcher {
+ GTMSessionCheckNotSynchronized(self);
+
+ NSURLSession *fetcherSession = fetcher.session;
+ if (fetcherSession) {
+ id<NSURLSessionDelegate> fetcherDelegate = fetcherSession.delegate;
+ // If the delegate is non-nil and claims to be a GTMSessionFetcher, there is no dispatcher;
+ // assume the fetcher is the delegate or has been proxied (some third-party frameworks
+ // are known to swizzle NSURLSession to proxy its delegate).
+ BOOL hasDispatcher = (fetcherDelegate != nil &&
+ ![fetcherDelegate isKindOfClass:[GTMSessionFetcher class]]);
+ if (hasDispatcher) {
+ GTMSESSION_ASSERT_DEBUG([fetcherDelegate isKindOfClass:[GTMSessionFetcherSessionDelegateDispatcher class]],
+ @"Fetcher delegate class: %@", [fetcherDelegate class]);
+ return (GTMSessionFetcherSessionDelegateDispatcher *)fetcherDelegate;
+ }
+ }
+ return nil;
+}
+
+- (void)fetcherDidCreateSession:(GTMSessionFetcher *)fetcher {
+ if (fetcher.canShareSession) {
+ NSURLSession *fetcherSession = fetcher.session;
+ GTMSESSION_ASSERT_DEBUG(fetcherSession != nil, @"Fetcher missing its session: %@", fetcher);
+
+ GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher =
+ [self delegateDispatcherForFetcher:fetcher];
+ if (delegateDispatcher) {
+ GTMSESSION_ASSERT_DEBUG(delegateDispatcher.session == nil,
+ @"Fetcher made an extra session: %@", fetcher);
+
+ // Save this fetcher's session.
+ delegateDispatcher.session = fetcherSession;
+
+ // Allow other fetchers to request this session now.
+ dispatch_semaphore_signal(_sessionCreationSemaphore);
+ }
+ }
+}
+
+- (void)fetcherDidBeginFetching:(GTMSessionFetcher *)fetcher {
+ // If this fetcher has a separate delegate with a shared session, then
+ // this fetcher should be added to the delegate's map of tasks to fetchers.
+ GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher =
+ [self delegateDispatcherForFetcher:fetcher];
+ if (delegateDispatcher) {
+ GTMSESSION_ASSERT_DEBUG(fetcher.canShareSession,
+ @"Inappropriate shared session: %@", fetcher);
+
+ // There should already be a session, from this or a previous fetcher.
+ //
+ // Sanity check that the fetcher's session is the delegate's shared session.
+ NSURLSession *sharedSession = delegateDispatcher.session;
+ NSURLSession *fetcherSession = fetcher.session;
+ GTMSESSION_ASSERT_DEBUG(sharedSession != nil, @"Missing delegate session: %@", fetcher);
+ GTMSESSION_ASSERT_DEBUG(fetcherSession == sharedSession,
+ @"Inconsistent session: %@ %@ (shared: %@)",
+ fetcher, fetcherSession, sharedSession);
+
+ if (sharedSession != nil && fetcherSession == sharedSession) {
+ NSURLSessionTask *task = fetcher.sessionTask;
+ GTMSESSION_ASSERT_DEBUG(task != nil, @"Missing session task: %@", fetcher);
+
+ if (task) {
+ [delegateDispatcher setFetcher:fetcher
+ forTask:task];
+ }
+ }
+ }
+}
+
+- (void)stopFetcher:(GTMSessionFetcher *)fetcher {
+ [fetcher stopFetching];
+}
+
+- (void)fetcherDidStop:(GTMSessionFetcher *)fetcher {
+ // Entry point from the fetcher
+ NSString *host = fetcher.serviceHost;
+ if (!host) {
+ // fetcher has been stopped previously
+ return;
+ }
+
+ // This removeFetcher: invocation is a fallback; typically, fetchers are removed from the task
+ // map when the task completes.
+ GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher =
+ [self delegateDispatcherForFetcher:fetcher];
+ [delegateDispatcher removeFetcher:fetcher];
+
+ NSMutableArray *fetchersToStart;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // If a test is waiting for all fetchers to stop, it needs to wait for this one
+ // to invoke its callbacks on the callback queue.
+ [_stoppedFetchersToWaitFor addObject:fetcher];
+
+ NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host];
+ [runningForHost removeObject:fetcher];
+
+ NSMutableArray *delayedForHost = [_delayedFetchersByHost objectForKey:host];
+ [delayedForHost removeObject:fetcher];
+
+ while (delayedForHost.count > 0
+ && [[self class] numberOfNonBackgroundSessionFetchers:runningForHost]
+ < _maxRunningFetchersPerHost) {
+ // Start another delayed fetcher running, scanning for the minimum
+ // priority value, defaulting to FIFO for equal priorities
+ GTMSessionFetcher *nextFetcher = nil;
+ for (GTMSessionFetcher *delayedFetcher in delayedForHost) {
+ if (nextFetcher == nil
+ || delayedFetcher.servicePriority < nextFetcher.servicePriority) {
+ nextFetcher = delayedFetcher;
+ }
+ }
+
+ if (nextFetcher) {
+ [self addRunningFetcher:nextFetcher forHost:host];
+ runningForHost = [_runningFetchersByHost objectForKey:host];
+
+ [delayedForHost removeObjectIdenticalTo:nextFetcher];
+
+ if (!fetchersToStart) {
+ fetchersToStart = [NSMutableArray array];
+ }
+ [fetchersToStart addObject:nextFetcher];
+ }
+ }
+
+ if (runningForHost.count == 0) {
+ // None left; remove the empty array
+ [_runningFetchersByHost removeObjectForKey:host];
+ }
+
+ if (delayedForHost.count == 0) {
+ [_delayedFetchersByHost removeObjectForKey:host];
+ }
+ } // @synchronized(self)
+
+ // Start fetchers outside of the synchronized block to avoid a deadlock.
+ for (GTMSessionFetcher *nextFetcher in fetchersToStart) {
+ [self startFetcher:nextFetcher];
+ }
+
+ // The fetcher is no longer in the running or the delayed array,
+ // so remove its host and thread properties
+ fetcher.serviceHost = nil;
+}
+
+- (NSUInteger)numberOfFetchers {
+ NSUInteger running = [self numberOfRunningFetchers];
+ NSUInteger delayed = [self numberOfDelayedFetchers];
+ return running + delayed;
+}
+
+- (NSUInteger)numberOfRunningFetchers {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSUInteger sum = 0;
+ for (NSString *host in _runningFetchersByHost) {
+ NSArray *fetchers = [_runningFetchersByHost objectForKey:host];
+ sum += fetchers.count;
+ }
+ return sum;
+ }
+}
+
+- (NSUInteger)numberOfDelayedFetchers {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSUInteger sum = 0;
+ for (NSString *host in _delayedFetchersByHost) {
+ NSArray *fetchers = [_delayedFetchersByHost objectForKey:host];
+ sum += fetchers.count;
+ }
+ return sum;
+ }
+}
+
+- (NSArray *)issuedFetchers {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSMutableArray *allFetchers = [NSMutableArray array];
+ void (^accumulateFetchers)(id, id, BOOL *) = ^(NSString *host,
+ NSArray *fetchersForHost,
+ BOOL *stop) {
+ [allFetchers addObjectsFromArray:fetchersForHost];
+ };
+ [_runningFetchersByHost enumerateKeysAndObjectsUsingBlock:accumulateFetchers];
+ [_delayedFetchersByHost enumerateKeysAndObjectsUsingBlock:accumulateFetchers];
+
+ GTMSESSION_ASSERT_DEBUG(allFetchers.count == [NSSet setWithArray:allFetchers].count,
+ @"Fetcher appears multiple times\n running: %@\n delayed: %@",
+ _runningFetchersByHost, _delayedFetchersByHost);
+
+ return allFetchers.count > 0 ? allFetchers : nil;
+ }
+}
+
+- (NSArray *)issuedFetchersWithRequestURL:(NSURL *)requestURL {
+ NSString *host = requestURL.host;
+ if (host.length == 0) return nil;
+
+ NSURL *targetURL = [requestURL absoluteURL];
+
+ NSArray *allFetchers = [self issuedFetchers];
+ NSIndexSet *indexes = [allFetchers indexesOfObjectsPassingTest:^BOOL(GTMSessionFetcher *fetcher,
+ NSUInteger idx,
+ BOOL *stop) {
+ NSURL *fetcherURL = [fetcher.request.URL absoluteURL];
+ return [fetcherURL isEqual:targetURL];
+ }];
+
+ NSArray *result = nil;
+ if (indexes.count > 0) {
+ result = [allFetchers objectsAtIndexes:indexes];
+ }
+ return result;
+}
+
+- (void)stopAllFetchers {
+ NSArray *delayedFetchersByHost;
+ NSArray *runningFetchersByHost;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // Set the time barrier so fetchers know not to call back even if
+ // the stop calls below occur after the fetchers naturally
+ // stopped and so were removed from _runningFetchersByHost,
+ // but while the callbacks were already enqueued before stopAllFetchers
+ // was invoked.
+ _stoppedAllFetchersDate = [[NSDate alloc] init];
+
+ // Remove fetchers from the delayed list to avoid fetcherDidStop: from
+ // starting more fetchers running as a side effect of stopping one
+ delayedFetchersByHost = _delayedFetchersByHost.allValues;
+ [_delayedFetchersByHost removeAllObjects];
+
+ runningFetchersByHost = _runningFetchersByHost.allValues;
+ [_runningFetchersByHost removeAllObjects];
+ }
+
+ for (NSArray *delayedForHost in delayedFetchersByHost) {
+ for (GTMSessionFetcher *fetcher in delayedForHost) {
+ [self stopFetcher:fetcher];
+ }
+ }
+
+ for (NSArray *runningForHost in runningFetchersByHost) {
+ for (GTMSessionFetcher *fetcher in runningForHost) {
+ [self stopFetcher:fetcher];
+ }
+ }
+}
+
+- (NSDate *)stoppedAllFetchersDate {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _stoppedAllFetchersDate;
+ }
+}
+
+#pragma mark Accessors
+
+- (BOOL)reuseSession {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _delegateDispatcher != nil;
+ }
+}
+
+- (void)setReuseSession:(BOOL)shouldReuse {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ BOOL wasReusing = (_delegateDispatcher != nil);
+ if (shouldReuse != wasReusing) {
+ [self abandonDispatcher];
+ if (shouldReuse) {
+ _delegateDispatcher =
+ [[GTMSessionFetcherSessionDelegateDispatcher alloc] initWithParentService:self
+ sessionDiscardInterval:_unusedSessionTimeout];
+ } else {
+ _delegateDispatcher = nil;
+ }
+ }
+ }
+}
+
+- (void)resetSession {
+ GTMSessionCheckNotSynchronized(self);
+ dispatch_semaphore_wait(_sessionCreationSemaphore, DISPATCH_TIME_FOREVER);
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+ [self resetSessionInternal];
+ }
+
+ dispatch_semaphore_signal(_sessionCreationSemaphore);
+}
+
+- (void)resetSessionInternal {
+ GTMSessionCheckSynchronized(self);
+
+ // The old dispatchers may be retained as delegates of any ongoing sessions by those sessions.
+ if (_delegateDispatcher) {
+ [self abandonDispatcher];
+ _delegateDispatcher =
+ [[GTMSessionFetcherSessionDelegateDispatcher alloc] initWithParentService:self
+ sessionDiscardInterval:_unusedSessionTimeout];
+ }
+}
+
+- (void)resetSessionForDispatcherDiscardTimer:(NSTimer *)timer {
+ GTMSessionCheckNotSynchronized(self);
+
+ dispatch_semaphore_wait(_sessionCreationSemaphore, DISPATCH_TIME_FOREVER);
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_delegateDispatcher.discardTimer == timer) {
+ // If the delegate dispatcher's current discardTimer is the same object as the timer
+ // that fired, no fetcher has recently attempted to start using the session by calling
+ // startSessionUsage, which invalidates and nils out the timer.
+ [self resetSessionInternal];
+ } else {
+ // A fetcher has invalidated the timer between its triggering and now, potentially
+ // meaning a fetcher has requested access to the NSURLSession, and may be in the process
+ // of starting a new task. The dispatcher should not be abandoned, as this can lead
+ // to a race condition between calling -finishTasksAndInvalidate on the NSURLSession
+ // and the fetcher attempting to create a new task.
+ }
+ }
+
+ dispatch_semaphore_signal(_sessionCreationSemaphore);
+}
+
+- (NSTimeInterval)unusedSessionTimeout {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _unusedSessionTimeout;
+ }
+}
+
+- (void)setUnusedSessionTimeout:(NSTimeInterval)timeout {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _unusedSessionTimeout = timeout;
+ _delegateDispatcher.discardInterval = timeout;
+ }
+}
+
+// This method should be called inside of @synchronized(self)
+- (void)abandonDispatcher {
+ GTMSessionCheckSynchronized(self);
+ [_delegateDispatcher abandon];
+}
+
+- (NSDictionary *)runningFetchersByHost {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [_runningFetchersByHost copy];
+ }
+}
+
+- (void)setRunningFetchersByHost:(NSDictionary *)dict {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _runningFetchersByHost = [dict mutableCopy];
+ }
+}
+
+- (NSDictionary *)delayedFetchersByHost {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [_delayedFetchersByHost copy];
+ }
+}
+
+- (void)setDelayedFetchersByHost:(NSDictionary *)dict {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _delayedFetchersByHost = [dict mutableCopy];
+ }
+}
+
+- (id<GTMFetcherAuthorizationProtocol>)authorizer {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _authorizer;
+ }
+}
+
+- (void)setAuthorizer:(id<GTMFetcherAuthorizationProtocol>)obj {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (obj != _authorizer) {
+ [self detachAuthorizer];
+ }
+
+ _authorizer = obj;
+ }
+
+ // Use the fetcher service for the authorization fetches if the auth
+ // object supports fetcher services
+ if ([obj respondsToSelector:@selector(setFetcherService:)]) {
+#if GTM_USE_SESSION_FETCHER
+ [obj setFetcherService:self];
+#else
+ [obj setFetcherService:(id)self];
+#endif
+ }
+}
+
+// This should be called inside a @synchronized(self) block except during dealloc.
+- (void)detachAuthorizer {
+ // This method is called by the fetcher service's dealloc and setAuthorizer:
+ // methods; do not override.
+ //
+ // The fetcher service retains the authorizer, and the authorizer has a
+ // weak pointer to the fetcher service (a non-zeroing pointer for
+ // compatibility with iOS 4 and Mac OS X 10.5/10.6.)
+ //
+ // When this fetcher service no longer uses the authorizer, we want to remove
+ // the authorizer's dependence on the fetcher service. Authorizers can still
+ // function without a fetcher service.
+ if ([_authorizer respondsToSelector:@selector(fetcherService)]) {
+ id authFetcherService = [_authorizer fetcherService];
+ if (authFetcherService == self) {
+ [_authorizer setFetcherService:nil];
+ }
+ }
+}
+
+- (dispatch_queue_t GTM_NONNULL_TYPE)callbackQueue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _callbackQueue;
+ } // @synchronized(self)
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _callbackQueue = queue ?: dispatch_get_main_queue();
+ } // @synchronized(self)
+}
+
+- (NSOperationQueue * GTM_NONNULL_TYPE)sessionDelegateQueue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _delegateQueue;
+ } // @synchronized(self)
+}
+
+- (void)setSessionDelegateQueue:(NSOperationQueue * GTM_NULLABLE_TYPE)queue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _delegateQueue = queue ?: [NSOperationQueue mainQueue];
+ } // @synchronized(self)
+}
+
+- (NSOperationQueue *)delegateQueue {
+ // Provided for compatibility with the old fetcher service. The gtm-oauth2 code respects
+ // any custom delegate queue for calling the app.
+ return nil;
+}
+
++ (NSUInteger)numberOfNonBackgroundSessionFetchers:(NSArray *)fetchers {
+ NSUInteger sum = 0;
+ for (GTMSessionFetcher *fetcher in fetchers) {
+ if (!fetcher.usingBackgroundSession) {
+ ++sum;
+ }
+ }
+ return sum;
+}
+
+@end
+
+@implementation GTMSessionFetcherService (TestingSupport)
+
++ (instancetype)mockFetcherServiceWithFakedData:(NSData *)fakedDataOrNil
+ fakedError:(NSError *)fakedErrorOrNil {
+#if !GTM_DISABLE_FETCHER_TEST_BLOCK
+ NSURL *url = [NSURL URLWithString:@"http://example.invalid"];
+ NSHTTPURLResponse *fakedResponse =
+ [[NSHTTPURLResponse alloc] initWithURL:url
+ statusCode:(fakedErrorOrNil ? 500 : 200)
+ HTTPVersion:@"HTTP/1.1"
+ headerFields:nil];
+ return [self mockFetcherServiceWithFakedData:fakedDataOrNil
+ fakedResponse:fakedResponse
+ fakedError:fakedErrorOrNil];
+#else
+ GTMSESSION_ASSERT_DEBUG(0, @"Test blocks disabled");
+ return nil;
+#endif // GTM_DISABLE_FETCHER_TEST_BLOCK
+}
+
++ (instancetype)mockFetcherServiceWithFakedData:(NSData *)fakedDataOrNil
+ fakedResponse:(NSHTTPURLResponse *)fakedResponse
+ fakedError:(NSError *)fakedErrorOrNil {
+#if !GTM_DISABLE_FETCHER_TEST_BLOCK
+ GTMSessionFetcherService *service = [[self alloc] init];
+ service.allowedInsecureSchemes = @[ @"http" ];
+ service.testBlock = ^(GTMSessionFetcher *fetcherToTest,
+ GTMSessionFetcherTestResponse testResponse) {
+ testResponse(fakedResponse, fakedDataOrNil, fakedErrorOrNil);
+ };
+ return service;
+#else
+ GTMSESSION_ASSERT_DEBUG(0, @"Test blocks disabled");
+ return nil;
+#endif // GTM_DISABLE_FETCHER_TEST_BLOCK
+}
+
+#pragma mark Synchronous Wait for Unit Testing
+
+- (BOOL)waitForCompletionOfAllFetchersWithTimeout:(NSTimeInterval)timeoutInSeconds {
+ NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
+ _stoppedFetchersToWaitFor = [NSMutableArray array];
+
+ BOOL shouldSpinRunLoop = [NSThread isMainThread];
+ const NSTimeInterval kSpinInterval = 0.001;
+ BOOL didTimeOut = NO;
+ while (([self numberOfFetchers] > 0 || _stoppedFetchersToWaitFor.count > 0)) {
+ didTimeOut = [giveUpDate timeIntervalSinceNow] < 0;
+ if (didTimeOut) break;
+
+ GTMSessionFetcher *stoppedFetcher = _stoppedFetchersToWaitFor.firstObject;
+ if (stoppedFetcher) {
+ [_stoppedFetchersToWaitFor removeObject:stoppedFetcher];
+ [stoppedFetcher waitForCompletionWithTimeout:10.0 * kSpinInterval];
+ }
+
+ if (shouldSpinRunLoop) {
+ NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:kSpinInterval];
+ [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
+ } else {
+ [NSThread sleepForTimeInterval:kSpinInterval];
+ }
+ }
+ _stoppedFetchersToWaitFor = nil;
+
+ return !didTimeOut;
+}
+
+@end
+
+@implementation GTMSessionFetcherService (BackwardsCompatibilityOnly)
+
+- (NSInteger)cookieStorageMethod {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _cookieStorageMethod;
+ }
+}
+
+- (void)setCookieStorageMethod:(NSInteger)cookieStorageMethod {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _cookieStorageMethod = cookieStorageMethod;
+ }
+}
+
+@end
+
+@implementation GTMSessionFetcherSessionDelegateDispatcher {
+ __weak GTMSessionFetcherService *_parentService;
+ NSURLSession *_session;
+
+ // The task map maps NSURLSessionTasks to GTMSessionFetchers
+ NSMutableDictionary *_taskToFetcherMap;
+ // The discard timer will invalidate sessions after the session's last task completes.
+ NSTimer *_discardTimer;
+ NSTimeInterval _discardInterval;
+}
+
+@synthesize discardInterval = _discardInterval,
+ session = _session;
+
+- (instancetype)init {
+ [self doesNotRecognizeSelector:_cmd];
+ return nil;
+}
+
+- (instancetype)initWithParentService:(GTMSessionFetcherService *)parentService
+ sessionDiscardInterval:(NSTimeInterval)discardInterval {
+ self = [super init];
+ if (self) {
+ _discardInterval = discardInterval;
+ _parentService = parentService;
+ }
+ return self;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"%@ %p %@ %@",
+ [self class], self,
+ _session ?: @"<no session>",
+ _taskToFetcherMap.count > 0 ? _taskToFetcherMap : @"<no tasks>"];
+}
+
+- (NSTimer *)discardTimer {
+ GTMSessionCheckNotSynchronized(self);
+ @synchronized(self) {
+ return _discardTimer;
+ }
+}
+
+// This method should be called inside of a @synchronized(self) block.
+- (void)startDiscardTimer {
+ GTMSessionCheckSynchronized(self);
+ [_discardTimer invalidate];
+ _discardTimer = nil;
+ if (_discardInterval > 0) {
+ _discardTimer = [NSTimer timerWithTimeInterval:_discardInterval
+ target:self
+ selector:@selector(discardTimerFired:)
+ userInfo:nil
+ repeats:NO];
+ [_discardTimer setTolerance:(_discardInterval / 10)];
+ [[NSRunLoop mainRunLoop] addTimer:_discardTimer forMode:NSRunLoopCommonModes];
+ }
+}
+
+// This method should be called inside of a @synchronized(self) block.
+- (void)destroyDiscardTimer {
+ GTMSessionCheckSynchronized(self);
+ [_discardTimer invalidate];
+ _discardTimer = nil;
+}
+
+- (void)discardTimerFired:(NSTimer *)timer {
+ GTMSessionFetcherService *service;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSUInteger numberOfTasks = _taskToFetcherMap.count;
+ if (numberOfTasks == 0) {
+ service = _parentService;
+ }
+ }
+
+ // Inform the service that the discard timer has fired, and should check whether the
+ // service can abandon us. -resetSession cannot be called directly, as there is a
+ // race condition that must be guarded against with the NSURLSession being returned
+ // from sessionForFetcherCreation outside other locks. The service can take steps
+ // to prevent resetting the session if that has occurred.
+ //
+ // The service must be called from outside the @synchronized block.
+ [service resetSessionForDispatcherDiscardTimer:timer];
+}
+
+- (void)abandon {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [self destroySessionAndTimer];
+ }
+}
+
+- (void)startSessionUsage {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [self destroyDiscardTimer];
+ }
+}
+
+// This method should be called inside of a @synchronized(self) block.
+- (void)destroySessionAndTimer {
+ GTMSessionCheckSynchronized(self);
+ [self destroyDiscardTimer];
+
+ // Break any retain cycle from the session holding the delegate.
+ [_session finishTasksAndInvalidate];
+
+ // Immediately clear the session so no new task may be issued with it.
+ //
+ // The _taskToFetcherMap needs to stay valid until the outstanding tasks finish.
+ _session = nil;
+}
+
+- (void)setFetcher:(GTMSessionFetcher *)fetcher forTask:(NSURLSessionTask *)task {
+ GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"missing fetcher");
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_taskToFetcherMap == nil) {
+ _taskToFetcherMap = [[NSMutableDictionary alloc] init];
+ }
+
+ if (fetcher) {
+ [_taskToFetcherMap setObject:fetcher forKey:task];
+ [self destroyDiscardTimer];
+ }
+ }
+}
+
+- (void)removeFetcher:(GTMSessionFetcher *)fetcher {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // Typically, a fetcher should be removed when its task invokes
+ // URLSession:task:didCompleteWithError:.
+ //
+ // When fetching with a testBlock, though, the task completed delegate
+ // method may not be invoked, requiring cleanup here.
+ NSArray *tasks = [_taskToFetcherMap allKeysForObject:fetcher];
+ GTMSESSION_ASSERT_DEBUG(tasks.count <= 1, @"fetcher task not unmapped: %@", tasks);
+ [_taskToFetcherMap removeObjectsForKeys:tasks];
+
+ if (_taskToFetcherMap.count == 0) {
+ [self startDiscardTimer];
+ }
+ }
+}
+
+// This helper method provides synchronized access to the task map for the delegate
+// methods below.
+- (id)fetcherForTask:(NSURLSessionTask *)task {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [_taskToFetcherMap objectForKey:task];
+ }
+}
+
+- (void)removeTaskFromMap:(NSURLSessionTask *)task {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [_taskToFetcherMap removeObjectForKey:task];
+ }
+}
+
+- (void)setSession:(NSURLSession *)session {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _session = session;
+ }
+}
+
+- (NSURLSession *)session {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _session;
+ }
+}
+
+- (NSTimeInterval)discardInterval {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _discardInterval;
+ }
+}
+
+- (void)setDiscardInterval:(NSTimeInterval)interval {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _discardInterval = interval;
+ }
+}
+
+// NSURLSessionDelegate protocol methods.
+
+// - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;
+//
+// TODO(seh): How do we route this to an appropriate fetcher?
+
+
+- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@",
+ [self class], self, session, error);
+ NSDictionary *localTaskToFetcherMap;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _session = nil;
+
+ localTaskToFetcherMap = [_taskToFetcherMap copy];
+ }
+
+ // Any "suspended" tasks may not have received callbacks from NSURLSession when the session
+ // completes; we'll call them now.
+ [localTaskToFetcherMap enumerateKeysAndObjectsUsingBlock:^(NSURLSessionTask *task,
+ GTMSessionFetcher *fetcher,
+ BOOL *stop) {
+ if (fetcher.session == session) {
+ // Our delegate method URLSession:task:didCompleteWithError: will rely on
+ // _taskToFetcherMap so that should still contain this fetcher.
+ NSError *canceledError = [NSError errorWithDomain:NSURLErrorDomain
+ code:NSURLErrorCancelled
+ userInfo:nil];
+ [self URLSession:session task:task didCompleteWithError:canceledError];
+ } else {
+ GTMSESSION_ASSERT_DEBUG(0, @"Unexpected session in fetcher: %@ has %@ (expected %@)",
+ fetcher, fetcher.session, session);
+ }
+ }];
+
+ // Our tests rely on this notification to know the session discard timer fired.
+ NSDictionary *userInfo = @{ kGTMSessionFetcherServiceSessionKey : session };
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc postNotificationName:kGTMSessionFetcherServiceSessionBecameInvalidNotification
+ object:_parentService
+ userInfo:userInfo];
+}
+
+
+#pragma mark - NSURLSessionTaskDelegate
+
+// NSURLSessionTaskDelegate protocol methods.
+//
+// We won't test here if the fetcher responds to these since we only want this
+// class to implement the same delegate methods the fetcher does (so NSURLSession's
+// tests for respondsToSelector: will have the same result whether the session
+// delegate is the fetcher or this dispatcher.)
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+willPerformHTTPRedirection:(NSHTTPURLResponse *)response
+ newRequest:(NSURLRequest *)request
+ completionHandler:(void (^)(NSURLRequest *))completionHandler {
+ id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
+ [fetcher URLSession:session
+ task:task
+willPerformHTTPRedirection:response
+ newRequest:request
+ completionHandler:completionHandler];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))handler {
+ id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
+ [fetcher URLSession:session
+ task:task
+ didReceiveChallenge:challenge
+ completionHandler:handler];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ needNewBodyStream:(void (^)(NSInputStream *bodyStream))handler {
+ id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
+ [fetcher URLSession:session
+ task:task
+ needNewBodyStream:handler];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didSendBodyData:(int64_t)bytesSent
+ totalBytesSent:(int64_t)totalBytesSent
+totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
+ id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
+ [fetcher URLSession:session
+ task:task
+ didSendBodyData:bytesSent
+ totalBytesSent:totalBytesSent
+totalBytesExpectedToSend:totalBytesExpectedToSend];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+didCompleteWithError:(NSError *)error {
+ id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
+
+ // This is the usual way tasks are removed from the task map.
+ [self removeTaskFromMap:task];
+
+ [fetcher URLSession:session
+ task:task
+ didCompleteWithError:error];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
+ API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)) {
+ id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
+ [fetcher URLSession:session task:task didFinishCollectingMetrics:metrics];
+}
+
+// NSURLSessionDataDelegate protocol methods.
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+didReceiveResponse:(NSURLResponse *)response
+ completionHandler:(void (^)(NSURLSessionResponseDisposition))handler {
+ id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
+ [fetcher URLSession:session
+ dataTask:dataTask
+ didReceiveResponse:response
+ completionHandler:handler];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
+ id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
+ GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"Missing fetcher for %@", dataTask);
+ [self removeTaskFromMap:dataTask];
+ if (fetcher) {
+ GTMSESSION_ASSERT_DEBUG([fetcher isKindOfClass:[GTMSessionFetcher class]],
+ @"Expecting GTMSessionFetcher");
+ [self setFetcher:(GTMSessionFetcher *)fetcher forTask:downloadTask];
+ }
+
+ [fetcher URLSession:session
+ dataTask:dataTask
+didBecomeDownloadTask:downloadTask];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+ didReceiveData:(NSData *)data {
+ id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
+ [fetcher URLSession:session
+ dataTask:dataTask
+ didReceiveData:data];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+ willCacheResponse:(NSCachedURLResponse *)proposedResponse
+ completionHandler:(void (^)(NSCachedURLResponse *))handler {
+ id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
+ [fetcher URLSession:session
+ dataTask:dataTask
+ willCacheResponse:proposedResponse
+ completionHandler:handler];
+}
+
+// NSURLSessionDownloadDelegate protocol methods.
+
+- (void)URLSession:(NSURLSession *)session
+ downloadTask:(NSURLSessionDownloadTask *)downloadTask
+didFinishDownloadingToURL:(NSURL *)location {
+ id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask];
+ [fetcher URLSession:session
+ downloadTask:downloadTask
+didFinishDownloadingToURL:location];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ downloadTask:(NSURLSessionDownloadTask *)downloadTask
+ didWriteData:(int64_t)bytesWritten
+ totalBytesWritten:(int64_t)totalWritten
+totalBytesExpectedToWrite:(int64_t)totalExpected {
+ id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask];
+ [fetcher URLSession:session
+ downloadTask:downloadTask
+ didWriteData:bytesWritten
+ totalBytesWritten:totalWritten
+totalBytesExpectedToWrite:totalExpected];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ downloadTask:(NSURLSessionDownloadTask *)downloadTask
+ didResumeAtOffset:(int64_t)fileOffset
+expectedTotalBytes:(int64_t)expectedTotalBytes {
+ id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask];
+ [fetcher URLSession:session
+ downloadTask:downloadTask
+ didResumeAtOffset:fileOffset
+ expectedTotalBytes:expectedTotalBytes];
+}
+
+@end
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.h b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.h
new file mode 100644
index 00000000..2f9023a9
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.h
@@ -0,0 +1,175 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// GTMSessionUploadFetcher implements Google's resumable upload protocol.
+
+//
+// This subclass of GTMSessionFetcher simulates the series of fetches
+// needed for chunked upload as a single fetch operation.
+//
+// Protocol document: TBD
+//
+// To the client, the only fetcher that exists is this class; the subsidiary
+// fetchers needed for uploading chunks are not visible (though the most recent
+// chunk fetcher may be accessed via the -activeFetcher or -chunkFetcher methods, and
+// -responseHeaders and -statusCode reflect results from the most recent chunk
+// fetcher.)
+//
+// Chunk fetchers are discarded as soon as they have completed.
+//
+// The protocol also allows for a cancellation notification request to be sent to the
+// server to allow discarding of the currently uploaded data and this will be sent
+// automatically upon calling stopFetching if the upload has already started.
+//
+// Note: Unlike the fetcher superclass, the methods of GTMSessionUploadFetcher should
+// only be used from the main thread until further work is done to make this subclass
+// thread-safe.
+
+#import "GTMSessionFetcher.h"
+#import "GTMSessionFetcherService.h"
+
+GTM_ASSUME_NONNULL_BEGIN
+
+// The value to use for file size parameters when the file size is not yet known.
+extern int64_t const kGTMSessionUploadFetcherUnknownFileSize;
+
+// Unless an application knows it needs a smaller chunk size, it should use the standard
+// chunk size, which sends the entire file as a single chunk to minimize upload overhead.
+// Setting an explicit chunk size that comfortably fits in memory is advisable for large
+// uploads.
+extern int64_t const kGTMSessionUploadFetcherStandardChunkSize;
+
+// When uploading requires data buffer allocations (such as uploading from an NSData or
+// an NSFileHandle) this is the maximum buffer size that will be created by the fetcher.
+extern int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize;
+
+// Notification that the upload location URL was provided by the server.
+extern NSString *const kGTMSessionFetcherUploadLocationObtainedNotification;
+
+// Block to provide data during uploads.
+//
+// Response data may be allocated with dataWithBytesNoCopy:length:freeWhenDone: for efficiency,
+// and released after the response block returns.
+//
+// If the length of the file being uploaded is unknown or already set, send
+// kGTMSessionUploadFetcherUnknownFileSize for |fullUploadLength|. Otherwise, set |fullUploadLength|
+// to its proper value.
+//
+// Pass nil as the data (and optionally an NSError) for a failure.
+typedef void (^GTMSessionUploadFetcherDataProviderResponse)(NSData * GTM_NULLABLE_TYPE data,
+ int64_t fullUploadLength,
+ NSError * GTM_NULLABLE_TYPE error);
+// Do not call the response with an NSData object with less data than the requested length unless
+// you are passing the fullUploadLength to the fetcher for the first time and it is the last chunk
+// of data in the file being uploaded.
+typedef void (^GTMSessionUploadFetcherDataProvider)(int64_t offset, int64_t length,
+ GTMSessionUploadFetcherDataProviderResponse response);
+
+// Block to be notified about the final status of the cancellation request started in stopFetching.
+//
+// |fetcher| will be the cancel request that was sent to the server, or nil if stopFetching is not
+// going to send a cancel request. If |fetcher| is provided, the other parameters correspond to the
+// completion handler of the cancellation request fetcher.
+typedef void (^GTMSessionUploadFetcherCancellationHandler)(
+ GTMSessionFetcher * GTM_NULLABLE_TYPE fetcher,
+ NSData * GTM_NULLABLE_TYPE data,
+ NSError * GTM_NULLABLE_TYPE error);
+
+@interface GTMSessionUploadFetcher : GTMSessionFetcher
+
+// Create an upload fetcher specifying either the request or the resume location URL,
+// then set an upload data source using one of these:
+//
+// setUploadFileURL:
+// setUploadDataLength:provider:
+// setUploadFileHandle:
+// setUploadData:
+
++ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
+ uploadMIMEType:(NSString *)uploadMIMEType
+ chunkSize:(int64_t)chunkSize
+ fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil;
+
+// Allows cellular access.
++ (instancetype)uploadFetcherWithLocation:(NSURL * GTM_NULLABLE_TYPE)uploadLocationURL
+ uploadMIMEType:(NSString *)uploadMIMEType
+ chunkSize:(int64_t)chunkSize
+ fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil;
+
++ (instancetype)uploadFetcherWithLocation:(NSURL *GTM_NULLABLE_TYPE)uploadLocationURL
+ uploadMIMEType:(NSString *)uploadMIMEType
+ chunkSize:(int64_t)chunkSize
+ allowsCellularAccess:(BOOL)allowsCellularAccess
+ fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil;
+
+// Allows dataProviders for files of unknown length. Pass kGTMSessionUploadFetcherUnknownFileSize as
+// |fullLength| if the length is unknown.
+- (void)setUploadDataLength:(int64_t)fullLength
+ provider:(GTM_NULLABLE GTMSessionUploadFetcherDataProvider)block;
+
++ (NSArray *)uploadFetchersForBackgroundSessions;
++ (GTM_NULLABLE instancetype)uploadFetcherForSessionIdentifier:(NSString *)sessionIdentifier;
+
+- (void)pauseFetching;
+- (void)resumeFetching;
+- (BOOL)isPaused;
+
+@property(atomic, strong, GTM_NULLABLE) NSURL *uploadLocationURL;
+@property(atomic, strong, GTM_NULLABLE) NSData *uploadData;
+@property(atomic, strong, GTM_NULLABLE) NSURL *uploadFileURL;
+@property(atomic, strong, GTM_NULLABLE) NSFileHandle *uploadFileHandle;
+@property(atomic, copy, readonly, GTM_NULLABLE) GTMSessionUploadFetcherDataProvider uploadDataProvider;
+@property(atomic, copy) NSString *uploadMIMEType;
+@property(atomic, readonly, assign) int64_t chunkSize;
+@property(atomic, readonly, assign) int64_t currentOffset;
+// Reflects the original NSURLRequest's @c allowCellularAccess property.
+@property(atomic, readonly, assign) BOOL allowsCellularAccess;
+
+// The fetcher for the current data chunk, if any
+@property(atomic, strong, GTM_NULLABLE) GTMSessionFetcher *chunkFetcher;
+
+// The active fetcher is the current chunk fetcher, or the upload fetcher itself
+// if no chunk fetcher has yet been created.
+@property(atomic, readonly) GTMSessionFetcher *activeFetcher;
+
+// The last request made by an active fetcher. Useful for testing.
+@property(atomic, readonly, GTM_NULLABLE) NSURLRequest *lastChunkRequest;
+
+// The status code from the most recently-completed fetch.
+@property(atomic, assign) NSInteger statusCode;
+
+// Invoked as part of the stop fetching process. Invoked immediately if there is no upload in
+// progress, otherwise invoked with the results of the attempt to notify the server that the
+// upload will not continue.
+//
+// Unlike other callbacks, since this is related specifically to the stopFetching flow it is not
+// cleared by stopFetching. It will instead clear itself after it is invoked or if the completion
+// has occured before stopFetching is called.
+@property(atomic, copy, GTM_NULLABLE) GTMSessionUploadFetcherCancellationHandler
+ cancellationHandler;
+
+// Exposed for testing only.
+@property(atomic, readonly, GTM_NULLABLE) dispatch_queue_t delegateCallbackQueue;
+@property(atomic, readonly, GTM_NULLABLE) GTMSessionFetcherCompletionHandler delegateCompletionHandler;
+
+@end
+
+@interface GTMSessionFetcher (GTMSessionUploadFetcherMethods)
+
+@property(readonly, GTM_NULLABLE) GTMSessionUploadFetcher *parentUploadFetcher;
+
+@end
+
+GTM_ASSUME_NONNULL_END
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.m b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.m
new file mode 100644
index 00000000..7759bb15
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.m
@@ -0,0 +1,1989 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "GTMSessionUploadFetcher.h"
+
+static NSString *const kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey = @"_upChunk";
+static NSString *const kGTMSessionIdentifierUploadFileURLMetadataKey = @"_upFileURL";
+static NSString *const kGTMSessionIdentifierUploadFileLengthMetadataKey = @"_upFileLen";
+static NSString *const kGTMSessionIdentifierUploadLocationURLMetadataKey = @"_upLocURL";
+static NSString *const kGTMSessionIdentifierUploadMIMETypeMetadataKey = @"_uploadMIME";
+static NSString *const kGTMSessionIdentifierUploadChunkSizeMetadataKey = @"_upChSize";
+static NSString *const kGTMSessionIdentifierUploadCurrentOffsetMetadataKey = @"_upOffset";
+static NSString *const kGTMSessionIdentifierUploadAllowsCellularAccess = @"_upAllowsCellularAccess";
+
+static NSString *const kGTMSessionHeaderXGoogUploadChunkGranularity = @"X-Goog-Upload-Chunk-Granularity";
+static NSString *const kGTMSessionHeaderXGoogUploadCommand = @"X-Goog-Upload-Command";
+static NSString *const kGTMSessionHeaderXGoogUploadContentLength = @"X-Goog-Upload-Content-Length";
+static NSString *const kGTMSessionHeaderXGoogUploadContentType = @"X-Goog-Upload-Content-Type";
+static NSString *const kGTMSessionHeaderXGoogUploadOffset = @"X-Goog-Upload-Offset";
+static NSString *const kGTMSessionHeaderXGoogUploadProtocol = @"X-Goog-Upload-Protocol";
+static NSString *const kGTMSessionXGoogUploadProtocolResumable = @"resumable";
+static NSString *const kGTMSessionHeaderXGoogUploadSizeReceived = @"X-Goog-Upload-Size-Received";
+static NSString *const kGTMSessionHeaderXGoogUploadStatus = @"X-Goog-Upload-Status";
+static NSString *const kGTMSessionHeaderXGoogUploadURL = @"X-Goog-Upload-URL";
+
+// Property of chunk fetchers identifying the parent upload fetcher. Non-retained NSValue.
+static NSString *const kGTMSessionUploadFetcherChunkParentKey = @"_uploadFetcherChunkParent";
+
+int64_t const kGTMSessionUploadFetcherUnknownFileSize = -1;
+
+int64_t const kGTMSessionUploadFetcherStandardChunkSize = (int64_t)LLONG_MAX;
+
+#if TARGET_OS_IPHONE
+int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize = 10 * 1024 * 1024; // 10 MB for iOS, watchOS, tvOS
+#else
+int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize = 100 * 1024 * 1024; // 100 MB for macOS
+#endif
+
+typedef NS_ENUM(NSUInteger, GTMSessionUploadFetcherStatus) {
+ kStatusUnknown,
+ kStatusActive,
+ kStatusFinal,
+ kStatusCancelled,
+};
+
+NSString *const kGTMSessionFetcherUploadLocationObtainedNotification =
+ @"kGTMSessionFetcherUploadLocationObtainedNotification";
+
+#if !GTMSESSION_BUILD_COMBINED_SOURCES
+@interface GTMSessionFetcher (ProtectedMethods)
+
+// Access to non-public method on the parent fetcher class.
+- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks;
+- (void)createSessionIdentifierWithMetadata:(NSDictionary *)metadata;
+- (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(id)target
+ didFinishSelector:(SEL)finishedSelector;
+- (void)invokeOnCallbackQueue:(dispatch_queue_t)callbackQueue
+ afterUserStopped:(BOOL)afterStopped
+ block:(void (^)(void))block;
+- (NSTimer *)retryTimer;
+- (void)beginFetchForRetry;
+
+@property(readwrite, strong) NSData *downloadedData;
+- (void)releaseCallbacks;
+
+- (NSInteger)statusCodeUnsynchronized;
+
+- (BOOL)userStoppedFetching;
+
+@end
+#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
+
+@interface GTMSessionUploadFetcher ()
+
+// Changing readonly to readwrite.
+@property(atomic, strong, readwrite) NSURLRequest *lastChunkRequest;
+@property(atomic, readwrite, assign) int64_t currentOffset;
+
+// Internal properties.
+@property(strong, atomic, GTM_NULLABLE) GTMSessionFetcher *fetcherInFlight; // Synchronized on self.
+
+@property(assign, atomic, getter=isSubdataGenerating) BOOL subdataGenerating;
+@property(assign, atomic) BOOL shouldInitiateOffsetQuery;
+@property(assign, atomic) int64_t uploadGranularity;
+@property(assign, atomic) BOOL allowsCellularAccess;
+
+@end
+
+@implementation GTMSessionUploadFetcher {
+ GTMSessionFetcher *_chunkFetcher;
+
+ // We'll call through to the delegate's completion handler.
+ GTMSessionFetcherCompletionHandler _delegateCompletionHandler;
+ dispatch_queue_t _delegateCallbackQueue;
+
+ // The initial fetch's body length and bytes actually sent are
+ // needed for calculating progress during subsequent chunk uploads
+ int64_t _initialBodyLength;
+ int64_t _initialBodySent;
+
+ // The upload server address for the chunks of this upload session.
+ NSURL *_uploadLocationURL;
+
+ // _uploadData, _uploadDataProvider, or _uploadFileHandle may be set, but only one.
+ NSData *_uploadData;
+ NSFileHandle *_uploadFileHandle;
+ GTMSessionUploadFetcherDataProvider _uploadDataProvider;
+ NSURL *_uploadFileURL;
+ int64_t _uploadFileLength;
+ NSString *_uploadMIMEType;
+ int64_t _chunkSize;
+ int64_t _uploadGranularity;
+ BOOL _isPaused;
+ BOOL _isRestartedUpload;
+ BOOL _shouldInitiateOffsetQuery;
+
+ // Tied to useBackgroundSession property, since this property is applicable to chunk fetchers.
+ BOOL _useBackgroundSessionOnChunkFetchers;
+
+ // We keep the latest offset into the upload data just for progress reporting.
+ int64_t _currentOffset;
+
+ NSDictionary *_recentChunkReponseHeaders;
+ NSInteger _recentChunkStatusCode;
+
+ // For waiting, we need to know the fetcher in flight, if any, and if subdata generation
+ // is in progress.
+ GTMSessionFetcher *_fetcherInFlight;
+ BOOL _isSubdataGenerating;
+ BOOL _isCancelInFlight;
+
+ GTMSessionUploadFetcherCancellationHandler _cancellationHandler;
+}
+
++ (void)load {
+ [self uploadFetchersForBackgroundSessions];
+}
+
++ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
+ uploadMIMEType:(NSString *)uploadMIMEType
+ chunkSize:(int64_t)chunkSize
+ fetcherService:(GTMSessionFetcherService *)fetcherService {
+ GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:request
+ fetcherService:fetcherService];
+ [fetcher setLocationURL:nil
+ uploadMIMEType:uploadMIMEType
+ chunkSize:chunkSize
+ allowsCellularAccess:request.allowsCellularAccess];
+ return fetcher;
+}
+
++ (instancetype)uploadFetcherWithLocation:(NSURL *GTM_NULLABLE_TYPE)uploadLocationURL
+ uploadMIMEType:(NSString *)uploadMIMEType
+ chunkSize:(int64_t)chunkSize
+ fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil {
+ return [self uploadFetcherWithLocation:uploadLocationURL
+ uploadMIMEType:uploadMIMEType
+ chunkSize:chunkSize
+ allowsCellularAccess:YES
+ fetcherService:fetcherServiceOrNil];
+}
+
++ (instancetype)uploadFetcherWithLocation:(NSURL *GTM_NULLABLE_TYPE)uploadLocationURL
+ uploadMIMEType:(NSString *)uploadMIMEType
+ chunkSize:(int64_t)chunkSize
+ allowsCellularAccess:(BOOL)allowsCellularAccess
+ fetcherService:(GTMSessionFetcherService *)fetcherService {
+ GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:nil
+ fetcherService:fetcherService];
+ [fetcher setLocationURL:uploadLocationURL
+ uploadMIMEType:uploadMIMEType
+ chunkSize:chunkSize
+ allowsCellularAccess:allowsCellularAccess];
+ return fetcher;
+}
+
++ (instancetype)uploadFetcherForSessionIdentifierMetadata:(NSDictionary *)metadata {
+ GTMSESSION_ASSERT_DEBUG(
+ [metadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] boolValue],
+ @"Session identifier metadata is not for an upload fetcher: %@", metadata);
+
+ NSNumber *uploadFileLengthNum = metadata[kGTMSessionIdentifierUploadFileLengthMetadataKey];
+ GTMSESSION_ASSERT_DEBUG(uploadFileLengthNum != nil,
+ @"Session metadata missing an UploadFileSize");
+ if (uploadFileLengthNum == nil) return nil;
+
+ int64_t uploadFileLength = [uploadFileLengthNum longLongValue];
+ GTMSESSION_ASSERT_DEBUG(uploadFileLength >= 0, @"Session metadata UploadFileSize is unknown");
+
+ NSString *uploadFileURLString = metadata[kGTMSessionIdentifierUploadFileURLMetadataKey];
+ GTMSESSION_ASSERT_DEBUG(uploadFileURLString, @"Session metadata missing an UploadFileURL");
+ if (uploadFileURLString == nil) return nil;
+
+ NSURL *uploadFileURL = [NSURL URLWithString:uploadFileURLString];
+ // There used to be a call here to NSURL checkResourceIsReachableAndReturnError: to check for the
+ // existence of the file (also tried NSFileManager fileExistsAtPath:). We've determined
+ // empirically that the check can fail at startup even when the upload file does in fact exist.
+ // For now, we'll go ahead and restore the background upload fetcher. If the file doesn't exist,
+ // it will fail later.
+
+ NSString *uploadLocationURLString = metadata[kGTMSessionIdentifierUploadLocationURLMetadataKey];
+ NSURL *uploadLocationURL =
+ uploadLocationURLString ? [NSURL URLWithString:uploadLocationURLString] : nil;
+
+ NSString *uploadMIMEType =
+ metadata[kGTMSessionIdentifierUploadMIMETypeMetadataKey];
+ int64_t uploadChunkSize =
+ [metadata[kGTMSessionIdentifierUploadChunkSizeMetadataKey] longLongValue];
+ if (uploadChunkSize <= 0) {
+ uploadChunkSize = kGTMSessionUploadFetcherStandardChunkSize;
+ }
+ int64_t currentOffset =
+ [metadata[kGTMSessionIdentifierUploadCurrentOffsetMetadataKey] longLongValue];
+
+ BOOL allowsCellularAccess = YES;
+ if (metadata[kGTMSessionIdentifierUploadAllowsCellularAccess]) {
+ allowsCellularAccess = [metadata[kGTMSessionIdentifierUploadAllowsCellularAccess] boolValue];
+ }
+
+ GTMSESSION_ASSERT_DEBUG(currentOffset <= uploadFileLength,
+ @"CurrentOffset (%lld) exceeds UploadFileSize (%lld)",
+ currentOffset, uploadFileLength);
+ if (currentOffset > uploadFileLength) return nil;
+
+ GTMSessionUploadFetcher *uploadFetcher = [self uploadFetcherWithLocation:uploadLocationURL
+ uploadMIMEType:uploadMIMEType
+ chunkSize:uploadChunkSize
+ allowsCellularAccess:allowsCellularAccess
+ fetcherService:nil];
+ // Set the upload file length before setting the upload file URL tries to determine the length.
+ [uploadFetcher setUploadFileLength:uploadFileLength];
+
+ uploadFetcher.uploadFileURL = uploadFileURL;
+ uploadFetcher.sessionUserInfo = metadata;
+ uploadFetcher.useBackgroundSession = YES;
+ uploadFetcher.currentOffset = currentOffset;
+ uploadFetcher.delegateCallbackQueue = uploadFetcher.callbackQueue;
+ uploadFetcher.allowedInsecureSchemes = @[ @"http" ]; // Allowed on restored upload fetcher.
+ return uploadFetcher;
+}
+
++ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
+ fetcherService:(GTMSessionFetcherService *)fetcherService {
+ // Internal utility method for instantiating fetchers
+ GTMSessionUploadFetcher *fetcher;
+ if ([fetcherService isKindOfClass:[GTMSessionFetcherService class]]) {
+ fetcher = [fetcherService fetcherWithRequest:request
+ fetcherClass:self];
+ } else {
+ fetcher = [self fetcherWithRequest:request];
+ }
+ fetcher.useBackgroundSession = YES;
+ return fetcher;
+}
+
++ (NSPointerArray *)uploadFetcherPointerArrayForBackgroundSessions {
+ static NSPointerArray *gUploadFetcherPointerArrayForBackgroundSessions = nil;
+
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ gUploadFetcherPointerArrayForBackgroundSessions = [NSPointerArray weakObjectsPointerArray];
+ });
+ return gUploadFetcherPointerArrayForBackgroundSessions;
+}
+
++ (instancetype)uploadFetcherForSessionIdentifier:(NSString *)sessionIdentifier {
+ GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
+ NSArray *uploadFetchersForBackgroundSessions = [self uploadFetchersForBackgroundSessions];
+ for (GTMSessionUploadFetcher *uploadFetcher in uploadFetchersForBackgroundSessions) {
+ if ([uploadFetcher.chunkFetcher.sessionIdentifier isEqual:sessionIdentifier]) {
+ return uploadFetcher;
+ }
+ }
+ return nil;
+}
+
++ (NSArray *)uploadFetchersForBackgroundSessions {
+ NSMutableSet *restoredSessionIdentifiers = [[NSMutableSet alloc] init];
+ NSMutableArray *uploadFetchers = [[NSMutableArray alloc] init];
+ NSPointerArray *uploadFetcherPointerArray = [self uploadFetcherPointerArrayForBackgroundSessions];
+
+ // Collect the background session upload fetchers that are still in memory.
+ @synchronized(uploadFetcherPointerArray) {
+ [uploadFetcherPointerArray compact];
+ for (GTMSessionUploadFetcher *uploadFetcher in uploadFetcherPointerArray) {
+ NSString *sessionIdentifier = uploadFetcher.chunkFetcher.sessionIdentifier;
+ if (sessionIdentifier) {
+ [restoredSessionIdentifiers addObject:sessionIdentifier];
+ [uploadFetchers addObject:uploadFetcher];
+ }
+ }
+ } // @synchronized(uploadFetcherPointerArray)
+
+ // The system may have other ongoing background upload sessions. Restore upload fetchers for those
+ // too.
+ NSArray *fetchers = [GTMSessionFetcher fetchersForBackgroundSessions];
+ for (GTMSessionFetcher *fetcher in fetchers) {
+ NSString *sessionIdentifier = fetcher.sessionIdentifier;
+ if (!sessionIdentifier || [restoredSessionIdentifiers containsObject:sessionIdentifier]) {
+ continue;
+ }
+ NSDictionary *sessionIdentifierMetadata = [fetcher sessionIdentifierMetadata];
+ if (sessionIdentifierMetadata == nil) {
+ continue;
+ }
+ if (![sessionIdentifierMetadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] boolValue]) {
+ continue;
+ }
+ GTMSessionUploadFetcher *uploadFetcher =
+ [self uploadFetcherForSessionIdentifierMetadata:sessionIdentifierMetadata];
+ if (uploadFetcher == nil) {
+ // Something went wrong with this upload fetcher, so kill the restored chunk fetcher.
+ [fetcher stopFetching];
+ continue;
+ }
+ [uploadFetchers addObject:uploadFetcher];
+ uploadFetcher->_chunkFetcher = fetcher;
+ uploadFetcher->_fetcherInFlight = fetcher;
+ [uploadFetcher attachSendProgressBlockToChunkFetcher:fetcher];
+ fetcher.completionHandler =
+ [fetcher completionHandlerWithTarget:uploadFetcher
+ didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
+
+ GTMSESSION_LOG_DEBUG(@"%@ restoring upload fetcher %@ for chunk fetcher %@",
+ [self class], uploadFetcher, fetcher);
+ }
+ return uploadFetchers;
+}
+
+- (void)setUploadData:(NSData *)data {
+ BOOL changed = NO;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_uploadData != data) {
+ _uploadData = data;
+ changed = YES;
+ }
+ }
+ if (changed) {
+ [self setupRequestHeaders];
+ }
+}
+
+- (NSData *)uploadData {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _uploadData;
+ }
+}
+
+- (void)setUploadFileHandle:(NSFileHandle *)fh {
+ BOOL changed = NO;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_uploadFileHandle != fh) {
+ _uploadFileHandle = fh;
+ changed = YES;
+ }
+ }
+ if (changed) {
+ [self setupRequestHeaders];
+ }
+}
+
+- (NSFileHandle *)uploadFileHandle {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _uploadFileHandle;
+ }
+}
+
+- (void)setUploadFileURL:(NSURL *)uploadURL {
+ BOOL changed = NO;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_uploadFileURL != uploadURL) {
+ _uploadFileURL = uploadURL;
+ changed = YES;
+ }
+ }
+ if (changed) {
+ [self setupRequestHeaders];
+ }
+}
+
+- (NSURL *)uploadFileURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _uploadFileURL;
+ }
+}
+
+- (void)setUploadFileLength:(int64_t)fullLength {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_uploadFileLength == kGTMSessionUploadFetcherUnknownFileSize &&
+ fullLength != kGTMSessionUploadFetcherUnknownFileSize) {
+ _uploadFileLength = fullLength;
+ }
+ }
+}
+
+- (void)setUploadDataLength:(int64_t)fullLength
+ provider:(GTMSessionUploadFetcherDataProvider)block {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _uploadDataProvider = [block copy];
+ _uploadFileLength = fullLength;
+ }
+ [self setupRequestHeaders];
+}
+
+- (GTMSessionUploadFetcherDataProvider)uploadDataProvider {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _uploadDataProvider;
+ }
+}
+
+
+- (void)setUploadMIMEType:(NSString *)uploadMIMEType {
+ GTMSESSION_ASSERT_DEBUG(0, @"TODO: disallow setUploadMIMEType by making declaration readonly");
+ // (and uploadMIMEType, chunksize, currentOffset)
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _uploadMIMEType = uploadMIMEType;
+ }
+}
+
+- (NSString *)uploadMIMEType {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _uploadMIMEType;
+ }
+}
+
+- (int64_t)chunkSize {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _chunkSize;
+ }
+}
+
+- (void)setupRequestHeaders {
+ GTMSessionCheckNotSynchronized(self);
+
+#if DEBUG
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ int hasData = (_uploadData != nil) ? 1 : 0;
+ int hasFileHandle = (_uploadFileHandle != nil) ? 1 : 0;
+ int hasFileURL = (_uploadFileURL != nil) ? 1 : 0;
+ int hasUploadDataProvider = (_uploadDataProvider != nil) ? 1 : 0;
+ int numberOfSources = hasData + hasFileHandle + hasFileURL + hasUploadDataProvider;
+ #pragma unused(numberOfSources)
+ GTMSESSION_ASSERT_DEBUG(numberOfSources == 1,
+ @"Need just one upload source (%d)", numberOfSources);
+ } // @synchronized(self)
+#endif
+
+ // Add our custom headers to the initial request indicating the data
+ // type and total size to be delivered later in the chunk requests.
+ NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
+
+ GTMSESSION_ASSERT_DEBUG((mutableRequest == nil) != (_uploadLocationURL == nil),
+ @"Request and location are mutually exclusive");
+ if (!mutableRequest) return;
+
+ [mutableRequest setValue:kGTMSessionXGoogUploadProtocolResumable
+ forHTTPHeaderField:kGTMSessionHeaderXGoogUploadProtocol];
+ [mutableRequest setValue:@"start"
+ forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
+ [mutableRequest setValue:_uploadMIMEType
+ forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentType];
+ [mutableRequest setValue:@([self fullUploadLength]).stringValue
+ forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentLength];
+
+ NSString *method = mutableRequest.HTTPMethod;
+ if (method == nil || [method caseInsensitiveCompare:@"GET"] == NSOrderedSame) {
+ [mutableRequest setHTTPMethod:@"POST"];
+ }
+
+ // Ensure the user agent header identifies this to the upload server as a
+ // GTMSessionUploadFetcher client. The /1 can be incremented in the unlikely circumstance
+ // we need to make a bug fix in the client that the server can recognize.
+ NSString *const kUserAgentStub = @"(GTMSUF/1)";
+ NSString *userAgent = [mutableRequest valueForHTTPHeaderField:@"User-Agent"];
+ if (userAgent == nil
+ || [userAgent rangeOfString:kUserAgentStub].location == NSNotFound) {
+ if (userAgent.length == 0) {
+ userAgent = GTMFetcherStandardUserAgentString(nil);
+ }
+ userAgent = [userAgent stringByAppendingFormat:@" %@", kUserAgentStub];
+ [mutableRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
+ }
+ [self setRequest:mutableRequest];
+}
+
+- (void)setLocationURL:(NSURL *GTM_NULLABLE_TYPE)location
+ uploadMIMEType:(NSString *)uploadMIMEType
+ chunkSize:(int64_t)chunkSize
+ allowsCellularAccess:(BOOL)allowsCellularAccess {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSESSION_ASSERT_DEBUG(chunkSize > 0, @"chunk size is zero");
+
+ _allowsCellularAccess = allowsCellularAccess;
+
+ // When resuming an upload, set the known upload target URL.
+ _uploadLocationURL = location;
+
+ _uploadMIMEType = uploadMIMEType;
+ _chunkSize = chunkSize;
+
+ // Indicate that we've not yet determined the file handle's length
+ _uploadFileLength = kGTMSessionUploadFetcherUnknownFileSize;
+
+ // Indicate that we've not yet determined the upload fetcher status
+ _recentChunkStatusCode = -1;
+
+ // If this is restarting an upload begun by another fetcher,
+ // the location is specified but the request is nil
+ _isRestartedUpload = (location != nil);
+ } // @synchronized(self)
+}
+
+- (int64_t)fullUploadLength {
+ int64_t result;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_uploadData) {
+ result = (int64_t)_uploadData.length;
+ } else {
+ if (_uploadFileLength == kGTMSessionUploadFetcherUnknownFileSize) {
+ if (_uploadFileHandle) {
+ // First time through, seek to end to determine file length
+ _uploadFileLength = (int64_t)[_uploadFileHandle seekToEndOfFile];
+ } else if (_uploadDataProvider) {
+ // _uploadFileLength is set when the _uploadDataProvider is set.
+ GTMSESSION_ASSERT_DEBUG(_uploadFileLength >= 0, @"No uploadDataProvider length set");
+ } else {
+ NSNumber *filesizeNum;
+ NSError *valueError;
+ if ([_uploadFileURL getResourceValue:&filesizeNum
+ forKey:NSURLFileSizeKey
+ error:&valueError]) {
+ _uploadFileLength = filesizeNum.longLongValue;
+ } else {
+ GTMSESSION_ASSERT_DEBUG(NO, @"Cannot get file size: %@\n %@",
+ valueError, _uploadFileURL.path);
+ _uploadFileLength = 0;
+ }
+ }
+ }
+ result = _uploadFileLength;
+ }
+ } // @synchronized(self)
+ return result;
+}
+
+// Make a subdata of the upload data.
+- (void)generateChunkSubdataWithOffset:(int64_t)offset
+ length:(int64_t)length
+ response:(GTMSessionUploadFetcherDataProviderResponse)response {
+ GTMSessionUploadFetcherDataProvider uploadDataProvider = self.uploadDataProvider;
+ if (uploadDataProvider) {
+ uploadDataProvider(offset, length, response);
+ return;
+ }
+
+ NSData *uploadData = self.uploadData;
+ if (uploadData) {
+ // NSData provided.
+ NSData *resultData;
+ if (offset == 0 && length == (int64_t)uploadData.length) {
+ resultData = uploadData;
+ } else {
+ int64_t dataLength = (int64_t)uploadData.length;
+ // Ensure our range is valid. b/18007814
+ if (offset + length > dataLength) {
+ NSString *errorMessage = [NSString stringWithFormat:
+ @"Range invalid for upload data. offset: %lld\tlength: %lld\tdataLength: %lld",
+ offset, length, dataLength];
+ GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
+ response(nil,
+ kGTMSessionUploadFetcherUnknownFileSize,
+ [self uploadChunkUnavailableErrorWithDescription:errorMessage]);
+ return;
+ }
+ NSRange range = NSMakeRange((NSUInteger)offset, (NSUInteger)length);
+
+ @try {
+ resultData = [uploadData subdataWithRange:range];
+ }
+ @catch (NSException *exception) {
+ NSString *errorMessage = exception.description;
+ GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
+ response(nil,
+ kGTMSessionUploadFetcherUnknownFileSize,
+ [self uploadChunkUnavailableErrorWithDescription:errorMessage]);
+ return;
+ }
+ }
+ response(resultData, kGTMSessionUploadFetcherUnknownFileSize, nil);
+ return;
+ }
+ NSURL *uploadFileURL = self.uploadFileURL;
+ if (uploadFileURL) {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ [self generateChunkSubdataFromFileURL:uploadFileURL
+ offset:offset
+ length:length
+ response:response];
+ });
+ return;
+ }
+ GTMSESSION_ASSERT_DEBUG(_uploadFileHandle, @"Unexpectedly missing upload data package");
+ NSFileHandle *uploadFileHandle = self.uploadFileHandle;
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ [self generateChunkSubdataFromFileHandle:uploadFileHandle
+ offset:offset
+ length:length
+ response:response];
+ });
+}
+
+- (void)generateChunkSubdataFromFileHandle:(NSFileHandle *)fileHandle
+ offset:(int64_t)offset
+ length:(int64_t)length
+ response:(GTMSessionUploadFetcherDataProviderResponse)response {
+ NSData *resultData;
+ NSError *error;
+ @try {
+ [fileHandle seekToFileOffset:(unsigned long long)offset];
+ resultData = [fileHandle readDataOfLength:(NSUInteger)length];
+ }
+ @catch (NSException *exception) {
+ GTMSESSION_ASSERT_DEBUG(NO, @"uploadFileHandle failed to read, %@", exception);
+ error = [self uploadChunkUnavailableErrorWithDescription:exception.description];
+ }
+ // The response always re-dispatches to the main thread, so we skip doing that here.
+ response(resultData, kGTMSessionUploadFetcherUnknownFileSize, error);
+}
+
+- (void)generateChunkSubdataFromFileURL:(NSURL *)fileURL
+ offset:(int64_t)offset
+ length:(int64_t)length
+ response:(GTMSessionUploadFetcherDataProviderResponse)response {
+ GTMSessionCheckNotSynchronized(self);
+
+ NSData *resultData;
+ NSError *error;
+ int64_t fullUploadLength = [self fullUploadLength];
+ NSData *mappedData =
+ [NSData dataWithContentsOfURL:fileURL
+ options:NSDataReadingMappedAlways + NSDataReadingUncached
+ error:&error];
+ if (!mappedData) {
+ // We could not create an NSData by memory-mapping the file.
+#if TARGET_IPHONE_SIMULATOR
+ // NSTemporaryDirectory() can differ in the simulator between app restarts,
+ // yet the contents for the new path remains unchanged, so try the latest temp path.
+ if ([error.domain isEqual:NSCocoaErrorDomain] && (error.code == NSFileReadNoSuchFileError)) {
+ NSString *filename = [fileURL lastPathComponent];
+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
+ NSURL *newFileURL = [NSURL fileURLWithPath:filePath];
+ if (![newFileURL isEqual:fileURL]) {
+ [self generateChunkSubdataFromFileURL:newFileURL
+ offset:offset
+ length:length
+ response:response];
+ return;
+ }
+ }
+#endif
+
+ // If the file is just too large to create an NSData for, or if for some other reason we can't
+ // map it, create an NSFileHandle instead to read a subset into an NSData.
+#if DEBUG
+ NSNumber *fileSizeNum;
+ BOOL hasFileSize = [fileURL getResourceValue:&fileSizeNum forKey:NSURLFileSizeKey error:NULL];
+ GTMSESSION_LOG_DEBUG(@"Note: uploadFileURL is falling back to creating upload chunks by reading"
+ @" an NSFileHandle since uploadFileURL failed to map the upload file,"
+ @" file size %@, %@",
+ hasFileSize ? fileSizeNum : @"unknown", error);
+#endif
+
+ NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL
+ error:&error];
+ if (fileHandle != nil) {
+ [self generateChunkSubdataFromFileHandle:fileHandle
+ offset:offset
+ length:length
+ response:response];
+ return;
+ }
+ GTMSESSION_ASSERT_DEBUG(NO, @"uploadFileURL failed to read, %@", error);
+ // Fall through with the error.
+ } else {
+ // Successfully created an NSData by memory-mapping the file.
+ if ((NSUInteger)(offset + length) > mappedData.length) {
+ NSString *errorMessage = [NSString stringWithFormat:
+ @"Range invalid for upload data. offset: %lld\tlength: %lld\tdataLength: %lld\texpected UploadLength: %lld",
+ offset, length, (long long)mappedData.length, fullUploadLength];
+ GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
+ response(nil,
+ kGTMSessionUploadFetcherUnknownFileSize,
+ [self uploadChunkUnavailableErrorWithDescription:errorMessage]);
+ return;
+ }
+ if (offset > 0 || length < fullUploadLength) {
+ NSRange range = NSMakeRange((NSUInteger)offset, (NSUInteger)length);
+ resultData = [mappedData subdataWithRange:range];
+ } else {
+ resultData = mappedData;
+ }
+ }
+ // The response always re-dispatches to the main thread, so we skip re-dispatching here.
+ response(resultData, kGTMSessionUploadFetcherUnknownFileSize, error);
+}
+
+- (NSError *)uploadChunkUnavailableErrorWithDescription:(NSString *)description {
+ // The description in the userInfo is intended as a clue to programmers, not
+ // for client code to examine or rely on.
+ NSDictionary *userInfo = @{ @"description" : description };
+ return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
+ code:GTMSessionFetcherErrorUploadChunkUnavailable
+ userInfo:userInfo];
+}
+
+- (NSError *)prematureFailureErrorWithUserInfo:(NSDictionary *)userInfo {
+ // An error for if we get an unexpected status from the upload server or
+ // otherwise cannot continue. This is an issue beyond the upload protocol;
+ // there's no way the client can do anything useful except give up.
+ NSError *error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
+ code:501 // Not implemented
+ userInfo:userInfo];
+ return error;
+}
+
++ (GTMSessionUploadFetcherStatus)uploadStatusFromResponseHeaders:(NSDictionary *)responseHeaders {
+ NSString *statusString = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus];
+ if ([statusString isEqual:@"active"]) {
+ return kStatusActive;
+ }
+ if ([statusString isEqual:@"final"]) {
+ return kStatusFinal;
+ }
+ if ([statusString isEqual:@"cancelled"]) {
+ return kStatusCancelled;
+ }
+ return kStatusUnknown;
+}
+
+#pragma mark Method overrides affecting the initial fetch only
+
+- (void)setCompletionHandler:(GTMSessionFetcherCompletionHandler)handler {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _delegateCompletionHandler = handler;
+ }
+}
+
+- (void)setDelegateCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _delegateCallbackQueue = queue;
+ }
+}
+
+- (dispatch_queue_t GTM_NULLABLE_TYPE)delegateCallbackQueue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _delegateCallbackQueue;
+ }
+}
+
+- (BOOL)isRestartedUpload {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _isRestartedUpload;
+ }
+}
+
+- (GTMSessionFetcher * GTM_NULLABLE_TYPE)chunkFetcher {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _chunkFetcher;
+ }
+}
+
+- (void)setChunkFetcher:(GTMSessionFetcher * GTM_NULLABLE_TYPE)fetcher {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _chunkFetcher = fetcher;
+ }
+}
+
+- (void)setFetcherInFlight:(GTMSessionFetcher * GTM_NULLABLE_TYPE)fetcher {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _fetcherInFlight = fetcher;
+ }
+}
+
+- (GTMSessionFetcher * GTM_NULLABLE_TYPE)fetcherInFlight {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _fetcherInFlight;
+ }
+}
+
+- (void)setCancellationHandler:(GTMSessionUploadFetcherCancellationHandler GTM_NULLABLE_TYPE)
+ cancellationHandler {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _cancellationHandler = cancellationHandler;
+ }
+}
+
+- (GTMSessionUploadFetcherCancellationHandler GTM_NULLABLE_TYPE)cancellationHandler {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _cancellationHandler;
+ }
+}
+
+- (void)beginFetchForRetry {
+ GTMSessionCheckNotSynchronized(self);
+
+ // Override the superclass to reset the initial body length and fetcher-in-flight,
+ // then call the superclass implementation.
+ [self setInitialBodyLength:[self bodyLength]];
+
+ GTMSESSION_ASSERT_DEBUG(self.fetcherInFlight == nil, @"unexpected fetcher in flight: %@",
+ self.fetcherInFlight);
+ self.fetcherInFlight = self;
+ [super beginFetchForRetry];
+}
+
+- (void)beginFetchWithCompletionHandler:(GTMSessionFetcherCompletionHandler)handler {
+ GTMSessionCheckNotSynchronized(self);
+
+ [self setInitialBodyLength:[self bodyLength]];
+
+ // We'll hold onto the superclass's callback queue so we can invoke the handler
+ // even after the superclass has released the queue and its callback handler, as
+ // happens during auth failure.
+ [self setDelegateCallbackQueue:self.callbackQueue];
+ self.completionHandler = handler;
+
+ if ([self isRestartedUpload]) {
+ // When restarting an upload, we know the destination location for chunk fetches,
+ // but we need to query to find the initial offset.
+ if (![self isPaused]) {
+ [self sendQueryForUploadOffsetWithFetcherProperties:self.properties];
+ }
+ return;
+ }
+ // We don't want to call into the client's completion block immediately
+ // after the finish of the initial connection (the delegate is called only
+ // when uploading finishes), so we substitute our own completion block to be
+ // called when the initial connection finishes
+ GTMSESSION_ASSERT_DEBUG(self.fetcherInFlight == nil, @"unexpected fetcher in flight: %@",
+ self.fetcherInFlight);
+
+ self.fetcherInFlight = self;
+ [super beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+ self.fetcherInFlight = nil;
+ // callback
+
+ BOOL hasTestBlock = (self.testBlock != nil);
+ if (![self isRestartedUpload] && !hasTestBlock) {
+ if (error == nil) {
+ [self beginChunkFetches];
+ } else {
+ if ([self retryTimer] == nil) {
+ [self invokeFinalCallbackWithData:nil
+ error:error
+ shouldInvalidateLocation:YES];
+ }
+ }
+ } else {
+ // If there was no initial request, then this fetch is resuming some
+ // other uploadFetcher's initial request, and the superclass's connection
+ // is never used, so at this point we call the user's actual completion
+ // block.
+ if (!hasTestBlock) {
+ [self invokeFinalCallbackWithData:data
+ error:error
+ shouldInvalidateLocation:YES];
+ } else {
+ // There was a test block, so we won't do chunk fetches, but we simulate obtaining
+ // the data to be uploaded from the upload data provider block or the file handle,
+ // and then call back.
+ [self generateChunkSubdataWithOffset:0
+ length:[self fullUploadLength]
+ response:^(NSData *generateData, int64_t fullUploadLength, NSError *generateError) {
+ [self invokeFinalCallbackWithData:data
+ error:error
+ shouldInvalidateLocation:YES];
+ }];
+ }
+ }
+ }];
+}
+
+- (void)beginChunkFetches {
+ GTMSessionCheckNotSynchronized(self);
+
+#if DEBUG
+ // The initial response of the resumable upload protocol should have an
+ // empty body
+ //
+ // This assert typically happens because the upload create/edit link URL was
+ // not supplied with the request, and the server is thus expecting a non-
+ // resumable request/response.
+ if (self.downloadedData.length > 0) {
+ NSData *downloadedData = self.downloadedData;
+ NSString *str = [[NSString alloc] initWithData:downloadedData
+ encoding:NSUTF8StringEncoding];
+ #pragma unused(str)
+ GTMSESSION_ASSERT_DEBUG(NO, @"unexpected response data (uploading to the wrong URL?)\n%@", str);
+ }
+#endif
+
+ // We need to get the upload URL from the location header to continue.
+ NSDictionary *responseHeaders = [self responseHeaders];
+
+ [self retrieveUploadChunkGranularityFromResponseHeaders:responseHeaders];
+
+ GTMSessionUploadFetcherStatus uploadStatus =
+ [[self class] uploadStatusFromResponseHeaders:responseHeaders];
+ GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown,
+ @"beginChunkFetches has unexpected upload status for headers %@", responseHeaders);
+
+ BOOL isPrematureStop = (uploadStatus == kStatusFinal) || (uploadStatus == kStatusCancelled);
+
+ NSString *uploadLocationURLStr = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadURL];
+ BOOL hasUploadLocation = (uploadLocationURLStr.length > 0);
+
+ if (isPrematureStop || !hasUploadLocation) {
+ GTMSESSION_ASSERT_DEBUG(NO, @"Premature failure: upload-status:\"%@\" location:%@",
+ [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus], uploadLocationURLStr);
+ // We cannot continue since we do not know the location to use
+ // as our upload destination.
+ NSDictionary *userInfo = nil;
+ NSData *downloadedData = self.downloadedData;
+ if (downloadedData.length > 0) {
+ userInfo = @{ kGTMSessionFetcherStatusDataKey : downloadedData };
+ }
+ NSError *failureError = [self prematureFailureErrorWithUserInfo:userInfo];
+ [self invokeFinalCallbackWithData:nil
+ error:failureError
+ shouldInvalidateLocation:YES];
+ return;
+ }
+
+ self.uploadLocationURL = [NSURL URLWithString:uploadLocationURLStr];
+
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc postNotificationName:kGTMSessionFetcherUploadLocationObtainedNotification
+ object:self];
+
+ // we've now sent all of the initial post body data, so we need to include
+ // its size in future progress indicator callbacks
+ [self setInitialBodySent:[self initialBodyLength]];
+
+ // just in case the user paused us during the initial fetch...
+ if (![self isPaused]) {
+ [self uploadNextChunkWithOffset:0];
+ }
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didSendBodyData:(int64_t)bytesSent
+ totalBytesSent:(int64_t)totalBytesSent
+ totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
+ // Overrides the superclass.
+ [self invokeDelegateWithDidSendBytes:bytesSent
+ totalBytesSent:totalBytesSent
+ totalBytesExpectedToSend:totalBytesExpectedToSend + [self fullUploadLength]];
+}
+
+- (BOOL)shouldReleaseCallbacksUponCompletion {
+ // Overrides the superclass.
+
+ // We don't want the superclass to release the delegate and callback
+ // blocks once the initial fetch has finished
+ //
+ // This is invoked for only successful completion of the connection;
+ // an error always will invoke and release the callbacks
+ return NO;
+}
+
+- (void)invokeFinalCallbackWithData:(NSData *)data
+ error:(NSError *)error
+ shouldInvalidateLocation:(BOOL)shouldInvalidateLocation {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (shouldInvalidateLocation) {
+ _uploadLocationURL = nil;
+ }
+
+ dispatch_queue_t queue = _delegateCallbackQueue;
+ GTMSessionFetcherCompletionHandler handler = _delegateCompletionHandler;
+ if (queue && handler) {
+ [self invokeOnCallbackQueue:queue
+ afterUserStopped:NO
+ block:^{
+ handler(data, error);
+ }];
+ }
+ } // @synchronized(self)
+
+ [self releaseUploadAndBaseCallbacks:!self.userStoppedFetching];
+}
+
+- (void)releaseUploadAndBaseCallbacks:(BOOL)shouldReleaseCancellation {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _delegateCallbackQueue = nil;
+ _delegateCompletionHandler = nil;
+ _uploadDataProvider = nil;
+ if (shouldReleaseCancellation) {
+ _cancellationHandler = nil;
+ }
+ }
+
+ // Release the base class's callbacks, too, if needed.
+ [self releaseCallbacks];
+}
+
+- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
+ GTMSessionCheckNotSynchronized(self);
+
+ // Clear _fetcherInFlight when stopped. Moved from stopFetching, since that's a public method,
+ // where this method does the work. Fixes issue clearing value when retryBlock included.
+ GTMSessionFetcher *fetcherInFlight = self.fetcherInFlight;
+ if (fetcherInFlight == self) {
+ self.fetcherInFlight = nil;
+ }
+
+ [super stopFetchReleasingCallbacks:shouldReleaseCallbacks];
+
+ if (shouldReleaseCallbacks) {
+ [self releaseUploadAndBaseCallbacks:NO];
+ }
+}
+
+#pragma mark Chunk fetching methods
+
+- (void)uploadNextChunkWithOffset:(int64_t)offset {
+ // use the properties in each chunk fetcher
+ NSDictionary *props = [self properties];
+
+ [self uploadNextChunkWithOffset:offset
+ fetcherProperties:props];
+}
+
+- (void)sendQueryForUploadOffsetWithFetcherProperties:(NSDictionary *)props {
+ GTMSessionFetcher *queryFetcher = [self uploadFetcherWithProperties:props
+ isQueryFetch:YES];
+ queryFetcher.bodyData = [NSData data];
+
+ NSString *originalComment = self.comment;
+ [queryFetcher setCommentWithFormat:@"%@ (query offset)",
+ originalComment ? originalComment : @"upload"];
+
+ [queryFetcher setRequestValue:@"query" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
+
+ self.fetcherInFlight = queryFetcher;
+ [queryFetcher beginFetchWithDelegate:self
+ didFinishSelector:@selector(queryFetcher:finishedWithData:error:)];
+}
+
+- (void)queryFetcher:(GTMSessionFetcher *)queryFetcher
+ finishedWithData:(NSData *)data
+ error:(NSError *)error {
+ self.fetcherInFlight = nil;
+
+ NSDictionary *responseHeaders = [queryFetcher responseHeaders];
+ NSString *sizeReceivedHeader;
+
+ GTMSessionUploadFetcherStatus uploadStatus =
+ [[self class] uploadStatusFromResponseHeaders:responseHeaders];
+ GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown || error != nil,
+ @"query fetcher completion has unexpected upload status for headers %@", responseHeaders);
+
+ if (error == nil) {
+ sizeReceivedHeader = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadSizeReceived];
+
+ if (uploadStatus == kStatusCancelled ||
+ (uploadStatus == kStatusActive && sizeReceivedHeader == nil)) {
+ NSDictionary *userInfo = nil;
+ if (data.length > 0) {
+ userInfo = @{ kGTMSessionFetcherStatusDataKey : data };
+ }
+ error = [self prematureFailureErrorWithUserInfo:userInfo];
+ }
+ }
+
+ if (error == nil) {
+ int64_t offset = [sizeReceivedHeader longLongValue];
+ int64_t fullUploadLength = [self fullUploadLength];
+ if (uploadStatus == kStatusFinal ||
+ (offset >= fullUploadLength &&
+ fullUploadLength != kGTMSessionUploadFetcherUnknownFileSize)) {
+ // Handle we're done
+ [self chunkFetcher:queryFetcher finishedWithData:data error:nil];
+ } else {
+ [self retrieveUploadChunkGranularityFromResponseHeaders:responseHeaders];
+ [self uploadNextChunkWithOffset:offset];
+ }
+ } else {
+ // Handle query error
+ [self chunkFetcher:queryFetcher finishedWithData:data error:error];
+ }
+}
+
+- (void)sendCancelUploadWithFetcherProperties:(NSDictionary *)props {
+ @synchronized(self) {
+ _isCancelInFlight = YES;
+ }
+ GTMSessionFetcher *cancelFetcher = [self uploadFetcherWithProperties:props
+ isQueryFetch:YES];
+ cancelFetcher.bodyData = [NSData data];
+
+ NSString *originalComment = self.comment;
+ [cancelFetcher setCommentWithFormat:@"%@ (cancel)",
+ originalComment ? originalComment : @"upload"];
+
+ [cancelFetcher setRequestValue:@"cancel" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
+
+ self.fetcherInFlight = cancelFetcher;
+ [cancelFetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+ self.fetcherInFlight = nil;
+ if (![self triggerCancellationHandlerForFetch:cancelFetcher data:data error:error]) {
+ if (error) {
+ GTMSESSION_LOG_DEBUG(@"cancelFetcher %@", error);
+ }
+ }
+ @synchronized(self) {
+ self->_isCancelInFlight = NO;
+ }
+ }];
+}
+
+- (void)uploadNextChunkWithOffset:(int64_t)offset
+ fetcherProperties:(NSDictionary *)props {
+ GTMSessionCheckNotSynchronized(self);
+
+ // Example chunk headers:
+ // X-Goog-Upload-Command: upload, finalize
+ // X-Goog-Upload-Offset: 0
+ // Content-Length: 2000000
+ // Content-Type: image/jpeg
+ //
+ // {bytes 0-1999999}
+
+ // The chunk upload URL requires no authentication header.
+ GTMSessionFetcher *chunkFetcher = [self uploadFetcherWithProperties:props
+ isQueryFetch:NO];
+ [self attachSendProgressBlockToChunkFetcher:chunkFetcher];
+ int64_t chunkSize = [self updateChunkFetcher:chunkFetcher
+ forChunkAtOffset:offset];
+ BOOL isUploadingFileURL = (self.uploadFileURL != nil);
+ int64_t fullUploadLength = [self fullUploadLength];
+
+ // The chunk size may have changed, so determine again if we're uploading the full file.
+ BOOL isUploadingFullFile = (offset == 0 &&
+ fullUploadLength != kGTMSessionUploadFetcherUnknownFileSize &&
+ chunkSize >= fullUploadLength);
+ if (isUploadingFullFile && isUploadingFileURL) {
+ // The data is the full upload file URL.
+ chunkFetcher.bodyFileURL = self.uploadFileURL;
+ [self beginChunkFetcher:chunkFetcher
+ offset:offset];
+ } else {
+ // Make an NSData for the subset for this upload chunk.
+ self.subdataGenerating = YES;
+ [self generateChunkSubdataWithOffset:offset
+ length:chunkSize
+ response:^(NSData *chunkData, int64_t uploadFileLength, NSError *chunkError) {
+ // The subdata methods may leave us on a background thread.
+ dispatch_async(dispatch_get_main_queue(), ^{
+ self.subdataGenerating = NO;
+
+ // dont allow the updating of fileLength for uploads not using a data provider as they
+ // should know the file length before the upload starts.
+ if (self->_uploadDataProvider != nil && uploadFileLength > 0) {
+ [self setUploadFileLength:uploadFileLength];
+ // Update the command and content-length headers if this is the last chunk to be sent.
+ if (offset + chunkSize >= uploadFileLength) {
+ int64_t updatedChunkSize = [self updateChunkFetcher:chunkFetcher
+ forChunkAtOffset:offset];
+ if (updatedChunkSize == 0) {
+ // Calling beginChunkFetcher early when there is no more data to send allows us to
+ // properly handle nil chunkData below without having to account for the case where
+ // we are just finalizing the file.
+ chunkFetcher.bodyData = [[NSData alloc] init];
+ [self beginChunkFetcher:chunkFetcher
+ offset:offset];
+ return;
+ }
+ }
+ }
+
+ if (chunkData == nil) {
+ NSError *responseError = chunkError;
+ if (!responseError) {
+ responseError = [self uploadChunkUnavailableErrorWithDescription:@"chunkData is nil"];
+ }
+ [self invokeFinalCallbackWithData:nil
+ error:responseError
+ shouldInvalidateLocation:YES];
+ return;
+ }
+
+ BOOL didWriteFile = NO;
+ if (isUploadingFileURL) {
+ // Make a temporary file with the data subset.
+ NSString *tempName =
+ [NSString stringWithFormat:@"GTMUpload_temp_%@", [[NSUUID UUID] UUIDString]];
+ NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:tempName];
+ NSError *writeError;
+ didWriteFile = [chunkData writeToFile:tempPath
+ options:NSDataWritingAtomic
+ error:&writeError];
+ if (didWriteFile) {
+ chunkFetcher.bodyFileURL = [NSURL fileURLWithPath:tempPath];
+ } else {
+ GTMSESSION_LOG_DEBUG(@"writeToFile failed: %@\n%@", writeError, tempPath);
+ }
+ }
+ if (!didWriteFile) {
+ chunkFetcher.bodyData = [chunkData copy];
+ }
+ [self beginChunkFetcher:chunkFetcher
+ offset:offset];
+ });
+ }];
+ }
+}
+
+- (void)beginChunkFetcher:(GTMSessionFetcher *)chunkFetcher
+ offset:(int64_t)offset {
+
+ // Track the current offset for progress reporting
+ self.currentOffset = offset;
+
+ // Hang on to the fetcher in case we need to cancel it. We set these before beginning the
+ // chunk fetch so the observers notified of chunk fetches can inspect the upload fetcher to
+ // match to the chunk.
+ self.chunkFetcher = chunkFetcher;
+ self.fetcherInFlight = chunkFetcher;
+
+ // Update the last chunk request, including any request headers.
+ self.lastChunkRequest = chunkFetcher.request;
+
+ [chunkFetcher beginFetchWithDelegate:self
+ didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
+}
+
+- (void)attachSendProgressBlockToChunkFetcher:(GTMSessionFetcher *)chunkFetcher {
+ chunkFetcher.sendProgressBlock = ^(int64_t bytesSent, int64_t totalBytesSent,
+ int64_t totalBytesExpectedToSend) {
+ // The total bytes expected include the initial body and the full chunked
+ // data, independent of how big this fetcher's chunk is.
+ int64_t initialBodySent = [self bodyLength]; // TODO(grobbins) use [self initialBodySent]
+ int64_t totalSent = initialBodySent + self.currentOffset + totalBytesSent;
+ int64_t totalExpected = initialBodySent + [self fullUploadLength];
+
+ [self invokeDelegateWithDidSendBytes:bytesSent
+ totalBytesSent:totalSent
+ totalBytesExpectedToSend:totalExpected];
+ };
+}
+
+- (NSDictionary *)uploadSessionIdentifierMetadata {
+ NSMutableDictionary *metadata = [NSMutableDictionary dictionary];
+ metadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] = @YES;
+ GTMSESSION_ASSERT_DEBUG(self.uploadFileURL,
+ @"Invalid upload fetcher to create session identifier for metadata");
+ metadata[kGTMSessionIdentifierUploadFileURLMetadataKey] = [self.uploadFileURL absoluteString];
+ metadata[kGTMSessionIdentifierUploadFileLengthMetadataKey] = @([self fullUploadLength]);
+
+ if (self.uploadLocationURL) {
+ metadata[kGTMSessionIdentifierUploadLocationURLMetadataKey] =
+ [self.uploadLocationURL absoluteString];
+ }
+ if (self.uploadMIMEType) {
+ metadata[kGTMSessionIdentifierUploadMIMETypeMetadataKey] = self.uploadMIMEType;
+ }
+ metadata[kGTMSessionIdentifierUploadChunkSizeMetadataKey] = @(self.chunkSize);
+ metadata[kGTMSessionIdentifierUploadCurrentOffsetMetadataKey] = @(self.currentOffset);
+ metadata[kGTMSessionIdentifierUploadAllowsCellularAccess] = @(self.request.allowsCellularAccess);
+
+ return metadata;
+}
+
+- (GTMSessionFetcher *)uploadFetcherWithProperties:(NSDictionary *)properties
+ isQueryFetch:(BOOL)isQueryFetch {
+ GTMSessionCheckNotSynchronized(self);
+
+ // Common code to make a request for a query command or for a chunk upload.
+ NSURL *uploadLocationURL = self.uploadLocationURL;
+ NSMutableURLRequest *chunkRequest = [NSMutableURLRequest requestWithURL:uploadLocationURL];
+ [chunkRequest setHTTPMethod:@"PUT"];
+
+ // copy the user-agent from the original connection
+ // n.b. that self.request is nil for upload fetchers created with an existing upload location
+ // URL.
+ NSURLRequest *origRequest = self.request;
+
+ chunkRequest.allowsCellularAccess = origRequest.allowsCellularAccess;
+ if (!origRequest) {
+ chunkRequest.allowsCellularAccess = _allowsCellularAccess;
+ }
+ NSString *userAgent = [origRequest valueForHTTPHeaderField:@"User-Agent"];
+ if (userAgent.length > 0) {
+ [chunkRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
+ }
+
+ [chunkRequest setValue:kGTMSessionXGoogUploadProtocolResumable
+ forHTTPHeaderField:kGTMSessionHeaderXGoogUploadProtocol];
+
+ // To avoid timeouts when debugging, copy the timeout of the initial fetcher.
+ NSTimeInterval origTimeout = [origRequest timeoutInterval];
+ [chunkRequest setTimeoutInterval:origTimeout];
+
+ //
+ // Make a new chunk fetcher.
+ //
+ GTMSessionFetcher *chunkFetcher = [GTMSessionFetcher fetcherWithRequest:chunkRequest];
+ chunkFetcher.callbackQueue = self.callbackQueue;
+ chunkFetcher.sessionUserInfo = self.sessionUserInfo;
+ chunkFetcher.configurationBlock = self.configurationBlock;
+ chunkFetcher.allowedInsecureSchemes = self.allowedInsecureSchemes;
+ chunkFetcher.allowLocalhostRequest = self.allowLocalhostRequest;
+ chunkFetcher.allowInvalidServerCertificates = self.allowInvalidServerCertificates;
+ chunkFetcher.useUploadTask = !isQueryFetch;
+
+ if (self.uploadFileURL && !isQueryFetch && self.useBackgroundSession) {
+ [chunkFetcher createSessionIdentifierWithMetadata:[self uploadSessionIdentifierMetadata]];
+ }
+
+ // Give the chunk fetcher the same properties as the previous chunk fetcher
+ chunkFetcher.properties = [properties mutableCopy];
+ [chunkFetcher setProperty:[NSValue valueWithNonretainedObject:self]
+ forKey:kGTMSessionUploadFetcherChunkParentKey];
+
+ // copy other fetcher settings to the new fetcher
+ chunkFetcher.retryEnabled = self.retryEnabled;
+ chunkFetcher.maxRetryInterval = self.maxRetryInterval;
+
+ if ([self isRetryEnabled]) {
+ // We interpose our own retry method both so we can change the request to ask the server to
+ // tell us where to resume the chunk.
+ chunkFetcher.retryBlock = ^(BOOL suggestedWillRetry, NSError *chunkError,
+ GTMSessionFetcherRetryResponse response) {
+ void (^finish)(BOOL) = ^(BOOL shouldRetry){
+ // We'll retry by sending an offset query.
+ if (shouldRetry) {
+ self.shouldInitiateOffsetQuery = !isQueryFetch;
+
+ // We don't know what our actual offset is anymore, but the server will tell us.
+ self.currentOffset = 0;
+ }
+ // We don't actually want to retry this specific fetcher.
+ response(NO);
+ };
+
+ GTMSessionFetcherRetryBlock retryBlock = self.retryBlock;
+ if (retryBlock) {
+ // Ask the client, then call the finish block above.
+ retryBlock(suggestedWillRetry, chunkError, finish);
+ } else {
+ finish(suggestedWillRetry);
+ }
+ };
+ }
+
+ return chunkFetcher;
+}
+
+- (void)chunkFetcher:(GTMSessionFetcher *)chunkFetcher
+ finishedWithData:(NSData *)data
+ error:(NSError *)error {
+ BOOL hasDestroyedOldChunkFetcher = NO;
+ self.fetcherInFlight = nil;
+
+ NSDictionary *responseHeaders = [chunkFetcher responseHeaders];
+ GTMSessionUploadFetcherStatus uploadStatus =
+ [[self class] uploadStatusFromResponseHeaders:responseHeaders];
+ GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown
+ || error != nil
+ || self.wasCreatedFromBackgroundSession,
+ @"chunk fetcher completion has kStatusUnknown upload status for headers %@ fetcher %@",
+ responseHeaders, self);
+ BOOL isUploadStatusStopped = (uploadStatus == kStatusFinal || uploadStatus == kStatusCancelled);
+
+ // Check if the fetcher was actually querying. If it failed, do not retry,
+ // as it would enter an infinite retry loop.
+ NSString *uploadCommand =
+ chunkFetcher.request.allHTTPHeaderFields[kGTMSessionHeaderXGoogUploadCommand];
+ BOOL isQueryFetch = [uploadCommand isEqual:@"query"];
+
+ // TODO
+ // Maybe here we can check to see if the request had x goog content length set. (the file length one).
+ NSString *previousContentLengthValue =
+ [chunkFetcher.request valueForHTTPHeaderField:@"Content-Length"];
+ // The Content-Length header may not be present if the chunk fetcher was recreated from
+ // a background session.
+ BOOL hasKnownChunkSize = (previousContentLengthValue != nil);
+ int64_t previousContentLength = [previousContentLengthValue longLongValue];
+
+ BOOL needsQuery = (!hasKnownChunkSize && !isUploadStatusStopped);
+
+ if (error || (needsQuery && !isQueryFetch)) {
+ NSInteger status = error.code;
+
+ // Status 4xx indicates a bad offset in the Google upload protocol. However, do not retry status
+ // 404 per spec, nor if the upload size appears to have been zero (since the server will just
+ // keep asking us to retry.)
+ if (self.shouldInitiateOffsetQuery ||
+ (needsQuery && !isQueryFetch) ||
+ ([error.domain isEqual:kGTMSessionFetcherStatusDomain] &&
+ status >= 400 && status <= 499 &&
+ status != 404 &&
+ uploadStatus == kStatusActive &&
+ previousContentLength > 0)) {
+ self.shouldInitiateOffsetQuery = NO;
+ [self destroyChunkFetcher];
+ hasDestroyedOldChunkFetcher = YES;
+ [self sendQueryForUploadOffsetWithFetcherProperties:chunkFetcher.properties];
+ } else {
+ // Some unexpected status has occurred; handle it as we would a regular
+ // object fetcher failure.
+ [self invokeFinalCallbackWithData:data
+ error:error
+ shouldInvalidateLocation:NO];
+ }
+ } else {
+ // The chunk has uploaded successfully.
+ int64_t newOffset = self.currentOffset + previousContentLength;
+#if DEBUG
+ // Verify that if we think all of the uploading data has been sent, the server responded with
+ // the "final" upload status.
+ BOOL hasUploadAllData = (newOffset == [self fullUploadLength]);
+ BOOL isFinalStatus = (uploadStatus == kStatusFinal);
+ #pragma unused(hasUploadAllData,isFinalStatus)
+ GTMSESSION_ASSERT_DEBUG(hasUploadAllData == isFinalStatus || !hasKnownChunkSize,
+ @"uploadStatus:%@ newOffset:%lld (%lld + %lld) fullUploadLength:%lld"
+ @" chunkFetcher:%@ requestHeaders:%@ responseHeaders:%@",
+ [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus],
+ newOffset, self.currentOffset, previousContentLength,
+ [self fullUploadLength],
+ chunkFetcher, chunkFetcher.request.allHTTPHeaderFields,
+ responseHeaders);
+#endif
+ if (isUploadStatusStopped ||
+ (!_uploadData && _uploadFileLength == 0) ||
+ (_currentOffset > _uploadFileLength && _uploadFileLength > 0)) {
+ // This was the last chunk.
+ if (error == nil && uploadStatus == kStatusCancelled) {
+ // Report cancelled status as an error.
+ NSDictionary *userInfo = nil;
+ if (data.length > 0) {
+ userInfo = @{ kGTMSessionFetcherStatusDataKey : data };
+ }
+ data = nil;
+ error = [self prematureFailureErrorWithUserInfo:userInfo];
+ } else {
+ // The upload is in final status.
+ //
+ // Take the chunk fetcher's data as the superclass data.
+ self.downloadedData = data;
+ self.statusCode = chunkFetcher.statusCode;
+ }
+
+ // we're done
+ [self invokeFinalCallbackWithData:data
+ error:error
+ shouldInvalidateLocation:YES];
+ } else {
+ // Start the next chunk.
+ self.currentOffset = newOffset;
+
+ // We want to destroy this chunk fetcher before creating the next one, but
+ // we want to pass on its properties
+ NSDictionary *props = [chunkFetcher properties];
+
+ // We no longer need to be able to cancel this chunkFetcher. Destroy it
+ // before we create a new chunk fetcher.
+ [self destroyChunkFetcher];
+ hasDestroyedOldChunkFetcher = YES;
+
+ [self uploadNextChunkWithOffset:newOffset
+ fetcherProperties:props];
+ }
+ }
+ if (!hasDestroyedOldChunkFetcher) {
+ [self destroyChunkFetcher];
+ }
+}
+
+- (void)destroyChunkFetcher {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_fetcherInFlight == _chunkFetcher) {
+ _fetcherInFlight = nil;
+ }
+
+ [_chunkFetcher stopFetching];
+
+ NSURL *chunkFileURL = _chunkFetcher.bodyFileURL;
+ BOOL wasTemporaryUploadFile = ![chunkFileURL isEqual:_uploadFileURL];
+ if (wasTemporaryUploadFile) {
+ NSError *error;
+ [[NSFileManager defaultManager] removeItemAtURL:chunkFileURL
+ error:&error];
+ if (error) {
+ GTMSESSION_LOG_DEBUG(@"removingItemAtURL failed: %@\n%@", error, chunkFileURL);
+ }
+ }
+
+ _recentChunkReponseHeaders = _chunkFetcher.responseHeaders;
+
+ // To avoid retain cycles, remove all properties except the parent identifier.
+ _chunkFetcher.properties =
+ @{ kGTMSessionUploadFetcherChunkParentKey : [NSValue valueWithNonretainedObject:self] };
+
+ _chunkFetcher.retryBlock = nil;
+ _chunkFetcher.sendProgressBlock = nil;
+ _chunkFetcher = nil;
+ } // @synchronized(self)
+}
+
+// This method calculates the proper values to pass to the client's send progress block.
+//
+// The actual total bytes sent include the initial body sent, plus the
+// offset into the batched data prior to the current chunk fetcher
+
+- (void)invokeDelegateWithDidSendBytes:(int64_t)bytesSent
+ totalBytesSent:(int64_t)totalBytesSent
+ totalBytesExpectedToSend:(int64_t)totalBytesExpected {
+ GTMSessionCheckNotSynchronized(self);
+
+ // Ensure the chunk fetcher survives the callback in case the user pauses the upload process.
+ __block GTMSessionFetcher *holdFetcher = self.chunkFetcher;
+
+ [self invokeOnCallbackQueue:self.delegateCallbackQueue
+ afterUserStopped:NO
+ block:^{
+ GTMSessionFetcherSendProgressBlock sendProgressBlock = self.sendProgressBlock;
+ if (sendProgressBlock) {
+ sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpected);
+ }
+ holdFetcher = nil;
+ }];
+}
+
+- (void)retrieveUploadChunkGranularityFromResponseHeaders:(NSDictionary *)responseHeaders {
+ GTMSessionCheckNotSynchronized(self);
+
+ // Standard granularity for Google uploads is 256K.
+ NSString *chunkGranularityHeader =
+ [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadChunkGranularity];
+ self.uploadGranularity = chunkGranularityHeader.longLongValue;
+}
+
+#pragma mark -
+
+- (BOOL)isPaused {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _isPaused;
+ } // @synchronized(self)
+}
+
+- (void)pauseFetching {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _isPaused = YES;
+ } // @synchronized(self)
+
+ // Pausing just means stopping the current chunk from uploading;
+ // when we resume, we will send a query request to the server to
+ // figure out what bytes to resume sending.
+ //
+ // We won't try to cancel the initial data upload, but rather will check
+ // for being paused in beginChunkFetches.
+ [self destroyChunkFetcher];
+}
+
+- (void)resumeFetching {
+ BOOL wasPaused;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ wasPaused = _isPaused;
+ _isPaused = NO;
+ } // @synchronized(self)
+
+ if (wasPaused) {
+ [self sendQueryForUploadOffsetWithFetcherProperties:self.properties];
+ }
+}
+
+- (void)stopFetching {
+ // Overrides the superclass
+ [self destroyChunkFetcher];
+
+ // If we think the server is waiting for more data, then tell it there won't be more.
+ if (self.uploadLocationURL) {
+ [self sendCancelUploadWithFetcherProperties:[self properties]];
+ self.uploadLocationURL = nil;
+ } else {
+ [self invokeOnCallbackQueue:self.callbackQueue
+ afterUserStopped:YES
+ block:^{
+ // Repeated calls to stopFetching may cause this path to be reached despite having sent a real
+ // cancel request, check here to ensure that the cancellation handler invocation which fires
+ // will definitely be for the real request sent previously.
+ @synchronized(self) {
+ if (self->_isCancelInFlight) {
+ return;
+ }
+ }
+ [self triggerCancellationHandlerForFetch:nil data:nil error:nil];
+ }];
+ }
+
+ [super stopFetching];
+}
+
+// Fires the cancellation handler, returning whether there was a handler to be fired.
+- (BOOL)triggerCancellationHandlerForFetch:(GTMSessionFetcher *)fetcher
+ data:(NSData *)data
+ error:(NSError *)error {
+ GTMSessionUploadFetcherCancellationHandler handler = self.cancellationHandler;
+ if (handler) {
+ handler(fetcher, data, error);
+ self.cancellationHandler = nil;
+ return YES;
+ }
+ return NO;
+}
+
+#pragma mark -
+
+- (int64_t)updateChunkFetcher:(GTMSessionFetcher *)chunkFetcher
+ forChunkAtOffset:(int64_t)offset {
+ BOOL isUploadingFileURL = (self.uploadFileURL != nil);
+
+ // Upload another chunk, meeting server-required granularity.
+ int64_t chunkSize = self.chunkSize;
+
+ int64_t fullUploadLength = [self fullUploadLength];
+ BOOL isFileLengthKnown = fullUploadLength >= 0;
+
+ BOOL isUploadingFullFile = (offset == 0 && isFileLengthKnown && chunkSize >= fullUploadLength);
+ if (!isUploadingFileURL || !isUploadingFullFile) {
+ // We're not uploading the entire file and given the file URL. Since we'll be
+ // allocating a subdata block for a chunk, we need to bound it to something that
+ // won't blow the process's memory.
+ if (chunkSize > kGTMSessionUploadFetcherMaximumDemandBufferSize) {
+ chunkSize = kGTMSessionUploadFetcherMaximumDemandBufferSize;
+ }
+ }
+
+ int64_t granularity = self.uploadGranularity;
+ if (granularity > 0) {
+ if (chunkSize < granularity) {
+ chunkSize = granularity;
+ } else {
+ chunkSize = chunkSize - (chunkSize % granularity);
+ }
+ }
+
+ GTMSESSION_ASSERT_DEBUG(offset < fullUploadLength || fullUploadLength == 0,
+ @"offset %lld exceeds data length %lld", offset, fullUploadLength);
+
+ if (granularity > 0) {
+ offset = offset - (offset % granularity);
+ }
+
+ // If the chunk size is bigger than the remaining data, or else
+ // it's close enough in size to the remaining data that we'd rather
+ // avoid having a whole extra http fetch for the leftover bit, then make
+ // this chunk size exactly match the remaining data size
+ NSString *command;
+ int64_t thisChunkSize = chunkSize;
+
+ BOOL isChunkTooBig = (thisChunkSize >= (fullUploadLength - offset));
+ BOOL isChunkAlmostBigEnough = (fullUploadLength - offset - 2500 < thisChunkSize);
+ BOOL isFinalChunk = (isChunkTooBig || isChunkAlmostBigEnough) && isFileLengthKnown;
+ if (isFinalChunk) {
+ thisChunkSize = fullUploadLength - offset;
+ if (thisChunkSize > 0) {
+ command = @"upload, finalize";
+ } else {
+ command = @"finalize";
+ }
+ } else {
+ command = @"upload";
+ }
+ NSString *lengthStr = @(thisChunkSize).stringValue;
+ NSString *offsetStr = @(offset).stringValue;
+
+ [chunkFetcher setRequestValue:command forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
+ [chunkFetcher setRequestValue:lengthStr forHTTPHeaderField:@"Content-Length"];
+ [chunkFetcher setRequestValue:offsetStr forHTTPHeaderField:kGTMSessionHeaderXGoogUploadOffset];
+ if (_uploadFileLength != kGTMSessionUploadFetcherUnknownFileSize) {
+ [chunkFetcher setRequestValue:@([self fullUploadLength]).stringValue
+ forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentLength];
+ }
+
+ // Append the range of bytes in this chunk to the fetcher comment.
+ NSString *baseComment = self.comment;
+ [chunkFetcher setCommentWithFormat:@"%@ (%lld-%lld)",
+ baseComment ? baseComment : @"upload", offset, MAX(0, offset + thisChunkSize - 1)];
+
+ return thisChunkSize;
+}
+
+// Public properties.
+@synthesize currentOffset = _currentOffset,
+ allowsCellularAccess = _allowsCellularAccess,
+ delegateCompletionHandler = _delegateCompletionHandler,
+ chunkFetcher = _chunkFetcher,
+ lastChunkRequest = _lastChunkRequest,
+ subdataGenerating = _subdataGenerating,
+ shouldInitiateOffsetQuery = _shouldInitiateOffsetQuery,
+ uploadGranularity = _uploadGranularity;
+
+// Internal properties.
+@dynamic fetcherInFlight;
+@dynamic activeFetcher;
+@dynamic statusCode;
+@dynamic delegateCallbackQueue;
+
++ (void)removePointer:(void *)pointer fromPointerArray:(NSPointerArray *)pointerArray {
+ for (NSUInteger index = 0, count = pointerArray.count; index < count; ++index) {
+ void *pointerAtIndex = [pointerArray pointerAtIndex:index];
+ if (pointerAtIndex == pointer) {
+ [pointerArray removePointerAtIndex:index];
+ return;
+ }
+ }
+}
+
+- (BOOL)useBackgroundSession {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _useBackgroundSessionOnChunkFetchers;
+ } // @synchronized(self
+}
+
+- (void)setUseBackgroundSession:(BOOL)useBackgroundSession {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_useBackgroundSessionOnChunkFetchers != useBackgroundSession) {
+ _useBackgroundSessionOnChunkFetchers = useBackgroundSession;
+ NSPointerArray *uploadFetcherPointerArrayForBackgroundSessions =
+ [[self class] uploadFetcherPointerArrayForBackgroundSessions];
+ @synchronized(uploadFetcherPointerArrayForBackgroundSessions) {
+ if (_useBackgroundSessionOnChunkFetchers) {
+ [uploadFetcherPointerArrayForBackgroundSessions addPointer:(__bridge void *)self];
+ } else {
+ [[self class] removePointer:(__bridge void *)self
+ fromPointerArray:uploadFetcherPointerArrayForBackgroundSessions];
+ }
+ } // @synchronized(uploadFetcherPointerArrayForBackgroundSessions)
+ }
+ } // @synchronized(self)
+}
+
+- (BOOL)canFetchWithBackgroundSession {
+ // The initial upload fetcher is always a foreground session; the
+ // useBackgroundSession property will apply only to chunk fetchers,
+ // not to queries.
+ return NO;
+}
+
+- (NSDictionary *)responseHeaders {
+ GTMSessionCheckNotSynchronized(self);
+ // Overrides the superclass
+
+ // If asked for the fetcher's response, use the most recent chunk fetcher's response,
+ // since the original request's response lacks useful information like the actual
+ // Content-Type.
+ NSDictionary *dict = self.chunkFetcher.responseHeaders;
+ if (dict) {
+ return dict;
+ }
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_recentChunkReponseHeaders) {
+ return _recentChunkReponseHeaders;
+ }
+ } // @synchronized(self
+
+ // No chunk fetcher yet completed, so return whatever we have from the initial fetch.
+ return [super responseHeaders];
+}
+
+- (NSInteger)statusCodeUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ if (_recentChunkStatusCode != -1) {
+ // Overrides the superclass to indicate status appropriate to the initial
+ // or latest chunk fetch
+ return _recentChunkStatusCode;
+ } else {
+ return [super statusCodeUnsynchronized];
+ }
+}
+
+
+- (void)setStatusCode:(NSInteger)val {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _recentChunkStatusCode = val;
+ }
+}
+
+- (int64_t)initialBodyLength {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _initialBodyLength;
+ }
+}
+
+- (void)setInitialBodyLength:(int64_t)length {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _initialBodyLength = length;
+ }
+}
+
+- (int64_t)initialBodySent {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _initialBodySent;
+ }
+}
+
+- (void)setInitialBodySent:(int64_t)length {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _initialBodySent = length;
+ }
+}
+
+- (NSURL *)uploadLocationURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _uploadLocationURL;
+ }
+}
+
+- (void)setUploadLocationURL:(NSURL *)locationURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _uploadLocationURL = locationURL;
+ }
+}
+
+- (GTMSessionFetcher *)activeFetcher {
+ GTMSessionFetcher *result = self.fetcherInFlight;
+ if (result) return result;
+
+ return self;
+}
+
+- (BOOL)isFetching {
+ // If there is an active chunk fetcher, then the upload fetcher is considered
+ // to still be fetching.
+ if (self.fetcherInFlight != nil) return YES;
+
+ return [super isFetching];
+}
+
+- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
+ NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
+
+ while (self.fetcherInFlight || self.subdataGenerating) {
+ if ([timeoutDate timeIntervalSinceNow] < 0) return NO;
+
+ if (self.subdataGenerating) {
+ // Allow time for subdata generation.
+ NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
+ [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
+ } else {
+ // Wait for any chunk or query fetchers that still have pending callbacks or
+ // notifications.
+ BOOL timedOut;
+
+ if (self.fetcherInFlight == self) {
+ timedOut = ![super waitForCompletionWithTimeout:timeoutInSeconds];
+ } else {
+ timedOut = ![self.fetcherInFlight waitForCompletionWithTimeout:timeoutInSeconds];
+ }
+ if (timedOut) return NO;
+ }
+ }
+ return YES;
+}
+
+@end
+
+@implementation GTMSessionFetcher (GTMSessionUploadFetcherMethods)
+
+- (GTMSessionUploadFetcher *)parentUploadFetcher {
+ NSValue *property = [self propertyForKey:kGTMSessionUploadFetcherChunkParentKey];
+ if (!property) return nil;
+
+ GTMSessionUploadFetcher *uploadFetcher = property.nonretainedObjectValue;
+
+ GTMSESSION_ASSERT_DEBUG([uploadFetcher isKindOfClass:[GTMSessionUploadFetcher class]],
+ @"Unexpected parent upload fetcher class: %@", [uploadFetcher class]);
+ return uploadFetcher;
+}
+
+@end