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 |