New file |
| | |
| | | // |
| | | // 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 |