Statistics
| Branch: | Revision:

root / asi-http-request-with-pithos / Classes / ASIWebPageRequest / ASIWebPageRequest.m @ be116d22

History | View | Annotate | Download (30.1 kB)

1
//
2
//  ASIWebPageRequest.m
3
//  Part of ASIHTTPRequest -> http://allseeing-i.com/ASIHTTPRequest
4
//
5
//  Created by Ben Copsey on 29/06/2010.
6
//  Copyright 2010 All-Seeing Interactive. All rights reserved.
7
//
8
//  This is an EXPERIMENTAL class - use at your own risk!
9

    
10
#import "ASIWebPageRequest.h"
11
#import "ASINetworkQueue.h"
12
#import <CommonCrypto/CommonHMAC.h>
13

    
14
// An xPath query that controls the external resources ASIWebPageRequest will fetch
15
// By default, it will fetch stylesheets, javascript files, images, frames, iframes, and html 5 video / audio
16
static xmlChar *xpathExpr = (xmlChar *)"//link/@href|//a/@href|//script/@src|//img/@src|//frame/@src|//iframe/@src|//style|//*/@style|//source/@src|//video/@poster|//audio/@src";
17

    
18
static NSLock *xmlParsingLock = nil;
19
static NSMutableArray *requestsUsingXMLParser = nil;
20

    
21
@interface ASIWebPageRequest ()
22
- (void)readResourceURLs;
23
- (void)updateResourceURLs;
24
- (void)parseAsHTML;
25
- (void)parseAsCSS;
26
- (void)addURLToFetch:(NSString *)newURL;
27
+ (NSArray *)CSSURLsFromString:(NSString *)string;
28
- (NSString *)relativePathTo:(NSString *)destinationPath fromPath:(NSString *)sourcePath;
29

    
30
- (void)finishedFetchingExternalResources:(ASINetworkQueue *)queue;
31
- (void)externalResourceFetchSucceeded:(ASIHTTPRequest *)externalResourceRequest;
32
- (void)externalResourceFetchFailed:(ASIHTTPRequest *)externalResourceRequest;
33

    
34
@property (retain, nonatomic) ASINetworkQueue *externalResourceQueue;
35
@property (retain, nonatomic) NSMutableDictionary *resourceList;
36
@end
37

    
38
@implementation ASIWebPageRequest
39

    
40
+ (void)initialize
41
{
42
	if (self == [ASIWebPageRequest class]) {
43
		xmlParsingLock = [[NSLock alloc] init];
44
		requestsUsingXMLParser = [[NSMutableArray alloc] init];
45
	}
46
}
47

    
48
- (void)dealloc
49
{
50
	[externalResourceQueue cancelAllOperations];
51
	[externalResourceQueue release];
52
	[resourceList release];
53
	[parentRequest release];
54
	[super dealloc];
55
}
56

    
57
// This is a bit of a hack
58
// The role of this method in normal ASIHTTPRequests is to tell the queue we are done with the request, and perform some cleanup
59
// We override it to stop that happening, and instead do that work in the bottom of finishedFetchingExternalResources:
60
- (void)markAsFinished
61
{
62
}
63

    
64
// This method is normally responsible for telling delegates we are done, but it happens to be the most convenient place to parse the responses
65
// Again, we call the super implementation in finishedFetchingExternalResources:, or here if this download was not an HTML or CSS file
66
- (void)requestFinished
67
{
68
	complete = NO;
69
	if ([self mainRequest] || [self didUseCachedResponse]) {
70
		[super requestFinished];
71
		[super markAsFinished];
72
		return;
73
	}
74
	webContentType = ASINotParsedWebContentType;
75
	NSString *contentType = [[[self responseHeaders] objectForKey:@"Content-Type"] lowercaseString];
76
	contentType = [[contentType componentsSeparatedByString:@";"] objectAtIndex:0];
77
	if ([contentType isEqualToString:@"text/html"] || [contentType isEqualToString:@"text/xhtml"] || [contentType isEqualToString:@"text/xhtml+xml"] || [contentType isEqualToString:@"application/xhtml+xml"]) {
78
		[self parseAsHTML];
79
		return;
80
	} else if ([contentType isEqualToString:@"text/css"]) {
81
		[self parseAsCSS];
82
		return;
83
	}
84
	[super requestFinished];
85
	[super markAsFinished];
86
}
87

    
88
- (void)parseAsCSS
89
{
90
	webContentType = ASICSSWebContentType;
91

    
92
	NSString *responseCSS = nil;
93
	NSError *err = nil;
94
	if ([self downloadDestinationPath]) {
95
		responseCSS = [NSString stringWithContentsOfFile:[self downloadDestinationPath] encoding:[self responseEncoding] error:&err];
96
	} else {
97
		responseCSS = [self responseString];
98
	}
99
	if (err) {
100
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:100 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to read HTML string from response",NSLocalizedDescriptionKey,err,NSUnderlyingErrorKey,nil]]];
101
		return;
102
	} else if (!responseCSS) {
103
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:100 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to read HTML string from response",NSLocalizedDescriptionKey,nil]]];
104
		return;
105
	}
106
	NSArray *urls = [[self class] CSSURLsFromString:responseCSS];
107

    
108
	[self setResourceList:[NSMutableDictionary dictionary]];
109

    
110
	for (NSString *theURL in urls) {
111
		[self addURLToFetch:theURL];
112
	}
113
	if (![[self resourceList] count]) {
114
		[super requestFinished];
115
		[super markAsFinished];
116
		return;
117
	}
118

    
119
	// Create a new request for every item in the queue
120
	[[self externalResourceQueue] cancelAllOperations];
121
	[self setExternalResourceQueue:[ASINetworkQueue queue]];
122
	[[self externalResourceQueue] setDelegate:self];
123
	[[self externalResourceQueue] setShowAccurateProgress:[self showAccurateProgress]];
124
	[[self externalResourceQueue] setQueueDidFinishSelector:@selector(finishedFetchingExternalResources:)];
125
	[[self externalResourceQueue] setRequestDidFinishSelector:@selector(externalResourceFetchSucceeded:)];
126
	[[self externalResourceQueue] setRequestDidFailSelector:@selector(externalResourceFetchFailed:)];
127
	for (NSString *theURL in [[self resourceList] keyEnumerator]) {
128
		ASIWebPageRequest *externalResourceRequest = [ASIWebPageRequest requestWithURL:[NSURL URLWithString:theURL relativeToURL:[self url]]];
129
		[externalResourceRequest setRequestHeaders:[self requestHeaders]];
130
		[externalResourceRequest setDownloadCache:[self downloadCache]];
131
		[externalResourceRequest setCachePolicy:[self cachePolicy]];
132
		[externalResourceRequest setCacheStoragePolicy:[self cacheStoragePolicy]];
133
		[externalResourceRequest setUserInfo:[NSDictionary dictionaryWithObject:theURL forKey:@"Path"]];
134
		[externalResourceRequest setParentRequest:self];
135
		[externalResourceRequest setUrlReplacementMode:[self urlReplacementMode]];
136
		[externalResourceRequest setShouldResetDownloadProgress:NO];
137
		[externalResourceRequest setDelegate:self];
138
		[externalResourceRequest setUploadProgressDelegate:self];
139
		[externalResourceRequest setDownloadProgressDelegate:self];
140
		if ([self downloadDestinationPath]) {
141
			[externalResourceRequest setDownloadDestinationPath:[self cachePathForRequest:externalResourceRequest]];
142
		}
143
		[[self externalResourceQueue] addOperation:externalResourceRequest];
144
	}
145
	[[self externalResourceQueue] go];
146
}
147

    
148
- (const char *)encodingName
149
{
150
	xmlCharEncoding encoding = XML_CHAR_ENCODING_NONE;
151
	switch ([self responseEncoding])
152
	{
153
		case NSASCIIStringEncoding:
154
			encoding = XML_CHAR_ENCODING_ASCII;
155
			break;
156
		case NSJapaneseEUCStringEncoding:
157
			encoding = XML_CHAR_ENCODING_EUC_JP;
158
			break;
159
		case NSUTF8StringEncoding:
160
			encoding = XML_CHAR_ENCODING_UTF8;
161
			break;
162
		case NSISOLatin1StringEncoding:
163
			encoding = XML_CHAR_ENCODING_8859_1;
164
			break;
165
		case NSShiftJISStringEncoding:
166
			encoding = XML_CHAR_ENCODING_SHIFT_JIS;
167
			break;
168
		case NSISOLatin2StringEncoding:
169
			encoding = XML_CHAR_ENCODING_8859_2;
170
			break;
171
		case NSISO2022JPStringEncoding:
172
			encoding = XML_CHAR_ENCODING_2022_JP;
173
			break;
174
		case NSUTF16BigEndianStringEncoding:
175
			encoding = XML_CHAR_ENCODING_UTF16BE;
176
			break;
177
		case NSUTF16LittleEndianStringEncoding:
178
			encoding = XML_CHAR_ENCODING_UTF16LE;
179
			break;
180
		case NSUTF32BigEndianStringEncoding:
181
			encoding = XML_CHAR_ENCODING_UCS4BE;
182
			break;
183
		case NSUTF32LittleEndianStringEncoding:
184
			encoding = XML_CHAR_ENCODING_UCS4LE;
185
			break;
186
		case NSNEXTSTEPStringEncoding:
187
		case NSSymbolStringEncoding:
188
		case NSNonLossyASCIIStringEncoding:
189
		case NSUnicodeStringEncoding:
190
		case NSMacOSRomanStringEncoding:
191
		case NSUTF32StringEncoding:
192
		default:
193
			encoding = XML_CHAR_ENCODING_ERROR;
194
			break;
195
	}
196
	return xmlGetCharEncodingName(encoding);
197
}
198

    
199
- (void)parseAsHTML
200
{
201
	webContentType = ASIHTMLWebContentType;
202

    
203
	// Only allow parsing of a single document at a time
204
	[xmlParsingLock lock];
205

    
206
	if (![requestsUsingXMLParser count]) {
207
		xmlInitParser();
208
	}
209
	[requestsUsingXMLParser addObject:self];
210

    
211

    
212
    /* Load XML document */
213
	if ([self downloadDestinationPath]) {
214
		doc = htmlReadFile([[self downloadDestinationPath] cStringUsingEncoding:NSUTF8StringEncoding], [self encodingName], HTML_PARSE_NONET | HTML_PARSE_NOWARNING | HTML_PARSE_NOERROR);
215
	} else {
216
		NSData *data = [self responseData];
217
		doc = htmlReadMemory([data bytes], (int)[data length], "", [self encodingName], HTML_PARSE_NONET | HTML_PARSE_NOWARNING | HTML_PARSE_NOERROR);
218
	}
219
    if (doc == NULL) {
220
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to parse reponse XML",NSLocalizedDescriptionKey,nil]]];
221
		return;
222
    }
223
	
224
	[self setResourceList:[NSMutableDictionary dictionary]];
225

    
226
    // Populate the list of URLS to download
227
    [self readResourceURLs];
228

    
229
	if ([self error] || ![[self resourceList] count]) {
230
		[requestsUsingXMLParser removeObject:self];
231
		xmlFreeDoc(doc);
232
		doc = NULL;
233
	}
234

    
235
	[xmlParsingLock unlock];
236

    
237
	if ([self error]) {
238
		return;
239
	} else if (![[self resourceList] count]) {
240
		[super requestFinished];
241
		[super markAsFinished];
242
		return;
243
	}
244
	
245
	// Create a new request for every item in the queue
246
	[[self externalResourceQueue] cancelAllOperations];
247
	[self setExternalResourceQueue:[ASINetworkQueue queue]];
248
	[[self externalResourceQueue] setDelegate:self];
249
	[[self externalResourceQueue] setShowAccurateProgress:[self showAccurateProgress]];
250
	[[self externalResourceQueue] setQueueDidFinishSelector:@selector(finishedFetchingExternalResources:)];
251
	[[self externalResourceQueue] setRequestDidFinishSelector:@selector(externalResourceFetchSucceeded:)];
252
	[[self externalResourceQueue] setRequestDidFailSelector:@selector(externalResourceFetchFailed:)];
253
	for (NSString *theURL in [[self resourceList] keyEnumerator]) {
254
		ASIWebPageRequest *externalResourceRequest = [ASIWebPageRequest requestWithURL:[NSURL URLWithString:theURL relativeToURL:[self url]]];
255
		[externalResourceRequest setRequestHeaders:[self requestHeaders]];
256
		[externalResourceRequest setDownloadCache:[self downloadCache]];
257
		[externalResourceRequest setCachePolicy:[self cachePolicy]];
258
		[externalResourceRequest setCacheStoragePolicy:[self cacheStoragePolicy]];
259
		[externalResourceRequest setUserInfo:[NSDictionary dictionaryWithObject:theURL forKey:@"Path"]];
260
		[externalResourceRequest setParentRequest:self];
261
		[externalResourceRequest setUrlReplacementMode:[self urlReplacementMode]];
262
		[externalResourceRequest setShouldResetDownloadProgress:NO];
263
		[externalResourceRequest setDelegate:self];
264
		[externalResourceRequest setUploadProgressDelegate:self];
265
		[externalResourceRequest setDownloadProgressDelegate:self];
266
		if ([self downloadDestinationPath]) {
267
			[externalResourceRequest setDownloadDestinationPath:[self cachePathForRequest:externalResourceRequest]];
268
		}
269
		[[self externalResourceQueue] addOperation:externalResourceRequest];
270
	}
271
	[[self externalResourceQueue] go];
272
}
273

    
274
- (void)externalResourceFetchSucceeded:(ASIHTTPRequest *)externalResourceRequest
275
{
276
	NSString *originalPath = [[externalResourceRequest userInfo] objectForKey:@"Path"];
277
	NSMutableDictionary *requestResponse = [[self resourceList] objectForKey:originalPath];
278
	NSString *contentType = [[externalResourceRequest responseHeaders] objectForKey:@"Content-Type"];
279
	if (!contentType) {
280
		contentType = @"application/octet-stream";
281
	}
282
	[requestResponse setObject:contentType forKey:@"ContentType"];
283
	if ([self downloadDestinationPath]) {
284
		[requestResponse setObject:[externalResourceRequest downloadDestinationPath] forKey:@"DataPath"];
285
	} else {
286
		NSData *data = [externalResourceRequest responseData];
287
		if (data) {
288
			[requestResponse setObject:data forKey:@"Data"];
289
		}
290
	}
291
}
292

    
293
- (void)externalResourceFetchFailed:(ASIHTTPRequest *)externalResourceRequest
294
{
295
	[self failWithError:[externalResourceRequest error]];
296
}
297

    
298
- (void)finishedFetchingExternalResources:(ASINetworkQueue *)queue
299
{
300
	if ([self urlReplacementMode] != ASIDontModifyURLs) {
301
		if (webContentType == ASICSSWebContentType) {
302
			NSMutableString *parsedResponse;
303
			NSError *err = nil;
304
			if ([self downloadDestinationPath]) {
305
				parsedResponse = [NSMutableString stringWithContentsOfFile:[self downloadDestinationPath] encoding:[self responseEncoding] error:&err];
306
			} else {
307
				parsedResponse = [[[self responseString] mutableCopy] autorelease];
308
			}
309
			if (err) {
310
				[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to read response CSS from disk",NSLocalizedDescriptionKey,nil]]];
311
				return;
312
			}
313
			if (![self error]) {
314
				for (NSString *resource in [[self resourceList] keyEnumerator]) {
315
					if ([parsedResponse rangeOfString:resource].location != NSNotFound) {
316
						NSString *newURL = [self contentForExternalURL:resource];
317
						if (newURL) {
318
							[parsedResponse replaceOccurrencesOfString:resource withString:newURL options:0 range:NSMakeRange(0, [parsedResponse length])];
319
						}
320
					}
321
				}
322
			}
323
			if ([self downloadDestinationPath]) {
324
				[parsedResponse writeToFile:[self downloadDestinationPath] atomically:NO encoding:[self responseEncoding] error:&err];
325
				if (err) {
326
					[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to write response CSS to disk",NSLocalizedDescriptionKey,nil]]];
327
					return;
328
				}
329
			} else {
330
				[self setRawResponseData:(id)[parsedResponse dataUsingEncoding:[self responseEncoding]]];
331
			}
332
		} else {
333
			[xmlParsingLock lock];
334

    
335
			[self updateResourceURLs];
336

    
337
			if (![self error]) {
338

    
339
				// We'll use the xmlsave API so we can strip the xml declaration
340
				xmlSaveCtxtPtr saveContext;
341

    
342
				if ([self downloadDestinationPath]) {
343

    
344
					// Truncate the file first
345
					[[[[NSFileManager alloc] init] autorelease] createFileAtPath:[self downloadDestinationPath] contents:nil attributes:nil];
346

    
347
					saveContext = xmlSaveToFd([[NSFileHandle fileHandleForWritingAtPath:[self downloadDestinationPath]] fileDescriptor],NULL,2); // 2 == XML_SAVE_NO_DECL, this isn't declared on Mac OS 10.5
348
					xmlSaveDoc(saveContext, doc);
349
					xmlSaveClose(saveContext);
350

    
351
				} else {
352
	#if TARGET_OS_MAC && MAC_OS_X_VERSION_MAX_ALLOWED <= __MAC_10_5
353
					// xmlSaveToBuffer() is not implemented in the 10.5 version of libxml
354
					NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
355
					[[[[NSFileManager alloc] init] autorelease] createFileAtPath:tempPath contents:nil attributes:nil];
356
					saveContext = xmlSaveToFd([[NSFileHandle fileHandleForWritingAtPath:tempPath] fileDescriptor],NULL,2); // 2 == XML_SAVE_NO_DECL, this isn't declared on Mac OS 10.5
357
					xmlSaveDoc(saveContext, doc);
358
					xmlSaveClose(saveContext);
359
					[self setRawResponseData:[NSMutableData dataWithContentsOfFile:tempPath]];
360
	#else
361
					xmlBufferPtr buffer = xmlBufferCreate();
362
					saveContext = xmlSaveToBuffer(buffer,NULL,2); // 2 == XML_SAVE_NO_DECL, this isn't declared on Mac OS 10.5
363
					xmlSaveDoc(saveContext, doc);
364
					xmlSaveClose(saveContext);
365
					[self setRawResponseData:[[[NSMutableData alloc] initWithBytes:buffer->content length:buffer->use] autorelease]];
366
					xmlBufferFree(buffer);
367
	#endif
368
				}
369

    
370
				// Strip the content encoding if the original response was gzipped
371
				if ([self isResponseCompressed]) {
372
					NSMutableDictionary *headers = [[[self responseHeaders] mutableCopy] autorelease];
373
					[headers removeObjectForKey:@"Content-Encoding"];
374
					[self setResponseHeaders:headers];
375
				}
376
			}
377

    
378
			xmlFreeDoc(doc);
379
			doc = nil;
380

    
381
			[requestsUsingXMLParser removeObject:self];
382
			if (![requestsUsingXMLParser count]) {
383
				xmlCleanupParser();
384
			}
385
			[xmlParsingLock unlock];
386
		}
387
	}
388
	if (![self parentRequest]) {
389
		[[self class] updateProgressIndicator:&downloadProgressDelegate withProgress:contentLength ofTotal:contentLength];
390
	}
391

    
392
	NSMutableDictionary *newHeaders = [[[self responseHeaders] mutableCopy] autorelease];
393
	[newHeaders removeObjectForKey:@"Content-Encoding"];
394
	[self setResponseHeaders:newHeaders];
395

    
396
	// Write the parsed content back to the cache
397
	if ([self urlReplacementMode] != ASIDontModifyURLs) {
398
		[[self downloadCache] storeResponseForRequest:self maxAge:[self secondsToCache]];
399
	}
400

    
401
	[super requestFinished];
402
	[super markAsFinished];
403
}
404

    
405
- (void)readResourceURLs
406
{
407
	// Create xpath evaluation context
408
    xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
409
    if(xpathCtx == NULL) {
410
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to create new XPath context",NSLocalizedDescriptionKey,nil]]];
411
		return;
412
    }
413

    
414
    // Evaluate xpath expression
415
    xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
416
    if(xpathObj == NULL) {
417
        xmlXPathFreeContext(xpathCtx); 
418
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to evaluate XPath expression!",NSLocalizedDescriptionKey,nil]]];
419
		return;
420
    }
421
	
422
	// Now loop through our matches
423
	xmlNodeSetPtr nodes = xpathObj->nodesetval;
424

    
425
    int size = (nodes) ? nodes->nodeNr : 0;
426
	int i;
427
    for(i = size - 1; i >= 0; i--) {
428
		assert(nodes->nodeTab[i]);
429
		NSString *parentName  = [NSString stringWithCString:(char *)nodes->nodeTab[i]->parent->name encoding:[self responseEncoding]];
430
		NSString *nodeName = [NSString stringWithCString:(char *)nodes->nodeTab[i]->name encoding:[self responseEncoding]];
431

    
432
		xmlChar *nodeValue = xmlNodeGetContent(nodes->nodeTab[i]);
433
		NSString *value = [NSString stringWithCString:(char *)nodeValue encoding:[self responseEncoding]];
434
		xmlFree(nodeValue);
435

    
436
		// Our xpath query matched all <link> elements, but we're only interested in stylesheets
437
		// We do the work here rather than in the xPath query because the query is case-sensitive, and we want to match on 'stylesheet', 'StyleSHEEt' etc
438
		if ([[parentName lowercaseString] isEqualToString:@"link"]) {
439
			xmlChar *relAttribute = xmlGetNoNsProp(nodes->nodeTab[i]->parent,(xmlChar *)"rel");
440
			if (relAttribute) {
441
				NSString *rel = [NSString stringWithCString:(char *)relAttribute encoding:[self responseEncoding]];
442
				xmlFree(relAttribute);
443
				if ([[rel lowercaseString] isEqualToString:@"stylesheet"]) {
444
					[self addURLToFetch:value];
445
				}
446
			}
447

    
448
		// Parse the content of <style> tags and style attributes to find external image urls or external css files
449
		} else if ([[nodeName lowercaseString] isEqualToString:@"style"]) {
450
			NSArray *externalResources = [[self class] CSSURLsFromString:value];
451
			for (NSString *theURL in externalResources) {
452
				[self addURLToFetch:theURL];
453
			}
454

    
455
		// Parse the content of <source src=""> tags (HTML 5 audio + video)
456
		// We explictly disable the download of files with .webm, .ogv and .ogg extensions, since it's highly likely they won't be useful to us
457
		} else if ([[parentName lowercaseString] isEqualToString:@"source"] || [[parentName lowercaseString] isEqualToString:@"audio"]) {
458
			NSString *fileExtension = [[value pathExtension] lowercaseString];
459
			if (![fileExtension isEqualToString:@"ogg"] && ![fileExtension isEqualToString:@"ogv"] && ![fileExtension isEqualToString:@"webm"]) {
460
				[self addURLToFetch:value];
461
			}
462

    
463
		// For all other elements matched by our xpath query (except hyperlinks), add the content as an external url to fetch
464
		} else if (![[parentName lowercaseString] isEqualToString:@"a"]) {
465
			[self addURLToFetch:value];
466
		}
467
		if (nodes->nodeTab[i]->type != XML_NAMESPACE_DECL) {
468
			nodes->nodeTab[i] = NULL;
469
		}
470
    }
471
	
472
	xmlXPathFreeObject(xpathObj);
473
    xmlXPathFreeContext(xpathCtx); 
474
}
475

    
476
- (void)addURLToFetch:(NSString *)newURL
477
{
478
	// Get rid of any surrounding whitespace
479
	newURL = [newURL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
480
	// Don't attempt to fetch data URIs
481
	if ([newURL length] > 4) {
482
		if (![[[newURL substringToIndex:5] lowercaseString] isEqualToString:@"data:"]) {
483
			NSURL *theURL = [NSURL URLWithString:newURL relativeToURL:[self url]];
484
			if (theURL) {
485
				if (![[self resourceList] objectForKey:newURL]) {
486
					[[self resourceList] setObject:[NSMutableDictionary dictionary] forKey:newURL];
487
				}
488
			}
489
		}
490
	}
491
}
492

    
493

    
494
- (void)updateResourceURLs
495
{
496
	// Create xpath evaluation context
497
	xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
498
	if(xpathCtx == NULL) {
499
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to create new XPath context",NSLocalizedDescriptionKey,nil]]];
500
		return;
501
	}
502

    
503
 	// Evaluate xpath expression
504
	xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
505
	if(xpathObj == NULL) {
506
		xmlXPathFreeContext(xpathCtx);
507
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to evaluate XPath expression!",NSLocalizedDescriptionKey,nil]]];
508
		return;
509
	}
510

    
511
	// Loop through all the matches, replacing urls where nescessary
512
	xmlNodeSetPtr nodes = xpathObj->nodesetval;
513
	int size = (nodes) ? nodes->nodeNr : 0;
514
	int i;
515
	for(i = size - 1; i >= 0; i--) {
516
		assert(nodes->nodeTab[i]);
517
		NSString *parentName  = [NSString stringWithCString:(char *)nodes->nodeTab[i]->parent->name encoding:[self responseEncoding]];
518
		NSString *nodeName  = [NSString stringWithCString:(char *)nodes->nodeTab[i]->name encoding:[self responseEncoding]];
519

    
520
		xmlChar *nodeValue = xmlNodeGetContent(nodes->nodeTab[i]);
521
		NSString *value = [NSString stringWithCString:(char *)nodeValue encoding:[self responseEncoding]];
522
		xmlFree(nodeValue);
523

    
524
		// Replace external urls in <style> tags or in style attributes
525
		if ([[nodeName lowercaseString] isEqualToString:@"style"]) {
526
			NSArray *externalResources = [[self class] CSSURLsFromString:value];
527
			for (NSString *theURL in externalResources) {
528
				if ([value rangeOfString:theURL].location != NSNotFound) {
529
					NSString *newURL = [self contentForExternalURL:theURL];
530
					if (newURL) {
531
						value = [value stringByReplacingOccurrencesOfString:theURL withString:newURL];
532
					}
533
				}
534
			}
535
			xmlNodeSetContent(nodes->nodeTab[i], (xmlChar *)[value cStringUsingEncoding:[self responseEncoding]]);
536

    
537
		// Replace relative hyperlinks with absolute ones, since we will need to set a local baseURL when loading this in a web view
538
		} else if ([self urlReplacementMode] == ASIReplaceExternalResourcesWithLocalURLs && [[parentName lowercaseString] isEqualToString:@"a"]) {
539
			NSString *newURL = [[NSURL URLWithString:value relativeToURL:[self url]] absoluteString];
540
			if (newURL) {
541
				xmlNodeSetContent(nodes->nodeTab[i], (xmlChar *)[newURL cStringUsingEncoding:[self responseEncoding]]);
542
			}
543

    
544
		// Replace all other external resource urls
545
		} else {
546
			NSString *newURL = [self contentForExternalURL:value];
547
			if (newURL) {
548
				xmlNodeSetContent(nodes->nodeTab[i], (xmlChar *)[newURL cStringUsingEncoding:[self responseEncoding]]);
549
			}
550
		}
551

    
552
		if (nodes->nodeTab[i]->type != XML_NAMESPACE_DECL) {
553
			nodes->nodeTab[i] = NULL;
554
		}
555
	}
556
	xmlXPathFreeObject(xpathObj);
557
	xmlXPathFreeContext(xpathCtx);
558
}
559

    
560
// The three methods below are responsible for forwarding delegate methods we want to handle to the parent request's approdiate delegate
561
// Certain delegate methods are ignored (eg setProgress: / setDoubleValue: / setMaxValue:)
562
- (BOOL)respondsToSelector:(SEL)selector
563
{
564
	if ([self parentRequest]) {
565
		return [[self parentRequest] respondsToSelector:selector];
566
	}
567
	//Ok, now check for selectors we want to pass on to the delegate
568
	if (selector == @selector(requestStarted:) || selector == @selector(request:didReceiveResponseHeaders:) || selector == @selector(request:willRedirectToURL:) || selector == @selector(requestFinished:) || selector == @selector(requestFailed:) || selector == @selector(request:didReceiveData:) || selector == @selector(authenticationNeededForRequest:) || selector == @selector(proxyAuthenticationNeededForRequest:)) {
569
		return [delegate respondsToSelector:selector];
570
	} else if (selector == @selector(request:didReceiveBytes:) || selector == @selector(request:incrementDownloadSizeBy:)) {
571
		return [downloadProgressDelegate respondsToSelector:selector];
572
	} else if (selector == @selector(request:didSendBytes:)  || selector == @selector(request:incrementUploadSizeBy:)) {
573
		return [uploadProgressDelegate respondsToSelector:selector];
574
	}
575
	return [super respondsToSelector:selector];
576
}
577

    
578
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
579
{
580
	if ([self parentRequest]) {
581
		return [[self parentRequest] methodSignatureForSelector:selector];
582
	}
583
	if (selector == @selector(requestStarted:) || selector == @selector(request:didReceiveResponseHeaders:) || selector == @selector(request:willRedirectToURL:) || selector == @selector(requestFinished:) || selector == @selector(requestFailed:) || selector == @selector(request:didReceiveData:) || selector == @selector(authenticationNeededForRequest:) || selector == @selector(proxyAuthenticationNeededForRequest:)) {
584
		return [(id)delegate methodSignatureForSelector:selector];
585
	} else if (selector == @selector(request:didReceiveBytes:) || selector == @selector(request:incrementDownloadSizeBy:)) {
586
		return [(id)downloadProgressDelegate methodSignatureForSelector:selector];
587
	} else if (selector == @selector(request:didSendBytes:)  || selector == @selector(request:incrementUploadSizeBy:)) {
588
		return [(id)uploadProgressDelegate methodSignatureForSelector:selector];
589
	}
590
	return nil;
591
}
592

    
593
- (void)forwardInvocation:(NSInvocation *)anInvocation
594
{
595
	if ([self parentRequest]) {
596
		return [[self parentRequest] forwardInvocation:anInvocation];
597
	}
598
	SEL selector = [anInvocation selector];
599
	if (selector == @selector(requestStarted:) || selector == @selector(request:didReceiveResponseHeaders:) || selector == @selector(request:willRedirectToURL:) || selector == @selector(requestFinished:) || selector == @selector(requestFailed:) || selector == @selector(request:didReceiveData:) || selector == @selector(authenticationNeededForRequest:) || selector == @selector(proxyAuthenticationNeededForRequest:)) {
600
		[anInvocation invokeWithTarget:delegate];
601
	} else if (selector == @selector(request:didReceiveBytes:) || selector == @selector(request:incrementDownloadSizeBy:)) {
602
		[anInvocation invokeWithTarget:downloadProgressDelegate];
603
	} else if (selector == @selector(request:didSendBytes:)  || selector == @selector(request:incrementUploadSizeBy:)) {
604
		[anInvocation invokeWithTarget:uploadProgressDelegate];
605
	}
606
}
607

    
608
// A quick and dirty way to build a list of external resource urls from a css string
609
+ (NSArray *)CSSURLsFromString:(NSString *)string
610
{
611
	NSMutableArray *urls = [NSMutableArray array];
612
	NSScanner *scanner = [NSScanner scannerWithString:string];
613
	[scanner setCaseSensitive:NO];
614
	while (1) {
615
		NSString *theURL = nil;
616
		[scanner scanUpToString:@"url(" intoString:NULL];
617
		[scanner scanString:@"url(" intoString:NULL];
618
		[scanner scanUpToString:@")" intoString:&theURL];
619
		if (!theURL) {
620
			break;
621
		}
622
		// Remove any quotes or whitespace around the url
623
		theURL = [theURL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
624
		theURL = [theURL stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\"'"]];
625
		theURL = [theURL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
626
		[urls addObject:theURL];
627
	}
628
	return urls;
629
}
630

    
631
// Returns a relative file path from sourcePath to destinationPath (eg ../../foo/bar.txt)
632
- (NSString *)relativePathTo:(NSString *)destinationPath fromPath:(NSString *)sourcePath
633
{
634
	NSArray *sourcePathComponents = [sourcePath pathComponents];
635
	NSArray *destinationPathComponents = [destinationPath pathComponents];
636
	NSUInteger i;
637
	NSString *newPath = @"";
638
	NSString *sourcePathComponent, *destinationPathComponent;
639
	for (i=0; i<[sourcePathComponents count]; i++) {
640
		sourcePathComponent = [sourcePathComponents objectAtIndex:i];
641
		if ([destinationPathComponents count] > i) {
642
			destinationPathComponent = [destinationPathComponents objectAtIndex:i];
643
			if (![sourcePathComponent isEqualToString:destinationPathComponent]) {
644
				NSUInteger i2;
645
				for (i2=i+1; i2<[sourcePathComponents count]; i2++) {
646
					newPath = [newPath stringByAppendingPathComponent:@".."];
647
				}
648
				newPath = [newPath stringByAppendingPathComponent:destinationPathComponent];
649
				for (i2=i+1; i2<[destinationPathComponents count]; i2++) {
650
					newPath = [newPath stringByAppendingPathComponent:[destinationPathComponents objectAtIndex:i2]];
651
				}
652
				break;
653
			}
654
		}
655
	}
656
	return newPath;
657
}
658

    
659
- (NSString *)contentForExternalURL:(NSString *)theURL
660
{
661
	if ([self urlReplacementMode] == ASIReplaceExternalResourcesWithLocalURLs) {
662
		NSString *resourcePath = [[resourceList objectForKey:theURL] objectForKey:@"DataPath"];
663
		return [self relativePathTo:resourcePath fromPath:[self downloadDestinationPath]];
664
	}
665
	NSData *data;
666
	if ([[resourceList objectForKey:theURL] objectForKey:@"DataPath"]) {
667
		data = [NSData dataWithContentsOfFile:[[resourceList objectForKey:theURL] objectForKey:@"DataPath"]];
668
	} else {
669
		data = [[resourceList objectForKey:theURL] objectForKey:@"Data"];
670
	}
671
	NSString *contentType = [[resourceList objectForKey:theURL] objectForKey:@"ContentType"];
672
	if (data && contentType) {
673
		NSString *dataURI = [NSString stringWithFormat:@"data:%@;base64,",contentType];
674
		dataURI = [dataURI stringByAppendingString:[ASIHTTPRequest base64forData:data]];
675
		return dataURI;
676
	}
677
	return nil;
678
}
679

    
680
- (NSString *)cachePathForRequest:(ASIWebPageRequest *)theRequest
681
{
682
	// If we're using a download cache (and its a good idea to do so when using ASIWebPageRequest), ask it for the location to store this file
683
	// This ends up being quite efficient, as we download directly to the cache
684
	if ([self downloadCache]) {
685
		return [[self downloadCache] pathToStoreCachedResponseDataForRequest:theRequest];
686

    
687
	// This is a fallback for when we don't have a download cache - we store the external resource in a file in the temporary directory
688
	} else {
689
		// Borrowed from: http://stackoverflow.com/questions/652300/using-md5-hash-on-a-string-in-cocoa
690
		const char *cStr = [[[theRequest url] absoluteString] UTF8String];
691
		unsigned char result[16];
692
		CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
693
		NSString *md5 = [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],result[8], result[9], result[10], result[11],result[12], result[13], result[14], result[15]]; 	
694
		return [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:[md5 stringByAppendingPathExtension:@"html"]];
695
	}
696
}
697

    
698

    
699
@synthesize externalResourceQueue;
700
@synthesize resourceList;
701
@synthesize parentRequest;
702
@synthesize urlReplacementMode;
703
@end