Statistics
| Branch: | Revision:

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

History | View | Annotate | Download (30.8 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
#import <libxml/HTMLparser.h>
14
#import <libxml/xmlsave.h>
15
#import <libxml/xpath.h>
16
#import <libxml/xpathInternals.h>
17

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

    
22
static NSLock *xmlParsingLock = nil;
23
static NSMutableArray *requestsUsingXMLParser = nil;
24

    
25
@interface ASIWebPageRequest ()
26
- (void)readResourceURLs;
27
- (void)updateResourceURLs;
28
- (void)parseAsHTML;
29
- (void)parseAsCSS;
30
- (void)addURLToFetch:(NSString *)newURL;
31
+ (NSArray *)CSSURLsFromString:(NSString *)string;
32
- (NSString *)relativePathTo:(NSString *)destinationPath fromPath:(NSString *)sourcePath;
33

    
34
- (void)finishedFetchingExternalResources:(ASINetworkQueue *)queue;
35
- (void)externalResourceFetchSucceeded:(ASIHTTPRequest *)externalResourceRequest;
36
- (void)externalResourceFetchFailed:(ASIHTTPRequest *)externalResourceRequest;
37

    
38
@property (retain, nonatomic) ASINetworkQueue *externalResourceQueue;
39
@property (retain, nonatomic) NSMutableDictionary *resourceList;
40
@end
41

    
42
@implementation ASIWebPageRequest
43

    
44
+ (void)initialize
45
{
46
	if (self == [ASIWebPageRequest class]) {
47
		xmlParsingLock = [[NSLock alloc] init];
48
		requestsUsingXMLParser = [[NSMutableArray alloc] init];
49
	}
50
}
51

    
52
- (id)initWithURL:(NSURL *)newURL
53
{
54
	self = [super initWithURL:newURL];
55
	[self setShouldIgnoreExternalResourceErrors:YES];
56
	return self;
57
}
58

    
59
- (void)dealloc
60
{
61
	[externalResourceQueue cancelAllOperations];
62
	[externalResourceQueue release];
63
	[resourceList release];
64
	[parentRequest release];
65
	[super dealloc];
66
}
67

    
68
// This is a bit of a hack
69
// The role of this method in normal ASIHTTPRequests is to tell the queue we are done with the request, and perform some cleanup
70
// We override it to stop that happening, and instead do that work in the bottom of finishedFetchingExternalResources:
71
- (void)markAsFinished
72
{
73
	if ([self error]) {
74
		[super markAsFinished];
75
	}
76
}
77

    
78
// This method is normally responsible for telling delegates we are done, but it happens to be the most convenient place to parse the responses
79
// Again, we call the super implementation in finishedFetchingExternalResources:, or here if this download was not an HTML or CSS file
80
- (void)requestFinished
81
{
82
	complete = NO;
83
	if ([self mainRequest] || [self didUseCachedResponse]) {
84
		[super requestFinished];
85
		[super markAsFinished];
86
		return;
87
	}
88
	webContentType = ASINotParsedWebContentType;
89
	NSString *contentType = [[[self responseHeaders] objectForKey:@"Content-Type"] lowercaseString];
90
	contentType = [[contentType componentsSeparatedByString:@";"] objectAtIndex:0];
91
	if ([contentType isEqualToString:@"text/html"] || [contentType isEqualToString:@"text/xhtml"] || [contentType isEqualToString:@"text/xhtml+xml"] || [contentType isEqualToString:@"application/xhtml+xml"]) {
92
		[self parseAsHTML];
93
		return;
94
	} else if ([contentType isEqualToString:@"text/css"]) {
95
		[self parseAsCSS];
96
		return;
97
	}
98
	[super requestFinished];
99
	[super markAsFinished];
100
}
101

    
102
- (void)parseAsCSS
103
{
104
	webContentType = ASICSSWebContentType;
105

    
106
	NSString *responseCSS = nil;
107
	NSError *err = nil;
108
	if ([self downloadDestinationPath]) {
109
		responseCSS = [NSString stringWithContentsOfFile:[self downloadDestinationPath] encoding:[self responseEncoding] error:&err];
110
	} else {
111
		responseCSS = [self responseString];
112
	}
113
	if (err) {
114
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:100 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to read HTML string from response",NSLocalizedDescriptionKey,err,NSUnderlyingErrorKey,nil]]];
115
		return;
116
	} else if (!responseCSS) {
117
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:100 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to read HTML string from response",NSLocalizedDescriptionKey,nil]]];
118
		return;
119
	}
120
	NSArray *urls = [[self class] CSSURLsFromString:responseCSS];
121

    
122
	[self setResourceList:[NSMutableDictionary dictionary]];
123

    
124
	for (NSString *theURL in urls) {
125
		[self addURLToFetch:theURL];
126
	}
127
	if (![[self resourceList] count]) {
128
		[super requestFinished];
129
		[super markAsFinished];
130
		return;
131
	}
132

    
133
	// Create a new request for every item in the queue
134
	[[self externalResourceQueue] cancelAllOperations];
135
	[self setExternalResourceQueue:[ASINetworkQueue queue]];
136
	[[self externalResourceQueue] setDelegate:self];
137
	[[self externalResourceQueue] setShouldCancelAllRequestsOnFailure:[self shouldIgnoreExternalResourceErrors]];
138
	[[self externalResourceQueue] setShowAccurateProgress:[self showAccurateProgress]];
139
	[[self externalResourceQueue] setQueueDidFinishSelector:@selector(finishedFetchingExternalResources:)];
140
	[[self externalResourceQueue] setRequestDidFinishSelector:@selector(externalResourceFetchSucceeded:)];
141
	[[self externalResourceQueue] setRequestDidFailSelector:@selector(externalResourceFetchFailed:)];
142
	for (NSString *theURL in [[self resourceList] keyEnumerator]) {
143
		ASIWebPageRequest *externalResourceRequest = [ASIWebPageRequest requestWithURL:[NSURL URLWithString:theURL relativeToURL:[self url]]];
144
		[externalResourceRequest setRequestHeaders:[self requestHeaders]];
145
		[externalResourceRequest setDownloadCache:[self downloadCache]];
146
		[externalResourceRequest setCachePolicy:[self cachePolicy]];
147
		[externalResourceRequest setCacheStoragePolicy:[self cacheStoragePolicy]];
148
		[externalResourceRequest setUserInfo:[NSDictionary dictionaryWithObject:theURL forKey:@"Path"]];
149
		[externalResourceRequest setParentRequest:self];
150
		[externalResourceRequest setUrlReplacementMode:[self urlReplacementMode]];
151
		[externalResourceRequest setShouldResetDownloadProgress:NO];
152
		[externalResourceRequest setDelegate:self];
153
		[externalResourceRequest setUploadProgressDelegate:self];
154
		[externalResourceRequest setDownloadProgressDelegate:self];
155
		if ([self downloadDestinationPath]) {
156
			[externalResourceRequest setDownloadDestinationPath:[self cachePathForRequest:externalResourceRequest]];
157
		}
158
		[[self externalResourceQueue] addOperation:externalResourceRequest];
159
	}
160
	[[self externalResourceQueue] go];
161
}
162

    
163
- (const char *)encodingName
164
{
165
	xmlCharEncoding encoding = XML_CHAR_ENCODING_NONE;
166
	switch ([self responseEncoding])
167
	{
168
		case NSASCIIStringEncoding:
169
			encoding = XML_CHAR_ENCODING_ASCII;
170
			break;
171
		case NSJapaneseEUCStringEncoding:
172
			encoding = XML_CHAR_ENCODING_EUC_JP;
173
			break;
174
		case NSUTF8StringEncoding:
175
			encoding = XML_CHAR_ENCODING_UTF8;
176
			break;
177
		case NSISOLatin1StringEncoding:
178
			encoding = XML_CHAR_ENCODING_8859_1;
179
			break;
180
		case NSShiftJISStringEncoding:
181
			encoding = XML_CHAR_ENCODING_SHIFT_JIS;
182
			break;
183
		case NSISOLatin2StringEncoding:
184
			encoding = XML_CHAR_ENCODING_8859_2;
185
			break;
186
		case NSISO2022JPStringEncoding:
187
			encoding = XML_CHAR_ENCODING_2022_JP;
188
			break;
189
		case NSUTF16BigEndianStringEncoding:
190
			encoding = XML_CHAR_ENCODING_UTF16BE;
191
			break;
192
		case NSUTF16LittleEndianStringEncoding:
193
			encoding = XML_CHAR_ENCODING_UTF16LE;
194
			break;
195
		case NSUTF32BigEndianStringEncoding:
196
			encoding = XML_CHAR_ENCODING_UCS4BE;
197
			break;
198
		case NSUTF32LittleEndianStringEncoding:
199
			encoding = XML_CHAR_ENCODING_UCS4LE;
200
			break;
201
		case NSNEXTSTEPStringEncoding:
202
		case NSSymbolStringEncoding:
203
		case NSNonLossyASCIIStringEncoding:
204
		case NSUnicodeStringEncoding:
205
		case NSMacOSRomanStringEncoding:
206
		case NSUTF32StringEncoding:
207
		default:
208
			encoding = XML_CHAR_ENCODING_ERROR;
209
			break;
210
	}
211
	return xmlGetCharEncodingName(encoding);
212
}
213

    
214
- (void)parseAsHTML
215
{
216
	webContentType = ASIHTMLWebContentType;
217

    
218
	// Only allow parsing of a single document at a time
219
	[xmlParsingLock lock];
220

    
221
	if (![requestsUsingXMLParser count]) {
222
		xmlInitParser();
223
	}
224
	[requestsUsingXMLParser addObject:self];
225

    
226

    
227
    /* Load XML document */
228
	if ([self downloadDestinationPath]) {
229
		doc = htmlReadFile([[self downloadDestinationPath] cStringUsingEncoding:NSUTF8StringEncoding], [self encodingName], HTML_PARSE_NONET | HTML_PARSE_NOWARNING | HTML_PARSE_NOERROR);
230
	} else {
231
		NSData *data = [self responseData];
232
		doc = htmlReadMemory([data bytes], (int)[data length], "", [self encodingName], HTML_PARSE_NONET | HTML_PARSE_NOWARNING | HTML_PARSE_NOERROR);
233
	}
234
    if (doc == NULL) {
235
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to parse reponse XML",NSLocalizedDescriptionKey,nil]]];
236
		return;
237
    }
238
	
239
	[self setResourceList:[NSMutableDictionary dictionary]];
240

    
241
    // Populate the list of URLS to download
242
    [self readResourceURLs];
243

    
244
	if ([self error] || ![[self resourceList] count]) {
245
		[requestsUsingXMLParser removeObject:self];
246
		xmlFreeDoc(doc);
247
		doc = NULL;
248
	}
249

    
250
	[xmlParsingLock unlock];
251

    
252
	if ([self error]) {
253
		return;
254
	} else if (![[self resourceList] count]) {
255
		[super requestFinished];
256
		[super markAsFinished];
257
		return;
258
	}
259
	
260
	// Create a new request for every item in the queue
261
	[[self externalResourceQueue] cancelAllOperations];
262
	[self setExternalResourceQueue:[ASINetworkQueue queue]];
263
	[[self externalResourceQueue] setDelegate:self];
264
	[[self externalResourceQueue] setShouldCancelAllRequestsOnFailure:[self shouldIgnoreExternalResourceErrors]];
265
	[[self externalResourceQueue] setShowAccurateProgress:[self showAccurateProgress]];
266
	[[self externalResourceQueue] setQueueDidFinishSelector:@selector(finishedFetchingExternalResources:)];
267
	[[self externalResourceQueue] setRequestDidFinishSelector:@selector(externalResourceFetchSucceeded:)];
268
	[[self externalResourceQueue] setRequestDidFailSelector:@selector(externalResourceFetchFailed:)];
269
	for (NSString *theURL in [[self resourceList] keyEnumerator]) {
270
		ASIWebPageRequest *externalResourceRequest = [ASIWebPageRequest requestWithURL:[NSURL URLWithString:theURL relativeToURL:[self url]]];
271
		[externalResourceRequest setRequestHeaders:[self requestHeaders]];
272
		[externalResourceRequest setDownloadCache:[self downloadCache]];
273
		[externalResourceRequest setCachePolicy:[self cachePolicy]];
274
		[externalResourceRequest setCacheStoragePolicy:[self cacheStoragePolicy]];
275
		[externalResourceRequest setUserInfo:[NSDictionary dictionaryWithObject:theURL forKey:@"Path"]];
276
		[externalResourceRequest setParentRequest:self];
277
		[externalResourceRequest setUrlReplacementMode:[self urlReplacementMode]];
278
		[externalResourceRequest setShouldResetDownloadProgress:NO];
279
		[externalResourceRequest setDelegate:self];
280
		[externalResourceRequest setUploadProgressDelegate:self];
281
		[externalResourceRequest setDownloadProgressDelegate:self];
282
		if ([self downloadDestinationPath]) {
283
			[externalResourceRequest setDownloadDestinationPath:[self cachePathForRequest:externalResourceRequest]];
284
		}
285
		[[self externalResourceQueue] addOperation:externalResourceRequest];
286
	}
287
	[[self externalResourceQueue] go];
288
}
289

    
290
- (void)externalResourceFetchSucceeded:(ASIHTTPRequest *)externalResourceRequest
291
{
292
	NSString *originalPath = [[externalResourceRequest userInfo] objectForKey:@"Path"];
293
	NSMutableDictionary *requestResponse = [[self resourceList] objectForKey:originalPath];
294
	NSString *contentType = [[externalResourceRequest responseHeaders] objectForKey:@"Content-Type"];
295
	if (!contentType) {
296
		contentType = @"application/octet-stream";
297
	}
298
	[requestResponse setObject:contentType forKey:@"ContentType"];
299
	if ([self downloadDestinationPath]) {
300
		[requestResponse setObject:[externalResourceRequest downloadDestinationPath] forKey:@"DataPath"];
301
	} else {
302
		NSData *data = [externalResourceRequest responseData];
303
		if (data) {
304
			[requestResponse setObject:data forKey:@"Data"];
305
		}
306
	}
307
}
308

    
309
- (void)externalResourceFetchFailed:(ASIHTTPRequest *)externalResourceRequest
310
{
311
	if ([[self externalResourceQueue] shouldCancelAllRequestsOnFailure]) {
312
		[self failWithError:[externalResourceRequest error]];
313
	}
314
}
315

    
316
- (void)finishedFetchingExternalResources:(ASINetworkQueue *)queue
317
{
318
	if ([self urlReplacementMode] != ASIDontModifyURLs) {
319
		if (webContentType == ASICSSWebContentType) {
320
			NSMutableString *parsedResponse;
321
			NSError *err = nil;
322
			if ([self downloadDestinationPath]) {
323
				parsedResponse = [NSMutableString stringWithContentsOfFile:[self downloadDestinationPath] encoding:[self responseEncoding] error:&err];
324
			} else {
325
				parsedResponse = [[[self responseString] mutableCopy] autorelease];
326
			}
327
			if (err) {
328
				[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to read response CSS from disk",NSLocalizedDescriptionKey,nil]]];
329
				return;
330
			}
331
			if (![self error]) {
332
				for (NSString *resource in [[self resourceList] keyEnumerator]) {
333
					if ([parsedResponse rangeOfString:resource].location != NSNotFound) {
334
						NSString *newURL = [self contentForExternalURL:resource];
335
						if (newURL) {
336
							[parsedResponse replaceOccurrencesOfString:resource withString:newURL options:0 range:NSMakeRange(0, [parsedResponse length])];
337
						}
338
					}
339
				}
340
			}
341
			if ([self downloadDestinationPath]) {
342
				[parsedResponse writeToFile:[self downloadDestinationPath] atomically:NO encoding:[self responseEncoding] error:&err];
343
				if (err) {
344
					[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to write response CSS to disk",NSLocalizedDescriptionKey,nil]]];
345
					return;
346
				}
347
			} else {
348
				[self setRawResponseData:(id)[parsedResponse dataUsingEncoding:[self responseEncoding]]];
349
			}
350
		} else {
351
			[xmlParsingLock lock];
352

    
353
			[self updateResourceURLs];
354

    
355
			if (![self error]) {
356

    
357
				// We'll use the xmlsave API so we can strip the xml declaration
358
				xmlSaveCtxtPtr saveContext;
359

    
360
				if ([self downloadDestinationPath]) {
361

    
362
					// Truncate the file first
363
					[[[[NSFileManager alloc] init] autorelease] createFileAtPath:[self downloadDestinationPath] contents:nil attributes:nil];
364

    
365
					saveContext = xmlSaveToFd([[NSFileHandle fileHandleForWritingAtPath:[self downloadDestinationPath]] fileDescriptor],NULL,2); // 2 == XML_SAVE_NO_DECL, this isn't declared on Mac OS 10.5
366
					xmlSaveDoc(saveContext, doc);
367
					xmlSaveClose(saveContext);
368

    
369
				} else {
370
	#if TARGET_OS_MAC && MAC_OS_X_VERSION_MAX_ALLOWED <= __MAC_10_5
371
					// xmlSaveToBuffer() is not implemented in the 10.5 version of libxml
372
					NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
373
					[[[[NSFileManager alloc] init] autorelease] createFileAtPath:tempPath contents:nil attributes:nil];
374
					saveContext = xmlSaveToFd([[NSFileHandle fileHandleForWritingAtPath:tempPath] fileDescriptor],NULL,2); // 2 == XML_SAVE_NO_DECL, this isn't declared on Mac OS 10.5
375
					xmlSaveDoc(saveContext, doc);
376
					xmlSaveClose(saveContext);
377
					[self setRawResponseData:[NSMutableData dataWithContentsOfFile:tempPath]];
378
	#else
379
					xmlBufferPtr buffer = xmlBufferCreate();
380
					saveContext = xmlSaveToBuffer(buffer,NULL,2); // 2 == XML_SAVE_NO_DECL, this isn't declared on Mac OS 10.5
381
					xmlSaveDoc(saveContext, doc);
382
					xmlSaveClose(saveContext);
383
					[self setRawResponseData:[[[NSMutableData alloc] initWithBytes:buffer->content length:buffer->use] autorelease]];
384
					xmlBufferFree(buffer);
385
	#endif
386
				}
387

    
388
				// Strip the content encoding if the original response was gzipped
389
				if ([self isResponseCompressed]) {
390
					NSMutableDictionary *headers = [[[self responseHeaders] mutableCopy] autorelease];
391
					[headers removeObjectForKey:@"Content-Encoding"];
392
					[self setResponseHeaders:headers];
393
				}
394
			}
395

    
396
			xmlFreeDoc(doc);
397
			doc = nil;
398

    
399
			[requestsUsingXMLParser removeObject:self];
400
			if (![requestsUsingXMLParser count]) {
401
				xmlCleanupParser();
402
			}
403
			[xmlParsingLock unlock];
404
		}
405
	}
406
	if (![self parentRequest]) {
407
		[[self class] updateProgressIndicator:&downloadProgressDelegate withProgress:contentLength ofTotal:contentLength];
408
	}
409

    
410
	NSMutableDictionary *newHeaders = [[[self responseHeaders] mutableCopy] autorelease];
411
	[newHeaders removeObjectForKey:@"Content-Encoding"];
412
	[self setResponseHeaders:newHeaders];
413

    
414
	// Write the parsed content back to the cache
415
	if ([self urlReplacementMode] != ASIDontModifyURLs) {
416
		[[self downloadCache] storeResponseForRequest:self maxAge:[self secondsToCache]];
417
	}
418

    
419
	[super requestFinished];
420
	[super markAsFinished];
421
}
422

    
423
- (void)readResourceURLs
424
{
425
	// Create xpath evaluation context
426
    xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
427
    if(xpathCtx == NULL) {
428
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to create new XPath context",NSLocalizedDescriptionKey,nil]]];
429
		return;
430
    }
431

    
432
    // Evaluate xpath expression
433
    xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
434
    if(xpathObj == NULL) {
435
        xmlXPathFreeContext(xpathCtx); 
436
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to evaluate XPath expression!",NSLocalizedDescriptionKey,nil]]];
437
		return;
438
    }
439
	
440
	// Now loop through our matches
441
	xmlNodeSetPtr nodes = xpathObj->nodesetval;
442

    
443
    int size = (nodes) ? nodes->nodeNr : 0;
444
	int i;
445
    for(i = size - 1; i >= 0; i--) {
446
		assert(nodes->nodeTab[i]);
447
		NSString *parentName  = [NSString stringWithCString:(char *)nodes->nodeTab[i]->parent->name encoding:[self responseEncoding]];
448
		NSString *nodeName = [NSString stringWithCString:(char *)nodes->nodeTab[i]->name encoding:[self responseEncoding]];
449

    
450
		xmlChar *nodeValue = xmlNodeGetContent(nodes->nodeTab[i]);
451
		NSString *value = [NSString stringWithCString:(char *)nodeValue encoding:[self responseEncoding]];
452
		xmlFree(nodeValue);
453

    
454
		// Our xpath query matched all <link> elements, but we're only interested in stylesheets
455
		// 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
456
		if ([[parentName lowercaseString] isEqualToString:@"link"]) {
457
			xmlChar *relAttribute = xmlGetNoNsProp(nodes->nodeTab[i]->parent,(xmlChar *)"rel");
458
			if (relAttribute) {
459
				NSString *rel = [NSString stringWithCString:(char *)relAttribute encoding:[self responseEncoding]];
460
				xmlFree(relAttribute);
461
				if ([[rel lowercaseString] isEqualToString:@"stylesheet"]) {
462
					[self addURLToFetch:value];
463
				}
464
			}
465

    
466
		// Parse the content of <style> tags and style attributes to find external image urls or external css files
467
		} else if ([[nodeName lowercaseString] isEqualToString:@"style"]) {
468
			NSArray *externalResources = [[self class] CSSURLsFromString:value];
469
			for (NSString *theURL in externalResources) {
470
				[self addURLToFetch:theURL];
471
			}
472

    
473
		// Parse the content of <source src=""> tags (HTML 5 audio + video)
474
		// 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
475
		} else if ([[parentName lowercaseString] isEqualToString:@"source"] || [[parentName lowercaseString] isEqualToString:@"audio"]) {
476
			NSString *fileExtension = [[value pathExtension] lowercaseString];
477
			if (![fileExtension isEqualToString:@"ogg"] && ![fileExtension isEqualToString:@"ogv"] && ![fileExtension isEqualToString:@"webm"]) {
478
				[self addURLToFetch:value];
479
			}
480

    
481
		// For all other elements matched by our xpath query (except hyperlinks), add the content as an external url to fetch
482
		} else if (![[parentName lowercaseString] isEqualToString:@"a"]) {
483
			[self addURLToFetch:value];
484
		}
485
		if (nodes->nodeTab[i]->type != XML_NAMESPACE_DECL) {
486
			nodes->nodeTab[i] = NULL;
487
		}
488
    }
489
	
490
	xmlXPathFreeObject(xpathObj);
491
    xmlXPathFreeContext(xpathCtx); 
492
}
493

    
494
- (void)addURLToFetch:(NSString *)newURL
495
{
496
	// Get rid of any surrounding whitespace
497
	newURL = [newURL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
498
	// Don't attempt to fetch data URIs
499
	if ([newURL length] > 4) {
500
		if (![[[newURL substringToIndex:5] lowercaseString] isEqualToString:@"data:"]) {
501
			NSURL *theURL = [NSURL URLWithString:newURL relativeToURL:[self url]];
502
			if (theURL) {
503
				if (![[self resourceList] objectForKey:newURL]) {
504
					[[self resourceList] setObject:[NSMutableDictionary dictionary] forKey:newURL];
505
				}
506
			}
507
		}
508
	}
509
}
510

    
511

    
512
- (void)updateResourceURLs
513
{
514
	// Create xpath evaluation context
515
	xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
516
	if(xpathCtx == NULL) {
517
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to create new XPath context",NSLocalizedDescriptionKey,nil]]];
518
		return;
519
	}
520

    
521
 	// Evaluate xpath expression
522
	xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
523
	if(xpathObj == NULL) {
524
		xmlXPathFreeContext(xpathCtx);
525
		[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to evaluate XPath expression!",NSLocalizedDescriptionKey,nil]]];
526
		return;
527
	}
528

    
529
	// Loop through all the matches, replacing urls where nescessary
530
	xmlNodeSetPtr nodes = xpathObj->nodesetval;
531
	int size = (nodes) ? nodes->nodeNr : 0;
532
	int i;
533
	for(i = size - 1; i >= 0; i--) {
534
		assert(nodes->nodeTab[i]);
535
		NSString *parentName  = [NSString stringWithCString:(char *)nodes->nodeTab[i]->parent->name encoding:[self responseEncoding]];
536
		NSString *nodeName  = [NSString stringWithCString:(char *)nodes->nodeTab[i]->name encoding:[self responseEncoding]];
537

    
538
		xmlChar *nodeValue = xmlNodeGetContent(nodes->nodeTab[i]);
539
		NSString *value = [NSString stringWithCString:(char *)nodeValue encoding:[self responseEncoding]];
540
		xmlFree(nodeValue);
541

    
542
		// Replace external urls in <style> tags or in style attributes
543
		if ([[nodeName lowercaseString] isEqualToString:@"style"]) {
544
			NSArray *externalResources = [[self class] CSSURLsFromString:value];
545
			for (NSString *theURL in externalResources) {
546
				if ([value rangeOfString:theURL].location != NSNotFound) {
547
					NSString *newURL = [self contentForExternalURL:theURL];
548
					if (newURL) {
549
						value = [value stringByReplacingOccurrencesOfString:theURL withString:newURL];
550
					}
551
				}
552
			}
553
			xmlNodeSetContent(nodes->nodeTab[i], (xmlChar *)[value cStringUsingEncoding:[self responseEncoding]]);
554

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

    
562
		// Replace all other external resource urls
563
		} else {
564
			NSString *newURL = [self contentForExternalURL:value];
565
			if (newURL) {
566
				xmlNodeSetContent(nodes->nodeTab[i], (xmlChar *)[newURL cStringUsingEncoding:[self responseEncoding]]);
567
			}
568
		}
569

    
570
		if (nodes->nodeTab[i]->type != XML_NAMESPACE_DECL) {
571
			nodes->nodeTab[i] = NULL;
572
		}
573
	}
574
	xmlXPathFreeObject(xpathObj);
575
	xmlXPathFreeContext(xpathCtx);
576
}
577

    
578
// The three methods below are responsible for forwarding delegate methods we want to handle to the parent request's approdiate delegate
579
// Certain delegate methods are ignored (eg setProgress: / setDoubleValue: / setMaxValue:)
580
- (BOOL)respondsToSelector:(SEL)selector
581
{
582
	if ([self parentRequest]) {
583
		return [[self parentRequest] respondsToSelector:selector];
584
	}
585
	//Ok, now check for selectors we want to pass on to the delegate
586
	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:)) {
587
		return [delegate respondsToSelector:selector];
588
	} else if (selector == @selector(request:didReceiveBytes:) || selector == @selector(request:incrementDownloadSizeBy:)) {
589
		return [downloadProgressDelegate respondsToSelector:selector];
590
	} else if (selector == @selector(request:didSendBytes:)  || selector == @selector(request:incrementUploadSizeBy:)) {
591
		return [uploadProgressDelegate respondsToSelector:selector];
592
	}
593
	return [super respondsToSelector:selector];
594
}
595

    
596
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
597
{
598
	if ([self parentRequest]) {
599
		return [[self parentRequest] methodSignatureForSelector:selector];
600
	}
601
	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:)) {
602
		return [(id)delegate methodSignatureForSelector:selector];
603
	} else if (selector == @selector(request:didReceiveBytes:) || selector == @selector(request:incrementDownloadSizeBy:)) {
604
		return [(id)downloadProgressDelegate methodSignatureForSelector:selector];
605
	} else if (selector == @selector(request:didSendBytes:)  || selector == @selector(request:incrementUploadSizeBy:)) {
606
		return [(id)uploadProgressDelegate methodSignatureForSelector:selector];
607
	}
608
	return nil;
609
}
610

    
611
- (void)forwardInvocation:(NSInvocation *)anInvocation
612
{
613
	if ([self parentRequest]) {
614
		return [[self parentRequest] forwardInvocation:anInvocation];
615
	}
616
	SEL selector = [anInvocation selector];
617
	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:)) {
618
		[anInvocation invokeWithTarget:delegate];
619
	} else if (selector == @selector(request:didReceiveBytes:) || selector == @selector(request:incrementDownloadSizeBy:)) {
620
		[anInvocation invokeWithTarget:downloadProgressDelegate];
621
	} else if (selector == @selector(request:didSendBytes:)  || selector == @selector(request:incrementUploadSizeBy:)) {
622
		[anInvocation invokeWithTarget:uploadProgressDelegate];
623
	}
