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 |