Skip to content

Commit

Permalink
Added XML App Link Resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
Conrad Kramer committed Jun 30, 2016
1 parent 3eb2c00 commit 8efd408
Show file tree
Hide file tree
Showing 13 changed files with 410 additions and 197 deletions.
2 changes: 2 additions & 0 deletions Bolts.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
Expand Down
24 changes: 24 additions & 0 deletions Bolts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -313,6 +321,10 @@
8E9C3D1C17DE9F6500427E62 /* TaskTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TaskTests.m; sourceTree = "<group>"; };
B242FAB819A567660097ECAE /* BFMeasurementEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFMeasurementEvent.h; sourceTree = "<group>"; };
B242FAB919A567660097ECAE /* BFMeasurementEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFMeasurementEvent.m; sourceTree = "<group>"; };
D0A9104D1A86BF8500BF399F /* BFXMLAppLinkResolver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFXMLAppLinkResolver.h; sourceTree = "<group>"; };
D0A9104E1A86BF8500BF399F /* BFXMLAppLinkResolver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFXMLAppLinkResolver.m; sourceTree = "<group>"; };
D0A910511A86C4AF00BF399F /* BFAppLinkResolving.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BFAppLinkResolving.m; sourceTree = "<group>"; };
D0A910531A86C83E00BF399F /* BFAppLinkResolvingPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BFAppLinkResolvingPrivate.h; sourceTree = "<group>"; };
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 = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand All @@ -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;
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};
Expand Down
1 change: 1 addition & 0 deletions Bolts/Common/Bolts.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#import <Bolts/BFMeasurementEvent.h>
#import <Bolts/BFURL.h>
#import <Bolts/BFWebViewAppLinkResolver.h>
#import <Bolts/BFXMLAppLinkResolver.h>
#endif


Expand Down
3 changes: 1 addition & 2 deletions Bolts/iOS/BFAppLinkNavigation.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@

#import <Foundation/Foundation.h>

#import <Bolts/BFAppLink.h>

/*!
The result of calling navigate on a BFAppLinkNavigation
*/
Expand All @@ -25,6 +23,7 @@ typedef NS_ENUM(NSInteger, BFAppLinkNavigationType) {
};

@protocol BFAppLinkResolving;
@class BFAppLink;
@class BFTask;

/*!
Expand Down
184 changes: 184 additions & 0 deletions Bolts/iOS/BFAppLinkResolving.m
Original file line number Diff line number Diff line change
@@ -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 <UIKit/UIKit.h>

#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;
}];
}

34 changes: 34 additions & 0 deletions Bolts/iOS/BFAppLinkResolvingPrivate.h
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 8efd408

Please sign in to comment.