From 9dee894b21a210ddf0fa3d0c4f7cec46e2224237 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Thu, 1 May 2014 04:03:08 -0400 Subject: [PATCH] Replace the UIWebView resolver with one based on libxml2 --- Bolts.podspec | 2 + Bolts.xcodeproj/project.pbxproj | 38 ++- Bolts/BFAppLinkNavigation.m | 4 +- Bolts/BFWebViewAppLinkResolver.m | 297 ------------------ ...pLinkResolver.h => BFXMLAppLinkResolver.h} | 8 +- Bolts/BFXMLAppLinkResolver.m | 238 ++++++++++++++ BoltsTests/AppLinkTests.m | 49 +-- 7 files changed, 303 insertions(+), 333 deletions(-) delete mode 100644 Bolts/BFWebViewAppLinkResolver.m rename Bolts/{BFWebViewAppLinkResolver.h => BFXMLAppLinkResolver.h} (66%) create mode 100644 Bolts/BFXMLAppLinkResolver.m diff --git a/Bolts.podspec b/Bolts.podspec index 5c1849aff..178634820 100644 --- a/Bolts.podspec +++ b/Bolts.podspec @@ -18,6 +18,8 @@ Pod::Spec.new do |s| s.requires_arc = true s.source_files = 'Bolts' + s.libraries = 'xml2' + s.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2' } s.public_header_files = 'Bolts/**/*.h' end diff --git a/Bolts.xcodeproj/project.pbxproj b/Bolts.xcodeproj/project.pbxproj index 45cd256fa..4ced7e5c6 100644 --- a/Bolts.xcodeproj/project.pbxproj +++ b/Bolts.xcodeproj/project.pbxproj @@ -17,7 +17,7 @@ 1E45E47618DA7F0000D23509 /* BFAppLinkResolving.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1EC3019318CDD79F00D06D07 /* BFAppLinkResolving.h */; }; 1E45E47718DA7F0000D23509 /* BFAppLinkTarget.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1E11AB7C18C7C50800522739 /* BFAppLinkTarget.h */; }; 1E45E47818DA7F0000D23509 /* BFURL.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1E11AB7718C561E300522739 /* BFURL.h */; }; - 1E45E47918DA7F0000D23509 /* BFWebViewAppLinkResolver.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1EC3019418CDD8A500D06D07 /* BFWebViewAppLinkResolver.h */; }; + 1E45E47918DA7F0000D23509 /* BFXMLAppLinkResolver.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1EC3019418CDD8A500D06D07 /* BFXMLAppLinkResolver.h */; }; 1E9BF8DC18EA0CAD00514B1E /* test.html in Resources */ = {isa = PBXBuildFile; fileRef = 1E9BF8DB18EA0CAD00514B1E /* test.html */; }; 1EC3016118CDAA8400D06D07 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E9C3CEC17DE9DE000427E62 /* Foundation.framework */; }; 1EC3016318CDAA8400D06D07 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1EC3016218CDAA8400D06D07 /* CoreGraphics.framework */; }; @@ -28,7 +28,7 @@ 1EC3017318CDAA8400D06D07 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EC3017218CDAA8400D06D07 /* Images.xcassets */; }; 1EC3019118CDABCE00D06D07 /* AppLinkTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EC3019018CDABCE00D06D07 /* AppLinkTests.m */; }; 1EC3019218CDB3CB00D06D07 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1EC3016418CDAA8400D06D07 /* UIKit.framework */; }; - 1EC3019618CDD8A500D06D07 /* BFWebViewAppLinkResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EC3019518CDD8A500D06D07 /* BFWebViewAppLinkResolver.m */; }; + 1EC3019618CDD8A500D06D07 /* BFXMLAppLinkResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EC3019518CDD8A500D06D07 /* BFXMLAppLinkResolver.m */; }; 8550FD2218ECCF1600976B4B /* BFAppLinkReturnToRefererView.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 85D5138618E4E27700D19D87 /* BFAppLinkReturnToRefererView.h */; }; 85D5138818E4E27700D19D87 /* BFAppLinkReturnToRefererView.m in Sources */ = {isa = PBXBuildFile; fileRef = 85D5138718E4E27700D19D87 /* BFAppLinkReturnToRefererView.m */; }; 85D5138A18E4E45800D19D87 /* AppLinkReturnToRefererViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 85D5138918E4E45800D19D87 /* AppLinkReturnToRefererViewTests.m */; }; @@ -63,6 +63,8 @@ 8EE768A81803E5DA009A53B1 /* BoltsVersion.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8E0B5CDA17E13B4C0066379B /* BoltsVersion.h */; }; 8EE768A91803E5E0009A53B1 /* Bolts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8EE768A61803E32F009A53B1 /* Bolts.h */; }; 8EE768AA1803E5E5009A53B1 /* BoltsVersion.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8E0B5CDA17E13B4C0066379B /* BoltsVersion.h */; }; + D029D2EB191220B900B62C70 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = D029D2E919121FC600B62C70 /* libxml2.dylib */; }; + D029D2EE191220C500B62C70 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = D029D2E919121FC600B62C70 /* libxml2.dylib */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -103,7 +105,7 @@ 1E45E47618DA7F0000D23509 /* BFAppLinkResolving.h in CopyFiles */, 1E45E47718DA7F0000D23509 /* BFAppLinkTarget.h in CopyFiles */, 1E45E47818DA7F0000D23509 /* BFURL.h in CopyFiles */, - 1E45E47918DA7F0000D23509 /* BFWebViewAppLinkResolver.h in CopyFiles */, + 1E45E47918DA7F0000D23509 /* BFXMLAppLinkResolver.h in CopyFiles */, 8E42701F1805E5A1000B84ED /* BFExecutor.h in CopyFiles */, 8EE768A81803E5DA009A53B1 /* BoltsVersion.h in CopyFiles */, 8EE768A71803E5D5009A53B1 /* Bolts.h in CopyFiles */, @@ -168,8 +170,8 @@ 1EC3017218CDAA8400D06D07 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 1EC3019018CDABCE00D06D07 /* AppLinkTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppLinkTests.m; sourceTree = ""; }; 1EC3019318CDD79F00D06D07 /* BFAppLinkResolving.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BFAppLinkResolving.h; sourceTree = ""; }; - 1EC3019418CDD8A500D06D07 /* BFWebViewAppLinkResolver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFWebViewAppLinkResolver.h; sourceTree = ""; }; - 1EC3019518CDD8A500D06D07 /* BFWebViewAppLinkResolver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFWebViewAppLinkResolver.m; sourceTree = ""; }; + 1EC3019418CDD8A500D06D07 /* BFXMLAppLinkResolver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFXMLAppLinkResolver.h; sourceTree = ""; }; + 1EC3019518CDD8A500D06D07 /* BFXMLAppLinkResolver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFXMLAppLinkResolver.m; sourceTree = ""; }; 8550FD2E18EE1B7A00976B4B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 85D5138618E4E27700D19D87 /* BFAppLinkReturnToRefererView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFAppLinkReturnToRefererView.h; sourceTree = ""; }; 85D5138718E4E27700D19D87 /* BFAppLinkReturnToRefererView.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.c.objc; path = BFAppLinkReturnToRefererView.m; sourceTree = ""; tabWidth = 4; wrapsLines = 1; }; @@ -199,6 +201,7 @@ 8EB2CB411805DDDB00323385 /* BFExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFExecutor.m; sourceTree = ""; }; 8EDDA63517E17DDD00655F8A /* libMacBolts.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libMacBolts.a; sourceTree = BUILT_PRODUCTS_DIR; }; 8EE768A61803E32F009A53B1 /* Bolts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Bolts.h; sourceTree = ""; }; + D029D2E919121FC600B62C70 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -206,6 +209,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D029D2EE191220C500B62C70 /* libxml2.dylib in Frameworks */, 1EC3016318CDAA8400D06D07 /* CoreGraphics.framework in Frameworks */, 1EC3016518CDAA8400D06D07 /* UIKit.framework in Frameworks */, 1EC3016118CDAA8400D06D07 /* Foundation.framework in Frameworks */, @@ -216,6 +220,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D029D2EB191220B900B62C70 /* libxml2.dylib in Frameworks */, 1EC3019218CDB3CB00D06D07 /* UIKit.framework in Frameworks */, 8E8C8EFC17F23E6E00E3F1C7 /* libBolts.a in Frameworks */, 8E8C8EEA17F23D1D00E3F1C7 /* XCTest.framework in Frameworks */, @@ -325,6 +330,7 @@ 8E9C3CEB17DE9DE000427E62 /* Frameworks */ = { isa = PBXGroup; children = ( + D029D2E919121FC600B62C70 /* libxml2.dylib */, 8E9C3CEC17DE9DE000427E62 /* Foundation.framework */, 8E9C3CFB17DE9DE000427E62 /* SenTestingKit.framework */, 8E8C8ED217F23C3B00E3F1C7 /* XCTest.framework */, @@ -357,8 +363,8 @@ 8E9C3D2117DEA35700427E62 /* BFTaskCompletionSource.m */, 1E11AB7718C561E300522739 /* BFURL.h */, 1E11AB7818C561E300522739 /* BFURL.m */, - 1EC3019418CDD8A500D06D07 /* BFWebViewAppLinkResolver.h */, - 1EC3019518CDD8A500D06D07 /* BFWebViewAppLinkResolver.m */, + 1EC3019418CDD8A500D06D07 /* BFXMLAppLinkResolver.h */, + 1EC3019518CDD8A500D06D07 /* BFXMLAppLinkResolver.m */, 8EE768A61803E32F009A53B1 /* Bolts.h */, 8EA6BF661805CED500337041 /* Bolts.m */, 8E0B5CDA17E13B4C0066379B /* BoltsVersion.h */, @@ -576,7 +582,7 @@ 8EB2CB421805DDDB00323385 /* BFExecutor.m in Sources */, 8E9C3D2417DEA35700427E62 /* BFTaskCompletionSource.m in Sources */, 85EEE11B18FCAB370007510B /* BFAppLinkReturnToRefererController.m in Sources */, - 1EC3019618CDD8A500D06D07 /* BFWebViewAppLinkResolver.m in Sources */, + 1EC3019618CDD8A500D06D07 /* BFXMLAppLinkResolver.m in Sources */, 1E11AB7918C561E300522739 /* BFAppLinkNavigation.m in Sources */, 85D5138818E4E27700D19D87 /* BFAppLinkReturnToRefererView.m in Sources */, ); @@ -944,6 +950,10 @@ DSTROOT = /tmp/Bolts.dst; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Bolts/Bolts-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)/usr/include/libxml2", + ); IPHONEOS_DEPLOYMENT_TARGET = 5.0; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -962,6 +972,10 @@ DSTROOT = /tmp/Bolts.dst; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Bolts/Bolts-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)/usr/include/libxml2", + ); IPHONEOS_DEPLOYMENT_TARGET = 5.0; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1035,6 +1049,10 @@ DSTROOT = /tmp/Bolts.dst; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Bolts/Bolts-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)/usr/include/libxml2", + ); IPHONEOS_DEPLOYMENT_TARGET = 7.0; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1152,6 +1170,10 @@ DSTROOT = /tmp/Bolts.dst; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Bolts/Bolts-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)/usr/include/libxml2", + ); IPHONEOS_DEPLOYMENT_TARGET = 7.0; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Bolts/BFAppLinkNavigation.m b/Bolts/BFAppLinkNavigation.m index d683fe1c0..fcfa29cda 100644 --- a/Bolts/BFAppLinkNavigation.m +++ b/Bolts/BFAppLinkNavigation.m @@ -14,7 +14,7 @@ #import "BFTaskCompletionSource.h" #import "BFAppLinkTarget.h" #import "BoltsVersion.h" -#import "BFWebViewAppLinkResolver.h" +#import "BFXMLAppLinkResolver.h" #import "BFExecutor.h" #import "BFTask.h" @@ -176,7 +176,7 @@ + (BFAppLinkNavigationType)navigateToAppLink:(BFAppLink *)link error:(NSError ** if (defaultResolver) { return defaultResolver; } - return [BFWebViewAppLinkResolver sharedInstance]; + return [BFXMLAppLinkResolver sharedInstance]; } + (void)setDefaultResolver:(id)resolver { diff --git a/Bolts/BFWebViewAppLinkResolver.m b/Bolts/BFWebViewAppLinkResolver.m deleted file mode 100644 index ffea98cb6..000000000 --- a/Bolts/BFWebViewAppLinkResolver.m +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright (c) 2014, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - */ - -#import - -#import "BFWebViewAppLinkResolver.h" -#import "BFAppLink.h" -#import "BFAppLinkTarget.h" -#import "BFTask.h" -#import "BFTaskCompletionSource.h" -#import "BFExecutor.h" - -// Defines JavaScript to extract app link tags from HTML content -static NSString *const BFWebViewAppLinkResolverTagExtractionJavaScript = @"" -"(function() {" -" var metaTags = document.getElementsByTagName('meta');" -" var results = [];" -" for (var i = 0; i < metaTags.length; i++) {" -" var property = metaTags[i].getAttribute('property');" -" if (property && property.substring(0, 'al:'.length) === 'al:') {" -" var tag = { \"property\": metaTags[i].getAttribute('property') };" -" if (metaTags[i].hasAttribute('content')) {" -" tag['content'] = metaTags[i].getAttribute('content');" -" }" -" results.push(tag);" -" }" -" }" -" return JSON.stringify(results);" -"})()"; -static NSString *const BFWebViewAppLinkResolverIOSURLKey = @"url"; -static NSString *const BFWebViewAppLinkResolverIOSAppStoreIdKey = @"app_store_id"; -static NSString *const BFWebViewAppLinkResolverIOSAppNameKey = @"app_name"; -static NSString *const BFWebViewAppLinkResolverDictionaryValueKey = @"_value"; -static NSString *const BFWebViewAppLinkResolverPreferHeader = @"Prefer-Html-Meta-Tags"; -static NSString *const BFWebViewAppLinkResolverMetaTagPrefix = @"al"; -static NSString *const BFWebViewAppLinkResolverWebKey = @"web"; -static NSString *const BFWebViewAppLinkResolverIOSKey = @"ios"; -static NSString *const BFWebViewAppLinkResolverIPhoneKey = @"iphone"; -static NSString *const BFWebViewAppLinkResolverIPadKey = @"ipad"; -static NSString *const BFWebViewAppLinkResolverWebURLKey = @"url"; -static NSString *const BFWebViewAppLinkResolverShouldFallbackKey = @"should_fallback"; - -@interface BFWebViewAppLinkResolverWebViewDelegate : NSObject - -@property (nonatomic, copy) void (^didFinishLoad)(UIWebView *webView); -@property (nonatomic, copy) void (^didFailLoadWithError)(UIWebView *webView, NSError *error); -@property (nonatomic, assign) BOOL hasLoaded; - -@end - -@implementation BFWebViewAppLinkResolverWebViewDelegate - -- (void)webViewDidFinishLoad:(UIWebView *)webView { - if (self.didFinishLoad) { - self.didFinishLoad(webView); - } -} - -- (void)webViewDidStartLoad:(UIWebView *)webView { -} - -- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { - if (self.didFailLoadWithError) { - self.didFailLoadWithError(webView, error); - } -} - -- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { - if (self.hasLoaded) { - // Consider loading a second resource to be "success", since it indicates an inner frame - // or redirect is happening. We can run the tag extraction script at this point. - self.didFinishLoad(webView); - return NO; - } - self.hasLoaded = YES; - return YES; -} - -@end - - -@implementation BFWebViewAppLinkResolver - -+ (instancetype)sharedInstance { - static id instance; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[self alloc] init]; - }); - return instance; -} - -- (BFTask *)followRedirects:(NSURL *)url { - // This task will be resolved with either the redirect NSURL - // or a dictionary with the response data to be returned. - BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; - [request setValue:BFWebViewAppLinkResolverMetaTagPrefix - forHTTPHeaderField:BFWebViewAppLinkResolverPreferHeader]; - [NSURLConnection sendAsynchronousRequest:request - queue:[NSOperationQueue mainQueue] - completionHandler:^(NSURLResponse *response, - NSData *data, - NSError *connectionError) { - if (connectionError) { - [tcs setError:connectionError]; - return; - } - - if ([response isKindOfClass:[NSHTTPURLResponse class]]) { - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; - - // NSURLConnection usually follows redirects automatically, but the - // documentation is unclear what the default is. This helps it along. - if (httpResponse.statusCode >= 300 && httpResponse.statusCode < 400) { - NSString *redirectString = httpResponse.allHeaderFields[@"Location"]; - NSURL *redirectURL = [NSURL URLWithString:redirectString]; - [tcs setResult:redirectURL]; - return; - } - } - - [tcs setResult:@{ - @"response" : response, - @"data" : data - }]; - }]; - return [tcs.task continueWithSuccessBlock:^id(BFTask *task) { - // If we redirected, just keep recursing. - if ([task.result isKindOfClass:[NSURL class]]) { - return [self followRedirects:task.result]; - } - return task; - }]; -} - -- (BFTask *)appLinkFromURLInBackground:(NSURL *)url { - return [[self followRedirects:url] continueWithExecutor:[BFExecutor mainThreadExecutor] - withSuccessBlock:^id(BFTask *task) { - NSData *responseData = task.result[@"data"]; - NSHTTPURLResponse *response = task.result[@"response"]; - BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource]; - - UIWebView *webView = [[UIWebView alloc] init]; - BFWebViewAppLinkResolverWebViewDelegate *listener = [[BFWebViewAppLinkResolverWebViewDelegate alloc] init]; - __block BFWebViewAppLinkResolverWebViewDelegate *retainedListener = listener; - listener.didFinishLoad = ^(UIWebView *view) { - if (retainedListener) { - NSDictionary *ogData = [self getALDataFromLoadedPage:view]; - [view removeFromSuperview]; - view.delegate = nil; - retainedListener = nil; - [tcs setResult:[self appLinkFromALData:ogData destination:url]]; - } - }; - listener.didFailLoadWithError = ^(UIWebView* view, NSError *error) { - if (retainedListener) { - [view removeFromSuperview]; - view.delegate = nil; - retainedListener = nil; - [tcs setError:error]; - } - }; - webView.delegate = listener; - webView.hidden = YES; - [webView loadData:responseData - MIMEType:response.MIMEType - textEncodingName:response.textEncodingName - baseURL:response.URL]; - UIWindow *window = [UIApplication sharedApplication].windows.firstObject; - [window addSubview:webView]; - - return tcs.task; - }]; -} - - -/* - Builds up a data structure filled with the app link data from the meta tags on a page. - The structure of this object is a dictionary where each key holds an array of app link - data dictionaries. Values are stored in a key called "_value". - */ -- (NSDictionary *)parseALData:(NSArray *)dataArray { - NSMutableDictionary *al = [NSMutableDictionary dictionary]; - for (NSDictionary *tag in dataArray) { - NSString *name = tag[@"property"]; - if (![name isKindOfClass:[NSString class]]) { - continue; - } - NSArray *nameComponents = [name componentsSeparatedByString:@":"]; - if (![nameComponents[0] isEqualToString:BFWebViewAppLinkResolverMetaTagPrefix]) { - continue; - } - NSMutableDictionary *root = al; - for (int i = 1; i < nameComponents.count; i++) { - NSMutableArray *children = root[nameComponents[i]]; - if (!children) { - children = [NSMutableArray array]; - root[nameComponents[i]] = children; - } - NSMutableDictionary *child = children.lastObject; - if (!child || i == nameComponents.count - 1) { - child = [NSMutableDictionary dictionary]; - [children addObject:child]; - } - root = child; - } - if (tag[@"content"]) { - root[BFWebViewAppLinkResolverDictionaryValueKey] = tag[@"content"]; - } - } - return al; -} - -- (NSDictionary *)getALDataFromLoadedPage:(UIWebView *)webView { - // Run some JavaScript in the webview to fetch the meta tags. - NSString *jsonString = [webView stringByEvaluatingJavaScriptFromString:BFWebViewAppLinkResolverTagExtractionJavaScript]; - NSError *error = nil; - NSArray *arr = [NSJSONSerialization JSONObjectWithData:[jsonString dataUsingEncoding:NSUTF8StringEncoding] - options:0 - error:&error]; - return [self parseALData:arr]; -} - -/* - Converts app link data into a BFAppLink containing the targets relevant for this platform. - */ -- (BFAppLink *)appLinkFromALData:(NSDictionary *)appLinkDict destination:(NSURL *)destination { - NSMutableArray *linkTargets = [NSMutableArray array]; - - NSArray *platformData = nil; - switch (UI_USER_INTERFACE_IDIOM()) { - case UIUserInterfaceIdiomPad: - platformData = @[appLinkDict[BFWebViewAppLinkResolverIPadKey] ?: @{}, - appLinkDict[BFWebViewAppLinkResolverIOSKey] ?: @{}]; - break; - case UIUserInterfaceIdiomPhone: - platformData = @[appLinkDict[BFWebViewAppLinkResolverIPhoneKey] ?: @{}, - appLinkDict[BFWebViewAppLinkResolverIOSKey] ?: @{}]; - break; - default: - // Future-proofing. Other User Interface idioms should only hit ios. - platformData = @[appLinkDict[BFWebViewAppLinkResolverIOSKey] ?: @{}]; - break; - } - - for (NSArray *platformObjects in platformData) { - for (NSDictionary *platformDict in platformObjects) { - // The schema requires a single url/app store id/app name, - // but we could find multiple of them. We'll make a best effort - // to interpret this data. - NSArray *urls = platformDict[BFWebViewAppLinkResolverIOSURLKey]; - NSArray *appStoreIds = platformDict[BFWebViewAppLinkResolverIOSAppStoreIdKey]; - NSArray *appNames = platformDict[BFWebViewAppLinkResolverIOSAppNameKey]; - - NSUInteger maxCount = MAX(urls.count, MAX(appStoreIds.count, appNames.count)); - - for (NSUInteger i = 0; i < maxCount; i++) { - NSString *urlString = urls[i][BFWebViewAppLinkResolverDictionaryValueKey]; - NSURL *url = urlString ? [NSURL URLWithString:urlString] : nil; - NSString *appStoreId = appStoreIds[i][BFWebViewAppLinkResolverDictionaryValueKey]; - NSString *appName = appNames[i][BFWebViewAppLinkResolverDictionaryValueKey]; - BFAppLinkTarget *target = [BFAppLinkTarget appLinkTargetWithURL:url - appStoreId:appStoreId - appName:appName]; - [linkTargets addObject:target]; - } - } - } - - NSDictionary *webDict = appLinkDict[BFWebViewAppLinkResolverWebKey][0]; - NSString *webUrlString = webDict[BFWebViewAppLinkResolverWebURLKey][0][BFWebViewAppLinkResolverDictionaryValueKey]; - NSString *shouldFallbackString = webDict[BFWebViewAppLinkResolverShouldFallbackKey][0][BFWebViewAppLinkResolverDictionaryValueKey]; - - NSURL *webUrl = destination; - - if (shouldFallbackString && - [@[@"no", @"false", @"0"] containsObject:[shouldFallbackString lowercaseString]]) { - webUrl = nil; - } - if (webUrl && webUrlString) { - webUrl = [NSURL URLWithString:webUrlString]; - } - - return [BFAppLink appLinkWithSourceURL:destination - targets:linkTargets - webURL:webUrl]; -} - -@end diff --git a/Bolts/BFWebViewAppLinkResolver.h b/Bolts/BFXMLAppLinkResolver.h similarity index 66% rename from Bolts/BFWebViewAppLinkResolver.h rename to Bolts/BFXMLAppLinkResolver.h index 5f91355cc..707429157 100644 --- a/Bolts/BFWebViewAppLinkResolver.h +++ b/Bolts/BFXMLAppLinkResolver.h @@ -13,14 +13,16 @@ #import "BFAppLinkResolving.h" /*! - A reference implementation for an App Link resolver that uses a hidden UIWebView + A reference implementation for an App Link resolver that uses libxml2 to parse the HTML containing App Link metadata. */ -@interface BFWebViewAppLinkResolver : NSObject +@interface BFXMLAppLinkResolver : NSObject /*! - Gets the instance of a BFWebViewAppLinkResolver. + Gets the instance of a BFXMLAppLinkResolver. */ + (instancetype)sharedInstance; @end + +extern NSString *const BFXMLAppLinkResolverErrorDomain; diff --git a/Bolts/BFXMLAppLinkResolver.m b/Bolts/BFXMLAppLinkResolver.m new file mode 100644 index 000000000..19774638c --- /dev/null +++ b/Bolts/BFXMLAppLinkResolver.m @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2014, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +#import +#import + +#import "BFXMLAppLinkResolver.h" +#import "BFAppLink.h" +#import "BFAppLinkTarget.h" +#import "BFTask.h" +#import "BFTaskCompletionSource.h" + +static NSString *const BFXMLAppLinkResolverIOSURLKey = @"url"; +static NSString *const BFXMLAppLinkResolverIOSAppStoreIdKey = @"app_store_id"; +static NSString *const BFXMLAppLinkResolverIOSAppNameKey = @"app_name"; +static NSString *const BFXMLAppLinkResolverDictionaryValueKey = @"_value"; +static NSString *const BFXMLAppLinkResolverPreferHeader = @"Prefer-Html-Meta-Tags"; +static NSString *const BFXMLAppLinkResolverMetaTagPrefix = @"al"; +static NSString *const BFXMLAppLinkResolverWebKey = @"web"; +static NSString *const BFXMLAppLinkResolverIOSKey = @"ios"; +static NSString *const BFXMLAppLinkResolverIPhoneKey = @"iphone"; +static NSString *const BFXMLAppLinkResolverIPadKey = @"ipad"; +static NSString *const BFXMLAppLinkResolverWebURLKey = @"url"; +static NSString *const BFXMLAppLinkResolverShouldFallbackKey = @"should_fallback"; + +NSString *const BFXMLAppLinkResolverErrorDomain = @"BFXMLAppLinkResolverErrorDomain"; + +@implementation BFXMLAppLinkResolver + ++ (instancetype)sharedInstance { + static id instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +- (BFTask *)followRedirects:(NSURL *)url { + // This task will be resolved with either the redirect NSURL + // or a dictionary with the response data to be returned. + BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request setValue:BFXMLAppLinkResolverMetaTagPrefix + forHTTPHeaderField:BFXMLAppLinkResolverPreferHeader]; + [NSURLConnection sendAsynchronousRequest:request + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse *response, + NSData *data, + NSError *connectionError) { + if (connectionError) { + [tcs setError:connectionError]; + return; + } + + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + + // NSURLConnection usually follows redirects automatically, but the + // documentation is unclear what the default is. This helps it along. + if ([[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(300, 100)] containsIndex:httpResponse.statusCode]) { + NSString *redirectString = httpResponse.allHeaderFields[@"Location"]; + NSURL *redirectURL = [NSURL URLWithString:redirectString]; + [tcs setResult:redirectURL]; + return; + } + } + + [tcs setResult:@{ + @"response" : response, + @"data" : data + }]; + }]; + return [tcs.task continueWithSuccessBlock:^id(BFTask *task) { + // If we redirected, just keep recursing. + if ([task.result isKindOfClass:[NSURL class]]) { + return [self followRedirects:task.result]; + } + return task; + }]; +} + +- (BFTask *)appLinkFromURLInBackground:(NSURL *)url { + return [[self followRedirects:url] continueWithSuccessBlock:^id(BFTask *task) { + NSData *responseData = task.result[@"data"]; + NSHTTPURLResponse *response = task.result[@"response"]; + + htmlDocPtr document = htmlReadMemory(responseData.bytes, (int)responseData.length, [url.absoluteString UTF8String], [response.textEncodingName UTF8String], HTML_PARSE_RECOVER); + xmlErrorPtr xmlError = xmlGetLastError(); + if (xmlError) { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + if (xmlError->message) + [userInfo setObject:@(xmlError->message) forKey:NSLocalizedDescriptionKey]; + NSError *error = [NSError errorWithDomain:BFXMLAppLinkResolverErrorDomain code:(xmlError->code) userInfo:userInfo]; + xmlResetError(xmlError); + return [BFTask taskWithError:error]; + } + + xmlXPathContextPtr context = xmlXPathNewContext(document); + xmlXPathObjectPtr xpathObj = xmlXPathNodeEval(xmlDocGetRootElement(document), BAD_CAST"//meta[starts-with(@property,'al')]", context); + + NSMutableArray *results = [NSMutableArray array]; + for (NSInteger idx = 0; idx < xmlXPathNodeSetGetLength(xpathObj->nodesetval); idx++) { + NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; + xmlNodePtr node = xmlXPathNodeSetItem(xpathObj->nodesetval, idx); + xmlChar *propertyValue = xmlGetProp(node, BAD_CAST"property"); + if (propertyValue) { + [attributes setObject:@((const char *)propertyValue) forKey:@"property"]; + xmlFree(propertyValue); + } + xmlChar *contentValue = xmlGetProp(node, BAD_CAST"content"); + if (contentValue) { + [attributes setObject:@((const char *)contentValue) forKey:@"content"]; + xmlFree(contentValue); + } + [results addObject:attributes]; + } + + xmlXPathFreeObject(xpathObj); + xmlXPathFreeContext(context); + xmlFreeDoc(document); + + return [BFTask taskWithResult:[self appLinkFromALData:[self parseALData:results] destination:url]]; + }]; +} + +/* + Builds up a data structure filled with the app link data from the meta tags on a page. + The structure of this object is a dictionary where each key holds an array of app link + data dictionaries. Values are stored in a key called "_value". + */ +- (NSDictionary *)parseALData:(NSArray *)dataArray { + NSMutableDictionary *al = [NSMutableDictionary dictionary]; + for (NSDictionary *tag in dataArray) { + NSString *name = tag[@"property"]; + if (![name isKindOfClass:[NSString class]]) { + continue; + } + NSArray *nameComponents = [name componentsSeparatedByString:@":"]; + if (![nameComponents[0] isEqualToString:BFXMLAppLinkResolverMetaTagPrefix]) { + continue; + } + NSMutableDictionary *root = al; + for (int i = 1; i < nameComponents.count; i++) { + NSMutableArray *children = root[nameComponents[i]]; + if (!children) { + children = [NSMutableArray array]; + root[nameComponents[i]] = children; + } + NSMutableDictionary *child = children.lastObject; + if (!child || i == nameComponents.count - 1) { + child = [NSMutableDictionary dictionary]; + [children addObject:child]; + } + root = child; + } + if (tag[@"content"]) { + root[BFXMLAppLinkResolverDictionaryValueKey] = tag[@"content"]; + } + } + return al; +} + +/* + Converts app link data into a BFAppLink containing the targets relevant for this platform. + */ +- (BFAppLink *)appLinkFromALData:(NSDictionary *)appLinkDict destination:(NSURL *)destination { + NSMutableArray *linkTargets = [NSMutableArray array]; + + NSArray *platformData = nil; + switch (UI_USER_INTERFACE_IDIOM()) { + case UIUserInterfaceIdiomPad: + platformData = @[appLinkDict[BFXMLAppLinkResolverIPadKey] ?: @{}, + appLinkDict[BFXMLAppLinkResolverIOSKey] ?: @{}]; + break; + case UIUserInterfaceIdiomPhone: + platformData = @[appLinkDict[BFXMLAppLinkResolverIPhoneKey] ?: @{}, + appLinkDict[BFXMLAppLinkResolverIOSKey] ?: @{}]; + break; + default: + // Future-proofing. Other User Interface idioms should only hit ios. + platformData = @[appLinkDict[BFXMLAppLinkResolverIOSKey] ?: @{}]; + break; + } + + for (NSArray *platformObjects in platformData) { + for (NSDictionary *platformDict in platformObjects) { + // The schema requires a single url/app store id/app name, + // but we could find multiple of them. We'll make a best effort + // to interpret this data. + NSArray *urls = platformDict[BFXMLAppLinkResolverIOSURLKey]; + NSArray *appStoreIds = platformDict[BFXMLAppLinkResolverIOSAppStoreIdKey]; + NSArray *appNames = platformDict[BFXMLAppLinkResolverIOSAppNameKey]; + + NSUInteger maxCount = MAX(urls.count, MAX(appStoreIds.count, appNames.count)); + + for (NSUInteger i = 0; i < maxCount; i++) { + NSString *urlString = urls[i][BFXMLAppLinkResolverDictionaryValueKey]; + NSURL *url = urlString ? [NSURL URLWithString:urlString] : nil; + NSString *appStoreId = appStoreIds[i][BFXMLAppLinkResolverDictionaryValueKey]; + NSString *appName = appNames[i][BFXMLAppLinkResolverDictionaryValueKey]; + BFAppLinkTarget *target = [BFAppLinkTarget appLinkTargetWithURL:url + appStoreId:appStoreId + appName:appName]; + [linkTargets addObject:target]; + } + } + } + + NSDictionary *webDict = appLinkDict[BFXMLAppLinkResolverWebKey][0]; + NSString *webUrlString = webDict[BFXMLAppLinkResolverWebURLKey][0][BFXMLAppLinkResolverDictionaryValueKey]; + NSString *shouldFallbackString = webDict[BFXMLAppLinkResolverShouldFallbackKey][0][BFXMLAppLinkResolverDictionaryValueKey]; + + NSURL *webUrl = destination; + + if (shouldFallbackString && + [@[@"no", @"false", @"0"] containsObject:[shouldFallbackString lowercaseString]]) { + webUrl = nil; + } + if (webUrl && webUrlString) { + webUrl = [NSURL URLWithString:webUrlString]; + } + + return [BFAppLink appLinkWithSourceURL:destination + targets:linkTargets + webURL:webUrl]; +} + +@end diff --git a/BoltsTests/AppLinkTests.m b/BoltsTests/AppLinkTests.m index d7b69f3a9..2e43e1a69 100644 --- a/BoltsTests/AppLinkTests.m +++ b/BoltsTests/AppLinkTests.m @@ -12,7 +12,7 @@ #import #import "Bolts.h" -#import "BFWebViewAppLinkResolver.h" +#import "BFXMLAppLinkResolver.h" NSMutableArray *openedUrls = nil; @@ -173,9 +173,9 @@ - (void)testOpenedURLWithAppLinkWithCustomAppLinkData { XCTAssertEqualObjects(url.absoluteString, openedURL.inputURL.absoluteString); } -#pragma mark WebView App Link resolution +#pragma mark XML App Link resolution -- (void)testWebViewSimpleAppLinkParsing { +- (void)testXMLSimpleAppLinkParsing { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @@ -186,7 +186,7 @@ - (void)testWebViewSimpleAppLinkParsing { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [[BFXMLAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -200,14 +200,17 @@ - (void)testWebViewSimpleAppLinkParsing { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewAppLinkParsingFailure { - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:[NSURL URLWithString:@"http://badurl"]]; - [self waitForTaskOnMainThread:task]; - - XCTAssertNotNil(task.error); +- (void)testXMLAppLinkParsingFailure { + BFTask *firstTask = [[BFXMLAppLinkResolver sharedInstance] appLinkFromURLInBackground:[NSURL URLWithString:@"http://badurl"]]; + [self waitForTaskOnMainThread:firstTask]; + BFTask *secondTask = [BFAppLinkNavigation resolveAppLinkInBackground:[self dataUrlForHtml:@""]]; + [self waitForTaskOnMainThread:secondTask]; + + XCTAssertNotNil(firstTask.error); + XCTAssertNotNil(secondTask.error); } -- (void)testWebViewSimpleAppLinkParsingZeroShouldFallback { +- (void)testXMLSimpleAppLinkParsingZeroShouldFallback { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @@ -219,7 +222,7 @@ - (void)testWebViewSimpleAppLinkParsingZeroShouldFallback { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [[BFXMLAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -233,7 +236,7 @@ - (void)testWebViewSimpleAppLinkParsingZeroShouldFallback { XCTAssertNil(link.webURL); } -- (void)testWebViewSimpleAppLinkParsingFalseShouldFallback { +- (void)testXMLSimpleAppLinkParsingFalseShouldFallback { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @@ -245,7 +248,7 @@ - (void)testWebViewSimpleAppLinkParsingFalseShouldFallback { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [[BFXMLAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -259,7 +262,7 @@ - (void)testWebViewSimpleAppLinkParsingFalseShouldFallback { XCTAssertNil(link.webURL); } -- (void)testWebViewSimpleAppLinkParsingWithWebUrl { +- (void)testXMLSimpleAppLinkParsingWithWebUrl { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @@ -271,7 +274,7 @@ - (void)testWebViewSimpleAppLinkParsingWithWebUrl { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [[BFXMLAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -285,7 +288,7 @@ - (void)testWebViewSimpleAppLinkParsingWithWebUrl { XCTAssertEqualObjects([NSURL URLWithString:@"http://www.example.com"], link.webURL); } -- (void)testWebViewVersionedAppLinkParsing { +- (void)testXMLVersionedAppLinkParsing { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @@ -302,7 +305,7 @@ - (void)testWebViewVersionedAppLinkParsing { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [[BFXMLAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -321,7 +324,7 @@ - (void)testWebViewVersionedAppLinkParsing { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewVersionedAppLinkParsingOnlyUrls { +- (void)testXMLVersionedAppLinkParsingOnlyUrls { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios:url": @"bolts://" @@ -332,7 +335,7 @@ - (void)testWebViewVersionedAppLinkParsingOnlyUrls { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [[BFXMLAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -347,7 +350,7 @@ - (void)testWebViewVersionedAppLinkParsingOnlyUrls { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewVersionedAppLinkParsingUrlsAndNames { +- (void)testXMLVersionedAppLinkParsingUrlsAndNames { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios:url": @"bolts://" @@ -364,7 +367,7 @@ - (void)testWebViewVersionedAppLinkParsingUrlsAndNames { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [[BFXMLAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -381,7 +384,7 @@ - (void)testWebViewVersionedAppLinkParsingUrlsAndNames { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewPlatformFiltering { +- (void)testXMLPlatformFiltering { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @@ -409,7 +412,7 @@ - (void)testWebViewPlatformFiltering { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [[BFXMLAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result;