/* * Copyright 2018 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #import #if TARGET_OS_IOS #import "FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.h" #import #include NS_ASSUME_NONNULL_BEGIN NSString *const kFIRDLParameterDeepLinkIdentifier = @"deep_link_id"; NSString *const kFIRDLParameterLink = @"link"; NSString *const kFIRDLParameterMinimumAppVersion = @"imv"; NSString *const kFIRDLParameterCampaign = @"utm_campaign"; NSString *const kFIRDLParameterContent = @"utm_content"; NSString *const kFIRDLParameterMedium = @"utm_medium"; NSString *const kFIRDLParameterSource = @"utm_source"; NSString *const kFIRDLParameterTerm = @"utm_term"; NSString *const kFIRDLParameterMatchType = @"match_type"; NSString *const kFIRDLParameterInviteId = @"invitation_id"; 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"; static NSString *const kFDLURLSchemeQueryParameterName = @"fdl_ios_url_scheme"; NSURLComponents *components = [[NSURLComponents alloc] init]; components.scheme = @"https"; components.host = @"goo.gl"; components.path = @"/app/_/deeplink"; NSMutableArray *queryItems = [NSMutableArray array]; [queryItems addObject:[NSURLQueryItem queryItemWithName:kFDLBundleIDQueryParameterName value:bundleID]]; [queryItems addObject:[NSURLQueryItem queryItemWithName:kFDLURLSchemeQueryParameterName value:urlScheme]]; [components setQueryItems:queryItems]; return [components URL]; } NSString *FIRDLURLQueryStringFromDictionary(NSDictionary *dictionary) { NSMutableString *parameters = [NSMutableString string]; NSCharacterSet *queryCharacterSet = [NSCharacterSet alphanumericCharacterSet]; NSString *parameterFormatString = @"%@%@=%@"; __block NSUInteger index = 0; [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSString *_Nonnull value, BOOL *_Nonnull stop) { NSString *delimiter = index++ == 0 ? @"?" : @"&"; NSString *encodedValue = [value stringByAddingPercentEncodingWithAllowedCharacters:queryCharacterSet]; NSString *parameter = [NSString stringWithFormat:parameterFormatString, delimiter, key, encodedValue]; [parameters appendString:parameter]; }]; return parameters; } NSDictionary *FIRDLDictionaryFromQuery(NSString *queryString) { NSArray *keyValuePairs = [queryString componentsSeparatedByString:@"&"]; NSMutableDictionary *queryDictionary = [NSMutableDictionary dictionaryWithCapacity:keyValuePairs.count]; for (NSString *pair in keyValuePairs) { NSArray *keyValuePair = [pair componentsSeparatedByString:@"="]; if (keyValuePair.count == 2) { NSString *key = keyValuePair[0]; NSString *value = [keyValuePair[1] stringByRemovingPercentEncoding]; [queryDictionary setObject:value forKey:key]; } } return [queryDictionary copy]; } NSURL *FIRDLDeepLinkURLWithInviteID(NSString *_Nullable inviteID, NSString *_Nullable deepLinkString, NSString *_Nullable utmSource, NSString *_Nullable utmMedium, NSString *_Nullable utmCampaign, NSString *_Nullable utmContent, NSString *_Nullable utmTerm, BOOL isWeakLink, NSString *_Nullable weakMatchEndpoint, NSString *_Nullable minAppVersion, NSString *URLScheme, NSString *_Nullable matchMessage) { // We are unable to use NSURLComponents as NSURLQueryItem is avilable beginning in iOS 8 and // appending our query string with NSURLComponents improperly formats the query by adding // a second '?' in the query. NSMutableDictionary *queryDictionary = [NSMutableDictionary dictionary]; if (inviteID != nil) { queryDictionary[kFIRDLParameterInviteId] = inviteID; } if (deepLinkString != nil) { queryDictionary[kFIRDLParameterDeepLinkIdentifier] = deepLinkString; } if (utmSource != nil) { queryDictionary[kFIRDLParameterSource] = utmSource; } if (utmMedium != nil) { queryDictionary[kFIRDLParameterMedium] = utmMedium; } if (utmCampaign != nil) { queryDictionary[kFIRDLParameterCampaign] = utmCampaign; } if (utmContent != nil) { queryDictionary[kFIRDLParameterContent] = utmContent; } if (utmTerm != nil) { queryDictionary[kFIRDLParameterTerm] = utmTerm; } if (isWeakLink) { queryDictionary[kFIRDLParameterMatchType] = @"weak"; } else { queryDictionary[kFIRDLParameterMatchType] = @"unique"; } if (weakMatchEndpoint != nil) { queryDictionary[kFIRDLParameterWeakMatchEndpoint] = weakMatchEndpoint; } if (minAppVersion != nil) { queryDictionary[kFIRDLParameterMinimumAppVersion] = minAppVersion; } if (matchMessage != nil) { queryDictionary[kFIRDLParameterMatchMessage] = matchMessage; } NSString *scheme = [URLScheme lowercaseString]; NSString *queryString = FIRDLURLQueryStringFromDictionary(queryDictionary); NSString *urlString = [NSString stringWithFormat:@"%@://google/link/%@", scheme, queryString]; return [NSURL URLWithString:urlString]; } BOOL FIRDLOSVersionSupported(NSString *_Nullable systemVersion, NSString *minSupportedVersion) { systemVersion = systemVersion ?: [UIDevice currentDevice].systemVersion; return [systemVersion compare:minSupportedVersion options:NSNumericSearch] != NSOrderedAscending; } NSDate *_Nullable FIRDLAppInstallationDate(void) { NSURL *documentsDirectoryURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] firstObject]; if (!documentsDirectoryURL) { return nil; } NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:documentsDirectoryURL.path error:NULL]; if (attributes[NSFileCreationDate] && [attributes[NSFileCreationDate] isKindOfClass:[NSDate class]]) { return attributes[NSFileCreationDate]; } return nil; } NSString *FIRDLDeviceModelName(void) { // this method will return string like iPad3,3 // for Simulator this will be x86_64 static NSString *machineString = @""; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ size_t size; // compute string size if (sysctlbyname("hw.machine", NULL, &size, NULL, 0) == 0) { // get device name char *machine = calloc(1, size); if (sysctlbyname("hw.machine", machine, &size, NULL, 0) == 0) { machineString = [NSString stringWithCString:machine encoding:NSUTF8StringEncoding]; } free(machine); } }); return machineString; } NSString *FIRDLDeviceLocale(void) { // expected return value from this method looks like: @"en-US" return [[[NSLocale currentLocale] localeIdentifier] stringByReplacingOccurrencesOfString:@"_" withString:@"-"]; } NSString *FIRDLDeviceLocaleRaw(void) { return [[NSLocale currentLocale] localeIdentifier]; } NSString *FIRDLDeviceTimezone(void) { NSString *timeZoneName = [[NSTimeZone localTimeZone] name]; return timeZoneName; } BOOL FIRDLIsURLForAllowedCustomDomain(NSURL *URL) { if (URL) { for (NSURL *allowedCustomDomain in FIRDLCustomDomains) { // At least one custom domain host name should match at a minimum. if ([URL.absoluteString hasPrefix:allowedCustomDomain.absoluteString]) { NSString *urlWithoutDomainURIPrefix = [URL.absoluteString substringFromIndex:allowedCustomDomain.absoluteString.length]; // The urlWithoutDomainURIPrefix should be starting with '/' or '?' otherwise it means the // allowed domain is not exactly matching the incoming URL domain prefix. if ([urlWithoutDomainURIPrefix hasPrefix:@"/"] || [urlWithoutDomainURIPrefix hasPrefix:@"?"]) { // For a valid custom domain DL Suffix the urlWithoutDomainURIPrefix should have: // 1. At least one path exists OR // 2. Should have a link query param with an http/https link NSURLComponents *components = [[NSURLComponents alloc] initWithString:urlWithoutDomainURIPrefix]; if (components.path && components.path.length > 1) { // Have a path exists. So valid custom domain. return true; } if (components.queryItems && components.queryItems.count > 0) { for (NSURLQueryItem *queryItem in components.queryItems) { // Checks whether we have a link query param if ([queryItem.name caseInsensitiveCompare:@"link"] == NSOrderedSame) { // Checks whether link query param value starts with http/https if (queryItem.value && ([queryItem.value hasPrefix:@"http://"] || [queryItem.value hasPrefix:@"https://"])) { return true; } } } } } } } } return false; } /* We are validating following domains in proper format. *.page.link *.app.goo.gl *.page.link/i/ *.app.goo.gl/i/ */ BOOL FIRDLIsAValidDLWithFDLDomain(NSURL *_Nullable URL) { BOOL matchesRegularExpression = false; NSString *urlStr = URL.absoluteString; if ([URL.host containsString:@".page.link"] || [URL.host containsString:@".app.goo.gl"] || [URL.host containsString:@".app.google"]) { // Matches the *.page.link and *.app.goo.gl domains. matchesRegularExpression = ([urlStr rangeOfString: @"^https?://" @"[a-zA-Z0-9]+((\\.app\\.goo\\.gl)|(\\.page\\.link)|(\\.app\\.google))((\\/" @"?\\?.*link=https?.*)|(\\/[a-zA-Z0-9-_]+)((\\/?\\?.*=.*)?$|$))" options:NSRegularExpressionSearch] .location != NSNotFound); if (!matchesRegularExpression) { // Matches the *.page.link/i/ and *.app.goo.gl/i/ domains. // Checks whether the URL is of the format : // http(s)://$DOMAIN(.page.link or .app.goo.gl)/i/$ANYTHING matchesRegularExpression = ([urlStr rangeOfString: @"^https?:\\/\\/" @"[a-zA-Z0-9]+((\\.app\\.goo\\.gl)|(\\.page\\.link)|(\\.app\\.google))\\/i\\/.*$" options:NSRegularExpressionSearch] .location != NSNotFound); } } return matchesRegularExpression; } /* DL can be parsed if it : 1. Has http(s)://goo.gl/app* or http(s)://page.link/app* format 2. OR http(s)://$DomainPrefix.page.link or http(s)://$DomainPrefix.app.goo.gl domain with specific format 3. OR the domain is a listed custom domain */ BOOL FIRDLCanParseUniversalLinkURL(NSURL *_Nullable URL) { // Handle universal links with format |https://goo.gl/app/?|. // Also support page.link format. BOOL isDDLWithAppcodeInPath = ([URL.host isEqual:@"goo.gl"] || [URL.host isEqual:@"page.link"] || [URL.host isEqual:@"app.google"]) && [URL.path hasPrefix:@"/app"]; return isDDLWithAppcodeInPath || FIRDLIsAValidDLWithFDLDomain(URL) || FIRDLIsURLForAllowedCustomDomain(URL); } BOOL FIRDLMatchesShortLinkFormat(NSURL *URL) { // Short Durable Link URLs always have a path or it should be a custom domain. BOOL hasPathOrCustomDomain = URL.path.length > 0 || FIRDLIsURLForAllowedCustomDomain(URL); // Must be able to parse (also checks if the URL conforms to *.app.goo.gl/* or goo.gl/app/* or // *.page.link or custom domain with valid suffix) BOOL canParse = FIRDLCanParseUniversalLinkURL(URL); // Path cannot be prefixed with /link/dismiss BOOL isDismiss = [[URL.path lowercaseString] hasPrefix:@"/link/dismiss"]; // Checks short link format by having only one path after domain prefix. BOOL matchesRegularExpression = ([URL.path rangeOfString:@"/[^/]+" options:NSRegularExpressionSearch].location != NSNotFound); return hasPathOrCustomDomain && !isDismiss && canParse && matchesRegularExpression; } NSString *FIRDLMatchTypeStringFromServerString(NSString *_Nullable serverMatchTypeString) { static NSDictionary *matchMap; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ matchMap = @{ @"WEAK" : @"weak", @"DEFAULT" : @"default", @"UNIQUE" : @"unique", }; }); 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 #endif // TARGET_OS_IOS