|
11 | 11 | #import "STPCardParams.h"
|
12 | 12 | #import "STPFormEncodable.h"
|
13 | 13 |
|
| 14 | +FOUNDATION_EXPORT NSString * STPPercentEscapedStringFromString(NSString *string); |
| 15 | +FOUNDATION_EXPORT NSString * STPQueryStringFromParameters(NSDictionary *parameters); |
| 16 | + |
14 | 17 | @implementation STPFormEncoder
|
15 | 18 |
|
| 19 | ++ (NSString *)stringByReplacingSnakeCaseWithCamelCase:(NSString *)input { |
| 20 | + NSArray *parts = [input componentsSeparatedByString:@"_"]; |
| 21 | + NSMutableString *camelCaseParam = [NSMutableString string]; |
| 22 | + [parts enumerateObjectsUsingBlock:^(NSString *part, NSUInteger idx, __unused BOOL *stop) { |
| 23 | + [camelCaseParam appendString:(idx == 0 ? part : [part capitalizedString])]; |
| 24 | + }]; |
| 25 | + |
| 26 | + return [camelCaseParam copy]; |
| 27 | +} |
| 28 | + |
16 | 29 | + (nonnull NSData *)formEncodedDataForObject:(nonnull NSObject<STPFormEncodable> *)object {
|
17 |
| - NSMutableArray *parts = [NSMutableArray array]; |
| 30 | + NSDictionary *dict = @{ |
| 31 | + [object.class rootObjectName]: [self keyPairDictionaryForObject:object] |
| 32 | + }; |
| 33 | + return [STPQueryStringFromParameters(dict) dataUsingEncoding:NSUTF8StringEncoding]; |
| 34 | +} |
| 35 | + |
| 36 | ++ (NSDictionary *)keyPairDictionaryForObject:(nonnull NSObject<STPFormEncodable> *)object { |
| 37 | + NSMutableDictionary *keyPairs = [NSMutableDictionary dictionary]; |
18 | 38 | [[object.class propertyNamesToFormFieldNamesMapping] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull propertyName, NSString * _Nonnull formFieldName, __unused BOOL * _Nonnull stop) {
|
19 |
| - NSString *formFieldValue = [[object valueForKey:propertyName] description]; |
20 |
| - if (formFieldValue) { |
21 |
| - [parts addObject:[NSString stringWithFormat:@"%@[%@]=%@", [object.class rootObjectName], formFieldName, [self.class stringByURLEncoding:formFieldValue]]]; |
| 39 | + id value = [self formEncodableValueForObject:[object valueForKey:propertyName]]; |
| 40 | + if (value) { |
| 41 | + keyPairs[formFieldName] = value; |
| 42 | + } |
| 43 | + }]; |
| 44 | + [object.additionalAPIParameters enumerateKeysAndObjectsUsingBlock:^(id _Nonnull additionalFieldName, id _Nonnull additionalFieldValue, __unused BOOL * _Nonnull stop) { |
| 45 | + id value = [self formEncodableValueForObject:additionalFieldValue]; |
| 46 | + if (value) { |
| 47 | + keyPairs[additionalFieldName] = value; |
22 | 48 | }
|
23 | 49 | }];
|
24 |
| - return [[parts componentsJoinedByString:@"&"] dataUsingEncoding:NSUTF8StringEncoding]; |
| 50 | + return [keyPairs copy]; |
| 51 | +} |
| 52 | + |
| 53 | ++ (id)formEncodableValueForObject:(NSObject *)object { |
| 54 | + if ([object conformsToProtocol:@protocol(STPFormEncodable)]) { |
| 55 | + return [self keyPairDictionaryForObject:(NSObject<STPFormEncodable>*)object]; |
| 56 | + } else { |
| 57 | + return object; |
| 58 | + } |
25 | 59 | }
|
26 | 60 |
|
27 |
| -/* This code is adapted from the code by David DeLong in this StackOverflow post: |
28 |
| - http://stackoverflow.com/questions/3423545/objective-c-iphone-percent-encode-a-string . It is protected under the terms of a Creative Commons |
29 |
| - license: http://creativecommons.org/licenses/by-sa/3.0/ |
30 |
| - */ |
31 | 61 | + (NSString *)stringByURLEncoding:(NSString *)string {
|
32 |
| - NSMutableString *output = [NSMutableString string]; |
33 |
| - const unsigned char *source = (const unsigned char *)[string UTF8String]; |
34 |
| - NSInteger sourceLen = strlen((const char *)source); |
35 |
| - for (int i = 0; i < sourceLen; ++i) { |
36 |
| - const unsigned char thisChar = source[i]; |
37 |
| - if (thisChar == ' ') { |
38 |
| - [output appendString:@"+"]; |
39 |
| - } else if (thisChar == '.' || thisChar == '-' || thisChar == '_' || thisChar == '~' || (thisChar >= 'a' && thisChar <= 'z') || |
40 |
| - (thisChar >= 'A' && thisChar <= 'Z') || (thisChar >= '0' && thisChar <= '9')) { |
41 |
| - [output appendFormat:@"%c", thisChar]; |
42 |
| - } else { |
43 |
| - [output appendFormat:@"%%%02X", thisChar]; |
44 |
| - } |
| 62 | + return STPPercentEscapedStringFromString(string); |
| 63 | +} |
| 64 | + |
| 65 | +@end |
| 66 | + |
| 67 | + |
| 68 | +// This code is adapted from https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFURLRequestSerialization.m . The only modifications are to replace the AF namespace with the STP namespace to avoid collisions with apps that are using both Stripe and AFNetworking. |
| 69 | +NSString * STPPercentEscapedStringFromString(NSString *string) { |
| 70 | + static NSString * const kSTPCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4 |
| 71 | + static NSString * const kSTPCharactersSubDelimitersToEncode = @"!$&'()*+,;="; |
| 72 | + |
| 73 | + NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; |
| 74 | + [allowedCharacterSet removeCharactersInString:[kSTPCharactersGeneralDelimitersToEncode stringByAppendingString:kSTPCharactersSubDelimitersToEncode]]; |
| 75 | + |
| 76 | + // FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028 |
| 77 | + // return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet]; |
| 78 | + |
| 79 | + static NSUInteger const batchSize = 50; |
| 80 | + |
| 81 | + NSUInteger index = 0; |
| 82 | + NSMutableString *escaped = @"".mutableCopy; |
| 83 | + |
| 84 | + while (index < string.length) { |
| 85 | +#pragma GCC diagnostic push |
| 86 | +#pragma GCC diagnostic ignored "-Wgnu" |
| 87 | + NSUInteger length = MIN(string.length - index, batchSize); |
| 88 | +#pragma GCC diagnostic pop |
| 89 | + NSRange range = NSMakeRange(index, length); |
| 90 | + |
| 91 | + // To avoid breaking up character sequences such as 👴🏻👮🏽 |
| 92 | + range = [string rangeOfComposedCharacterSequencesForRange:range]; |
| 93 | + |
| 94 | + NSString *substring = [string substringWithRange:range]; |
| 95 | + NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet]; |
| 96 | + [escaped appendString:encoded]; |
| 97 | + |
| 98 | + index += range.length; |
45 | 99 | }
|
46 |
| - return output; |
| 100 | + |
| 101 | + return escaped; |
47 | 102 | }
|
48 | 103 |
|
49 |
| -+ (NSString *)stringByReplacingSnakeCaseWithCamelCase:(NSString *)input { |
50 |
| - NSArray *parts = [input componentsSeparatedByString:@"_"]; |
51 |
| - NSMutableString *camelCaseParam = [NSMutableString string]; |
52 |
| - [parts enumerateObjectsUsingBlock:^(NSString *part, NSUInteger idx, __unused BOOL *stop) { |
53 |
| - [camelCaseParam appendString:(idx == 0 ? part : [part capitalizedString])]; |
54 |
| - }]; |
| 104 | +#pragma mark - |
| 105 | + |
| 106 | +@interface STPQueryStringPair : NSObject |
| 107 | +@property (readwrite, nonatomic, strong) id field; |
| 108 | +@property (readwrite, nonatomic, strong) id value; |
| 109 | + |
| 110 | +- (instancetype)initWithField:(id)field value:(id)value; |
| 111 | + |
| 112 | +- (NSString *)URLEncodedStringValue; |
| 113 | +@end |
| 114 | + |
| 115 | +@implementation STPQueryStringPair |
| 116 | + |
| 117 | +- (instancetype)initWithField:(id)field value:(id)value { |
| 118 | + self = [super init]; |
| 119 | + if (!self) { |
| 120 | + return nil; |
| 121 | + } |
55 | 122 |
|
56 |
| - return [camelCaseParam copy]; |
| 123 | + _field = field; |
| 124 | + _value = value; |
| 125 | + |
| 126 | + return self; |
| 127 | +} |
| 128 | + |
| 129 | +- (NSString *)URLEncodedStringValue { |
| 130 | + if (!self.value || [self.value isEqual:[NSNull null]]) { |
| 131 | + return STPPercentEscapedStringFromString([self.field description]); |
| 132 | + } else { |
| 133 | + return [NSString stringWithFormat:@"%@=%@", STPPercentEscapedStringFromString([self.field description]), STPPercentEscapedStringFromString([self.value description])]; |
| 134 | + } |
57 | 135 | }
|
58 | 136 |
|
59 | 137 | @end
|
| 138 | + |
| 139 | +#pragma mark - |
| 140 | + |
| 141 | +FOUNDATION_EXPORT NSArray * STPQueryStringPairsFromDictionary(NSDictionary *dictionary); |
| 142 | +FOUNDATION_EXPORT NSArray * STPQueryStringPairsFromKeyAndValue(NSString *key, id value); |
| 143 | + |
| 144 | +NSString * STPQueryStringFromParameters(NSDictionary *parameters) { |
| 145 | + NSMutableArray *mutablePairs = [NSMutableArray array]; |
| 146 | + for (STPQueryStringPair *pair in STPQueryStringPairsFromDictionary(parameters)) { |
| 147 | + [mutablePairs addObject:[pair URLEncodedStringValue]]; |
| 148 | + } |
| 149 | + |
| 150 | + return [mutablePairs componentsJoinedByString:@"&"]; |
| 151 | +} |
| 152 | + |
| 153 | +NSArray * STPQueryStringPairsFromDictionary(NSDictionary *dictionary) { |
| 154 | + return STPQueryStringPairsFromKeyAndValue(nil, dictionary); |
| 155 | +} |
| 156 | + |
| 157 | +NSArray * STPQueryStringPairsFromKeyAndValue(NSString *key, id value) { |
| 158 | + NSMutableArray *mutableQueryStringComponents = [NSMutableArray array]; |
| 159 | + NSString *descriptionSelector = NSStringFromSelector(@selector(description)); |
| 160 | + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:descriptionSelector ascending:YES selector:@selector(compare:)]; |
| 161 | + |
| 162 | + if ([value isKindOfClass:[NSDictionary class]]) { |
| 163 | + NSDictionary *dictionary = value; |
| 164 | + // Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries |
| 165 | + for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { |
| 166 | + id nestedValue = dictionary[nestedKey]; |
| 167 | + if (nestedValue) { |
| 168 | + [mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)]; |
| 169 | + } |
| 170 | + } |
| 171 | + } else if ([value isKindOfClass:[NSArray class]]) { |
| 172 | + NSArray *array = value; |
| 173 | + for (id nestedValue in array) { |
| 174 | + [mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)]; |
| 175 | + } |
| 176 | + } else if ([value isKindOfClass:[NSSet class]]) { |
| 177 | + NSSet *set = value; |
| 178 | + for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { |
| 179 | + [mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue(key, obj)]; |
| 180 | + } |
| 181 | + } else { |
| 182 | + [mutableQueryStringComponents addObject:[[STPQueryStringPair alloc] initWithField:key value:value]]; |
| 183 | + } |
| 184 | + |
| 185 | + return mutableQueryStringComponents; |
| 186 | +} |
0 commit comments