From 90b710b8d0c53096a08f899a4ab459c837b04177 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Thu, 26 Feb 2015 14:07:57 -0800 Subject: [PATCH] Added XML App Link Resolver --- Bolts.podspec | 2 + Bolts.xcodeproj/project.pbxproj | 26 ++++++ Bolts/iOS/BFAppLinkResolving.m | 124 ++++++++++++++++++++++++++ Bolts/iOS/BFAppLinkResolvingPrivate.h | 26 ++++++ Bolts/iOS/BFWebViewAppLinkResolver.m | 124 ++------------------------ Bolts/iOS/BFXMLAppLinkResolver.h | 28 ++++++ Bolts/iOS/BFXMLAppLinkResolver.m | 124 ++++++++++++++++++++++++++ BoltsTests/AppLinkTests.m | 71 ++++++++++----- 8 files changed, 384 insertions(+), 141 deletions(-) create mode 100644 Bolts/iOS/BFAppLinkResolving.m create mode 100644 Bolts/iOS/BFAppLinkResolvingPrivate.h create mode 100644 Bolts/iOS/BFXMLAppLinkResolver.h create mode 100644 Bolts/iOS/BFXMLAppLinkResolver.m diff --git a/Bolts.podspec b/Bolts.podspec index ce17d2686..34f5dc7bc 100644 --- a/Bolts.podspec +++ b/Bolts.podspec @@ -34,6 +34,8 @@ Pod::Spec.new do |s| ss.ios.source_files = 'Bolts/iOS/*.[hm]' ss.ios.public_header_files = 'Bolts/iOS/*.h' + ss.ios.libraries = 'xml2' + ss.ios.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2' } ss.osx.source_files = '' end diff --git a/Bolts.xcodeproj/project.pbxproj b/Bolts.xcodeproj/project.pbxproj index 5e709706b..bb7b0a187 100644 --- a/Bolts.xcodeproj/project.pbxproj +++ b/Bolts.xcodeproj/project.pbxproj @@ -63,6 +63,10 @@ 8EA6BF691805CF5600337041 /* BoltsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8EA6BF681805CF5600337041 /* BoltsTests.m */; }; 8EDDA63017E17DDC00655F8A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E9C3CEC17DE9DE000427E62 /* Foundation.framework */; }; B242FABB19A567660097ECAE /* BFMeasurementEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = B242FAB919A567660097ECAE /* BFMeasurementEvent.m */; }; + D0A9104F1A86BF8500BF399F /* BFXMLAppLinkResolver.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A9104D1A86BF8500BF399F /* BFXMLAppLinkResolver.h */; }; + D0A910501A86BF8500BF399F /* BFXMLAppLinkResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A9104E1A86BF8500BF399F /* BFXMLAppLinkResolver.m */; }; + D0A910521A86C4AF00BF399F /* BFAppLinkResolving.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A910511A86C4AF00BF399F /* BFAppLinkResolving.m */; }; + D0A910541A86C83E00BF399F /* BFAppLinkResolvingPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A910531A86C83E00BF399F /* BFAppLinkResolvingPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -150,6 +154,10 @@ B242FAB919A567660097ECAE /* BFMeasurementEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFMeasurementEvent.m; sourceTree = ""; }; B242FABA19A567660097ECAE /* BFURL_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFURL_Internal.h; sourceTree = ""; }; B242FAC019A599CD0097ECAE /* BFMeasurementEvent_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BFMeasurementEvent_Internal.h; sourceTree = ""; }; + D0A9104D1A86BF8500BF399F /* BFXMLAppLinkResolver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFXMLAppLinkResolver.h; sourceTree = ""; }; + D0A9104E1A86BF8500BF399F /* BFXMLAppLinkResolver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFXMLAppLinkResolver.m; sourceTree = ""; }; + D0A910511A86C4AF00BF399F /* BFAppLinkResolving.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFAppLinkResolving.m; sourceTree = ""; }; + D0A910531A86C83E00BF399F /* BFAppLinkResolvingPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFAppLinkResolvingPrivate.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -249,8 +257,12 @@ 8103FA5A19900A84000BAE3F /* BFAppLinkNavigation.h */, 8103FA5B19900A84000BAE3F /* BFAppLinkNavigation.m */, 8103FA5C19900A84000BAE3F /* BFAppLinkResolving.h */, + D0A910531A86C83E00BF399F /* BFAppLinkResolvingPrivate.h */, + D0A910511A86C4AF00BF399F /* BFAppLinkResolving.m */, 8103FA6619900A84000BAE3F /* BFWebViewAppLinkResolver.h */, 8103FA6719900A84000BAE3F /* BFWebViewAppLinkResolver.m */, + D0A9104D1A86BF8500BF399F /* BFXMLAppLinkResolver.h */, + D0A9104E1A86BF8500BF399F /* BFXMLAppLinkResolver.m */, 8103FA5D19900A84000BAE3F /* BFAppLinkReturnToRefererController.h */, 8103FA5E19900A84000BAE3F /* BFAppLinkReturnToRefererController.m */, 8103FA5F19900A84000BAE3F /* BFAppLinkReturnToRefererView.h */, @@ -362,11 +374,13 @@ 81D0EE8F19AFAA5F0000AE75 /* BFAppLinkReturnToRefererController.h in Headers */, 81D0EE8E19AFAA5F0000AE75 /* BFAppLinkResolving.h in Headers */, 81D0EE9019AFAA5F0000AE75 /* BFAppLinkReturnToRefererView.h in Headers */, + D0A9104F1A86BF8500BF399F /* BFXMLAppLinkResolver.h in Headers */, 81D0EE8719AFAA220000AE75 /* BFExecutor.h in Headers */, 81D0EE8919AFAA2B0000AE75 /* BFTaskCompletionSource.h in Headers */, 81D0EE9119AFAA6F0000AE75 /* BFAppLinkTarget.h in Headers */, 81D0EE8019AFA9E20000AE75 /* BoltsVersion.h in Headers */, 81D0EE8C19AFAA5F0000AE75 /* BFAppLink.h in Headers */, + D0A910541A86C83E00BF399F /* BFAppLinkResolvingPrivate.h in Headers */, 81D0EE9219AFAA6F0000AE75 /* BFMeasurementEvent.h in Headers */, 81D0EE9319AFAA6F0000AE75 /* BFURL.h in Headers */, 81D0EE8419AFAA100000AE75 /* Bolts.h in Headers */, @@ -575,8 +589,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D0A910501A86BF8500BF399F /* BFXMLAppLinkResolver.m in Sources */, 8103FA7819900A84000BAE3F /* BFAppLinkTarget.m in Sources */, 8103FA6C19900A84000BAE3F /* BFTaskCompletionSource.m in Sources */, + D0A910521A86C4AF00BF399F /* BFAppLinkResolving.m in Sources */, 8103FA7619900A84000BAE3F /* BFAppLinkReturnToRefererView.m in Sources */, 8103FA7419900A84000BAE3F /* BFAppLinkReturnToRefererController.m in Sources */, 8103FA7C19900A84000BAE3F /* BFWebViewAppLinkResolver.m in Sources */, @@ -864,7 +880,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNKNOWN_PRAGMAS = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SDKROOT)/usr/include/libxml2\"", + ); ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "-lxml2"; }; name = Debug; }; @@ -896,6 +917,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNKNOWN_PRAGMAS = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SDKROOT)/usr/include/libxml2\"", + ); + OTHER_LDFLAGS = "-lxml2"; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/Bolts/iOS/BFAppLinkResolving.m b/Bolts/iOS/BFAppLinkResolving.m new file mode 100644 index 000000000..0b702315d --- /dev/null +++ b/Bolts/iOS/BFAppLinkResolving.m @@ -0,0 +1,124 @@ +/* + * 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 "BFAppLinkResolving.h" +#import "BFAppLinkResolvingPrivate.h" +#import "BFAppLink.h" +#import "BFAppLinkTarget.h" + +NSString *const BFAppLinkResolverPreferHeader = @"Prefer-Html-Meta-Tags"; +NSString *const BFAppLinkResolverMetaTagPrefix = @"al"; + +static NSString *const BFAppLinkResolverIOSURLKey = @"url"; +static NSString *const BFAppLinkResolverIOSAppStoreIdKey = @"app_store_id"; +static NSString *const BFAppLinkResolverIOSAppNameKey = @"app_name"; +static NSString *const BFAppLinkResolverDictionaryValueKey = @"_value"; +static NSString *const BFAppLinkResolverWebKey = @"web"; +static NSString *const BFAppLinkResolverIOSKey = @"ios"; +static NSString *const BFAppLinkResolverIPhoneKey = @"iphone"; +static NSString *const BFAppLinkResolverIPadKey = @"ipad"; +static NSString *const BFAppLinkResolverWebURLKey = @"url"; +static NSString *const BFAppLinkResolverShouldFallbackKey = @"should_fallback"; + +NSDictionary *BFAppLinkResolverParseALData(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:BFAppLinkResolverMetaTagPrefix]) { + 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[BFAppLinkResolverDictionaryValueKey] = tag[@"content"]; + } + } + return al; +} + +BFAppLink *BFAppLinkResolverAppLinkFromALData(NSDictionary *appLinkDict, NSURL *destination) { + NSMutableArray *linkTargets = [NSMutableArray array]; + + NSArray *platformData = nil; + switch (UI_USER_INTERFACE_IDIOM()) { + case UIUserInterfaceIdiomPad: + platformData = @[appLinkDict[BFAppLinkResolverIPadKey] ?: @{}, + appLinkDict[BFAppLinkResolverIOSKey] ?: @{}]; + break; + case UIUserInterfaceIdiomPhone: + platformData = @[appLinkDict[BFAppLinkResolverIPhoneKey] ?: @{}, + appLinkDict[BFAppLinkResolverIOSKey] ?: @{}]; + break; + default: + // Future-proofing. Other User Interface idioms should only hit ios. + platformData = @[appLinkDict[BFAppLinkResolverIOSKey] ?: @{}]; + 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[BFAppLinkResolverIOSURLKey]; + NSArray *appStoreIds = platformDict[BFAppLinkResolverIOSAppStoreIdKey]; + NSArray *appNames = platformDict[BFAppLinkResolverIOSAppNameKey]; + + NSUInteger maxCount = MAX(urls.count, MAX(appStoreIds.count, appNames.count)); + + for (NSUInteger i = 0; i < maxCount; i++) { + NSString *urlString = urls[i][BFAppLinkResolverDictionaryValueKey]; + NSURL *url = urlString ? [NSURL URLWithString:urlString] : nil; + NSString *appStoreId = appStoreIds[i][BFAppLinkResolverDictionaryValueKey]; + NSString *appName = appNames[i][BFAppLinkResolverDictionaryValueKey]; + BFAppLinkTarget *target = [BFAppLinkTarget appLinkTargetWithURL:url + appStoreId:appStoreId + appName:appName]; + [linkTargets addObject:target]; + } + } + } + + NSDictionary *webDict = appLinkDict[BFAppLinkResolverWebKey][0]; + NSString *webUrlString = webDict[BFAppLinkResolverWebURLKey][0][BFAppLinkResolverDictionaryValueKey]; + NSString *shouldFallbackString = webDict[BFAppLinkResolverShouldFallbackKey][0][BFAppLinkResolverDictionaryValueKey]; + + 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]; +} diff --git a/Bolts/iOS/BFAppLinkResolvingPrivate.h b/Bolts/iOS/BFAppLinkResolvingPrivate.h new file mode 100644 index 000000000..a12d3c4a9 --- /dev/null +++ b/Bolts/iOS/BFAppLinkResolvingPrivate.h @@ -0,0 +1,26 @@ +/* + * 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. + * + */ + +@class BFAppLink; + +/* + 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". + */ +extern NSDictionary *BFAppLinkResolverParseALData(NSArray *dataArray); + +/* + Converts app link data into a BFAppLink containing the targets relevant for this platform. + */ +extern BFAppLink *BFAppLinkResolverAppLinkFromALData(NSDictionary *appLinkDict, NSURL *destination); + +extern NSString *const BFAppLinkResolverPreferHeader; +extern NSString *const BFAppLinkResolverMetaTagPrefix; diff --git a/Bolts/iOS/BFWebViewAppLinkResolver.m b/Bolts/iOS/BFWebViewAppLinkResolver.m index ffea98cb6..35a7e992b 100644 --- a/Bolts/iOS/BFWebViewAppLinkResolver.m +++ b/Bolts/iOS/BFWebViewAppLinkResolver.m @@ -11,6 +11,7 @@ #import #import "BFWebViewAppLinkResolver.h" +#import "BFAppLinkResolvingPrivate.h" #import "BFAppLink.h" #import "BFAppLinkTarget.h" #import "BFTask.h" @@ -34,18 +35,6 @@ " }" " 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 @@ -102,8 +91,8 @@ - (BFTask *)followRedirects:(NSURL *)url { // or a dictionary with the response data to be returned. BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; - [request setValue:BFWebViewAppLinkResolverMetaTagPrefix - forHTTPHeaderField:BFWebViewAppLinkResolverPreferHeader]; + [request setValue:BFAppLinkResolverMetaTagPrefix + forHTTPHeaderField:BFAppLinkResolverPreferHeader]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, @@ -157,7 +146,7 @@ - (BFTask *)appLinkFromURLInBackground:(NSURL *)url { [view removeFromSuperview]; view.delegate = nil; retainedListener = nil; - [tcs setResult:[self appLinkFromALData:ogData destination:url]]; + [tcs setResult:BFAppLinkResolverAppLinkFromALData(ogData, url)]; } }; listener.didFailLoadWithError = ^(UIWebView* view, NSError *error) { @@ -181,44 +170,6 @@ - (BFTask *)appLinkFromURLInBackground:(NSURL *)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: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]; @@ -226,72 +177,7 @@ - (NSDictionary *)getALDataFromLoadedPage:(UIWebView *)webView { 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]; + return BFAppLinkResolverParseALData(arr); } @end diff --git a/Bolts/iOS/BFXMLAppLinkResolver.h b/Bolts/iOS/BFXMLAppLinkResolver.h new file mode 100644 index 000000000..707429157 --- /dev/null +++ b/Bolts/iOS/BFXMLAppLinkResolver.h @@ -0,0 +1,28 @@ +/* + * 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 "BFAppLinkResolving.h" + +/*! + A reference implementation for an App Link resolver that uses libxml2 + to parse the HTML containing App Link metadata. + */ +@interface BFXMLAppLinkResolver : NSObject + +/*! + Gets the instance of a BFXMLAppLinkResolver. + */ ++ (instancetype)sharedInstance; + +@end + +extern NSString *const BFXMLAppLinkResolverErrorDomain; diff --git a/Bolts/iOS/BFXMLAppLinkResolver.m b/Bolts/iOS/BFXMLAppLinkResolver.m new file mode 100644 index 000000000..1e94b7a28 --- /dev/null +++ b/Bolts/iOS/BFXMLAppLinkResolver.m @@ -0,0 +1,124 @@ +/* + * 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 "BFAppLinkResolvingPrivate.h" +#import "BFAppLink.h" +#import "BFAppLinkTarget.h" +#import "BFTask.h" +#import "BFTaskCompletionSource.h" + +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:BFAppLinkResolverMetaTagPrefix + forHTTPHeaderField:BFAppLinkResolverPreferHeader]; + [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:BFAppLinkResolverAppLinkFromALData(BFAppLinkResolverParseALData(results), url)]; + }]; +} + +@end diff --git a/BoltsTests/AppLinkTests.m b/BoltsTests/AppLinkTests.m index 70e335a29..1d9f32ab0 100644 --- a/BoltsTests/AppLinkTests.m +++ b/BoltsTests/AppLinkTests.m @@ -13,6 +13,7 @@ #import "Bolts.h" #import "BFWebViewAppLinkResolver.h" +#import "BFXMLAppLinkResolver.h" NSMutableArray *openedUrls = nil; @@ -22,6 +23,31 @@ @interface AppLinkTests : XCTestCase @implementation AppLinkTests ++ (NSArray *)testInvocations { + NSMutableArray *testInvocations = [[super testInvocations] mutableCopy]; + + NSArray *resolvers = @[[BFWebViewAppLinkResolver sharedInstance], [BFXMLAppLinkResolver sharedInstance]]; + + unsigned count; + Method *methods = class_copyMethodList(self, &count); + for (unsigned i = 0; i < count; i++) { + SEL selector = method_getName(methods[i]); + NSString *name = NSStringFromSelector(selector); + if ([name hasPrefix:@"test"] && [name hasSuffix:@"WithResolver:"]) { + for (id resolver in resolvers) { + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self instanceMethodSignatureForSelector:selector]]; + [invocation setSelector:selector]; + [invocation setArgument:(void *)&resolver atIndex:2]; + [invocation retainArguments]; + [testInvocations addObject:invocation]; + } + + } + } + + return [testInvocations copy]; +} + - (NSString *)stringByEscapingQueryString:(NSString *)string { return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)string, @@ -196,9 +222,10 @@ - (void)testOpenedIncomingURLWithAppLinkWillPostEvent { XCTAssertTrue(notificationSent, @"URLWithInboundURL didn't sent notification."); } -#pragma mark WebView App Link resolution +#pragma mark Built in App Link resolution + -- (void)testWebViewSimpleAppLinkParsing { +- (void)testSimpleAppLinkParsingWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @@ -209,7 +236,7 @@ - (void)testWebViewSimpleAppLinkParsing { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -223,26 +250,26 @@ - (void)testWebViewSimpleAppLinkParsing { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewAppLinkParsingFailure { - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:[NSURL URLWithString:@"http://badurl"]]; +- (void)testAppLinkParsingFailureWithResolver:(id)resolver { + BFTask *task = [resolver appLinkFromURLInBackground:[NSURL URLWithString:@"http://badurl"]]; [self waitForTaskOnMainThread:task]; XCTAssertNotNil(task.error); } -- (void)testWebViewSimpleAppLinkParsingZeroShouldFallback { +- (void)testSimpleAppLinkParsingNoShouldFallbackWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @"al:ios:url": @"bolts://", @"al:ios:app_name": @"Bolts", @"al:ios:app_store_id": @"12345", - @"al:web:should_fallback": @"0" + @"al:web:should_fallback": @"No" // case insensitive } ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -256,19 +283,19 @@ - (void)testWebViewSimpleAppLinkParsingZeroShouldFallback { XCTAssertNil(link.webURL); } -- (void)testWebViewSimpleAppLinkParsingFalseShouldFallback { +- (void)testSimpleAppLinkParsingFalseShouldFallbackWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @"al:ios:url": @"bolts://", @"al:ios:app_name": @"Bolts", @"al:ios:app_store_id": @"12345", - @"al:web:should_fallback": @"fAlse" // case insensitive + @"al:web:should_fallback": @"false" } ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -282,7 +309,7 @@ - (void)testWebViewSimpleAppLinkParsingFalseShouldFallback { XCTAssertNil(link.webURL); } -- (void)testWebViewSimpleAppLinkParsingWithWebUrl { +- (void)testSimpleAppLinkParsingWithWebUrlWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @@ -294,7 +321,7 @@ - (void)testWebViewSimpleAppLinkParsingWithWebUrl { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -308,7 +335,7 @@ - (void)testWebViewSimpleAppLinkParsingWithWebUrl { XCTAssertEqualObjects([NSURL URLWithString:@"http://www.example.com"], link.webURL); } -- (void)testWebViewVersionedAppLinkParsing { +- (void)testVersionedAppLinkParsingWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @@ -325,7 +352,7 @@ - (void)testWebViewVersionedAppLinkParsing { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -344,7 +371,7 @@ - (void)testWebViewVersionedAppLinkParsing { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewVersionedAppLinkParsingOnlyUrls { +- (void)testVersionedAppLinkParsingOnlyUrlsWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios:url": @"bolts://" @@ -355,7 +382,7 @@ - (void)testWebViewVersionedAppLinkParsingOnlyUrls { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -370,7 +397,7 @@ - (void)testWebViewVersionedAppLinkParsingOnlyUrls { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewVersionedAppLinkParsingUrlsAndNames { +- (void)testVersionedAppLinkParsingUrlsAndNamesWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios:url": @"bolts://" @@ -387,7 +414,7 @@ - (void)testWebViewVersionedAppLinkParsingUrlsAndNames { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -404,7 +431,7 @@ - (void)testWebViewVersionedAppLinkParsingUrlsAndNames { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewPlatformFiltering { +- (void)testPlatformFilteringWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios": [NSNull null] }, @{ @@ -426,13 +453,13 @@ - (void)testWebViewPlatformFiltering { }, @{ @"al:android": [NSNull null] }, @{ - @"al:android:url": @"bolts2://android", + @"al:android:url": @"bolts2://ipad", @"al:android:package": @"com.bolts2", }, ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result;