单军华
2018-07-31 21d3023a9b7b6aff68c1170e345951396b1c6cfd
screendisplay/Pods/ASIHTTPRequest/Classes/ASIWebPageRequest/ASIWebPageRequest.m
New file
@@ -0,0 +1,722 @@
//
//  ASIWebPageRequest.m
//  Part of ASIHTTPRequest -> http://allseeing-i.com/ASIHTTPRequest
//
//  Created by Ben Copsey on 29/06/2010.
//  Copyright 2010 All-Seeing Interactive. All rights reserved.
//
//  This is an EXPERIMENTAL class - use at your own risk!
#import "ASIWebPageRequest.h"
#import "ASINetworkQueue.h"
#import <CommonCrypto/CommonHMAC.h>
#import <libxml/HTMLparser.h>
#import <libxml/xmlsave.h>
#import <libxml/xpath.h>
#import <libxml/xpathInternals.h>
// An xPath query that controls the external resources ASIWebPageRequest will fetch
// By default, it will fetch stylesheets, javascript files, images, frames, iframes, and html 5 video / audio
static xmlChar *xpathExpr = (xmlChar *)"//link/@href|//a/@href|//script/@src|//img/@src|//frame/@src|//iframe/@src|//style|//*/@style|//source/@src|//video/@poster|//audio/@src";
static NSLock *xmlParsingLock = nil;
static NSMutableArray *requestsUsingXMLParser = nil;
@interface ASIWebPageRequest ()
- (void)readResourceURLs;
- (void)updateResourceURLs;
- (void)parseAsHTML;
- (void)parseAsCSS;
- (void)addURLToFetch:(NSString *)newURL;
+ (NSArray *)CSSURLsFromString:(NSString *)string;
- (NSString *)relativePathTo:(NSString *)destinationPath fromPath:(NSString *)sourcePath;
- (void)finishedFetchingExternalResources:(ASINetworkQueue *)queue;
- (void)externalResourceFetchSucceeded:(ASIHTTPRequest *)externalResourceRequest;
- (void)externalResourceFetchFailed:(ASIHTTPRequest *)externalResourceRequest;
@property (retain, nonatomic) ASINetworkQueue *externalResourceQueue;
@property (retain, nonatomic) NSMutableDictionary *resourceList;
@end
@implementation ASIWebPageRequest
+ (void)initialize
{
   if (self == [ASIWebPageRequest class]) {
      xmlParsingLock = [[NSLock alloc] init];
      requestsUsingXMLParser = [[NSMutableArray alloc] init];
   }
}
- (id)initWithURL:(NSURL *)newURL
{
   self = [super initWithURL:newURL];
   [self setShouldIgnoreExternalResourceErrors:YES];
   return self;
}
- (void)dealloc
{
   [externalResourceQueue cancelAllOperations];
   [externalResourceQueue release];
   [resourceList release];
   [parentRequest release];
   [super dealloc];
}
// This is a bit of a hack
// The role of this method in normal ASIHTTPRequests is to tell the queue we are done with the request, and perform some cleanup
// We override it to stop that happening, and instead do that work in the bottom of finishedFetchingExternalResources:
- (void)markAsFinished
{
   if ([self error]) {
      [super markAsFinished];
   }
}
// This method is normally responsible for telling delegates we are done, but it happens to be the most convenient place to parse the responses
// Again, we call the super implementation in finishedFetchingExternalResources:, or here if this download was not an HTML or CSS file
- (void)requestFinished
{
   complete = NO;
   if ([self mainRequest] || [self didUseCachedResponse]) {
      [super requestFinished];
      [super markAsFinished];
      return;
   }
   webContentType = ASINotParsedWebContentType;
   NSString *contentType = [[[self responseHeaders] objectForKey:@"Content-Type"] lowercaseString];
   contentType = [[contentType componentsSeparatedByString:@";"] objectAtIndex:0];
   if ([contentType isEqualToString:@"text/html"] || [contentType isEqualToString:@"text/xhtml"] || [contentType isEqualToString:@"text/xhtml+xml"] || [contentType isEqualToString:@"application/xhtml+xml"]) {
      [self parseAsHTML];
      return;
   } else if ([contentType isEqualToString:@"text/css"]) {
      [self parseAsCSS];
      return;
   }
   [super requestFinished];
   [super markAsFinished];
}
- (void)parseAsCSS
{
   webContentType = ASICSSWebContentType;
   NSString *responseCSS = nil;
   NSError *err = nil;
   if ([self downloadDestinationPath]) {
      responseCSS = [NSString stringWithContentsOfFile:[self downloadDestinationPath] encoding:[self responseEncoding] error:&err];
   } else {
      responseCSS = [self responseString];
   }
   if (err) {
      [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:100 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to read HTML string from response",NSLocalizedDescriptionKey,err,NSUnderlyingErrorKey,nil]]];
      return;
   } else if (!responseCSS) {
      [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:100 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to read HTML string from response",NSLocalizedDescriptionKey,nil]]];
      return;
   }
   NSArray *urls = [[self class] CSSURLsFromString:responseCSS];
   [self setResourceList:[NSMutableDictionary dictionary]];
   for (NSString *theURL in urls) {
      [self addURLToFetch:theURL];
   }
   if (![[self resourceList] count]) {
      [super requestFinished];
      [super markAsFinished];
      return;
   }
   // Create a new request for every item in the queue
   [[self externalResourceQueue] cancelAllOperations];
   [self setExternalResourceQueue:[ASINetworkQueue queue]];
   [[self externalResourceQueue] setDelegate:self];
   [[self externalResourceQueue] setShouldCancelAllRequestsOnFailure:[self shouldIgnoreExternalResourceErrors]];
   [[self externalResourceQueue] setShowAccurateProgress:[self showAccurateProgress]];
   [[self externalResourceQueue] setQueueDidFinishSelector:@selector(finishedFetchingExternalResources:)];
   [[self externalResourceQueue] setRequestDidFinishSelector:@selector(externalResourceFetchSucceeded:)];
   [[self externalResourceQueue] setRequestDidFailSelector:@selector(externalResourceFetchFailed:)];
   for (NSString *theURL in [[self resourceList] keyEnumerator]) {
      ASIWebPageRequest *externalResourceRequest = [ASIWebPageRequest requestWithURL:[NSURL URLWithString:theURL relativeToURL:[self url]]];
      [externalResourceRequest setRequestHeaders:[self requestHeaders]];
      [externalResourceRequest setDownloadCache:[self downloadCache]];
      [externalResourceRequest setCachePolicy:[self cachePolicy]];
      [externalResourceRequest setCacheStoragePolicy:[self cacheStoragePolicy]];
      [externalResourceRequest setUserInfo:[NSDictionary dictionaryWithObject:theURL forKey:@"Path"]];
      [externalResourceRequest setParentRequest:self];
      [externalResourceRequest setUrlReplacementMode:[self urlReplacementMode]];
      [externalResourceRequest setShouldResetDownloadProgress:NO];
      [externalResourceRequest setDelegate:self];
      [externalResourceRequest setUploadProgressDelegate:self];
      [externalResourceRequest setDownloadProgressDelegate:self];
      if ([self downloadDestinationPath]) {
         [externalResourceRequest setDownloadDestinationPath:[self cachePathForRequest:externalResourceRequest]];
      }
      [[self externalResourceQueue] addOperation:externalResourceRequest];
   }
   [[self externalResourceQueue] go];
}
- (const char *)encodingName
{
   xmlCharEncoding encoding = XML_CHAR_ENCODING_NONE;
   switch ([self responseEncoding])
   {
      case NSASCIIStringEncoding:
         encoding = XML_CHAR_ENCODING_ASCII;
         break;
      case NSJapaneseEUCStringEncoding:
         encoding = XML_CHAR_ENCODING_EUC_JP;
         break;
      case NSUTF8StringEncoding:
         encoding = XML_CHAR_ENCODING_UTF8;
         break;
      case NSISOLatin1StringEncoding:
         encoding = XML_CHAR_ENCODING_8859_1;
         break;
      case NSShiftJISStringEncoding:
         encoding = XML_CHAR_ENCODING_SHIFT_JIS;
         break;
      case NSISOLatin2StringEncoding:
         encoding = XML_CHAR_ENCODING_8859_2;
         break;
      case NSISO2022JPStringEncoding:
         encoding = XML_CHAR_ENCODING_2022_JP;
         break;
      case NSUTF16BigEndianStringEncoding:
         encoding = XML_CHAR_ENCODING_UTF16BE;
         break;
      case NSUTF16LittleEndianStringEncoding:
         encoding = XML_CHAR_ENCODING_UTF16LE;
         break;
      case NSUTF32BigEndianStringEncoding:
         encoding = XML_CHAR_ENCODING_UCS4BE;
         break;
      case NSUTF32LittleEndianStringEncoding:
         encoding = XML_CHAR_ENCODING_UCS4LE;
         break;
      case NSNEXTSTEPStringEncoding:
      case NSSymbolStringEncoding:
      case NSNonLossyASCIIStringEncoding:
      case NSUnicodeStringEncoding:
      case NSMacOSRomanStringEncoding:
      case NSUTF32StringEncoding:
      default:
         encoding = XML_CHAR_ENCODING_ERROR;
         break;
   }
   return xmlGetCharEncodingName(encoding);
}
- (void)parseAsHTML
{
   webContentType = ASIHTMLWebContentType;
   // Only allow parsing of a single document at a time
   [xmlParsingLock lock];
   if (![requestsUsingXMLParser count]) {
      xmlInitParser();
   }
   [requestsUsingXMLParser addObject:self];
    /* Load XML document */
   if ([self downloadDestinationPath]) {
      doc = htmlReadFile([[self downloadDestinationPath] cStringUsingEncoding:NSUTF8StringEncoding], [self encodingName], HTML_PARSE_NONET | HTML_PARSE_NOWARNING | HTML_PARSE_NOERROR);
   } else {
      NSData *data = [self responseData];
      doc = htmlReadMemory([data bytes], (int)[data length], "", [self encodingName], HTML_PARSE_NONET | HTML_PARSE_NOWARNING | HTML_PARSE_NOERROR);
   }
    if (doc == NULL) {
      [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to parse reponse XML",NSLocalizedDescriptionKey,nil]]];
      return;
    }
   [self setResourceList:[NSMutableDictionary dictionary]];
    // Populate the list of URLS to download
    [self readResourceURLs];
   if ([self error] || ![[self resourceList] count]) {
      [requestsUsingXMLParser removeObject:self];
      xmlFreeDoc(doc);
      doc = NULL;
   }
   [xmlParsingLock unlock];
   if ([self error]) {
      return;
   } else if (![[self resourceList] count]) {
      [super requestFinished];
      [super markAsFinished];
      return;
   }
   // Create a new request for every item in the queue
   [[self externalResourceQueue] cancelAllOperations];
   [self setExternalResourceQueue:[ASINetworkQueue queue]];
   [[self externalResourceQueue] setDelegate:self];
   [[self externalResourceQueue] setShouldCancelAllRequestsOnFailure:[self shouldIgnoreExternalResourceErrors]];
   [[self externalResourceQueue] setShowAccurateProgress:[self showAccurateProgress]];
   [[self externalResourceQueue] setQueueDidFinishSelector:@selector(finishedFetchingExternalResources:)];
   [[self externalResourceQueue] setRequestDidFinishSelector:@selector(externalResourceFetchSucceeded:)];
   [[self externalResourceQueue] setRequestDidFailSelector:@selector(externalResourceFetchFailed:)];
   for (NSString *theURL in [[self resourceList] keyEnumerator]) {
      ASIWebPageRequest *externalResourceRequest = [ASIWebPageRequest requestWithURL:[NSURL URLWithString:theURL relativeToURL:[self url]]];
      [externalResourceRequest setRequestHeaders:[self requestHeaders]];
      [externalResourceRequest setDownloadCache:[self downloadCache]];
      [externalResourceRequest setCachePolicy:[self cachePolicy]];
      [externalResourceRequest setCacheStoragePolicy:[self cacheStoragePolicy]];
      [externalResourceRequest setUserInfo:[NSDictionary dictionaryWithObject:theURL forKey:@"Path"]];
      [externalResourceRequest setParentRequest:self];
      [externalResourceRequest setUrlReplacementMode:[self urlReplacementMode]];
      [externalResourceRequest setShouldResetDownloadProgress:NO];
      [externalResourceRequest setDelegate:self];
      [externalResourceRequest setUploadProgressDelegate:self];
      [externalResourceRequest setDownloadProgressDelegate:self];
      if ([self downloadDestinationPath]) {
         [externalResourceRequest setDownloadDestinationPath:[self cachePathForRequest:externalResourceRequest]];
      }
      [[self externalResourceQueue] addOperation:externalResourceRequest];
   }
   [[self externalResourceQueue] go];
}
- (void)externalResourceFetchSucceeded:(ASIHTTPRequest *)externalResourceRequest
{
   NSString *originalPath = [[externalResourceRequest userInfo] objectForKey:@"Path"];
   NSMutableDictionary *requestResponse = [[self resourceList] objectForKey:originalPath];
   NSString *contentType = [[externalResourceRequest responseHeaders] objectForKey:@"Content-Type"];
   if (!contentType) {
      contentType = @"application/octet-stream";
   }
   [requestResponse setObject:contentType forKey:@"ContentType"];
   if ([self downloadDestinationPath]) {
      [requestResponse setObject:[externalResourceRequest downloadDestinationPath] forKey:@"DataPath"];
   } else {
      NSData *data = [externalResourceRequest responseData];
      if (data) {
         [requestResponse setObject:data forKey:@"Data"];
      }
   }
}
- (void)externalResourceFetchFailed:(ASIHTTPRequest *)externalResourceRequest
{
   if ([[self externalResourceQueue] shouldCancelAllRequestsOnFailure]) {
      [self failWithError:[externalResourceRequest error]];
   }
}
- (void)finishedFetchingExternalResources:(ASINetworkQueue *)queue
{
   if ([self urlReplacementMode] != ASIDontModifyURLs) {
      if (webContentType == ASICSSWebContentType) {
         NSMutableString *parsedResponse;
         NSError *err = nil;
         if ([self downloadDestinationPath]) {
            parsedResponse = [NSMutableString stringWithContentsOfFile:[self downloadDestinationPath] encoding:[self responseEncoding] error:&err];
         } else {
            parsedResponse = [[[self responseString] mutableCopy] autorelease];
         }
         if (err) {
            [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to read response CSS from disk",NSLocalizedDescriptionKey,nil]]];
            return;
         }
         if (![self error]) {
            for (NSString *resource in [[self resourceList] keyEnumerator]) {
               if ([parsedResponse rangeOfString:resource].location != NSNotFound) {
                  NSString *newURL = [self contentForExternalURL:resource];
                  if (newURL) {
                     [parsedResponse replaceOccurrencesOfString:resource withString:newURL options:0 range:NSMakeRange(0, [parsedResponse length])];
                  }
               }
            }
         }
         if ([self downloadDestinationPath]) {
            [parsedResponse writeToFile:[self downloadDestinationPath] atomically:NO encoding:[self responseEncoding] error:&err];
            if (err) {
               [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to write response CSS to disk",NSLocalizedDescriptionKey,nil]]];
               return;
            }
         } else {
            [self setRawResponseData:(id)[parsedResponse dataUsingEncoding:[self responseEncoding]]];
         }
      } else {
         [xmlParsingLock lock];
         [self updateResourceURLs];
         if (![self error]) {
            // We'll use the xmlsave API so we can strip the xml declaration
            xmlSaveCtxtPtr saveContext;
            if ([self downloadDestinationPath]) {
               // Truncate the file first
               [[[[NSFileManager alloc] init] autorelease] createFileAtPath:[self downloadDestinationPath] contents:nil attributes:nil];
               saveContext = xmlSaveToFd([[NSFileHandle fileHandleForWritingAtPath:[self downloadDestinationPath]] fileDescriptor],NULL,2); // 2 == XML_SAVE_NO_DECL, this isn't declared on Mac OS 10.5
               xmlSaveDoc(saveContext, doc);
               xmlSaveClose(saveContext);
            } else {
   #if TARGET_OS_MAC && MAC_OS_X_VERSION_MAX_ALLOWED <= __MAC_10_5
               // xmlSaveToBuffer() is not implemented in the 10.5 version of libxml
               NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
               [[[[NSFileManager alloc] init] autorelease] createFileAtPath:tempPath contents:nil attributes:nil];
               saveContext = xmlSaveToFd([[NSFileHandle fileHandleForWritingAtPath:tempPath] fileDescriptor],NULL,2); // 2 == XML_SAVE_NO_DECL, this isn't declared on Mac OS 10.5
               xmlSaveDoc(saveContext, doc);
               xmlSaveClose(saveContext);
               [self setRawResponseData:[NSMutableData dataWithContentsOfFile:tempPath]];
   #else
               xmlBufferPtr buffer = xmlBufferCreate();
               saveContext = xmlSaveToBuffer(buffer,NULL,2); // 2 == XML_SAVE_NO_DECL, this isn't declared on Mac OS 10.5
               xmlSaveDoc(saveContext, doc);
               xmlSaveClose(saveContext);
               [self setRawResponseData:[[[NSMutableData alloc] initWithBytes:buffer->content length:buffer->use] autorelease]];
               xmlBufferFree(buffer);
   #endif
            }
            // Strip the content encoding if the original response was gzipped
            if ([self isResponseCompressed]) {
               NSMutableDictionary *headers = [[[self responseHeaders] mutableCopy] autorelease];
               [headers removeObjectForKey:@"Content-Encoding"];
               [self setResponseHeaders:headers];
            }
         }
         xmlFreeDoc(doc);
         doc = nil;
         [requestsUsingXMLParser removeObject:self];
         if (![requestsUsingXMLParser count]) {
            xmlCleanupParser();
         }
         [xmlParsingLock unlock];
      }
   }
   if (![self parentRequest]) {
      [[self class] updateProgressIndicator:&downloadProgressDelegate withProgress:contentLength ofTotal:contentLength];
   }
   NSMutableDictionary *newHeaders = [[[self responseHeaders] mutableCopy] autorelease];
   [newHeaders removeObjectForKey:@"Content-Encoding"];
   [self setResponseHeaders:newHeaders];
   // Write the parsed content back to the cache
   if ([self urlReplacementMode] != ASIDontModifyURLs) {
      [[self downloadCache] storeResponseForRequest:self maxAge:[self secondsToCache]];
   }
   [super requestFinished];
   [super markAsFinished];
}
- (void)readResourceURLs
{
   // Create xpath evaluation context
    xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
    if(xpathCtx == NULL) {
      [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to create new XPath context",NSLocalizedDescriptionKey,nil]]];
      return;
    }
    // Evaluate xpath expression
    xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
    if(xpathObj == NULL) {
        xmlXPathFreeContext(xpathCtx);
      [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to evaluate XPath expression!",NSLocalizedDescriptionKey,nil]]];
      return;
    }
   // Now loop through our matches
   xmlNodeSetPtr nodes = xpathObj->nodesetval;
    int size = (nodes) ? nodes->nodeNr : 0;
   int i;
    for(i = size - 1; i >= 0; i--) {
      assert(nodes->nodeTab[i]);
      NSString *parentName  = [NSString stringWithCString:(char *)nodes->nodeTab[i]->parent->name encoding:[self responseEncoding]];
      NSString *nodeName = [NSString stringWithCString:(char *)nodes->nodeTab[i]->name encoding:[self responseEncoding]];
      xmlChar *nodeValue = xmlNodeGetContent(nodes->nodeTab[i]);
      NSString *value = [NSString stringWithCString:(char *)nodeValue encoding:[self responseEncoding]];
      xmlFree(nodeValue);
      // Our xpath query matched all <link> elements, but we're only interested in stylesheets
      // 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
      if ([[parentName lowercaseString] isEqualToString:@"link"]) {
         xmlChar *relAttribute = xmlGetNoNsProp(nodes->nodeTab[i]->parent,(xmlChar *)"rel");
         if (relAttribute) {
            NSString *rel = [NSString stringWithCString:(char *)relAttribute encoding:[self responseEncoding]];
            xmlFree(relAttribute);
            if ([[rel lowercaseString] isEqualToString:@"stylesheet"]) {
               [self addURLToFetch:value];
            }
         }
      // Parse the content of <style> tags and style attributes to find external image urls or external css files
      } else if ([[nodeName lowercaseString] isEqualToString:@"style"]) {
         NSArray *externalResources = [[self class] CSSURLsFromString:value];
         for (NSString *theURL in externalResources) {
            [self addURLToFetch:theURL];
         }
      // Parse the content of <source src=""> tags (HTML 5 audio + video)
      // 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
      } else if ([[parentName lowercaseString] isEqualToString:@"source"] || [[parentName lowercaseString] isEqualToString:@"audio"]) {
         NSString *fileExtension = [[value pathExtension] lowercaseString];
         if (![fileExtension isEqualToString:@"ogg"] && ![fileExtension isEqualToString:@"ogv"] && ![fileExtension isEqualToString:@"webm"]) {
            [self addURLToFetch:value];
         }
      // For all other elements matched by our xpath query (except hyperlinks), add the content as an external url to fetch
      } else if (![[parentName lowercaseString] isEqualToString:@"a"]) {
         [self addURLToFetch:value];
      }
      if (nodes->nodeTab[i]->type != XML_NAMESPACE_DECL) {
         nodes->nodeTab[i] = NULL;
      }
    }
   xmlXPathFreeObject(xpathObj);
    xmlXPathFreeContext(xpathCtx);
}
- (void)addURLToFetch:(NSString *)newURL
{
   // Get rid of any surrounding whitespace
   newURL = [newURL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
   // Don't attempt to fetch data URIs
   if ([newURL length] > 4) {
      if (![[[newURL substringToIndex:5] lowercaseString] isEqualToString:@"data:"]) {
         NSURL *theURL = [NSURL URLWithString:newURL relativeToURL:[self url]];
         if (theURL) {
            if (![[self resourceList] objectForKey:newURL]) {
               [[self resourceList] setObject:[NSMutableDictionary dictionary] forKey:newURL];
            }
         }
      }
   }
}
- (void)updateResourceURLs
{
   // Create xpath evaluation context
   xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
   if(xpathCtx == NULL) {
      [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to create new XPath context",NSLocalizedDescriptionKey,nil]]];
      return;
   }
    // Evaluate xpath expression
   xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
   if(xpathObj == NULL) {
      xmlXPathFreeContext(xpathCtx);
      [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:101 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Error: unable to evaluate XPath expression!",NSLocalizedDescriptionKey,nil]]];
      return;
   }
   // Loop through all the matches, replacing urls where nescessary
   xmlNodeSetPtr nodes = xpathObj->nodesetval;
   int size = (nodes) ? nodes->nodeNr : 0;
   int i;
   for(i = size - 1; i >= 0; i--) {
      assert(nodes->nodeTab[i]);
      NSString *parentName  = [NSString stringWithCString:(char *)nodes->nodeTab[i]->parent->name encoding:[self responseEncoding]];
      NSString *nodeName  = [NSString stringWithCString:(char *)nodes->nodeTab[i]->name encoding:[self responseEncoding]];
      xmlChar *nodeValue = xmlNodeGetContent(nodes->nodeTab[i]);
      NSString *value = [NSString stringWithCString:(char *)nodeValue encoding:[self responseEncoding]];
      xmlFree(nodeValue);
      // Replace external urls in <style> tags or in style attributes
      if ([[nodeName lowercaseString] isEqualToString:@"style"]) {
         NSArray *externalResources = [[self class] CSSURLsFromString:value];
         for (NSString *theURL in externalResources) {
            if ([value rangeOfString:theURL].location != NSNotFound) {
               NSString *newURL = [self contentForExternalURL:theURL];
               if (newURL) {
                  value = [value stringByReplacingOccurrencesOfString:theURL withString:newURL];
               }
            }
         }
         xmlNodeSetContent(nodes->nodeTab[i], (xmlChar *)[value cStringUsingEncoding:[self responseEncoding]]);
      // Replace relative hyperlinks with absolute ones, since we will need to set a local baseURL when loading this in a web view
      } else if ([self urlReplacementMode] == ASIReplaceExternalResourcesWithLocalURLs && [[parentName lowercaseString] isEqualToString:@"a"]) {
         NSString *newURL = [[NSURL URLWithString:value relativeToURL:[self url]] absoluteString];
         if (newURL) {
            xmlNodeSetContent(nodes->nodeTab[i], (xmlChar *)[newURL cStringUsingEncoding:[self responseEncoding]]);
         }
      // Replace all other external resource urls
      } else {
         NSString *newURL = [self contentForExternalURL:value];
         if (newURL) {
            xmlNodeSetContent(nodes->nodeTab[i], (xmlChar *)[newURL cStringUsingEncoding:[self responseEncoding]]);
         }
      }
      if (nodes->nodeTab[i]->type != XML_NAMESPACE_DECL) {
         nodes->nodeTab[i] = NULL;
      }
   }
   xmlXPathFreeObject(xpathObj);
   xmlXPathFreeContext(xpathCtx);
}
// The three methods below are responsible for forwarding delegate methods we want to handle to the parent request's approdiate delegate
// Certain delegate methods are ignored (eg setProgress: / setDoubleValue: / setMaxValue:)
- (BOOL)respondsToSelector:(SEL)selector
{
   if ([self parentRequest]) {
      return [[self parentRequest] respondsToSelector:selector];
   }
   //Ok, now check for selectors we want to pass on to the delegate
   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:)) {
      return [delegate respondsToSelector:selector];
   } else if (selector == @selector(request:didReceiveBytes:) || selector == @selector(request:incrementDownloadSizeBy:)) {
      return [downloadProgressDelegate respondsToSelector:selector];
   } else if (selector == @selector(request:didSendBytes:)  || selector == @selector(request:incrementUploadSizeBy:)) {
      return [uploadProgressDelegate respondsToSelector:selector];
   }
   return [super respondsToSelector:selector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
   if ([self parentRequest]) {
      return [[self parentRequest] methodSignatureForSelector:selector];
   }
   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:)) {
      return [(id)delegate methodSignatureForSelector:selector];
   } else if (selector == @selector(request:didReceiveBytes:) || selector == @selector(request:incrementDownloadSizeBy:)) {
      return [(id)downloadProgressDelegate methodSignatureForSelector:selector];
   } else if (selector == @selector(request:didSendBytes:)  || selector == @selector(request:incrementUploadSizeBy:)) {
      return [(id)uploadProgressDelegate methodSignatureForSelector:selector];
   }
   return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
   if ([self parentRequest]) {
      return [[self parentRequest] forwardInvocation:anInvocation];
   }
   SEL selector = [anInvocation selector];
   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:)) {
      [anInvocation invokeWithTarget:delegate];
   } else if (selector == @selector(request:didReceiveBytes:) || selector == @selector(request:incrementDownloadSizeBy:)) {
      [anInvocation invokeWithTarget:downloadProgressDelegate];
   } else if (selector == @selector(request:didSendBytes:)  || selector == @selector(request:incrementUploadSizeBy:)) {
      [anInvocation invokeWithTarget:uploadProgressDelegate];
   }
}
// A quick and dirty way to build a list of external resource urls from a css string
+ (NSArray *)CSSURLsFromString:(NSString *)string
{
   NSMutableArray *urls = [NSMutableArray array];
   NSScanner *scanner = [NSScanner scannerWithString:string];
   [scanner setCaseSensitive:NO];
   while (1) {
      NSString *theURL = nil;
      [scanner scanUpToString:@"url(" intoString:NULL];
      [scanner scanString:@"url(" intoString:NULL];
      [scanner scanUpToString:@")" intoString:&theURL];
      if (!theURL) {
         break;
      }
      // Remove any quotes or whitespace around the url
      theURL = [theURL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
      theURL = [theURL stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\"'"]];
      theURL = [theURL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
      [urls addObject:theURL];
   }
   return urls;
}
// Returns a relative file path from sourcePath to destinationPath (eg ../../foo/bar.txt)
- (NSString *)relativePathTo:(NSString *)destinationPath fromPath:(NSString *)sourcePath
{
   NSArray *sourcePathComponents = [sourcePath pathComponents];
   NSArray *destinationPathComponents = [destinationPath pathComponents];
   NSUInteger i;
   NSString *newPath = @"";
   NSString *sourcePathComponent, *destinationPathComponent;
   for (i=0; i<[sourcePathComponents count]; i++) {
      sourcePathComponent = [sourcePathComponents objectAtIndex:i];
      if ([destinationPathComponents count] > i) {
         destinationPathComponent = [destinationPathComponents objectAtIndex:i];
         if (![sourcePathComponent isEqualToString:destinationPathComponent]) {
            NSUInteger i2;
            for (i2=i+1; i2<[sourcePathComponents count]; i2++) {
               newPath = [newPath stringByAppendingPathComponent:@".."];
            }
            newPath = [newPath stringByAppendingPathComponent:destinationPathComponent];
            for (i2=i+1; i2<[destinationPathComponents count]; i2++) {
               newPath = [newPath stringByAppendingPathComponent:[destinationPathComponents objectAtIndex:i2]];
            }
            break;
         }
      }
   }
   return newPath;
}
- (NSString *)contentForExternalURL:(NSString *)theURL
{
   if ([self urlReplacementMode] == ASIReplaceExternalResourcesWithLocalURLs) {
      NSString *resourcePath = [[resourceList objectForKey:theURL] objectForKey:@"DataPath"];
      return [self relativePathTo:resourcePath fromPath:[self downloadDestinationPath]];
   }
   NSData *data;
   if ([[resourceList objectForKey:theURL] objectForKey:@"DataPath"]) {
      data = [NSData dataWithContentsOfFile:[[resourceList objectForKey:theURL] objectForKey:@"DataPath"]];
   } else {
      data = [[resourceList objectForKey:theURL] objectForKey:@"Data"];
   }
   NSString *contentType = [[resourceList objectForKey:theURL] objectForKey:@"ContentType"];
   if (data && contentType) {
      NSString *dataURI = [NSString stringWithFormat:@"data:%@;base64,",contentType];
      dataURI = [dataURI stringByAppendingString:[ASIHTTPRequest base64forData:data]];
      return dataURI;
   }
   return nil;
}
- (NSString *)cachePathForRequest:(ASIWebPageRequest *)theRequest
{
   // 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
   // This ends up being quite efficient, as we download directly to the cache
   if ([self downloadCache]) {
      return [[self downloadCache] pathToStoreCachedResponseDataForRequest:theRequest];
   // 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
   } else {
      // Borrowed from: http://stackoverflow.com/questions/652300/using-md5-hash-on-a-string-in-cocoa
      const char *cStr = [[[theRequest url] absoluteString] UTF8String];
      unsigned char result[16];
      CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
      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]];
      return [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:[md5 stringByAppendingPathExtension:@"html"]];
   }
}
@synthesize externalResourceQueue;
@synthesize resourceList;
@synthesize parentRequest;
@synthesize urlReplacementMode;
@synthesize shouldIgnoreExternalResourceErrors;
@end