FDLUtilities.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. /*
  2. * Copyright 2018 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.h"
  17. #import <UIKit/UIDevice.h>
  18. #include <sys/sysctl.h>
  19. NS_ASSUME_NONNULL_BEGIN
  20. NSString *const kFIRDLParameterDeepLinkIdentifier = @"deep_link_id";
  21. NSString *const kFIRDLParameterLink = @"link";
  22. NSString *const kFIRDLParameterMinimumAppVersion = @"imv";
  23. NSString *const kFIRDLParameterSource = @"utm_source";
  24. NSString *const kFIRDLParameterMedium = @"utm_medium";
  25. NSString *const kFIRDLParameterCampaign = @"utm_campaign";
  26. NSString *const kFIRDLParameterMatchType = @"match_type";
  27. NSString *const kFIRDLParameterInviteId = @"invitation_id";
  28. NSString *const kFIRDLParameterWeakMatchEndpoint = @"invitation_weakMatchEndpoint";
  29. NSString *const kFIRDLParameterMatchMessage = @"match_message";
  30. NSString *const kFIRDLParameterRequestIPVersion = @"request_ip_version";
  31. static NSSet *FIRDLCustomDomains = nil;
  32. NSURL *FIRDLCookieRetrievalURL(NSString *urlScheme, NSString *bundleID) {
  33. static NSString *const kFDLBundleIDQueryParameterName = @"fdl_ios_bundle_id";
  34. static NSString *const kFDLURLSchemeQueryParameterName = @"fdl_ios_url_scheme";
  35. NSURLComponents *components = [[NSURLComponents alloc] init];
  36. components.scheme = @"https";
  37. components.host = @"goo.gl";
  38. components.path = @"/app/_/deeplink";
  39. NSMutableArray *queryItems = [NSMutableArray array];
  40. [queryItems addObject:[NSURLQueryItem queryItemWithName:kFDLBundleIDQueryParameterName
  41. value:bundleID]];
  42. [queryItems addObject:[NSURLQueryItem queryItemWithName:kFDLURLSchemeQueryParameterName
  43. value:urlScheme]];
  44. [components setQueryItems:queryItems];
  45. return [components URL];
  46. }
  47. NSString *FIRDLURLQueryStringFromDictionary(NSDictionary<NSString *, NSString *> *dictionary) {
  48. NSMutableString *parameters = [NSMutableString string];
  49. NSCharacterSet *queryCharacterSet = [NSCharacterSet alphanumericCharacterSet];
  50. NSString *parameterFormatString = @"%@%@=%@";
  51. __block NSUInteger index = 0;
  52. [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSString *_Nonnull value,
  53. BOOL *_Nonnull stop) {
  54. NSString *delimiter = index++ == 0 ? @"?" : @"&";
  55. NSString *encodedValue =
  56. [value stringByAddingPercentEncodingWithAllowedCharacters:queryCharacterSet];
  57. NSString *parameter =
  58. [NSString stringWithFormat:parameterFormatString, delimiter, key, encodedValue];
  59. [parameters appendString:parameter];
  60. }];
  61. return parameters;
  62. }
  63. NSDictionary *FIRDLDictionaryFromQuery(NSString *queryString) {
  64. NSArray<NSString *> *keyValuePairs = [queryString componentsSeparatedByString:@"&"];
  65. NSMutableDictionary *queryDictionary =
  66. [NSMutableDictionary dictionaryWithCapacity:keyValuePairs.count];
  67. for (NSString *pair in keyValuePairs) {
  68. NSArray *keyValuePair = [pair componentsSeparatedByString:@"="];
  69. if (keyValuePair.count == 2) {
  70. NSString *key = keyValuePair[0];
  71. NSString *value = [keyValuePair[1] stringByRemovingPercentEncoding];
  72. [queryDictionary setObject:value forKey:key];
  73. }
  74. }
  75. return [queryDictionary copy];
  76. }
  77. NSURL *FIRDLDeepLinkURLWithInviteID(NSString *_Nullable inviteID,
  78. NSString *_Nullable deepLinkString,
  79. NSString *_Nullable utmSource,
  80. NSString *_Nullable utmMedium,
  81. NSString *_Nullable utmCampaign,
  82. BOOL isWeakLink,
  83. NSString *_Nullable weakMatchEndpoint,
  84. NSString *_Nullable minAppVersion,
  85. NSString *URLScheme,
  86. NSString *_Nullable matchMessage) {
  87. // We are unable to use NSURLComponents as NSURLQueryItem is avilable beginning in iOS 8 and
  88. // appending our query string with NSURLComponents improperly formats the query by adding
  89. // a second '?' in the query.
  90. NSMutableDictionary *queryDictionary = [NSMutableDictionary dictionary];
  91. if (inviteID != nil) {
  92. queryDictionary[kFIRDLParameterInviteId] = inviteID;
  93. }
  94. if (deepLinkString != nil) {
  95. queryDictionary[kFIRDLParameterDeepLinkIdentifier] = deepLinkString;
  96. }
  97. if (utmSource != nil) {
  98. queryDictionary[kFIRDLParameterSource] = utmSource;
  99. }
  100. if (utmMedium != nil) {
  101. queryDictionary[kFIRDLParameterMedium] = utmMedium;
  102. }
  103. if (utmCampaign != nil) {
  104. queryDictionary[kFIRDLParameterCampaign] = utmCampaign;
  105. }
  106. if (isWeakLink) {
  107. queryDictionary[kFIRDLParameterMatchType] = @"weak";
  108. } else {
  109. queryDictionary[kFIRDLParameterMatchType] = @"unique";
  110. }
  111. if (weakMatchEndpoint != nil) {
  112. queryDictionary[kFIRDLParameterWeakMatchEndpoint] = weakMatchEndpoint;
  113. }
  114. if (minAppVersion != nil) {
  115. queryDictionary[kFIRDLParameterMinimumAppVersion] = minAppVersion;
  116. }
  117. if (matchMessage != nil) {
  118. queryDictionary[kFIRDLParameterMatchMessage] = matchMessage;
  119. }
  120. NSString *scheme = [URLScheme lowercaseString];
  121. NSString *queryString = FIRDLURLQueryStringFromDictionary(queryDictionary);
  122. NSString *urlString = [NSString stringWithFormat:@"%@://google/link/%@", scheme, queryString];
  123. return [NSURL URLWithString:urlString];
  124. }
  125. BOOL FIRDLOSVersionSupported(NSString *_Nullable systemVersion, NSString *minSupportedVersion) {
  126. systemVersion = systemVersion ?: [UIDevice currentDevice].systemVersion;
  127. return [systemVersion compare:minSupportedVersion options:NSNumericSearch] != NSOrderedAscending;
  128. }
  129. NSDate *_Nullable FIRDLAppInstallationDate() {
  130. NSURL *documentsDirectoryURL =
  131. [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
  132. inDomains:NSUserDomainMask] firstObject];
  133. if (!documentsDirectoryURL) {
  134. return nil;
  135. }
  136. NSDictionary<NSString *, id> *attributes =
  137. [[NSFileManager defaultManager] attributesOfItemAtPath:documentsDirectoryURL.path error:NULL];
  138. if (attributes[NSFileCreationDate] &&
  139. [attributes[NSFileCreationDate] isKindOfClass:[NSDate class]]) {
  140. return attributes[NSFileCreationDate];
  141. }
  142. return nil;
  143. }
  144. NSString *FIRDLDeviceModelName() {
  145. // this method will return string like iPad3,3
  146. // for Simulator this will be x86_64
  147. static NSString *machineString = @"";
  148. static dispatch_once_t onceToken;
  149. dispatch_once(&onceToken, ^{
  150. size_t size;
  151. // compute string size
  152. if (sysctlbyname("hw.machine", NULL, &size, NULL, 0) == 0) {
  153. // get device name
  154. char *machine = calloc(1, size);
  155. if (sysctlbyname("hw.machine", machine, &size, NULL, 0) == 0) {
  156. machineString = [NSString stringWithCString:machine encoding:NSUTF8StringEncoding];
  157. }
  158. free(machine);
  159. }
  160. });
  161. return machineString;
  162. }
  163. NSString *FIRDLDeviceLocale() {
  164. // expected return value from this method looks like: @"en-US"
  165. return [[[NSLocale currentLocale] localeIdentifier] stringByReplacingOccurrencesOfString:@"_"
  166. withString:@"-"];
  167. }
  168. NSString *FIRDLDeviceLocaleRaw() {
  169. return [[NSLocale currentLocale] localeIdentifier];
  170. }
  171. NSString *FIRDLDeviceTimezone() {
  172. NSString *timeZoneName = [[NSTimeZone localTimeZone] name];
  173. return timeZoneName;
  174. }
  175. BOOL FIRDLIsURLForWhiteListedCustomDomain(NSURL *_Nullable URL) {
  176. BOOL customDomainMatchFound = false;
  177. for (NSURL *allowedCustomDomain in FIRDLCustomDomains) {
  178. // All custom domain host names should match at a minimum.
  179. if ([allowedCustomDomain.host isEqualToString:URL.host]) {
  180. NSString *urlStr = URL.absoluteString;
  181. NSString *domainURIPrefixStr = allowedCustomDomain.absoluteString;
  182. // Next, do a string compare to check if entire domainURIPrefix matches as well.
  183. if (([URL.absoluteString rangeOfString:allowedCustomDomain.absoluteString
  184. options:NSCaseInsensitiveSearch | NSAnchoredSearch]
  185. .location) == 0) {
  186. // The (short) URL needs to be longer than the domainURIPrefix, it's first character after
  187. // the domainURIPrefix needs to be '/' and should be followed by at-least one more
  188. // character.
  189. if (urlStr.length > domainURIPrefixStr.length + 1 &&
  190. ([urlStr characterAtIndex:domainURIPrefixStr.length] == '/')) {
  191. // Check if there are any more '/' after the first '/'trailing the
  192. // domainURIPrefix. This does not apply to unique match links copied from the clipboard.
  193. // The clipboard links will have '?link=' after the domainURIPrefix.
  194. NSString *urlWithoutDomainURIPrefix =
  195. [urlStr substringFromIndex:domainURIPrefixStr.length + 1];
  196. if ([urlWithoutDomainURIPrefix rangeOfString:@"/"].location == NSNotFound ||
  197. [urlWithoutDomainURIPrefix rangeOfString:@"?link="].location != NSNotFound) {
  198. customDomainMatchFound = true;
  199. break;
  200. }
  201. }
  202. }
  203. }
  204. }
  205. return customDomainMatchFound;
  206. }
  207. BOOL FIRDLCanParseUniversalLinkURL(NSURL *_Nullable URL) {
  208. // Handle universal links with format |https://goo.gl/app/<appcode>?<parameters>|.
  209. // Also support page.link format.
  210. BOOL isDDLWithAppcodeInPath = ([URL.host isEqual:@"goo.gl"] || [URL.host isEqual:@"page.link"]) &&
  211. [URL.path hasPrefix:@"/app"];
  212. // Handle universal links with format |https://<appcode>.app.goo.gl?<parameters>| and page.link.
  213. BOOL isDDLWithSubdomain =
  214. [URL.host hasSuffix:@".app.goo.gl"] || [URL.host hasSuffix:@".page.link"];
  215. // Handle universal links for custom domains.
  216. BOOL isDDLWithCustomDomain = FIRDLIsURLForWhiteListedCustomDomain(URL);
  217. return isDDLWithAppcodeInPath || isDDLWithSubdomain || isDDLWithCustomDomain;
  218. }
  219. BOOL FIRDLMatchesShortLinkFormat(NSURL *URL) {
  220. // Short Durable Link URLs always have a path.
  221. BOOL hasPath = URL.path.length > 0;
  222. BOOL matchesRegularExpression =
  223. ([URL.path rangeOfString:@"/[^/]+" options:NSRegularExpressionSearch].location != NSNotFound);
  224. // Must be able to parse (also checks if the URL conforms to *.app.goo.gl/* or goo.gl/app/*)
  225. BOOL canParse = FIRDLCanParseUniversalLinkURL(URL) | FIRDLIsURLForWhiteListedCustomDomain(URL);
  226. ;
  227. // Path cannot be prefixed with /link/dismiss
  228. BOOL isDismiss = [[URL.path lowercaseString] hasPrefix:@"/link/dismiss"];
  229. return hasPath && matchesRegularExpression && !isDismiss && canParse;
  230. }
  231. NSString *FIRDLMatchTypeStringFromServerString(NSString *_Nullable serverMatchTypeString) {
  232. static NSDictionary *matchMap;
  233. static dispatch_once_t onceToken;
  234. dispatch_once(&onceToken, ^{
  235. matchMap = @{
  236. @"WEAK" : @"weak",
  237. @"DEFAULT" : @"default",
  238. @"UNIQUE" : @"unique",
  239. };
  240. });
  241. return matchMap[serverMatchTypeString] ?: @"none";
  242. }
  243. void FIRDLAddToAllowListForCustomDomainsArray(NSArray *_Nonnull customDomains) {
  244. // Duplicates will be weeded out when converting to a set.
  245. NSMutableArray *validCustomDomains =
  246. [[NSMutableArray alloc] initWithCapacity:customDomains.count];
  247. for (NSString *customDomainEntry in customDomains) {
  248. // We remove trailing slashes in the path if present.
  249. NSString *domainEntry =
  250. [customDomainEntry hasSuffix:@"/"]
  251. ? [customDomainEntry substringToIndex:[customDomainEntry length] - 1]
  252. : customDomainEntry;
  253. NSURL *customDomainURL = [NSURL URLWithString:domainEntry];
  254. // We require a valid scheme for each custom domain enumerated in the info.plist file.
  255. if (customDomainURL && customDomainURL.scheme) {
  256. [validCustomDomains addObject:customDomainURL];
  257. }
  258. }
  259. // Duplicates will be weeded out when converting to a set.
  260. FIRDLCustomDomains = [NSSet setWithArray:validCustomDomains];
  261. }
  262. NS_ASSUME_NONNULL_END