diff --git a/Example/DynamicLinks/App/iOS/DL-Info.plist b/Example/DynamicLinks/App/iOS/DL-Info.plist index fc26896d71d..87945bc6090 100644 --- a/Example/DynamicLinks/App/iOS/DL-Info.plist +++ b/Example/DynamicLinks/App/iOS/DL-Info.plist @@ -50,5 +50,11 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + FirebaseDynamicLinksCustomDomains + + https://google.com + https://google.com/one/ + https://a.firebase.com/mypath + diff --git a/Example/DynamicLinks/FDLBuilderTestAppObjC/Info.plist b/Example/DynamicLinks/FDLBuilderTestAppObjC/Info.plist index ee3a448f005..d1df0bfa0cb 100644 --- a/Example/DynamicLinks/FDLBuilderTestAppObjC/Info.plist +++ b/Example/DynamicLinks/FDLBuilderTestAppObjC/Info.plist @@ -2,6 +2,20 @@ + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 CFBundleURLTypes @@ -55,22 +69,22 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 CFBundleVersion 1 + FirebaseDynamicLinksCustomDomains + + https://mydomain.com + https://mydomain2.com + https://google.com + https://google.com + google + mydomain.com + https://mydomain + https://mydomain3.com + https://google.com/one + https://custom.com/one/two + https://custom1.com/one/ + LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/Example/DynamicLinks/FDLBuilderTestAppObjC/ViewController.m b/Example/DynamicLinks/FDLBuilderTestAppObjC/ViewController.m index cfacbc060ad..51450db9b4c 100644 --- a/Example/DynamicLinks/FDLBuilderTestAppObjC/ViewController.m +++ b/Example/DynamicLinks/FDLBuilderTestAppObjC/ViewController.m @@ -155,9 +155,9 @@ - (void)_initDefaultValues { }, // The default value of domain appcode belongs to project: app-invites-qa @{ - @"id" : @"domain", - @"label" : @"App domain (required)", - @"defaultValue" : @"testfdl.page.link", + @"id" : @"domainURIPrefix", + @"label" : @"App domainURIPrefix (required)", + @"defaultValue" : @"https://testfdl.page.link", }, // analytics params @{ @@ -289,7 +289,8 @@ - (void)_initDefaultValues { - (void)_buildFDLLink { NSURL *link = [NSURL URLWithString:_paramValues[@"linkString"]]; FIRDynamicLinkComponents *components = - [FIRDynamicLinkComponents componentsWithLink:link domain:_paramValues[@"domain"]]; + [FIRDynamicLinkComponents componentsWithLink:link + domainURIPrefix:_paramValues[@"https://domain"]]; FIRDynamicLinkGoogleAnalyticsParameters *analyticsParams = [FIRDynamicLinkGoogleAnalyticsParameters diff --git a/Example/DynamicLinks/FDLBuilderTestAppObjCTests/Info.plist b/Example/DynamicLinks/FDLBuilderTestAppObjCTests/Info.plist index 6c6c23c43ad..13fcbb5045d 100644 --- a/Example/DynamicLinks/FDLBuilderTestAppObjCTests/Info.plist +++ b/Example/DynamicLinks/FDLBuilderTestAppObjCTests/Info.plist @@ -18,5 +18,24 @@ 1.0 CFBundleVersion 1 + <?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<array> + <string>https://mydomain.com</string> + <string>https://mydomain2.com</string> + <string>https://google.com</string> + <string>https://google.com</string> + <string>go</string> + <string>g.co</string> + <string>https://go</string> + <string>https://g.co</string> + <string>https://google.com/one</string> + <string>https://custom.com/one/two</string> + <string>https://custom1.com/one/</string> +</array> +</plist> + + diff --git a/Example/DynamicLinks/Tests/FDLURLComponentsTests.m b/Example/DynamicLinks/Tests/FDLURLComponentsTests.m index b1a51ee5f3b..d2cd4b8ae9a 100644 --- a/Example/DynamicLinks/Tests/FDLURLComponentsTests.m +++ b/Example/DynamicLinks/Tests/FDLURLComponentsTests.m @@ -21,7 +21,8 @@ #import -static NSString *const kFDLURLDomain = @"xyz.page.link"; +static NSString *const kFDLURLDomain = @"https://xyz.page.link"; +static NSString *const kFDLURLCustomDomain = @"https://foo.com/path"; @interface FDLURLComponentsTests : XCTestCase @end @@ -461,14 +462,14 @@ - (void)testLinkOptionsParamsPropertiesSetProperly { - (void)testFDLComponentsFactoryReturnsInstanceOfCorrectClass { NSURL *link = [NSURL URLWithString:@"https://google.com"]; - id returnValue = [FIRDynamicLinkComponents componentsWithLink:link domain:kFDLURLDomain]; + id returnValue = [FIRDynamicLinkComponents componentsWithLink:link domainURIPrefix:kFDLURLDomain]; XCTAssertTrue([returnValue isKindOfClass:[FIRDynamicLinkComponents class]]); } - (void)testFDLComponentsFactoryReturnsInstanceWithAllNilProperties { NSURL *link = [NSURL URLWithString:@"https://google.com"]; FIRDynamicLinkComponents *components = - [FIRDynamicLinkComponents componentsWithLink:link domain:kFDLURLDomain]; + [FIRDynamicLinkComponents componentsWithLink:link domainURIPrefix:kFDLURLDomain]; XCTAssertNil(components.analyticsParameters); XCTAssertNil(components.socialMetaTagParameters); @@ -484,11 +485,27 @@ - (void)testFDLComponentsCreatesSimplestLinkCorrectly { NSURL *link = [NSURL URLWithString:linkString]; NSString *expectedURLString = - [NSString stringWithFormat:@"https://%@/?link=%@", kFDLURLDomain, endcodedLinkString]; + [NSString stringWithFormat:@"%@/?link=%@", kFDLURLDomain, endcodedLinkString]; NSURL *expectedURL = [NSURL URLWithString:expectedURLString]; FIRDynamicLinkComponents *components = - [FIRDynamicLinkComponents componentsWithLink:link domain:kFDLURLDomain]; + [FIRDynamicLinkComponents componentsWithLink:link domainURIPrefix:kFDLURLDomain]; + NSURL *actualURL = components.url; + + XCTAssertEqualObjects(actualURL, expectedURL); +} + +- (void)testFDLComponentsCustomDomainWithPath { + NSString *linkString = @"https://google.com"; + NSString *endcodedLinkString = @"https%3A%2F%2Fgoogle%2Ecom"; + NSURL *link = [NSURL URLWithString:linkString]; + + NSString *expectedURLString = + [NSString stringWithFormat:@"%@/?link=%@", kFDLURLCustomDomain, endcodedLinkString]; + NSURL *expectedURL = [NSURL URLWithString:expectedURLString]; + + FIRDynamicLinkComponents *components = + [FIRDynamicLinkComponents componentsWithLink:link domainURIPrefix:kFDLURLCustomDomain]; NSURL *actualURL = components.url; XCTAssertEqualObjects(actualURL, expectedURL); @@ -499,7 +516,8 @@ - (void)testFDLComponentsFailsOnMalformedDomain { NSURL *link = [NSURL URLWithString:linkString]; FIRDynamicLinkComponents *components = - [FIRDynamicLinkComponents componentsWithLink:link domain:@"this is invalid domain"]; + [FIRDynamicLinkComponents componentsWithLink:link + domainURIPrefix:@"this is invalid domain URI Prefix"]; XCTAssertNil(components.url); } @@ -553,7 +571,7 @@ - (void)testFDLComponentsCreatesFullLinkCorrectly { NSURL *link = [NSURL URLWithString:@"https://google.com"]; FIRDynamicLinkComponents *fdlComponents = - [FIRDynamicLinkComponents componentsWithLink:link domain:kFDLURLDomain]; + [FIRDynamicLinkComponents componentsWithLink:link domainURIPrefix:kFDLURLDomain]; fdlComponents.analyticsParameters = analyticsParams; fdlComponents.iOSParameters = iosParams; fdlComponents.iTunesConnectParameters = itcParams; @@ -642,7 +660,80 @@ - (void)testShortenURL { XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; NSURL *link = [NSURL URLWithString:@"https://google.com/abc"]; FIRDynamicLinkComponents *components = - [FIRDynamicLinkComponents componentsWithLink:link domain:kFDLURLDomain]; + [FIRDynamicLinkComponents componentsWithLink:link domainURIPrefix:kFDLURLDomain]; + [components + shortenWithCompletion:^(NSURL *_Nullable shortURL, NSArray *_Nullable warnings, + NSError *_Nullable error) { + XCTAssertEqualObjects(shortURL.absoluteString, shortURLString); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + + [keyProviderClassMock verify]; + [keyProviderClassMock stopMocking]; + [componentsClassMock verify]; + [componentsClassMock stopMocking]; +} + +- (void)testDeprecatedMethodComponentsWithLinkForDomain { + NSString *shortURLString = @"https://xyz.page.link/abcd"; + + // Mock key provider + id keyProviderClassMock = OCMClassMock([FIRDynamicLinkComponentsKeyProvider class]); + [[[keyProviderClassMock expect] andReturn:@"fake-api-key"] APIKey]; + + id componentsClassMock = OCMClassMock([FIRDynamicLinkComponents class]); + [[componentsClassMock expect] + sendHTTPRequest:OCMOCK_ANY + completion:[OCMArg checkWithBlock:^BOOL(id obj) { + void (^completion)(NSData *_Nullable, NSError *_Nullable) = obj; + NSDictionary *JSON = @{@"shortLink" : shortURLString}; + NSData *JSONData = [NSJSONSerialization dataWithJSONObject:JSON options:0 error:0]; + completion(JSONData, nil); + return YES; + }]]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; + NSURL *link = [NSURL URLWithString:@"https://google.com/abc"]; + FIRDynamicLinkComponents *components = + [FIRDynamicLinkComponents componentsWithLink:link domain:@"xyz.page.link"]; + [components + shortenWithCompletion:^(NSURL *_Nullable shortURL, NSArray *_Nullable warnings, + NSError *_Nullable error) { + XCTAssertEqualObjects(shortURL.absoluteString, shortURLString); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:0.1 handler:nil]; + + [keyProviderClassMock verify]; + [keyProviderClassMock stopMocking]; + [componentsClassMock verify]; + [componentsClassMock stopMocking]; +} + +- (void)testDeprecatedMethodComponentsWithLinkForDomainWithInvalidDomainScheme { + NSString *shortURLString = @"https://xyz.page.link/abcd"; + + // Mock key provider + id keyProviderClassMock = OCMClassMock([FIRDynamicLinkComponentsKeyProvider class]); + [[[keyProviderClassMock expect] andReturn:@"fake-api-key"] APIKey]; + + id componentsClassMock = OCMClassMock([FIRDynamicLinkComponents class]); + [[componentsClassMock expect] + sendHTTPRequest:OCMOCK_ANY + completion:[OCMArg checkWithBlock:^BOOL(id obj) { + void (^completion)(NSData *_Nullable, NSError *_Nullable) = obj; + NSDictionary *JSON = @{@"shortLink" : shortURLString}; + NSData *JSONData = [NSJSONSerialization dataWithJSONObject:JSON options:0 error:0]; + completion(JSONData, nil); + return YES; + }]]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; + NSURL *link = [NSURL URLWithString:@"https://google.com/abc"]; + FIRDynamicLinkComponents *components = + [FIRDynamicLinkComponents componentsWithLink:link domain:@"http://xyz.page.link"]; + XCTAssertNotNil(components); [components shortenWithCompletion:^(NSURL *_Nullable shortURL, NSArray *_Nullable warnings, NSError *_Nullable error) { @@ -679,7 +770,7 @@ - (void)testShortenURLReturnsErrorWhenAPIKeyMissing { [self expectationWithDescription:@"completion called with error"]; NSURL *link = [NSURL URLWithString:@"https://google.com/abc"]; FIRDynamicLinkComponents *components = - [FIRDynamicLinkComponents componentsWithLink:link domain:kFDLURLDomain]; + [FIRDynamicLinkComponents componentsWithLink:link domainURIPrefix:kFDLURLDomain]; [components shortenWithCompletion:^(NSURL *_Nullable shortURL, NSArray *_Nullable warnings, NSError *_Nullable error) { @@ -714,20 +805,11 @@ - (void)testShortenURLReturnsErrorWhenDomainIsMalformed { return YES; }]]; - XCTestExpectation *expectation = - [self expectationWithDescription:@"completion called with error"]; NSURL *link = [NSURL URLWithString:@"https://google.com/abc"]; FIRDynamicLinkComponents *components = - [FIRDynamicLinkComponents componentsWithLink:link domain:@"this is invalid domain"]; - [components - shortenWithCompletion:^(NSURL *_Nullable shortURL, NSArray *_Nullable warnings, - NSError *_Nullable error) { - XCTAssertNil(shortURL); - if (error) { - [expectation fulfill]; - } - }]; - [self waitForExpectationsWithTimeout:0.1 handler:nil]; + [FIRDynamicLinkComponents componentsWithLink:link + domainURIPrefix:@"this is invalid domain URI Prefix"]; + XCTAssertNil(components); [keyProviderClassMock verify]; [keyProviderClassMock stopMocking]; diff --git a/Example/DynamicLinks/Tests/FIRDynamicLinkNetworkingTests.m b/Example/DynamicLinks/Tests/FIRDynamicLinkNetworkingTests.m index 3a8070e2c49..80be311184e 100644 --- a/Example/DynamicLinks/Tests/FIRDynamicLinkNetworkingTests.m +++ b/Example/DynamicLinks/Tests/FIRDynamicLinkNetworkingTests.m @@ -16,7 +16,7 @@ #import -#import "OCMock.h" +#import #import #import "DynamicLinks/FIRDynamicLinkNetworking+Private.h" diff --git a/Example/DynamicLinks/Tests/FIRDynamicLinksTest.m b/Example/DynamicLinks/Tests/FIRDynamicLinksTest.m index 8dbb0e9a32d..7a50951eb2e 100644 --- a/Example/DynamicLinks/Tests/FIRDynamicLinksTest.m +++ b/Example/DynamicLinks/Tests/FIRDynamicLinksTest.m @@ -161,6 +161,9 @@ @implementation FIRDynamicLinksTest - (void)setUp { [super setUp]; + if (!(FIRApp.defaultApp)) { + [FIRApp configure]; + } self.service = [[FIRDynamicLinks alloc] init]; self.userDefaults = [[NSUserDefaults alloc] init]; [self.userDefaults removePersistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]]; @@ -223,7 +226,6 @@ - (void)testURLScheme_MinimumParameters { } - (void)testFactoryMethodReturnsProperClassObject { - [FIRApp configure]; id service = [FIRDynamicLinks dynamicLinks]; XCTAssertNotNil(service, @"Factory method returned nil"); @@ -1015,6 +1017,61 @@ - (void)testSelfDiagnoseCompletionCalled { [self waitForExpectationsWithTimeout:2.0 handler:nil]; } +#pragma mark - Custom domain tests +- (void)testValidCustomDomainNames { + // Entries in plist file: + // https://google.com + // https://google.com/one + // https://a.firebase.com/mypath + + NSArray *urlStrings = @[ + @"https://google.com/mylink", // Short FDL starting with 'https://google.com' + @"https://google.com/one", // Short FDL starting with 'https://google.com' + @"https://google.com?link=abcd", // Long FDL starting with 'https://google.com' + @"https://google.com/one/mylink", // Long FDL starting with 'https://google.com/one' + @"https://a.firebase.com/mypath/mylink", // Short FDL starting https://a.firebase.com/mypath + @"https://a.firebase.com/mypath?link=abcd&test=1", // Long FDL starting with + // https://a.firebase.com/mypath + ]; + + for (NSString *urlString in urlStrings) { + NSURL *url = [NSURL URLWithString:urlString]; + BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; + + XCTAssertTrue(matchesShortLinkFormat, + @"Non-DDL domain URL matched short link format with URL: %@", url); + } +} + +- (void)testInvalidCustomDomainNames { + // Entries in plist file: + // https://google.com + // https://google.com/one + // https://a.firebase.com/mypath + + NSArray *urlStrings = @[ + @"google.com", // Valid domain. No scheme. + @"https://google.com", // Valid domain. No path after domainURIPrefix. + @"https://google.com/", // Valid domain. No path after domainURIPrefix. + @"https://google.com/one/", // Valid domain. No path after domainURIPrefix. + @"https://google.com/one/two/mylink", // domainURIPrefix not exact match. + @"https://google.co.in/mylink", // No matching domainURIPrefix. + @"https://firebase.com/mypath", // No matching domainURIPrefix: Invalid (sub)domain. + @"https://b.firebase.com/mypath", // No matching domainURIPrefix: Invalid subdomain. + @"https://a.firebase.com/mypathabc", // No matching domainURIPrefix: Invalid subdomain. + @"mydomain.com", // https scheme not specified for domainURIPrefix. + @"http://mydomain", // Domain not in plist. No path after domainURIPrefix. + ]; + + for (NSString *urlString in urlStrings) { + NSURL *url = [NSURL URLWithString:urlString]; + BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; + + XCTAssertFalse(matchesShortLinkFormat, + @"Non-DDL domain URL matched short link format with URL: %@", url); + } +} + #pragma mark - Private Helpers - (void)removeAllFIRApps { diff --git a/Firebase/DynamicLinks/CHANGELOG.md b/Firebase/DynamicLinks/CHANGELOG.md index 48c29fe0237..c2aaf325693 100644 --- a/Firebase/DynamicLinks/CHANGELOG.md +++ b/Firebase/DynamicLinks/CHANGELOG.md @@ -1,3 +1,6 @@ +# v3.3.0 +- Introduced a new componentsWithLink:domainURIPrefix: and deprecated the existing componentsWithLink:domain:. (#1962, #2017, #2078, #2097, #2112) + # v3.2.0 - Delete deprecated source files. (#2038) diff --git a/Firebase/DynamicLinks/FDLURLComponents/FDLURLComponents.m b/Firebase/DynamicLinks/FDLURLComponents/FDLURLComponents.m index 9c6a0062ae0..5b200bf9397 100644 --- a/Firebase/DynamicLinks/FDLURLComponents/FDLURLComponents.m +++ b/Firebase/DynamicLinks/FDLURLComponents/FDLURLComponents.m @@ -20,6 +20,7 @@ #import "DynamicLinks/FDLURLComponents/FIRDynamicLinkComponentsKeyProvider.h" #import "DynamicLinks/Public/FDLURLComponents.h" +#import "DynamicLinks/Logging/FDLLogging.h" #import "DynamicLinks/Utilities/FDLUtilities.h" /// The exact behavior of dict[key] = value is unclear when value is nil. This function safely adds @@ -448,15 +449,49 @@ - (instancetype)init { @implementation FIRDynamicLinkComponents +#pragma mark Deprecated Initializers. + (instancetype)componentsWithLink:(NSURL *)link domain:(NSString *)domain { return [[self alloc] initWithLink:link domain:domain]; } - (instancetype)initWithLink:(NSURL *)link domain:(NSString *)domain { + NSURL *domainURL = [NSURL URLWithString:domain]; + if (domainURL.scheme) { + FDLLog(FDLLogLevelWarning, FDLLogIdentifierSetupWarnHTTPSScheme, + @"You have supplied a domain with a scheme. Please enter a domain name without the " + @"scheme."); + } + NSString *domainURIPrefix = [NSString stringWithFormat:@"https://%@", domain]; + self = [super init]; + if (self) { + _link = link; + _domain = domainURIPrefix; + } + return self; +} + +#pragma mark Initializers. ++ (instancetype)componentsWithLink:(NSURL *)link domainURIPrefix:(NSString *)domainURIPrefix { + return [[self alloc] initWithLink:link domainURIPrefix:domainURIPrefix]; +} + +- (instancetype)initWithLink:(NSURL *)link domainURIPrefix:(NSString *)domainURIPrefix { self = [super init]; if (self) { _link = link; - _domain = [domain copy]; + /// Must be a URL that conforms to RFC 2396. + NSURL *domainURIPrefixURL = [NSURL URLWithString:domainURIPrefix]; + if (!domainURIPrefixURL) { + FDLLog(FDLLogLevelError, FDLLogIdentifierSetupInvalidDomainURIPrefix, + @"Invalid domainURIPrefix. Please input a valid URL."); + return nil; + } + if (![[domainURIPrefixURL.scheme lowercaseString] isEqualToString:@"https"]) { + FDLLog(FDLLogLevelError, FDLLogIdentifierSetupInvalidDomainURIPrefixScheme, + @"Invalid domainURIPrefix scheme. Scheme needs to be https"); + return nil; + } + _domain = [domainURIPrefix copy]; } return self; } @@ -593,7 +628,7 @@ - (NSURL *)url { addEntriesFromDictionaryRepresentingConformerToDictionary(_otherPlatformParameters); NSString *queryString = FIRDLURLQueryStringFromDictionary(queryDictionary); - NSString *urlString = [NSString stringWithFormat:@"https://%@/%@", _domain, queryString]; + NSString *urlString = [NSString stringWithFormat:@"%@/%@", _domain, queryString]; return [NSURL URLWithString:urlString]; } diff --git a/Firebase/DynamicLinks/FIRDynamicLinks.m b/Firebase/DynamicLinks/FIRDynamicLinks.m index f12c0ed84c0..06e65070b42 100644 --- a/Firebase/DynamicLinks/FIRDynamicLinks.m +++ b/Firebase/DynamicLinks/FIRDynamicLinks.m @@ -60,6 +60,9 @@ // We should only open url once. We use the following key to store the state in the user defaults. static NSString *const kFIRDLOpenURLKey = @"com.google.appinvite.openURL"; +// Custom domains to be whitelisted are optionally added as an array to the info.plist. +static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustomDomains"; + NS_ASSUME_NONNULL_BEGIN @interface FIRDynamicLinks () @@ -216,6 +219,12 @@ - (void)configureDynamicLinks:(FIRApp *)app { } [NSException raise:kFirebaseDurableDeepLinkErrorDomain format:@"%@", message]; } + // Check to see if FirebaseDynamicLinksCustomDomains array is present. + NSDictionary *infoDictionary = [NSBundle mainBundle].infoDictionary; + NSArray *customDomains = infoDictionary[kInfoPlistCustomDomainsKey]; + if (customDomains) { + FIRDLAddToAllowListForCustomDomainsArray(customDomains); + } } - (instancetype)initWithAnalytics:(nullable id)analytics { diff --git a/Firebase/DynamicLinks/Logging/FDLLogging.h b/Firebase/DynamicLinks/Logging/FDLLogging.h index 5a9ea134d47..2d0efc572e2 100644 --- a/Firebase/DynamicLinks/Logging/FDLLogging.h +++ b/Firebase/DynamicLinks/Logging/FDLLogging.h @@ -33,6 +33,9 @@ typedef NS_ENUM(NSInteger, FDLLogIdentifier) { FDLLogIdentifierSetupNilAPIKey = 0, FDLLogIdentifierSetupNilClientID = 1, FDLLogIdentifierSetupNonDefaultApp = 2, + FDLLogIdentifierSetupInvalidDomainURIPrefixScheme = 3, + FDLLogIdentifierSetupInvalidDomainURIPrefix = 4, + FDLLogIdentifierSetupWarnHTTPSScheme = 5, }; /** The appropriate formatter for using NSInteger in FIRLogger. */ diff --git a/Firebase/DynamicLinks/Public/FDLURLComponents.h b/Firebase/DynamicLinks/Public/FDLURLComponents.h index cc27736f545..1cc8397560c 100644 --- a/Firebase/DynamicLinks/Public/FDLURLComponents.h +++ b/Firebase/DynamicLinks/Public/FDLURLComponents.h @@ -512,11 +512,14 @@ FIR_SWIFT_NAME(DynamicLinkComponents) * @param link Deep link to be stored in created Dynamic link. This link also called "payload" of * the Dynamic link. * @param domain Domain of your App. This value must be equal to your assigned domain from Firebase - * Console. + * Console. (e.g. xyz.page.link). Note that the domain scheme is required to be https and is + * assumed as such by this API. */ + (instancetype)componentsWithLink:(NSURL *)link domain:(NSString *)domain - NS_SWIFT_UNAVAILABLE("Use init(link:domain:)"); + NS_SWIFT_UNAVAILABLE("Use init(link:domain:)")DEPRECATED_MSG_ATTRIBUTE( + "This method is deprecated. Please use the new method with support for " + "domainURIPrefix- init(link:domainURIPrefix:)."); /** * @method initWithLink:domain: @@ -525,9 +528,44 @@ FIR_SWIFT_NAME(DynamicLinkComponents) * @param link Deep link to be stored in created Dynamic link. This link also called "payload" of * the Dynamic link. * @param domain Domain of your App. This value must be equal to your assigned domain from Firebase - * Console. + * Console. (e.g. xyz.page.link). Note that the domain scheme is required to be https and is + * assumed as such by this API. */ -- (instancetype)initWithLink:(NSURL *)link domain:(NSString *)domain; +- (instancetype)initWithLink:(NSURL *)link + domain:(NSString *)domain + DEPRECATED_MSG_ATTRIBUTE( + "This method is deprecated. Please use the new method with support for " + "domainURIPrefix- init(link:domainURIPrefix:)."); + +/** + * @method componentsWithLink:domainURIPrefix: + * @abstract Generates a Dynamic Link URL components object with the minimum necessary parameters + * set to generate a fully-functional Dynamic Link. + * @param link Deep link to be stored in created Dynamic link. This link also called "payload" of + * the Dynamic link. + * @param domainURIPrefix Domain URI Prefix of your App. This value must be your assigned + * domain from the Firebase console. (e.g. https://xyz.page.link) The domain URI prefix must + * start with a valid HTTPS scheme (https://). + * @return Returns an instance of FIRDynamicLinkComponents if the parameters succeed validation, + * else returns nil. + */ ++ (nullable instancetype)componentsWithLink:(NSURL *)link + domainURIPrefix:(NSString *)domainURIPrefix + NS_SWIFT_UNAVAILABLE("Use init(link:domainURIPrefix:)"); + +/** + * @method initWithLink:domainURIPrefix: + * @abstract Generates a Dynamic Link URL components object with the minimum necessary parameters + * set to generate a fully-functional Dynamic Link. + * @param link Deep link to be stored in created Dynamic link. This link also called "payload" of + * the Dynamic link. + * @param domainURIPrefix Domain URI Prefix of your App. This value must be your assigned + * domain from the Firebase console. (e.g. https://xyz.page.link) The domain URI prefix must + * start with a valid HTTPS scheme (https://). + * @return Returns an instance of FIRDynamicLinkComponents if the parameters succeed validation, + * else returns nil. + */ +- (nullable instancetype)initWithLink:(NSURL *)link domainURIPrefix:(NSString *)domainURIPrefix; /** * @method shortenURL:options:completion: diff --git a/Firebase/DynamicLinks/Utilities/FDLUtilities.h b/Firebase/DynamicLinks/Utilities/FDLUtilities.h index 1aa664f136f..adeeabdf50a 100644 --- a/Firebase/DynamicLinks/Utilities/FDLUtilities.h +++ b/Firebase/DynamicLinks/Utilities/FDLUtilities.h @@ -136,4 +136,9 @@ BOOL FIRDLMatchesShortLinkFormat(NSURL *URL); */ NSString *FIRDLMatchTypeStringFromServerString(NSString *_Nullable serverMatchTypeString); +/** + Add custom domains from the info.plist to the internal whitelist. + */ +void FIRDLAddToAllowListForCustomDomainsArray(NSArray *customDomains); + NS_ASSUME_NONNULL_END diff --git a/Firebase/DynamicLinks/Utilities/FDLUtilities.m b/Firebase/DynamicLinks/Utilities/FDLUtilities.m index 425e2a52909..5ded3c42d84 100644 --- a/Firebase/DynamicLinks/Utilities/FDLUtilities.m +++ b/Firebase/DynamicLinks/Utilities/FDLUtilities.m @@ -32,6 +32,7 @@ NSString *const kFIRDLParameterWeakMatchEndpoint = @"invitation_weakMatchEndpoint"; NSString *const kFIRDLParameterMatchMessage = @"match_message"; NSString *const kFIRDLParameterRequestIPVersion = @"request_ip_version"; +static NSSet *FIRDLCustomDomains = nil; NSURL *FIRDLCookieRetrievalURL(NSString *urlScheme, NSString *bundleID) { static NSString *const kFDLBundleIDQueryParameterName = @"fdl_ios_bundle_id"; @@ -192,6 +193,39 @@ BOOL FIRDLOSVersionSupported(NSString *_Nullable systemVersion, NSString *minSup return timeZoneName; } +BOOL FIRDLIsURLForWhiteListedCustomDomain(NSURL *_Nullable URL) { + BOOL customDomainMatchFound = false; + for (NSURL *allowedCustomDomain in FIRDLCustomDomains) { + // All custom domain host names should match at a minimum. + if ([allowedCustomDomain.host isEqualToString:URL.host]) { + NSString *urlStr = URL.absoluteString; + NSString *domainURIPrefixStr = allowedCustomDomain.absoluteString; + + // Next, do a string compare to check if entire domainURIPrefix matches as well. + if (([URL.absoluteString rangeOfString:allowedCustomDomain.absoluteString + options:NSCaseInsensitiveSearch | NSAnchoredSearch] + .location) == 0) { + // The (short) URL needs to be longer than the domainURIPrefix, it's first character after + // the domainURIPrefix needs to be '/' or '?' and should be followed by at-least one more + // character. + if (urlStr.length > domainURIPrefixStr.length + 1 && + ([urlStr characterAtIndex:domainURIPrefixStr.length] == '/' || + [urlStr characterAtIndex:domainURIPrefixStr.length] == '?')) { + // Check if there are any more '/' after the first '/' or '?' trailing the + // domainURIPrefix. + NSString *urlWithoutDomainURIPrefix = + [urlStr substringFromIndex:domainURIPrefixStr.length + 1]; + if ([urlWithoutDomainURIPrefix rangeOfString:@"/"].location == NSNotFound) { + customDomainMatchFound = true; + break; + } + } + } + } + } + return customDomainMatchFound; +} + BOOL FIRDLCanParseUniversalLinkURL(NSURL *_Nullable URL) { // Handle universal links with format |https://goo.gl/app/?|. // Also support page.link format. @@ -200,12 +234,19 @@ BOOL FIRDLCanParseUniversalLinkURL(NSURL *_Nullable URL) { // Handle universal links with format |https://.app.goo.gl?| and page.link. BOOL isDDLWithSubdomain = [URL.host hasSuffix:@".app.goo.gl"] || [URL.host hasSuffix:@".page.link"]; - return isDDLWithAppcodeInPath || isDDLWithSubdomain; + + // Handle universal links for custom domains. + BOOL isDDLWithCustomDomain = FIRDLIsURLForWhiteListedCustomDomain(URL); + + return isDDLWithAppcodeInPath || isDDLWithSubdomain || isDDLWithCustomDomain; } BOOL FIRDLMatchesShortLinkFormat(NSURL *URL) { - // Short Durable Link URLs always have a path. - BOOL hasPath = URL.path.length > 0; + // Short Durable Link URLs always have a path, except for certain custom domain URLs e.g. + // 'https://google.com?link=abcd' will not have a path component. + // FIRDLIsURLForWhiteListedCustomDomain implicitely checks for path component in custom domain + // URLs. + BOOL hasPath = URL.path.length > 0 || FIRDLIsURLForWhiteListedCustomDomain(URL); // Must be able to parse (also checks if the URL conforms to *.app.goo.gl/* or goo.gl/app/*) BOOL canParse = FIRDLCanParseUniversalLinkURL(URL); // Path cannot be prefixed with /link/dismiss @@ -227,4 +268,24 @@ BOOL FIRDLMatchesShortLinkFormat(NSURL *URL) { return matchMap[serverMatchTypeString] ?: @"none"; } +void FIRDLAddToAllowListForCustomDomainsArray(NSArray *_Nonnull customDomains) { + // Duplicates will be weeded out when converting to a set. + NSMutableArray *validCustomDomains = + [[NSMutableArray alloc] initWithCapacity:customDomains.count]; + for (NSString *customDomainEntry in customDomains) { + // We remove trailing slashes in the path if present. + NSString *domainEntry = + [customDomainEntry hasSuffix:@"/"] + ? [customDomainEntry substringToIndex:[customDomainEntry length] - 1] + : customDomainEntry; + NSURL *customDomainURL = [NSURL URLWithString:domainEntry]; + // We require a valid scheme for each custom domain enumerated in the info.plist file. + if (customDomainURL && customDomainURL.scheme) { + [validCustomDomains addObject:customDomainURL]; + } + } + // Duplicates will be weeded out when converting to a set. + FIRDLCustomDomains = [NSSet setWithArray:validCustomDomains]; +} + NS_ASSUME_NONNULL_END