624
}
625

    
626
// A quick and dirty way to build a list of external resource urls from a css string
627
+ (NSArray *)CSSURLsFromString:(NSString *)string
628
{
629
	NSMutableArray *urls = [NSMutableArray array];
630
	NSScanner *scanner = [NSScanner scannerWithString:string];
631
	[scanner setCaseSensitive:NO];
632
	while (1) {
633
		NSString *theURL = nil;
634
		[scanner scanUpToString:@"url(" intoString:NULL];
635
		[scanner scanString:@"url(" intoString:NULL];
636
		[scanner scanUpToString:@")" intoString:&theURL];
637
		if (!theURL) {
638
			break;
639
		}
640
		// Remove any quotes or whitespace around the url
641
		theURL = [theURL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
642
		theURL = [theURL stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\"'"]];
643
		theURL = [theURL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
644
		[urls addObject:theURL];
645
	}
646
	return urls;
647
}
648

    
649
// Returns a relative file path from sourcePath to destinationPath (eg ../../foo/bar.txt)
650
- (NSString *)relativePathTo:(NSString *)destinationPath fromPath:(NSString *)sourcePath
651
{
652
	NSArray *sourcePathComponents = [sourcePath pathComponents];
653
	NSArray *destinationPathComponents = [destinationPath pathComponents];
654
	NSUInteger i;
655
	NSString *newPath = @"";
656
	NSString *sourcePathComponent, *destinationPathComponent;
657
	for (i=0; i<[sourcePathComponents count]; i++) {
658
		sourcePathComponent = [sourcePathComponents objectAtIndex:i];
659
		if ([destinationPathComponents count] > i) {
660
			destinationPathComponent = [destinationPathComponents objectAtIndex:i];
661
			if (![sourcePathComponent isEqualToString:destinationPathComponent]) {
662
				NSUInteger i2;
663
				for (i2=i+1; i2<[sourcePathComponents count]; i2++) {
664
					newPath = [newPath stringByAppendingPathComponent:@".."];
665
				}
666
				newPath = [newPath stringByAppendingPathComponent:destinationPathComponent];
667
				for (i2=i+1; i2<[destinationPathComponents count]; i2++) {
668
					newPath = [newPath stringByAppendingPathComponent:[destinationPathComponents objectAtIndex:i2]];
669
				}
670
				break;
671
			}
672
		}
673
	}
674
	return newPath;
675
}
676

    
677
- (NSString *)contentForExternalURL:(NSString *)theURL
678
{
679
	if ([self urlReplacementMode] == ASIReplaceExternalResourcesWithLocalURLs) {
680
		NSString *resourcePath = [[resourceList objectForKey:theURL] objectForKey:@"DataPath"];
681
		return [self relativePathTo:resourcePath fromPath:[self downloadDestinationPath]];
682
	}
683
	NSData *data;
684
	if ([[resourceList objectForKey:theURL] objectForKey:@"DataPath"]) {
685
		data = [NSData dataWithContentsOfFile:[[resourceList objectForKey:theURL] objectForKey:@"DataPath"]];
686
	} else {
687
		data = [[resourceList objectForKey:theURL] objectForKey:@"Data"];
688
	}
689
	NSString *contentType = [[resourceList objectForKey:theURL] objectForKey:@"ContentType"];
690
	if (data && contentType) {
691
		NSString *dataURI = [NSString stringWithFormat:@"data:%@;base64,",contentType];
692
		dataURI = [dataURI stringByAppendingString:[ASIHTTPRequest base64forData:data]];
693
		return dataURI;
694
	}
695
	return nil;
696
}
697

    
698
- (NSString *)cachePathForRequest:(ASIWebPageRequest *)theRequest
699
{
700
	// 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
701
	// This ends up being quite efficient, as we download directly to the cache
702
	if ([self downloadCache]) {
703
		return [[self downloadCache] pathToStoreCachedResponseDataForRequest:theRequest];
704

    
705
	// 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
706
	} else {
707
		// Borrowed from: http://stackoverflow.com/questions/652300/using-md5-hash-on-a-string-in-cocoa
708
		const char *cStr = [[[theRequest url] absoluteString] UTF8String];
709
		unsigned char result[16];
710
		CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
711
		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]]; 	
712
		return [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:[md5 stringByAppendingPathExtension:@"html"]];
713
	}
714
}
715

    
716

    
717
@synthesize externalResourceQueue;
718
@synthesize resourceList;
719
@synthesize parentRequest;
720
@synthesize urlReplacementMode;
721
@synthesize shouldIgnoreExternalResourceErrors;
722
@end