forked from ViennaRSS/vienna-rss
-
Notifications
You must be signed in to change notification settings - Fork 1
/
AsyncConnection.m
461 lines (408 loc) · 13.4 KB
/
AsyncConnection.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
//
// AsyncConnection.m
// Vienna
//
// Created by Steve on 6/16/05.
// Copyright (c) 2004-2005 Steve Palmer. 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 "ViennaApp.h"
#import "Constants.h"
#import "AsyncConnection.h"
#import "StringExtensions.h"
// Private functions
@interface AsyncConnection (Private)
-(void)sendConnectionCompleteNotification;
@end
@implementation AsyncConnection
/* init
* Initialise an instance of the class.
*/
-(id)init
{
if ((self = [super init]) != nil)
{
receivedData = [[NSMutableData alloc] init];
username = nil;
password = nil;
handler = nil;
delegate = nil;
aItem = nil;
contextData = nil;
connector = nil;
httpHeaders = nil;
URLString = nil;
status = MA_Connect_Succeeded;
isConnectionComplete = YES;
}
return self;
}
/* receivedData
* Return the data received by this connection.
*/
-(NSData *)receivedData
{
return receivedData;
}
/* responseHeaders
* Return the dictionary of the responses from the request. This will be nil if no
* request has been initiated or any response was obtained.
*/
-(NSDictionary *)responseHeaders
{
return responseHeaders;
}
/* aItem
* Return the activity log associated with this connection.
*/
-(ActivityItem *)aItem
{
return aItem;
}
/* URLString
* Returns the URL of this connection.
*/
-(NSString *)URLString
{
return URLString;
}
/* contextData
* Returns the context data object that was originally passed when the connection
* was created.
*/
-(id)contextData
{
return contextData;
}
/* status
* Return the status of the last connection.
*/
-(ConnectStatus)status
{
return status;
}
/* setHttpHeaders
* Set the HTTP header fields to be passed to the connection.
*/
-(void)setHttpHeaders:(NSDictionary *)headerFields
{
[headerFields retain];
[httpHeaders release];
httpHeaders = headerFields;
}
/* setURLString
* Sets the current URL of this connection.
*/
-(void)setURLString:(NSString *)newURLString
{
[newURLString retain];
[URLString release];
URLString = newURLString;
}
/* beginLoadDataFromURL
* Begin an asynchronous connection using the specified URL, username, password and callback information. On completion of
* the connection, whether or not the connection succeeded, the callback is invoked. The user will need to query the object
* passed to determine whether it succeeded and to get at the raw data.
*/
-(BOOL)beginLoadDataFromURL:(NSURL *)theUrl
username:(NSString *)theUsername
password:(NSString *)thePassword
delegate:(id)theDelegate
contextData:(id)theData
log:(ActivityItem *)theItem
didEndSelector:(SEL)endSelector
{
[username release];
[password release];
[contextData release];
[aItem release];
username = [theUsername retain];
password = [thePassword retain];
contextData = [theData retain];
aItem = [theItem retain];
delegate = theDelegate;
handler = endSelector;
[self setURLString:[theUrl absoluteString]];
NSMutableURLRequest * theRequest = [NSMutableURLRequest requestWithURL:theUrl cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60.0];
if (theRequest == nil)
return NO;
if (httpHeaders != nil)
{
for (NSString * httpFieldName in httpHeaders)
{
NSString * fieldValue = [httpHeaders objectForKey:httpFieldName];
if (fieldValue != nil && ![fieldValue isBlank])
[theRequest addValue:fieldValue forHTTPHeaderField:httpFieldName];
}
}
// Some sites refuse to respond without a User-agent string.
[theRequest addValue:[NSString stringWithFormat:MA_DefaultUserAgentString, [((ViennaApp *)NSApp) applicationVersion]] forHTTPHeaderField:@"User-agent"];
status = MA_Connect_Stopped;
isConnectionComplete = NO;
// Changed to not use the [NSURLConnection connectionWithRequest:delegate: and then retain,
// no sense putting it in an autorelease pool if it has no reason to be there
connector = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
return connector != nil;
}
/* cancel
* Cancel the existing connection.
*/
-(void)cancel
{
if (!isConnectionComplete)
{
isConnectionComplete = YES;
[aItem setStatus:NSLocalizedString(@"Refresh cancelled", nil)];
status = MA_Connect_Cancelled;
[connector cancel];
[self sendConnectionCompleteNotification];
}
else if (status == MA_Connect_NeedCredentials)
{
[aItem setStatus:NSLocalizedString(@"Refresh cancelled", nil)];
status = MA_Connect_Cancelled;
[connector cancel];
// Complete notification already scheduled.
}
}
/* close
* Closes the active connection.
*/
-(void)close
{
[connector cancel];
[connector release];
connector = nil;
}
/* sendConnectionCompleteNotification
* Sends a connection completion notification to the main code.
*/
-(void)sendConnectionCompleteNotification
{
isConnectionComplete = YES;
[[NSRunLoop currentRunLoop] performSelector:handler target:delegate argument:self order:0 modes:[NSArray arrayWithObjects:NSDefaultRunLoopMode, nil]];
}
/* didReceiveResponse
* 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.
*/
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
if ([response isKindOfClass:[NSHTTPURLResponse class]])
{
NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)response;
[responseHeaders release];
responseHeaders = [[httpResponse allHeaderFields] retain];
// Report HTTP code to the log
if (aItem != nil)
{
NSMutableString * headerDetail = [[NSMutableString alloc] init];
NSString * logText = [NSString stringWithFormat:NSLocalizedString(@"HTTP code %d reported from server", nil), [httpResponse statusCode]];
[aItem appendDetail:logText];
// Add the HTTP headers details to the log for this connection for
// debugging purposes.
[headerDetail setString:NSLocalizedString(@"Headers:\n", nil)];
for (NSString * headerField in responseHeaders)
{
[headerDetail appendFormat:@"\t%@: %@\n", headerField, [[httpResponse allHeaderFields] valueForKey:headerField]];
}
[aItem appendDetail:headerDetail];
[headerDetail release];
}
// Get the HTTP response code and handle appropriately:
// Code 200 means OK, more data to come.
// Code 304 means the feed hasn't changed since the last refresh.
// Code 410 means the feed has been intentionally removed by the server.
if ([httpResponse statusCode] == 200)
{
// Is this GZIP encoded?
// If so, report it to the detail log.
NSString * contentEncoding = [responseHeaders valueForKey:@"Content-Encoding"];
if ([[contentEncoding lowercaseString] isEqualToString:@"gzip"])
[aItem appendDetail:NSLocalizedString(@"Article feed will be compressed", nil)];
}
else if ([httpResponse statusCode] == 410)
{
// The URL has been intentionally removed.
[connection cancel];
// Report to the activity log
[aItem setStatus:NSLocalizedString(@"Feed has been removed by the server", nil)];
// Complete this connection
status = MA_Connect_URLIsGone;
[self sendConnectionCompleteNotification];
}
else if ([httpResponse statusCode] == 304)
{
// The feed hasn't changed since our last modification so abort this
// connection to save time.
[connection cancel];
// Report to the activity log
[aItem setStatus:NSLocalizedString(@"No new articles available", nil)];
// Complete this connection
status = MA_Connect_Stopped;
[self sendConnectionCompleteNotification];
}
}
}
/* didReceiveData
* We received a new block of data from the remote. Append it to what we
* have so far.
*/
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}
/* willCacheResponse
* Handle the connection's caching of the response data. We return nil to tell it not to cache anything in order to
* try and cut down our memory usage.
*/
-(NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
return nil;
}
/* didFailWithError
* The remote connection failed somehow. Don't do anything with the data we got
* so far but report an error against this connection.
*/
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
// Report the failure to the log
NSString * logText = [NSString stringWithFormat:NSLocalizedString(@"Error: %@", nil), [error localizedDescription]];
[aItem setStatus:logText];
// More details to go into the log for troubleshooting
// purposes.
NSMutableString * logDetail = [[NSMutableString alloc] init];
[logDetail appendFormat:NSLocalizedString(@"Connection error (%d, %@):\n", nil), [error code], [error domain]];
if ([error localizedDescription] != nil)
[logDetail appendFormat:NSLocalizedString(@"\tDescription: %@\n", nil), [error localizedDescription]];
NSString * suggestionString = [error localizedRecoverySuggestion];
if (suggestionString != nil)
[logDetail appendFormat:NSLocalizedString(@"\tSuggestion: %@\n", nil), suggestionString];
NSString * reasonString = [error localizedFailureReason];
if (reasonString != nil)
[logDetail appendFormat:NSLocalizedString(@"\tCause: %@\n", nil), reasonString];
[aItem appendDetail:logDetail];
[logDetail release];
// Complete the connection
if (status != MA_Connect_NeedCredentials)
status = MA_Connect_Failed;
[self sendConnectionCompleteNotification];
}
/* didReceiveAuthenticationChallenge
* We got an authentication challenge from the remote site. OK. Dig out that username
* that ought to be on the folder and try it. Otherwise prompt the user. If this is
* the second time we got this challenge then the previous credentials didn't work so
* fail.
*/
-(void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
BOOL succeeded = NO;
if ([challenge previousFailureCount] < 2)
{
if (![username isBlank])
{
NSURLCredential * newCredential = [NSURLCredential credentialWithUser:username password:password persistence:NSURLCredentialPersistencePermanent];
[[challenge sender] useCredential:newCredential forAuthenticationChallenge:challenge];
// More details in the log
NSString * logText = [NSString stringWithFormat:NSLocalizedString(@"Attempting authentication for user '%@'", nil), username];
[aItem appendDetail:logText];
succeeded = YES;
}
}
else
{
// Report the failure to the log (both as status and detail)
NSString * logText = [NSString stringWithFormat:NSLocalizedString(@"Authentication failed for user '%@'", nil), username];
[aItem setStatus:logText];
[aItem appendDetail:logText];
}
// If we failed, cancel the authentication challenge which will, in turn, cancel the
// entire connection.
if (!succeeded)
{
[[challenge sender] cancelAuthenticationChallenge:challenge];
status = MA_Connect_NeedCredentials;
}
}
/* willSendRequest
* Handle connect redirection. Always allow it. NOTE: This gets called once
* per request whether or not there is a redirect request in process or not.
*
* According to the Apple Docs:
*
* redirectResponse
* The redirect server response. If nil, there is no redirect in progress.
*
* So we don't do any logging if redirectResponse is nil.
*/
-(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse
{
if (redirectResponse != nil)
{
NSString * newURLString = [[request URL] absoluteString];
NSString * text = nil;
// Remember the new URL string.
[self setURLString:newURLString];
// Get the redirect detail text.
if ([redirectResponse isKindOfClass:[NSHTTPURLResponse class]])
{
NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)redirectResponse;
text = [NSString stringWithFormat:NSLocalizedString(@"Redirecting (%d) to %@", nil), [httpResponse statusCode], newURLString];
}
else
{
text = [NSString stringWithFormat:NSLocalizedString(@"Redirecting to %@", nil), newURLString];
}
/*if ([redirectResponse isKindOfClass:[NSHTTPURLResponse class]])
{
NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)redirectResponse;
if ([httpResponse statusCode] == 301)
{
status = MA_Connect_PermanentRedirect;
[delegate performSelector:handler withObject:self];
}
}*/
// Add the detail text.
[aItem appendDetail:text];
}
// Just return the unaltered request.
return request;
}
/* connectionDidFinishLoading
* Called when all data has been retrieved.
*/
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
status = MA_Connect_Succeeded;
[self sendConnectionCompleteNotification];
}
/* dealloc
* Clean up after ourselves.
*/
-(void)dealloc
{
[connector release];
[httpHeaders release];
[responseHeaders release];
[contextData release];
[URLString release];
[receivedData release];
[username release];
[password release];
[aItem release];
[super dealloc];
}
@end