FDLUtilities.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  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 <TargetConditionals.h>
  17. #if TARGET_OS_IOS
  18. #import "FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.h"
  19. #import <UIKit/UIDevice.h>
  20. #include <sys/sysctl.h>
  21. NS_ASSUME_NONNULL_BEGIN
  22. NSString *const kFIRDLParameterDeepLinkIdentifier = @"deep_link_id";
  23. NSString *const kFIRDLParameterLink = @"link";
  24. NSString *const kFIRDLParameterMinimumAppVersion = @"imv";
  25. NSString *const kFIRDLParameterCampaign = @"utm_campaign";
  26. NSString *const kFIRDLParameterContent = @"utm_content";
  27. NSString *const kFIRDLParameterMedium = @"utm_medium";
  28. NSString *const kFIRDLParameterSource = @"utm_source";
  29. NSString *const kFIRDLParameterTerm = @"utm_term";
  30. NSString *const kFIRDLParameterMatchType = @"match_type";
  31. NSString *const kFIRDLParameterInviteId = @"invitation_id";
  32. NSString *const kFIRDLParameterWeakMatchEndpoint = @"invitation_weakMatchEndpoint";
  33. NSString *const kFIRDLParameterMatchMessage = @"match_message";
  34. NSString *const kFIRDLParameterRequestIPVersion = @"request_ip_version";
  35. static NSSet *FIRDLCustomDomains = nil;
  36. NSURL *FIRDLCookieRetrievalURL(NSString *urlScheme, NSString *bundleID) {
  37. static NSString *const kFDLBundleIDQueryParameterName = @"fdl_ios_bundle_id";
  38. static NSString *const kFDLURLSchemeQueryParameterName = @"fdl_ios_url_scheme";
  39. NSURLComponents *components = [[NSURLComponents alloc] init];
  40. components.scheme = @"https";
  41. components.host = @"goo.gl";
  42. components.path = @"/app/_/deeplink";
  43. NSMutableArray *queryItems = [NSMutableArray array];
  44. [queryItems addObject:[NSURLQueryItem queryItemWithName:kFDLBundleIDQueryParameterName
  45. value:bundleID]];
  46. [queryItems addObject:[NSURLQueryItem queryItemWithName:kFDLURLSchemeQueryParameterName
  47. value:urlScheme]];
  48. [components setQueryItems:queryItems];
  49. return [components URL];
  50. }
  51. NSString *FIRDLURLQueryStringFromDictionary(NSDictionary<NSString *, NSString *> *dictionary) {
  52. NSMutableString *parameters = [NSMutableString string];
  53. NSCharacterSet *queryCharacterSet = [NSCharacterSet alphanumericCharacterSet];
  54. NSString *parameterFormatString = @"%@%@=%@";
  55. __block NSUInteger index = 0;
  56. [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSString *_Nonnull value,
  57. BOOL *_Nonnull stop) {
  58. NSString *delimiter = index++ == 0 ? @"?" : @"&";
  59. NSString *encodedValue =
  60. [value stringByAddingPercentEncodingWithAllowedCharacters:queryCharacterSet];
  61. NSString *parameter =
  62. [NSString stringWithFormat:parameterFormatString, delimiter, key, encodedValue];
  63. [parameters appendString:parameter];
  64. }];
  65. return parameters;
  66. }
  67. NSDictionary *FIRDLDictionaryFromQuery(NSString *queryString) {
  68. NSArray<NSString *> *keyValuePairs = [queryString componentsSeparatedByString:@"&"];
  69. NSMutableDictionary *queryDictionary =
  70. [NSMutableDictionary dictionaryWithCapacity:keyValuePairs.count];
  71. for (NSString *pair in keyValuePairs) {
  72. NSArray *keyValuePair = [pair componentsSeparatedByString:@"="];
  73. if (keyValuePair.count == 2) {
  74. NSString *key = keyValuePair[0];
  75. NSString *value = [keyValuePair[1] stringByRemovingPercentEncoding];
  76. [queryDictionary setObject:value forKey:key];
  77. }
  78. }
  79. return [queryDictionary copy];
  80. }
  81. NSURL *FIRDLDeepLinkURLWithInviteID(NSString *_Nullable inviteID,
  82. NSString *_Nullable deepLinkString,
  83. NSString *_Nullable utmSource,
  84. NSString *_Nullable utmMedium,
  85. NSString *_Nullable utmCampaign,
  86. NSString *_Nullable utmContent,
  87. NSString *_Nullable utmTerm,
  88. BOOL isWeakLink,
  89. NSString *_Nullable weakMatchEndpoint,
  90. NSString *_Nullable minAppVersion,
  91. NSString *URLScheme,
  92. NSString *_Nullable matchMessage) {
  93. // We are unable to use NSURLComponents as NSURLQueryItem is available beginning in iOS 8 and
  94. // appending our query string with NSURLComponents improperly formats the query by adding
  95. // a second '?' in the query.
  96. NSMutableDictionary *queryDictionary = [NSMutableDictionary dictionary];
  97. if (inviteID != nil) {
  98. queryDictionary[kFIRDLParameterInviteId] = inviteID;
  99. }
  100. if (deepLinkString != nil) {
  101. queryDictionary[kFIRDLParameterDeepLinkIdentifier] = deepLinkString;
  102. }
  103. if (utmSource != nil) {
  104. queryDictionary[kFIRDLParameterSource] = utmSource;
  105. }
  106. if (utmMedium != nil) {
  107. queryDictionary[kFIRDLParameterMedium] = utmMedium;
  108. }
  109. if (utmCampaign != nil) {
  110. queryDictionary[kFIRDLParameterCampaign] = utmCampaign;
  111. }
  112. if (utmContent != nil) {
  113. queryDictionary[kFIRDLParameterContent] = utmContent;
  114. }
  115. if (utmTerm != nil) {
  116. queryDictionary[kFIRDLParameterTerm] = utmTerm;
  117. }
  118. if (isWeakLink) {
  119. queryDictionary[kFIRDLParameterMatchType] = @"weak";
  120. } else {
  121. queryDictionary[kFIRDLParameterMatchType] = @"unique";
  122. }
  123. if (weakMatchEndpoint != nil) {
  124. queryDictionary[kFIRDLParameterWeakMatchEndpoint] = weakMatchEndpoint;
  125. }
  126. if (minAppVersion != nil) {
  127. queryDictionary[kFIRDLParameterMinimumAppVersion] = minAppVersion;
  128. }
  129. if (matchMessage != nil) {
  130. queryDictionary[kFIRDLParameterMatchMessage] = matchMessage;
  131. }
  132. NSString *scheme = [URLScheme lowercaseString];
  133. NSString *queryString = FIRDLURLQueryStringFromDictionary(queryDictionary);
  134. NSString *urlString = [NSString stringWithFormat:@"%@://google/link/%@", scheme, queryString];
  135. return [NSURL URLWithString:urlString];
  136. }
  137. BOOL FIRDLOSVersionSupported(NSString *_Nullable systemVersion, NSString *minSupportedVersion) {
  138. systemVersion = systemVersion ?: [UIDevice currentDevice].systemVersion;
  139. return [systemVersion compare:minSupportedVersion options:NSNumericSearch] != NSOrderedAscending;
  140. }
  141. NSDate *_Nullable FIRDLAppInstallationDate(void) {
  142. NSURL *documentsDirectoryURL =
  143. [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
  144. inDomains:NSUserDomainMask] firstObject];
  145. if (!documentsDirectoryURL) {
  146. return nil;
  147. }
  148. NSDictionary<NSString *, id> *attributes =
  149. [[NSFileManager defaultManager] attributesOfItemAtPath:documentsDirectoryURL.path error:NULL];
  150. if (attributes[NSFileCreationDate] &&
  151. [attributes[NSFileCreationDate] isKindOfClass:[NSDate class]]) {
  152. return attributes[NSFileCreationDate];
  153. }
  154. return nil;
  155. }
  156. NSString *FIRDLDeviceModelName(void) {
  157. // this method will return string like iPad3,3
  158. // for Simulator this will be x86_64
  159. static NSString *machineString = @"";
  160. static dispatch_once_t onceToken;
  161. dispatch_once(&onceToken, ^{
  162. size_t size;
  163. // compute string size
  164. if (sysctlbyname("hw.machine", NULL, &size, NULL, 0) == 0) {
  165. // get device name
  166. char *machine = calloc(1, size);
  167. if (sysctlbyname("hw.machine", machine, &size, NULL, 0) == 0) {
  168. machineString = [NSString stringWithCString:machine encoding:NSUTF8StringEncoding];
  169. }
  170. free(machine);
  171. }
  172. });
  173. return machineString;
  174. }
  175. NSString *FIRDLDeviceLocale(void) {
  176. // expected return value from this method looks like: @"en-US"
  177. return [[[NSLocale currentLocale] localeIdentifier] stringByReplacingOccurrencesOfString:@"_"
  178. withString:@"-"];
  179. }
  180. NSString *FIRDLDeviceLocaleRaw(void) {
  181. return [[NSLocale currentLocale] localeIdentifier];
  182. }
  183. NSString *FIRDLDeviceTimezone(void) {
  184. NSString *timeZoneName = [[NSTimeZone localTimeZone] name];
  185. return timeZoneName;
  186. }
  187. BOOL FIRDLIsURLForAllowedCustomDomain(NSURL *URL) {
  188. if (URL) {
  189. for (NSURL *allowedCustomDomain in FIRDLCustomDomains) {
  190. // At least one custom domain host name should match at a minimum.
  191. if ([URL.absoluteString hasPrefix:allowedCustomDomain.absoluteString]) {
  192. NSString *urlWithoutDomainURIPrefix =
  193. [URL.absoluteString substringFromIndex:allowedCustomDomain.absoluteString.length];
  194. // The urlWithoutDomainURIPrefix should be starting with '/' or '?' otherwise it means the
  195. // allowed domain is not exactly matching the incoming URL domain prefix.
  196. if ([urlWithoutDomainURIPrefix hasPrefix:@"/"] ||
  197. [urlWithoutDomainURIPrefix hasPrefix:@"?"]) {
  198. // For a valid custom domain DL Suffix the urlWithoutDomainURIPrefix should have:
  199. // 1. At least one path exists OR
  200. // 2. Should have a link query param with an http/https link
  201. NSURLComponents *components =
  202. [[NSURLComponents alloc] initWithString:urlWithoutDomainURIPrefix];
  203. if (components.path && components.path.length > 1) {
  204. // Have a path exists. So valid custom domain.
  205. return true;
  206. }
  207. if (components.queryItems && components.queryItems.count > 0) {
  208. for (NSURLQueryItem *queryItem in components.queryItems) {
  209. // Checks whether we have a link query param
  210. if ([queryItem.name caseInsensitiveCompare:@"link"] == NSOrderedSame) {
  211. // Checks whether link query param value starts with http/https
  212. if (queryItem.value && ([queryItem.value hasPrefix:@"http://"] ||
  213. [queryItem.value hasPrefix:@"https://"])) {
  214. return true;
  215. }
  216. }
  217. }
  218. }
  219. }
  220. }
  221. }
  222. }
  223. return false;
  224. }
  225. /* We are validating following domains in proper format.
  226. *.page.link
  227. *.app.goo.gl
  228. *.page.link/i/
  229. *.app.goo.gl/i/
  230. */
  231. BOOL FIRDLIsAValidDLWithFDLDomain(NSURL *_Nullable URL) {
  232. BOOL matchesRegularExpression = false;
  233. NSString *urlStr = URL.absoluteString;
  234. if ([URL.host containsString:@".page.link"] || [URL.host containsString:@".app.goo.gl"] ||
  235. [URL.host containsString:@".app.google"]) {
  236. // Matches the *.page.link and *.app.goo.gl domains.
  237. matchesRegularExpression =
  238. ([urlStr rangeOfString:
  239. @"^https?://"
  240. @"[a-zA-Z0-9]+((\\.app\\.goo\\.gl)|(\\.page\\.link)|(\\.app\\.google))((\\/"
  241. @"?\\?.*link=https?.*)|(\\/[a-zA-Z0-9-_]+)((\\/?\\?.*=.*)?$|$))"
  242. options:NSRegularExpressionSearch]
  243. .location != NSNotFound);
  244. if (!matchesRegularExpression) {
  245. // Matches the *.page.link/i/ and *.app.goo.gl/i/ domains.
  246. // Checks whether the URL is of the format :
  247. // http(s)://$DOMAIN(.page.link or .app.goo.gl)/i/$ANYTHING
  248. matchesRegularExpression =
  249. ([urlStr
  250. rangeOfString:
  251. @"^https?:\\/\\/"
  252. @"[a-zA-Z0-9]+((\\.app\\.goo\\.gl)|(\\.page\\.link)|(\\.app\\.google))\\/i\\/.*$"
  253. options:NSRegularExpressionSearch]
  254. .location != NSNotFound);
  255. }
  256. }
  257. return matchesRegularExpression;
  258. }
  259. /*
  260. DL can be parsed if it :
  261. 1. Has http(s)://goo.gl/app* or http(s)://page.link/app* format
  262. 2. OR http(s)://$DomainPrefix.page.link or http(s)://$DomainPrefix.app.goo.gl domain with specific
  263. format
  264. 3. OR the domain is a listed custom domain
  265. */
  266. BOOL FIRDLCanParseUniversalLinkURL(NSURL *_Nullable URL) {
  267. // Handle universal links with format |https://goo.gl/app/<appcode>?<parameters>|.
  268. // Also support page.link format.
  269. BOOL isDDLWithAppcodeInPath = ([URL.host isEqual:@"goo.gl"] || [URL.host isEqual:@"page.link"] ||
  270. [URL.host isEqual:@"app.google"]) &&
  271. [URL.path hasPrefix:@"/app"];
  272. return isDDLWithAppcodeInPath || FIRDLIsAValidDLWithFDLDomain(URL) ||
  273. FIRDLIsURLForAllowedCustomDomain(URL);
  274. }
  275. BOOL FIRDLMatchesShortLinkFormat(NSURL *URL) {
  276. // Short Durable Link URLs always have a path or it should be a custom domain.
  277. BOOL hasPathOrCustomDomain = URL.path.length > 0 || FIRDLIsURLForAllowedCustomDomain(URL);
  278. // Must be able to parse (also checks if the URL conforms to *.app.goo.gl/* or goo.gl/app/* or
  279. // *.page.link or custom domain with valid suffix)
  280. BOOL canParse = FIRDLCanParseUniversalLinkURL(URL);
  281. // Path cannot be prefixed with /link/dismiss
  282. BOOL isDismiss = [[URL.path lowercaseString] hasPrefix:@"/link/dismiss"];
  283. // Checks short link format by having only one path after domain prefix.
  284. BOOL matchesRegularExpression =
  285. ([URL.path rangeOfString:@"/[^/]+" options:NSRegularExpressionSearch].location != NSNotFound);
  286. return hasPathOrCustomDomain && !isDismiss && canParse && matchesRegularExpression;
  287. }
  288. NSString *FIRDLMatchTypeStringFromServerString(NSString *_Nullable serverMatchTypeString) {
  289. static NSDictionary *matchMap;
  290. static dispatch_once_t onceToken;
  291. dispatch_once(&onceToken, ^{
  292. matchMap = @{
  293. @"WEAK" : @"weak",
  294. @"DEFAULT" : @"default",
  295. @"UNIQUE" : @"unique",
  296. };
  297. });
  298. return matchMap[serverMatchTypeString] ?: @"none";
  299. }
  300. void FIRDLAddToAllowListForCustomDomainsArray(NSArray *_Nonnull customDomains) {
  301. // Duplicates will be weeded out when converting to a set.
  302. NSMutableArray *validCustomDomains =
  303. [[NSMutableArray alloc] initWithCapacity:customDomains.count];
  304. for (NSString *customDomainEntry in customDomains) {
  305. // We remove trailing slashes in the path if present.
  306. NSString *domainEntry =
  307. [customDomainEntry hasSuffix:@"/"]
  308. ? [customDomainEntry substringToIndex:[customDomainEntry length] - 1]
  309. : customDomainEntry;
  310. NSURL *customDomainURL = [NSURL URLWithString:domainEntry];
  311. // We require a valid scheme for each custom domain enumerated in the info.plist file.
  312. if (customDomainURL && customDomainURL.scheme) {
  313. [validCustomDomains addObject:customDomainURL];
  314. }
  315. }
  316. // Duplicates will be weeded out when converting to a set.
  317. FIRDLCustomDomains = [NSSet setWithArray:validCustomDomains];
  318. }
  319. NS_ASSUME_NONNULL_END
  320. #endif // TARGET_OS_IOS