/* * 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 kFIRDLParameterSource = @"utm_source"; NSString *const kFIRDLParameterMedium = @"utm_medium"; NSString *const kFIRDLParameterCampaign = @"utm_campaign"; 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, 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 (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() { 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() { // 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() { // expected return value from this method looks like: @"en-US" return [[[NSLocale currentLocale] localeIdentifier] stringByReplacingOccurrencesOfString:@"_" withString:@"-"]; } NSString *FIRDLDeviceLocaleRaw() { return [[NSLocale currentLocale] localeIdentifier]; } NSString *FIRDLDeviceTimezone() { NSString *timeZoneName = [[NSTimeZone localTimeZone] name]; return timeZoneName; } BOOL FIRDLIsURLForAllowedCustomDomain(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 '/' and should be followed by at-least one more // character. if (urlStr.length > domainURIPrefixStr.length + 1 && ([urlStr characterAtIndex:domainURIPrefixStr.length] == '/')) { // Check if there are any more '/' after the first '/'trailing the // domainURIPrefix. This does not apply to unique match links copied from the clipboard. // The clipboard links will have '?link=' after the domainURIPrefix. NSString *urlWithoutDomainURIPrefix = [urlStr substringFromIndex:domainURIPrefixStr.length + 1]; if ([urlWithoutDomainURIPrefix rangeOfString:@"/"].location == NSNotFound || [urlWithoutDomainURIPrefix rangeOfString:@"?link="].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. BOOL isDDLWithAppcodeInPath = ([URL.host isEqual:@"goo.gl"] || [URL.host isEqual:@"page.link"]) && [URL.path hasPrefix:@"/app"]; // 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"]; // Handle universal links for custom domains. BOOL isDDLWithCustomDomain = FIRDLIsURLForAllowedCustomDomain(URL); return isDDLWithAppcodeInPath || isDDLWithSubdomain || isDDLWithCustomDomain; } BOOL FIRDLMatchesShortLinkFormat(NSURL *URL) { // Short Durable Link URLs always have a path. BOOL hasPath = URL.path.length > 0; BOOL matchesRegularExpression = ([URL.path rangeOfString:@"/[^/]+" options:NSRegularExpressionSearch].location != NSNotFound); // Must be able to parse (also checks if the URL conforms to *.app.goo.gl/* or goo.gl/app/*) BOOL canParse = FIRDLCanParseUniversalLinkURL(URL) | FIRDLIsURLForAllowedCustomDomain(URL); ; // Path cannot be prefixed with /link/dismiss BOOL isDismiss = [[URL.path lowercaseString] hasPrefix:@"/link/dismiss"]; return hasPath && matchesRegularExpression && !isDismiss && canParse; } 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