diff --git a/Bolts.podspec b/Bolts.podspec index 7d96c73eb..2ae2055a5 100644 --- a/Bolts.podspec +++ b/Bolts.podspec @@ -40,6 +40,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 = '' ss.watchos.source_files = '' ss.tvos.source_files = '' diff --git a/Bolts.xcodeproj/project.pbxproj b/Bolts.xcodeproj/project.pbxproj index 2d9e2f83f..ad00588d9 100644 --- a/Bolts.xcodeproj/project.pbxproj +++ b/Bolts.xcodeproj/project.pbxproj @@ -172,6 +172,14 @@ 8E8C8EFB17F23E5F00E3F1C7 /* TaskTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E9C3D1C17DE9F6500427E62 /* TaskTests.m */; }; 8E8C8F2917F241FF00E3F1C7 /* TaskTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E9C3D1C17DE9F6500427E62 /* TaskTests.m */; }; 8EDDA63017E17DDC00655F8A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8E9C3CEC17DE9DE000427E62 /* Foundation.framework */; }; + D0A9104F1A86BF8500BF399F /* BFXMLAppLinkResolver.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A9104D1A86BF8500BF399F /* BFXMLAppLinkResolver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0A910541A86C83E00BF399F /* BFAppLinkResolvingPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A910531A86C83E00BF399F /* BFAppLinkResolvingPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + D0B41D791C3EF97A00510849 /* BFAppLinkResolving.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A910511A86C4AF00BF399F /* BFAppLinkResolving.m */; }; + D0B41D7A1C3EF97B00510849 /* BFAppLinkResolving.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A910511A86C4AF00BF399F /* BFAppLinkResolving.m */; }; + D0B41D7B1C3EF98200510849 /* BFXMLAppLinkResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A9104E1A86BF8500BF399F /* BFXMLAppLinkResolver.m */; }; + D0B41D7C1C3EF98300510849 /* BFXMLAppLinkResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A9104E1A86BF8500BF399F /* BFXMLAppLinkResolver.m */; }; + D0B41D7D1C3EFB6300510849 /* BFAppLinkResolvingPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A910531A86C83E00BF399F /* BFAppLinkResolvingPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + D0B41D7E1C3EFB6F00510849 /* BFXMLAppLinkResolver.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A9104D1A86BF8500BF399F /* BFXMLAppLinkResolver.h */; settings = {ATTRIBUTES = (Public, ); }; }; F5AFC9EC1BA752750076E927 /* BFTaskCompletionSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA5319900A84000BAE3F /* BFTaskCompletionSource.m */; }; F5AFC9ED1BA752750076E927 /* BFTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA5119900A84000BAE3F /* BFTask.m */; }; F5AFC9EE1BA752750076E927 /* Bolts.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA5519900A84000BAE3F /* Bolts.m */; }; @@ -313,6 +321,10 @@ 8E9C3D1C17DE9F6500427E62 /* TaskTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TaskTests.m; sourceTree = ""; }; B242FAB819A567660097ECAE /* BFMeasurementEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFMeasurementEvent.h; sourceTree = ""; }; B242FAB919A567660097ECAE /* BFMeasurementEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFMeasurementEvent.m; 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 = ""; }; F5AFCA021BA752750076E927 /* Bolts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Bolts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F5AFCA131BA752770076E927 /* BoltsTests-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "BoltsTests-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; F5AFCA151BA752AF0076E927 /* Bolts-tvOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Bolts-tvOS.xcconfig"; sourceTree = ""; }; @@ -469,8 +481,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 */, @@ -654,6 +670,7 @@ buildActionMask = 2147483647; files = ( 1D5D7DBA1BE3CE8200FD67C7 /* BFWebViewAppLinkResolver.h in Headers */, + D0A9104F1A86BF8500BF399F /* BFXMLAppLinkResolver.h in Headers */, 81CD062B1CEED28A00497F47 /* BFTask+Exceptions.h in Headers */, 81CF830D1D0B559800633946 /* BFAppLinkReturnToRefererView_Internal.h in Headers */, 81CF83111D0B559800633946 /* BFURL_Internal.h in Headers */, @@ -673,6 +690,7 @@ 1D5D7DCC1BE3CE8200FD67C7 /* Bolts.h in Headers */, 1D5D7DCD1BE3CE8200FD67C7 /* BFCancellationToken.h in Headers */, 1D5D7DCE1BE3CE8200FD67C7 /* BFAppLink.h in Headers */, + D0A910541A86C83E00BF399F /* BFAppLinkResolvingPrivate.h in Headers */, 1D5D7DCF1BE3CE8200FD67C7 /* BFAppLinkReturnToRefererController.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -741,6 +759,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + D0B41D7E1C3EFB6F00510849 /* BFXMLAppLinkResolver.h in Headers */, 81ED94311BE1481900795F05 /* BFWebViewAppLinkResolver.h in Headers */, 81CD062A1CEED28A00497F47 /* BFTask+Exceptions.h in Headers */, 81CF830C1D0B559800633946 /* BFAppLinkReturnToRefererView_Internal.h in Headers */, @@ -754,6 +773,7 @@ 81ED94201BE147CF00795F05 /* BFExecutor.h in Headers */, 81ED94381BE1481900795F05 /* BFAppLinkTarget.h in Headers */, 81ED94301BE1481900795F05 /* BFAppLinkResolving.h in Headers */, + D0B41D7D1C3EFB6300510849 /* BFAppLinkResolvingPrivate.h in Headers */, 81ED943E1BE1481900795F05 /* BFURL.h in Headers */, 81CF830E1D0B559800633946 /* BFMeasurementEvent_Internal.h in Headers */, 81ED94221BE147CF00795F05 /* BFTaskCompletionSource.h in Headers */, @@ -1070,6 +1090,8 @@ 1D5D7DB41BE3CE8200FD67C7 /* BFAppLink.m in Sources */, 1D5D7DB51BE3CE8200FD67C7 /* BFExecutor.m in Sources */, 1D5D7DB61BE3CE8200FD67C7 /* BFCancellationToken.m in Sources */, + D0B41D7B1C3EF98200510849 /* BFXMLAppLinkResolver.m in Sources */, + D0B41D791C3EF97A00510849 /* BFAppLinkResolving.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1147,6 +1169,8 @@ 81ED942D1BE1481900795F05 /* BFAppLink.m in Sources */, 81ED94181BE147CF00795F05 /* BFExecutor.m in Sources */, 81ED94191BE147CF00795F05 /* BFCancellationToken.m in Sources */, + D0B41D7C1C3EF98300510849 /* BFXMLAppLinkResolver.m in Sources */, + D0B41D7A1C3EF97B00510849 /* BFAppLinkResolving.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Bolts/Common/Bolts.h b/Bolts/Common/Bolts.h index 907e4a2b6..e90583dd8 100644 --- a/Bolts/Common/Bolts.h +++ b/Bolts/Common/Bolts.h @@ -26,6 +26,7 @@ #import #import #import +#import #endif diff --git a/Bolts/iOS/BFAppLinkNavigation.h b/Bolts/iOS/BFAppLinkNavigation.h index 886b888c8..5d9c264bc 100644 --- a/Bolts/iOS/BFAppLinkNavigation.h +++ b/Bolts/iOS/BFAppLinkNavigation.h @@ -10,8 +10,6 @@ #import -#import - /*! The result of calling navigate on a BFAppLinkNavigation */ @@ -25,6 +23,7 @@ typedef NS_ENUM(NSInteger, BFAppLinkNavigationType) { }; @protocol BFAppLinkResolving; +@class BFAppLink; @class BFTask; /*! diff --git a/Bolts/iOS/BFAppLinkResolving.m b/Bolts/iOS/BFAppLinkResolving.m new file mode 100644 index 000000000..5ff476679 --- /dev/null +++ b/Bolts/iOS/BFAppLinkResolving.m @@ -0,0 +1,184 @@ +/* + * 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" +#import "BFTask.h" +#import "BFTaskCompletionSource.h" + +NSString *const BFAppLinkResolverPreferHeader = @"Prefer-Html-Meta-Tags"; +NSString *const BFAppLinkResolverMetaTagPrefix = @"al"; + +NSString *const BFAppLinkResolverRedirectDataKey = @"data"; +NSString *const BFAppLinkResolverRedirectResponseKey = @"response"; + +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; +#ifdef __TVOS_9_0 + case UIUserInterfaceIdiomTV: +#endif +#ifdef __IPHONE_9_3 + case UIUserInterfaceIdiomCarPlay: +#endif + case UIUserInterfaceIdiomUnspecified: + 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]; +} + +BFTask *BFFollowRedirects(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]; + + void (^completion)(NSURLResponse *response, NSData *data, NSError *error) = ^(NSURLResponse *response, NSData *data, NSError *error) { + if (error) { + [tcs setError:error]; + 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:@{ BFAppLinkResolverRedirectResponseKey : response, BFAppLinkResolverRedirectDataKey : data }]; + }; + +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9 + NSURLSession *session = [NSURLSession sharedSession]; + [[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + completion(response, data, error); + }] resume]; +#else + [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:completion]; +#endif + + return [tcs.task continueWithSuccessBlock:^id(BFTask *task) { + // If we redirected, just keep recursing. + if ([task.result isKindOfClass:[NSURL class]]) { + return BFFollowRedirects(task.result); + } + return task; + }]; +} + diff --git a/Bolts/iOS/BFAppLinkResolvingPrivate.h b/Bolts/iOS/BFAppLinkResolvingPrivate.h new file mode 100644 index 000000000..117c87489 --- /dev/null +++ b/Bolts/iOS/BFAppLinkResolvingPrivate.h @@ -0,0 +1,34 @@ +/* + * 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); + +/* + The returned task will be resolved with a dictionary containing the response data + */ +extern BFTask *BFFollowRedirects(NSURL *url); + +extern NSString *const BFAppLinkResolverPreferHeader; +extern NSString *const BFAppLinkResolverMetaTagPrefix; + +extern NSString *const BFAppLinkResolverRedirectDataKey; +extern NSString *const BFAppLinkResolverRedirectResponseKey; diff --git a/Bolts/iOS/BFWebViewAppLinkResolver.m b/Bolts/iOS/BFWebViewAppLinkResolver.m index b20891ef2..d8e42ae78 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 @@ -96,58 +85,11 @@ + (instancetype)sharedInstance { 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]; - - void (^completion)(NSURLResponse *response, NSData *data, NSError *error) = ^(NSURLResponse *response, NSData *data, NSError *error) { - if (error) { - [tcs setError:error]; - 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 }]; - }; - -#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9 - NSURLSession *session = [NSURLSession sharedSession]; - [[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - completion(response, data, error); - }] resume]; -#else - [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:completion]; -#endif - - 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] + return [BFFollowRedirects(url) continueWithExecutor:[BFExecutor mainThreadExecutor] withSuccessBlock:^id(BFTask *task) { - NSData *responseData = task.result[@"data"]; - NSHTTPURLResponse *response = task.result[@"response"]; + NSData *responseData = task.result[BFAppLinkResolverRedirectDataKey]; + NSHTTPURLResponse *response = task.result[BFAppLinkResolverRedirectResponseKey]; BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource]; UIWebView *webView = [[UIWebView alloc] init]; @@ -159,7 +101,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) { @@ -183,43 +125,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]; @@ -227,79 +132,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; -#ifdef __TVOS_9_0 - case UIUserInterfaceIdiomTV: -#endif -#ifdef __IPHONE_9_3 - case UIUserInterfaceIdiomCarPlay: -#endif - case UIUserInterfaceIdiomUnspecified: - 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..e4367c6f3 --- /dev/null +++ b/Bolts/iOS/BFXMLAppLinkResolver.m @@ -0,0 +1,80 @@ +/* + * 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 *)appLinkFromURLInBackground:(NSURL *)url { + return [BFFollowRedirects(url) continueWithSuccessBlock:^id(BFTask *task) { + NSData *responseData = task.result[BFAppLinkResolverRedirectDataKey]; + NSHTTPURLResponse *response = task.result[BFAppLinkResolverRedirectResponseKey]; + + 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 53b655568..53d797c9f 100644 --- a/BoltsTests/AppLinkTests.m +++ b/BoltsTests/AppLinkTests.m @@ -20,6 +20,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, @@ -210,9 +235,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] }, @{ @@ -223,7 +249,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; @@ -237,14 +263,14 @@ - (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] }, @{ @@ -256,7 +282,7 @@ - (void)testWebViewSimpleAppLinkParsingZeroShouldFallback { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -270,7 +296,7 @@ - (void)testWebViewSimpleAppLinkParsingZeroShouldFallback { XCTAssertNil(link.webURL); } -- (void)testWebViewSimpleAppLinkParsingFalseShouldFallback { +- (void)testSimpleAppLinkParsingFalseShouldFallbackWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios" : [NSNull null] }, @{ @@ -282,7 +308,7 @@ - (void)testWebViewSimpleAppLinkParsingFalseShouldFallback { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; @@ -296,7 +322,7 @@ - (void)testWebViewSimpleAppLinkParsingFalseShouldFallback { XCTAssertNil(link.webURL); } -- (void)testWebViewSimpleAppLinkParsingWithWebUrl { +- (void)testSimpleAppLinkParsingWithWebUrlWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios" : [NSNull null] }, @{ @@ -308,7 +334,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; @@ -322,7 +348,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] }, @{ @@ -339,7 +365,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; @@ -358,7 +384,7 @@ - (void)testWebViewVersionedAppLinkParsing { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewVersionedAppLinkParsingOnlyUrls { +- (void)testVersionedAppLinkParsingOnlyUrlsWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios:url" : @"bolts://" @@ -369,7 +395,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; @@ -384,7 +410,7 @@ - (void)testWebViewVersionedAppLinkParsingOnlyUrls { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewVersionedAppLinkParsingUrlsAndNames { +- (void)testVersionedAppLinkParsingUrlsAndNamesWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios:url" : @"bolts://" @@ -401,7 +427,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; @@ -418,7 +444,7 @@ - (void)testWebViewVersionedAppLinkParsingUrlsAndNames { XCTAssertEqualObjects(url, link.webURL); } -- (void)testWebViewPlatformFiltering { +- (void)testPlatformFilteringWithResolver:(id)resolver { NSString *html = [self htmlWithMetaTags:@[ @{ @"al:ios" : [NSNull null] }, @{ @@ -446,7 +472,7 @@ - (void)testWebViewPlatformFiltering { ]]; NSURL *url = [self dataUrlForHtml:html]; - BFTask *task = [[BFWebViewAppLinkResolver sharedInstance] appLinkFromURLInBackground:url]; + BFTask *task = [resolver appLinkFromURLInBackground:url]; [self waitForTaskOnMainThread:task]; BFAppLink *link = task.result; diff --git a/Configurations/Bolts-iOS-Dynamic.xcconfig b/Configurations/Bolts-iOS-Dynamic.xcconfig index 9a314c15c..81574cb9b 100644 --- a/Configurations/Bolts-iOS-Dynamic.xcconfig +++ b/Configurations/Bolts-iOS-Dynamic.xcconfig @@ -20,8 +20,8 @@ DYLIB_INSTALL_NAME_BASE = @rpath DEFINES_MODULE = YES MODULEMAP_FILE = $(SRCROOT)/Bolts/Resources/iOS.modulemap -OTHER_CFLAGS[sdk=iphoneos9.*] = $(inherited) -fembed-bitcode +OTHER_CFLAGS = $(inherited) -fembed-bitcode -I$(SDKROOT)/usr/include/libxml2 -OTHER_LDFLAGS = $(inherited) -ObjC -framework CoreGraphics -framework UIKit +OTHER_LDFLAGS = $(inherited) -ObjC -lxml2 -framework CoreGraphics -framework UIKit INFOPLIST_FILE = $(SRCROOT)/Bolts/Resources/iOS-Info.plist diff --git a/Configurations/Bolts-iOS.xcconfig b/Configurations/Bolts-iOS.xcconfig index 60869172d..c93663339 100644 --- a/Configurations/Bolts-iOS.xcconfig +++ b/Configurations/Bolts-iOS.xcconfig @@ -19,7 +19,7 @@ MACH_O_TYPE = staticlib DEFINES_MODULE = YES MODULEMAP_FILE = $(SRCROOT)/Bolts/Resources/iOS.modulemap -OTHER_CFLAGS[sdk=iphoneos9.*] = $(inherited) -fembed-bitcode +OTHER_CFLAGS = $(inherited) -fembed-bitcode -I$(SDKROOT)/usr/include/libxml2 OTHER_LDFLAGS = $(inherited) -ObjC diff --git a/Configurations/BoltsTests-iOS.xcconfig b/Configurations/BoltsTests-iOS.xcconfig index bc3005475..74974f9c8 100644 --- a/Configurations/BoltsTests-iOS.xcconfig +++ b/Configurations/BoltsTests-iOS.xcconfig @@ -21,4 +21,6 @@ TEST_HOST = $(BUNDLE_LOADER) INFOPLIST_FILE = BoltsTests/BoltsTests-Info.plist +OTHER_LDFLAGS = $(inherited) -lxml2 + CLANG_ENABLE_MODULES = YES