Change S3 API to take a real S3 key as a parameter when creating a request, rath…
…er than forcing the user to encode the key themselves or add a '/' on the front My thanks to Tom Andersen for his report!
Showing
6 changed files
with
59 additions
and
47 deletions
@@ -21,7 +21,7 @@ | @@ -21,7 +21,7 @@ | ||
21 | #import "ASIInputStream.h" | 21 | #import "ASIInputStream.h" |
22 | 22 | ||
23 | // Automatically set on build | 23 | // Automatically set on build |
24 | -NSString *ASIHTTPRequestVersion = @"v1.6-3 2010-03-11"; | 24 | +NSString *ASIHTTPRequestVersion = @"v1.6-4 2010-03-16"; |
25 | 25 | ||
26 | NSString* const NetworkRequestErrorDomain = @"ASIHTTPRequestErrorDomain"; | 26 | NSString* const NetworkRequestErrorDomain = @"ASIHTTPRequestErrorDomain"; |
27 | 27 |
@@ -30,17 +30,17 @@ | @@ -30,17 +30,17 @@ | ||
30 | 30 | ||
31 | - (ASIS3Request *)GETRequest | 31 | - (ASIS3Request *)GETRequest |
32 | { | 32 | { |
33 | - return [ASIS3Request requestWithBucket:[self bucket] path:[NSString stringWithFormat:@"/%@",[self key]]]; | 33 | + return [ASIS3Request requestWithBucket:[self bucket] key:[self key]]; |
34 | } | 34 | } |
35 | 35 | ||
36 | - (ASIS3Request *)PUTRequestWithFile:(NSString *)filePath | 36 | - (ASIS3Request *)PUTRequestWithFile:(NSString *)filePath |
37 | { | 37 | { |
38 | - return [ASIS3Request PUTRequestForFile:filePath withBucket:[self bucket] path:[NSString stringWithFormat:@"/%@",[self key]]]; | 38 | + return [ASIS3Request PUTRequestForFile:filePath withBucket:[self bucket] key:[self key]]; |
39 | } | 39 | } |
40 | 40 | ||
41 | - (ASIS3Request *)DELETERequest | 41 | - (ASIS3Request *)DELETERequest |
42 | { | 42 | { |
43 | - ASIS3Request *request = [ASIS3Request requestWithBucket:[self bucket] path:[NSString stringWithFormat:@"/%@",[self key]]]; | 43 | + ASIS3Request *request = [ASIS3Request requestWithBucket:[self bucket] key:[self key]]; |
44 | [request setRequestMethod:@"DELETE"]; | 44 | [request setRequestMethod:@"DELETE"]; |
45 | return request; | 45 | return request; |
46 | } | 46 | } |
@@ -132,7 +132,7 @@ static NSDateFormatter *dateFormatter = nil; | @@ -132,7 +132,7 @@ static NSDateFormatter *dateFormatter = nil; | ||
132 | [newRequest setPrefix:[self prefix]]; | 132 | [newRequest setPrefix:[self prefix]]; |
133 | [newRequest setMarker:[self marker]]; | 133 | [newRequest setMarker:[self marker]]; |
134 | [newRequest setMaxResultCount:[self maxResultCount]]; | 134 | [newRequest setMaxResultCount:[self maxResultCount]]; |
135 | - [newRequest setDelimiter:[self path]]; | 135 | + [newRequest setDelimiter:[self delimiter]]; |
136 | return newRequest; | 136 | return newRequest; |
137 | } | 137 | } |
138 | 138 |
@@ -37,8 +37,8 @@ typedef enum _ASIS3ErrorType { | @@ -37,8 +37,8 @@ typedef enum _ASIS3ErrorType { | ||
37 | // Name of the bucket to talk to | 37 | // Name of the bucket to talk to |
38 | NSString *bucket; | 38 | NSString *bucket; |
39 | 39 | ||
40 | - // Path to the resource you want to access on S3. Leave empty for the bucket root | 40 | + // Key of the resource you want to access on S3. Leave empty for the bucket root |
41 | - NSString *path; | 41 | + NSString *key; |
42 | 42 | ||
43 | // The string that will be used in the HTTP date header. Generally you'll want to ignore this and let the class add the current date for you, but the accessor is used by the tests | 43 | // The string that will be used in the HTTP date header. Generally you'll want to ignore this and let the class add the current date for you, but the accessor is used by the tests |
44 | NSString *dateString; | 44 | NSString *dateString; |
@@ -53,7 +53,7 @@ typedef enum _ASIS3ErrorType { | @@ -53,7 +53,7 @@ typedef enum _ASIS3ErrorType { | ||
53 | 53 | ||
54 | // The bucket + path of the object to be copied (used with COPYRequestFromBucket:path:toBucket:path:) | 54 | // The bucket + path of the object to be copied (used with COPYRequestFromBucket:path:toBucket:path:) |
55 | NSString *sourceBucket; | 55 | NSString *sourceBucket; |
56 | - NSString *sourcePath; | 56 | + NSString *sourceKey; |
57 | 57 | ||
58 | // Internally used while parsing errors | 58 | // Internally used while parsing errors |
59 | NSString *currentErrorString; | 59 | NSString *currentErrorString; |
@@ -63,28 +63,30 @@ typedef enum _ASIS3ErrorType { | @@ -63,28 +63,30 @@ typedef enum _ASIS3ErrorType { | ||
63 | #pragma mark Constructors | 63 | #pragma mark Constructors |
64 | 64 | ||
65 | // Create a request, building an appropriate url | 65 | // Create a request, building an appropriate url |
66 | -+ (id)requestWithBucket:(NSString *)bucket path:(NSString *)path; | 66 | ++ (id)requestWithBucket:(NSString *)bucket key:(NSString *)key; |
67 | 67 | ||
68 | // Create a PUT request using the file at filePath as the body | 68 | // Create a PUT request using the file at filePath as the body |
69 | -+ (id)PUTRequestForFile:(NSString *)filePath withBucket:(NSString *)bucket path:(NSString *)path; | 69 | ++ (id)PUTRequestForFile:(NSString *)filePath withBucket:(NSString *)bucket key:(NSString *)key; |
70 | 70 | ||
71 | // Create a PUT request using the supplied NSData as the body (set the mime-type manually with setMimeType: if necessary) | 71 | // Create a PUT request using the supplied NSData as the body (set the mime-type manually with setMimeType: if necessary) |
72 | -+ (id)PUTRequestForData:(NSData *)data withBucket:(NSString *)bucket path:(NSString *)path; | 72 | ++ (id)PUTRequestForData:(NSData *)data withBucket:(NSString *)bucket key:(NSString *)key; |
73 | 73 | ||
74 | // Create a DELETE request for the object at path | 74 | // Create a DELETE request for the object at path |
75 | -+ (id)DELETERequestWithBucket:(NSString *)bucket path:(NSString *)path; | 75 | ++ (id)DELETERequestWithBucket:(NSString *)bucket key:(NSString *)key; |
76 | 76 | ||
77 | // Create a PUT request to copy an object from one location to another | 77 | // Create a PUT request to copy an object from one location to another |
78 | // Clang will complain because it thinks this method should return an object with +1 retain :( | 78 | // Clang will complain because it thinks this method should return an object with +1 retain :( |
79 | -+ (id)COPYRequestFromBucket:(NSString *)sourceBucket path:(NSString *)sourcePath toBucket:(NSString *)bucket path:(NSString *)path; | 79 | ++ (id)COPYRequestFromBucket:(NSString *)sourceBucket key:(NSString *)sourceKey toBucket:(NSString *)bucket key:(NSString *)key; |
80 | 80 | ||
81 | // Creates a HEAD request for the object at path | 81 | // Creates a HEAD request for the object at path |
82 | -+ (id)HEADRequestWithBucket:(NSString *)bucket path:(NSString *)path; | 82 | ++ (id)HEADRequestWithBucket:(NSString *)bucket key:(NSString *)key; |
83 | 83 | ||
84 | 84 | ||
85 | // Uses the supplied date to create a Date header string | 85 | // Uses the supplied date to create a Date header string |
86 | - (void)setDate:(NSDate *)date; | 86 | - (void)setDate:(NSDate *)date; |
87 | 87 | ||
88 | ++ (NSString *)stringByURLEncodingForS3Path:(NSString *)key; | ||
89 | + | ||
88 | #pragma mark Shared access keys | 90 | #pragma mark Shared access keys |
89 | 91 | ||
90 | // Get and set the global access key, this will be used for all requests the access key hasn't been set for | 92 | // Get and set the global access key, this will be used for all requests the access key hasn't been set for |
@@ -95,12 +97,12 @@ typedef enum _ASIS3ErrorType { | @@ -95,12 +97,12 @@ typedef enum _ASIS3ErrorType { | ||
95 | 97 | ||
96 | 98 | ||
97 | @property (retain) NSString *bucket; | 99 | @property (retain) NSString *bucket; |
98 | -@property (retain) NSString *path; | 100 | +@property (retain) NSString *key; |
99 | @property (retain) NSString *dateString; | 101 | @property (retain) NSString *dateString; |
100 | @property (retain) NSString *mimeType; | 102 | @property (retain) NSString *mimeType; |
101 | @property (retain) NSString *accessKey; | 103 | @property (retain) NSString *accessKey; |
102 | @property (retain) NSString *secretAccessKey; | 104 | @property (retain) NSString *secretAccessKey; |
103 | @property (retain) NSString *accessPolicy; | 105 | @property (retain) NSString *accessPolicy; |
104 | @property (retain) NSString *sourceBucket; | 106 | @property (retain) NSString *sourceBucket; |
105 | -@property (retain) NSString *sourcePath; | 107 | +@property (retain) NSString *sourceKey; |
106 | @end | 108 | @end |
@@ -29,25 +29,38 @@ static NSString *sharedSecretAccessKey = nil; | @@ -29,25 +29,38 @@ static NSString *sharedSecretAccessKey = nil; | ||
29 | 29 | ||
30 | #pragma mark Constructors | 30 | #pragma mark Constructors |
31 | 31 | ||
32 | -+ (id)requestWithBucket:(NSString *)bucket path:(NSString *)path | 32 | ++ (NSString *)stringByURLEncodingForS3Path:(NSString *)key |
33 | { | 33 | { |
34 | + if (!key) { | ||
35 | + return @"/"; | ||
36 | + } | ||
37 | + NSString *path = [(NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)key, NULL, CFSTR(":?#[]@!$ &'()*+,;=\"<>%{}|\\^~`"), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)) autorelease]; | ||
38 | + if (![[path substringWithRange:NSMakeRange(0, 1)] isEqualToString:@"/"]) { | ||
39 | + path = [@"/" stringByAppendingString:path]; | ||
40 | + } | ||
41 | + return path; | ||
42 | +} | ||
43 | + | ||
44 | ++ (id)requestWithBucket:(NSString *)bucket key:(NSString *)key | ||
45 | +{ | ||
46 | + NSString *path = [ASIS3Request stringByURLEncodingForS3Path:key]; | ||
34 | ASIS3Request *request = [[[self alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com%@",bucket,path]]] autorelease]; | 47 | ASIS3Request *request = [[[self alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com%@",bucket,path]]] autorelease]; |
35 | [request setBucket:bucket]; | 48 | [request setBucket:bucket]; |
36 | - [request setPath:path]; | 49 | + [request setKey:key]; |
37 | return request; | 50 | return request; |
38 | } | 51 | } |
39 | 52 | ||
40 | -+ (id)PUTRequestForData:(NSData *)data withBucket:(NSString *)bucket path:(NSString *)path | 53 | ++ (id)PUTRequestForData:(NSData *)data withBucket:(NSString *)bucket key:(NSString *)key |
41 | { | 54 | { |
42 | - ASIS3Request *request = [self requestWithBucket:bucket path:path]; | 55 | + ASIS3Request *request = [self requestWithBucket:bucket key:key]; |
43 | [request appendPostData:data]; | 56 | [request appendPostData:data]; |
44 | [request setRequestMethod:@"PUT"]; | 57 | [request setRequestMethod:@"PUT"]; |
45 | return request; | 58 | return request; |
46 | } | 59 | } |
47 | 60 | ||
48 | -+ (id)PUTRequestForFile:(NSString *)filePath withBucket:(NSString *)bucket path:(NSString *)path | 61 | ++ (id)PUTRequestForFile:(NSString *)filePath withBucket:(NSString *)bucket key:(NSString *)key |
49 | { | 62 | { |
50 | - ASIS3Request *request = [self requestWithBucket:bucket path:path]; | 63 | + ASIS3Request *request = [self requestWithBucket:bucket key:key]; |
51 | [request setPostBodyFilePath:filePath]; | 64 | [request setPostBodyFilePath:filePath]; |
52 | [request setShouldStreamPostDataFromDisk:YES]; | 65 | [request setShouldStreamPostDataFromDisk:YES]; |
53 | [request setRequestMethod:@"PUT"]; | 66 | [request setRequestMethod:@"PUT"]; |
@@ -55,25 +68,25 @@ static NSString *sharedSecretAccessKey = nil; | @@ -55,25 +68,25 @@ static NSString *sharedSecretAccessKey = nil; | ||
55 | return request; | 68 | return request; |
56 | } | 69 | } |
57 | 70 | ||
58 | -+ (id)DELETERequestWithBucket:(NSString *)bucket path:(NSString *)path | 71 | ++ (id)DELETERequestWithBucket:(NSString *)bucket key:(NSString *)key |
59 | { | 72 | { |
60 | - ASIS3Request *request = [self requestWithBucket:bucket path:path]; | 73 | + ASIS3Request *request = [self requestWithBucket:bucket key:key]; |
61 | [request setRequestMethod:@"DELETE"]; | 74 | [request setRequestMethod:@"DELETE"]; |
62 | return request; | 75 | return request; |
63 | } | 76 | } |
64 | 77 | ||
65 | -+ (id)COPYRequestFromBucket:(NSString *)sourceBucket path:(NSString *)sourcePath toBucket:(NSString *)bucket path:(NSString *)path | 78 | ++ (id)COPYRequestFromBucket:(NSString *)sourceBucket key:(NSString *)sourceKey toBucket:(NSString *)bucket key:(NSString *)key |
66 | { | 79 | { |
67 | - ASIS3Request *request = [self requestWithBucket:bucket path:path]; | 80 | + ASIS3Request *request = [self requestWithBucket:bucket key:key]; |
68 | [request setRequestMethod:@"PUT"]; | 81 | [request setRequestMethod:@"PUT"]; |
69 | [request setSourceBucket:sourceBucket]; | 82 | [request setSourceBucket:sourceBucket]; |
70 | - [request setSourcePath:sourcePath]; | 83 | + [request setSourceKey:sourceKey]; |
71 | return request; | 84 | return request; |
72 | } | 85 | } |
73 | 86 | ||
74 | -+ (id)HEADRequestWithBucket:(NSString *)bucket path:(NSString *)path | 87 | ++ (id)HEADRequestWithBucket:(NSString *)bucket key:(NSString *)key |
75 | { | 88 | { |
76 | - ASIS3Request *request = [self requestWithBucket:bucket path:path]; | 89 | + ASIS3Request *request = [self requestWithBucket:bucket key:key]; |
77 | [request setRequestMethod:@"HEAD"]; | 90 | [request setRequestMethod:@"HEAD"]; |
78 | return request; | 91 | return request; |
79 | } | 92 | } |
@@ -81,12 +94,12 @@ static NSString *sharedSecretAccessKey = nil; | @@ -81,12 +94,12 @@ static NSString *sharedSecretAccessKey = nil; | ||
81 | - (void)dealloc | 94 | - (void)dealloc |
82 | { | 95 | { |
83 | [bucket release]; | 96 | [bucket release]; |
84 | - [path release]; | 97 | + [key release]; |
85 | [dateString release]; | 98 | [dateString release]; |
86 | [mimeType release]; | 99 | [mimeType release]; |
87 | [accessKey release]; | 100 | [accessKey release]; |
88 | [secretAccessKey release]; | 101 | [secretAccessKey release]; |
89 | - [sourcePath release]; | 102 | + [sourceKey release]; |
90 | [sourceBucket release]; | 103 | [sourceBucket release]; |
91 | [super dealloc]; | 104 | [super dealloc]; |
92 | } | 105 | } |
@@ -106,7 +119,7 @@ static NSString *sharedSecretAccessKey = nil; | @@ -106,7 +119,7 @@ static NSString *sharedSecretAccessKey = nil; | ||
106 | ASIS3Request *headRequest = (ASIS3Request *)[super HEADRequest]; | 119 | ASIS3Request *headRequest = (ASIS3Request *)[super HEADRequest]; |
107 | [headRequest setAccessKey:[self accessKey]]; | 120 | [headRequest setAccessKey:[self accessKey]]; |
108 | [headRequest setSecretAccessKey:[self secretAccessKey]]; | 121 | [headRequest setSecretAccessKey:[self secretAccessKey]]; |
109 | - [headRequest setPath:[self path]]; | 122 | + [headRequest setKey:[self key]]; |
110 | [headRequest setBucket:[self bucket]]; | 123 | [headRequest setBucket:[self bucket]]; |
111 | return headRequest; | 124 | return headRequest; |
112 | } | 125 | } |
@@ -130,11 +143,7 @@ static NSString *sharedSecretAccessKey = nil; | @@ -130,11 +143,7 @@ static NSString *sharedSecretAccessKey = nil; | ||
130 | [self addRequestHeader:@"Date" value:[self dateString]]; | 143 | [self addRequestHeader:@"Date" value:[self dateString]]; |
131 | 144 | ||
132 | // Ensure our formatted string doesn't use '(null)' for the empty path | 145 | // Ensure our formatted string doesn't use '(null)' for the empty path |
133 | - if (![self path]) { | 146 | + NSString *canonicalizedResource = [NSString stringWithFormat:@"/%@%@",[self bucket],[ASIS3Request stringByURLEncodingForS3Path:[self key]]]; |
134 | - [self setPath:@"/"]; | ||
135 | - } | ||
136 | - | ||
137 | - NSString *canonicalizedResource = [NSString stringWithFormat:@"/%@%@",[self bucket],[self path]]; | ||
138 | 147 | ||
139 | // Add a header for the access policy if one was set, otherwise we won't add one (and S3 will default to private) | 148 | // Add a header for the access policy if one was set, otherwise we won't add one (and S3 will default to private) |
140 | NSMutableDictionary *amzHeaders = [[[NSMutableDictionary alloc] init] autorelease]; | 149 | NSMutableDictionary *amzHeaders = [[[NSMutableDictionary alloc] init] autorelease]; |
@@ -142,18 +151,19 @@ static NSString *sharedSecretAccessKey = nil; | @@ -142,18 +151,19 @@ static NSString *sharedSecretAccessKey = nil; | ||
142 | if ([self accessPolicy]) { | 151 | if ([self accessPolicy]) { |
143 | [amzHeaders setObject:[self accessPolicy] forKey:@"x-amz-acl"]; | 152 | [amzHeaders setObject:[self accessPolicy] forKey:@"x-amz-acl"]; |
144 | } | 153 | } |
145 | - if ([self sourcePath]) { | 154 | + if ([self sourceKey]) { |
146 | - [amzHeaders setObject:[[self sourceBucket] stringByAppendingString:[self sourcePath]] forKey:@"x-amz-copy-source"]; | 155 | + NSString *path = [ASIS3Request stringByURLEncodingForS3Path:[self sourceKey]]; |
156 | + [amzHeaders setObject:[[self sourceBucket] stringByAppendingString:path] forKey:@"x-amz-copy-source"]; | ||
147 | } | 157 | } |
148 | - for (NSString *key in [amzHeaders keyEnumerator]) { | 158 | + for (NSString *header in [amzHeaders keyEnumerator]) { |
149 | - canonicalizedAmzHeaders = [NSString stringWithFormat:@"%@%@:%@\n",canonicalizedAmzHeaders,[key lowercaseString],[amzHeaders objectForKey:key]]; | 159 | + canonicalizedAmzHeaders = [NSString stringWithFormat:@"%@%@:%@\n",canonicalizedAmzHeaders,[header lowercaseString],[amzHeaders objectForKey:header]]; |
150 | [self addRequestHeader:key value:[amzHeaders objectForKey:key]]; | 160 | [self addRequestHeader:key value:[amzHeaders objectForKey:key]]; |
151 | } | 161 | } |
152 | 162 | ||
153 | 163 | ||
154 | // Jump through hoops while eating hot food | 164 | // Jump through hoops while eating hot food |
155 | NSString *stringToSign; | 165 | NSString *stringToSign; |
156 | - if ([[self requestMethod] isEqualToString:@"PUT"] && ![self sourcePath]) { | 166 | + if ([[self requestMethod] isEqualToString:@"PUT"] && ![self sourceKey]) { |
157 | [self addRequestHeader:@"Content-Type" value:[self mimeType]]; | 167 | [self addRequestHeader:@"Content-Type" value:[self mimeType]]; |
158 | stringToSign = [NSString stringWithFormat:@"PUT\n\n%@\n%@\n%@%@",[self mimeType],dateString,canonicalizedAmzHeaders,canonicalizedResource]; | 168 | stringToSign = [NSString stringWithFormat:@"PUT\n\n%@\n%@\n%@%@",[self mimeType],dateString,canonicalizedAmzHeaders,canonicalizedResource]; |
159 | } else { | 169 | } else { |
@@ -170,7 +180,7 @@ static NSString *sharedSecretAccessKey = nil; | @@ -170,7 +180,7 @@ static NSString *sharedSecretAccessKey = nil; | ||
170 | - (void)requestFinished | 180 | - (void)requestFinished |
171 | { | 181 | { |
172 | // COPY requests return a 200 whether they succeed or fail, so we need to look at the XML to see if we were successful. | 182 | // COPY requests return a 200 whether they succeed or fail, so we need to look at the XML to see if we were successful. |
173 | - if ([self responseStatusCode] == 200 && [self sourcePath] && [self sourceBucket]) { | 183 | + if ([self responseStatusCode] == 200 && [self sourceKey] && [self sourceBucket]) { |
174 | [self parseError]; | 184 | [self parseError]; |
175 | return; | 185 | return; |
176 | } | 186 | } |
@@ -222,11 +232,11 @@ static NSString *sharedSecretAccessKey = nil; | @@ -222,11 +232,11 @@ static NSString *sharedSecretAccessKey = nil; | ||
222 | [newRequest setAccessKey:[self accessKey]]; | 232 | [newRequest setAccessKey:[self accessKey]]; |
223 | [newRequest setSecretAccessKey:[self secretAccessKey]]; | 233 | [newRequest setSecretAccessKey:[self secretAccessKey]]; |
224 | [newRequest setBucket:[self bucket]]; | 234 | [newRequest setBucket:[self bucket]]; |
225 | - [newRequest setPath:[self path]]; | 235 | + [newRequest setKey:[self key]]; |
226 | [newRequest setMimeType:[self mimeType]]; | 236 | [newRequest setMimeType:[self mimeType]]; |
227 | [newRequest setAccessPolicy:[self accessPolicy]]; | 237 | [newRequest setAccessPolicy:[self accessPolicy]]; |
228 | [newRequest setSourceBucket:[self sourceBucket]]; | 238 | [newRequest setSourceBucket:[self sourceBucket]]; |
229 | - [newRequest setSourcePath:[self sourcePath]]; | 239 | + [newRequest setSourceKey:[self sourceKey]]; |
230 | return newRequest; | 240 | return newRequest; |
231 | } | 241 | } |
232 | 242 | ||
@@ -277,7 +287,7 @@ static NSString *sharedSecretAccessKey = nil; | @@ -277,7 +287,7 @@ static NSString *sharedSecretAccessKey = nil; | ||
277 | } | 287 | } |
278 | 288 | ||
279 | @synthesize bucket; | 289 | @synthesize bucket; |
280 | -@synthesize path; | 290 | +@synthesize key; |
281 | @synthesize dateString; | 291 | @synthesize dateString; |
282 | @synthesize mimeType; | 292 | @synthesize mimeType; |
283 | @synthesize accessKey; | 293 | @synthesize accessKey; |
@@ -285,5 +295,5 @@ static NSString *sharedSecretAccessKey = nil; | @@ -285,5 +295,5 @@ static NSString *sharedSecretAccessKey = nil; | ||
285 | @synthesize accessPolicy; | 295 | @synthesize accessPolicy; |
286 | @synthesize currentErrorString; | 296 | @synthesize currentErrorString; |
287 | @synthesize sourceBucket; | 297 | @synthesize sourceBucket; |
288 | -@synthesize sourcePath; | 298 | +@synthesize sourceKey; |
289 | @end | 299 | @end |
This diff is collapsed. Click to expand it.
-
Please register or login to post a comment