You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

737 lines
28 KiB

6 years ago
  1. // Copyright 2017 Google
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #include <sys/utsname.h>
  15. #import "FIRApp.h"
  16. #import "FIRConfiguration.h"
  17. #import "Private/FIRAnalyticsConfiguration+Internal.h"
  18. #import "Private/FIRAppInternal.h"
  19. #import "Private/FIRBundleUtil.h"
  20. #import "Private/FIRLogger.h"
  21. #import "Private/FIROptionsInternal.h"
  22. #import "third_party/FIRAppEnvironmentUtil.h"
  23. NSString *const kFIRServiceAdMob = @"AdMob";
  24. NSString *const kFIRServiceAuth = @"Auth";
  25. NSString *const kFIRServiceAuthUI = @"AuthUI";
  26. NSString *const kFIRServiceCrash = @"Crash";
  27. NSString *const kFIRServiceDatabase = @"Database";
  28. NSString *const kFIRServiceDynamicLinks = @"DynamicLinks";
  29. NSString *const kFIRServiceFirestore = @"Firestore";
  30. NSString *const kFIRServiceFunctions = @"Functions";
  31. NSString *const kFIRServiceInstanceID = @"InstanceID";
  32. NSString *const kFIRServiceInvites = @"Invites";
  33. NSString *const kFIRServiceMessaging = @"Messaging";
  34. NSString *const kFIRServiceMeasurement = @"Measurement";
  35. NSString *const kFIRServicePerformance = @"Performance";
  36. NSString *const kFIRServiceRemoteConfig = @"RemoteConfig";
  37. NSString *const kFIRServiceStorage = @"Storage";
  38. NSString *const kGGLServiceAnalytics = @"Analytics";
  39. NSString *const kGGLServiceSignIn = @"SignIn";
  40. NSString *const kFIRDefaultAppName = @"__FIRAPP_DEFAULT";
  41. NSString *const kFIRAppReadyToConfigureSDKNotification = @"FIRAppReadyToConfigureSDKNotification";
  42. NSString *const kFIRAppDeleteNotification = @"FIRAppDeleteNotification";
  43. NSString *const kFIRAppIsDefaultAppKey = @"FIRAppIsDefaultAppKey";
  44. NSString *const kFIRAppNameKey = @"FIRAppNameKey";
  45. NSString *const kFIRGoogleAppIDKey = @"FIRGoogleAppIDKey";
  46. NSString *const kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat =
  47. @"/google/firebase/global_data_collection_enabled:%@";
  48. NSString *const kFIRGlobalAppDataCollectionEnabledPlistKey =
  49. @"FirebaseAutomaticDataCollectionEnabled";
  50. NSString *const kFIRAppDiagnosticsNotification = @"FIRAppDiagnosticsNotification";
  51. NSString *const kFIRAppDiagnosticsConfigurationTypeKey = @"ConfigType";
  52. NSString *const kFIRAppDiagnosticsErrorKey = @"Error";
  53. NSString *const kFIRAppDiagnosticsFIRAppKey = @"FIRApp";
  54. NSString *const kFIRAppDiagnosticsSDKNameKey = @"SDKName";
  55. NSString *const kFIRAppDiagnosticsSDKVersionKey = @"SDKVersion";
  56. // Auth internal notification notification and key.
  57. NSString *const FIRAuthStateDidChangeInternalNotification =
  58. @"FIRAuthStateDidChangeInternalNotification";
  59. NSString *const FIRAuthStateDidChangeInternalNotificationAppKey =
  60. @"FIRAuthStateDidChangeInternalNotificationAppKey";
  61. NSString *const FIRAuthStateDidChangeInternalNotificationTokenKey =
  62. @"FIRAuthStateDidChangeInternalNotificationTokenKey";
  63. NSString *const FIRAuthStateDidChangeInternalNotificationUIDKey =
  64. @"FIRAuthStateDidChangeInternalNotificationUIDKey";
  65. /**
  66. * The URL to download plist files.
  67. */
  68. static NSString *const kPlistURL = @"https://console.firebase.google.com/";
  69. @interface FIRApp ()
  70. @property(nonatomic) BOOL alreadySentConfigureNotification;
  71. @property(nonatomic) BOOL alreadySentDeleteNotification;
  72. @end
  73. @implementation FIRApp
  74. // This is necessary since our custom getter prevents `_options` from being created.
  75. @synthesize options = _options;
  76. static NSMutableDictionary *sAllApps;
  77. static FIRApp *sDefaultApp;
  78. static NSMutableDictionary *sLibraryVersions;
  79. + (void)configure {
  80. FIROptions *options = [FIROptions defaultOptions];
  81. if (!options) {
  82. [[NSNotificationCenter defaultCenter]
  83. postNotificationName:kFIRAppDiagnosticsNotification
  84. object:nil
  85. userInfo:@{
  86. kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
  87. kFIRAppDiagnosticsErrorKey : [FIRApp errorForMissingOptions]
  88. }];
  89. [NSException raise:kFirebaseCoreErrorDomain
  90. format:
  91. @"`[FIRApp configure];` (`FirebaseApp.configure()` in Swift) could not find "
  92. @"a valid GoogleService-Info.plist in your project. Please download one "
  93. @"from %@.",
  94. kPlistURL];
  95. }
  96. [FIRApp configureDefaultAppWithOptions:options sendingNotifications:YES];
  97. #if TARGET_OS_OSX || TARGET_OS_TV
  98. FIRLogNotice(kFIRLoggerCore, @"I-COR000028",
  99. @"tvOS and macOS SDK support is not part of the official Firebase product. "
  100. @"Instead they are community supported. Details at "
  101. @"https://github.com/firebase/firebase-ios-sdk/blob/master/README.md.");
  102. #endif
  103. }
  104. + (void)configureWithOptions:(FIROptions *)options {
  105. if (!options) {
  106. [NSException raise:kFirebaseCoreErrorDomain
  107. format:@"Options is nil. Please pass a valid options."];
  108. }
  109. [FIRApp configureDefaultAppWithOptions:options sendingNotifications:YES];
  110. }
  111. + (void)configureDefaultAppWithOptions:(FIROptions *)options
  112. sendingNotifications:(BOOL)sendNotifications {
  113. if (sDefaultApp) {
  114. // FIRApp sets up FirebaseAnalytics and does plist validation, but does not cause it
  115. // to fire notifications. So, if the default app already exists, but has not sent out
  116. // configuration notifications, then continue re-initializing it.
  117. if (!sendNotifications || sDefaultApp.alreadySentConfigureNotification) {
  118. [NSException raise:kFirebaseCoreErrorDomain
  119. format:@"Default app has already been configured."];
  120. }
  121. }
  122. @synchronized(self) {
  123. FIRLogDebug(kFIRLoggerCore, @"I-COR000001", @"Configuring the default app.");
  124. sDefaultApp = [[FIRApp alloc] initInstanceWithName:kFIRDefaultAppName options:options];
  125. [FIRApp addAppToAppDictionary:sDefaultApp];
  126. if (!sDefaultApp.alreadySentConfigureNotification && sendNotifications) {
  127. [FIRApp sendNotificationsToSDKs:sDefaultApp];
  128. sDefaultApp.alreadySentConfigureNotification = YES;
  129. }
  130. }
  131. }
  132. + (void)configureWithName:(NSString *)name options:(FIROptions *)options {
  133. if (!name || !options) {
  134. [NSException raise:kFirebaseCoreErrorDomain format:@"Neither name nor options can be nil."];
  135. }
  136. if (name.length == 0) {
  137. [NSException raise:kFirebaseCoreErrorDomain format:@"Name cannot be empty."];
  138. }
  139. if ([name isEqualToString:kFIRDefaultAppName]) {
  140. [NSException raise:kFirebaseCoreErrorDomain format:@"Name cannot be __FIRAPP_DEFAULT."];
  141. }
  142. for (NSInteger charIndex = 0; charIndex < name.length; charIndex++) {
  143. char character = [name characterAtIndex:charIndex];
  144. if (!((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') ||
  145. (character >= '0' && character <= '9') || character == '_' || character == '-')) {
  146. [NSException raise:kFirebaseCoreErrorDomain
  147. format:
  148. @"App name should only contain Letters, "
  149. @"Numbers, Underscores, and Dashes."];
  150. }
  151. }
  152. if (sAllApps && sAllApps[name]) {
  153. [NSException raise:kFirebaseCoreErrorDomain
  154. format:@"App named %@ has already been configured.", name];
  155. }
  156. @synchronized(self) {
  157. FIRLogDebug(kFIRLoggerCore, @"I-COR000002", @"Configuring app named %@", name);
  158. FIRApp *app = [[FIRApp alloc] initInstanceWithName:name options:options];
  159. [FIRApp addAppToAppDictionary:app];
  160. if (!app.alreadySentConfigureNotification) {
  161. [FIRApp sendNotificationsToSDKs:app];
  162. app.alreadySentConfigureNotification = YES;
  163. }
  164. }
  165. }
  166. + (FIRApp *)defaultApp {
  167. if (sDefaultApp) {
  168. return sDefaultApp;
  169. }
  170. FIRLogError(kFIRLoggerCore, @"I-COR000003",
  171. @"The default Firebase app has not yet been "
  172. @"configured. Add `[FIRApp configure];` (`FirebaseApp.configure()` in Swift) to your "
  173. @"application initialization. Read more: https://goo.gl/ctyzm8.");
  174. return nil;
  175. }
  176. + (FIRApp *)appNamed:(NSString *)name {
  177. @synchronized(self) {
  178. if (sAllApps) {
  179. FIRApp *app = sAllApps[name];
  180. if (app) {
  181. return app;
  182. }
  183. }
  184. FIRLogError(kFIRLoggerCore, @"I-COR000004", @"App with name %@ does not exist.", name);
  185. return nil;
  186. }
  187. }
  188. + (NSDictionary *)allApps {
  189. @synchronized(self) {
  190. if (!sAllApps) {
  191. FIRLogError(kFIRLoggerCore, @"I-COR000005", @"No app has been configured yet.");
  192. }
  193. NSDictionary *dict = [NSDictionary dictionaryWithDictionary:sAllApps];
  194. return dict;
  195. }
  196. }
  197. // Public only for tests
  198. + (void)resetApps {
  199. sDefaultApp = nil;
  200. [sAllApps removeAllObjects];
  201. sAllApps = nil;
  202. [sLibraryVersions removeAllObjects];
  203. sLibraryVersions = nil;
  204. }
  205. - (void)deleteApp:(FIRAppVoidBoolCallback)completion {
  206. @synchronized([self class]) {
  207. if (sAllApps && sAllApps[self.name]) {
  208. FIRLogDebug(kFIRLoggerCore, @"I-COR000006", @"Deleting app named %@", self.name);
  209. [sAllApps removeObjectForKey:self.name];
  210. [self clearDataCollectionSwitchFromUserDefaults];
  211. if ([self.name isEqualToString:kFIRDefaultAppName]) {
  212. sDefaultApp = nil;
  213. }
  214. if (!self.alreadySentDeleteNotification) {
  215. NSDictionary *appInfoDict = @{kFIRAppNameKey : self.name};
  216. [[NSNotificationCenter defaultCenter] postNotificationName:kFIRAppDeleteNotification
  217. object:[self class]
  218. userInfo:appInfoDict];
  219. self.alreadySentDeleteNotification = YES;
  220. }
  221. completion(YES);
  222. } else {
  223. FIRLogError(kFIRLoggerCore, @"I-COR000007", @"App does not exist.");
  224. completion(NO);
  225. }
  226. }
  227. }
  228. + (void)addAppToAppDictionary:(FIRApp *)app {
  229. if (!sAllApps) {
  230. sAllApps = [NSMutableDictionary dictionary];
  231. }
  232. if ([app configureCore]) {
  233. sAllApps[app.name] = app;
  234. [[NSNotificationCenter defaultCenter]
  235. postNotificationName:kFIRAppDiagnosticsNotification
  236. object:nil
  237. userInfo:@{
  238. kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
  239. kFIRAppDiagnosticsFIRAppKey : app
  240. }];
  241. } else {
  242. [NSException raise:kFirebaseCoreErrorDomain
  243. format:
  244. @"Configuration fails. It may be caused by an invalid GOOGLE_APP_ID in "
  245. @"GoogleService-Info.plist or set in the customized options."];
  246. }
  247. }
  248. - (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options {
  249. self = [super init];
  250. if (self) {
  251. _name = [name copy];
  252. _options = [options copy];
  253. _options.editingLocked = YES;
  254. FIRApp *app = sAllApps[name];
  255. _alreadySentConfigureNotification = app.alreadySentConfigureNotification;
  256. _alreadySentDeleteNotification = app.alreadySentDeleteNotification;
  257. }
  258. return self;
  259. }
  260. - (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(FIRTokenCallback)callback {
  261. if (!_getTokenImplementation) {
  262. callback(nil, nil);
  263. return;
  264. }
  265. _getTokenImplementation(forceRefresh, callback);
  266. }
  267. - (BOOL)configureCore {
  268. [self checkExpectedBundleID];
  269. if (![self isAppIDValid]) {
  270. if (_options.usingOptionsFromDefaultPlist) {
  271. [[NSNotificationCenter defaultCenter]
  272. postNotificationName:kFIRAppDiagnosticsNotification
  273. object:nil
  274. userInfo:@{
  275. kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
  276. kFIRAppDiagnosticsErrorKey : [FIRApp errorForInvalidAppID],
  277. }];
  278. }
  279. return NO;
  280. }
  281. // Initialize the Analytics once there is a valid options under default app. Analytics should
  282. // always initialize first by itself before the other SDKs.
  283. if ([self.name isEqualToString:kFIRDefaultAppName]) {
  284. Class firAnalyticsClass = NSClassFromString(@"FIRAnalytics");
  285. if (!firAnalyticsClass) {
  286. FIRLogError(kFIRLoggerCore, @"I-COR000022", @"Firebase Analytics is not available.");
  287. } else {
  288. #pragma clang diagnostic push
  289. #pragma clang diagnostic ignored "-Wundeclared-selector"
  290. SEL startWithConfigurationSelector = @selector(startWithConfiguration:options:);
  291. #pragma clang diagnostic pop
  292. if ([firAnalyticsClass respondsToSelector:startWithConfigurationSelector]) {
  293. #pragma clang diagnostic push
  294. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  295. [firAnalyticsClass performSelector:startWithConfigurationSelector
  296. withObject:[FIRConfiguration sharedInstance].analyticsConfiguration
  297. withObject:_options];
  298. #pragma clang diagnostic pop
  299. }
  300. }
  301. }
  302. return YES;
  303. }
  304. - (FIROptions *)options {
  305. return [_options copy];
  306. }
  307. - (void)setAutomaticDataCollectionEnabled:(BOOL)automaticDataCollectionEnabled {
  308. NSString *key =
  309. [NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, self.name];
  310. [[NSUserDefaults standardUserDefaults] setBool:automaticDataCollectionEnabled forKey:key];
  311. // Core also controls the FirebaseAnalytics flag, so check if the Analytics flags are set
  312. // within FIROptions and change the Analytics value if necessary. Analytics only works with the
  313. // default app, so return if this isn't the default app.
  314. if (self != sDefaultApp) {
  315. return;
  316. }
  317. // Check if the Analytics flag is explicitly set. If so, no further actions are necessary.
  318. if ([self.options isAnalyticsCollectionExpicitlySet]) {
  319. return;
  320. }
  321. // The Analytics flag has not been explicitly set, so update with the value being set.
  322. [[FIRAnalyticsConfiguration sharedInstance]
  323. setAnalyticsCollectionEnabled:automaticDataCollectionEnabled
  324. persistSetting:NO];
  325. }
  326. - (BOOL)isAutomaticDataCollectionEnabled {
  327. // Check if it's been manually set before in code, and use that as the higher priority value.
  328. NSNumber *defaultsObject = [[self class] readDataCollectionSwitchFromUserDefaultsForApp:self];
  329. if (defaultsObject) {
  330. return [defaultsObject boolValue];
  331. }
  332. // Read the Info.plist to see if the flag is set. If it's not set, it should default to `YES`.
  333. // As per the implementation of `readDataCollectionSwitchFromPlist`, it's a cached value and has
  334. // no performance impact calling multiple times.
  335. NSNumber *collectionEnabledPlistValue = [[self class] readDataCollectionSwitchFromPlist];
  336. if (collectionEnabledPlistValue) {
  337. return [collectionEnabledPlistValue boolValue];
  338. }
  339. return YES;
  340. }
  341. #pragma mark - private
  342. + (void)sendNotificationsToSDKs:(FIRApp *)app {
  343. NSNumber *isDefaultApp = [NSNumber numberWithBool:(app == sDefaultApp)];
  344. NSDictionary *appInfoDict = @{
  345. kFIRAppNameKey : app.name,
  346. kFIRAppIsDefaultAppKey : isDefaultApp,
  347. kFIRGoogleAppIDKey : app.options.googleAppID
  348. };
  349. [[NSNotificationCenter defaultCenter] postNotificationName:kFIRAppReadyToConfigureSDKNotification
  350. object:self
  351. userInfo:appInfoDict];
  352. }
  353. + (NSError *)errorForMissingOptions {
  354. NSDictionary *errorDict = @{
  355. NSLocalizedDescriptionKey :
  356. @"Unable to parse GoogleService-Info.plist in order to configure services.",
  357. NSLocalizedRecoverySuggestionErrorKey :
  358. @"Check formatting and location of GoogleService-Info.plist."
  359. };
  360. return [NSError errorWithDomain:kFirebaseCoreErrorDomain
  361. code:FIRErrorCodeInvalidPlistFile
  362. userInfo:errorDict];
  363. }
  364. + (NSError *)errorForSubspecConfigurationFailureWithDomain:(NSString *)domain
  365. errorCode:(FIRErrorCode)code
  366. service:(NSString *)service
  367. reason:(NSString *)reason {
  368. NSString *description =
  369. [NSString stringWithFormat:@"Configuration failed for service %@.", service];
  370. NSDictionary *errorDict =
  371. @{NSLocalizedDescriptionKey : description, NSLocalizedFailureReasonErrorKey : reason};
  372. return [NSError errorWithDomain:domain code:code userInfo:errorDict];
  373. }
  374. + (NSError *)errorForInvalidAppID {
  375. NSDictionary *errorDict = @{
  376. NSLocalizedDescriptionKey : @"Unable to validate Google App ID",
  377. NSLocalizedRecoverySuggestionErrorKey :
  378. @"Check formatting and location of GoogleService-Info.plist or GoogleAppID set in the "
  379. @"customized options."
  380. };
  381. return [NSError errorWithDomain:kFirebaseCoreErrorDomain
  382. code:FIRErrorCodeInvalidAppID
  383. userInfo:errorDict];
  384. }
  385. + (BOOL)isDefaultAppConfigured {
  386. return (sDefaultApp != nil);
  387. }
  388. + (void)registerLibrary:(nonnull NSString *)library withVersion:(nonnull NSString *)version {
  389. // Create the set of characters which aren't allowed, only if this feature is used.
  390. NSMutableCharacterSet *allowedSet = [NSMutableCharacterSet alphanumericCharacterSet];
  391. [allowedSet addCharactersInString:@"-_."];
  392. NSCharacterSet *disallowedSet = [allowedSet invertedSet];
  393. // Make sure the library name and version strings do not contain unexpected characters, and
  394. // add the name/version pair to the dictionary.
  395. if ([library rangeOfCharacterFromSet:disallowedSet].location == NSNotFound &&
  396. [version rangeOfCharacterFromSet:disallowedSet].location == NSNotFound) {
  397. if (!sLibraryVersions) {
  398. sLibraryVersions = [[NSMutableDictionary alloc] init];
  399. }
  400. sLibraryVersions[library] = version;
  401. } else {
  402. FIRLogError(kFIRLoggerCore, @"I-COR000027",
  403. @"The library name (%@) or version number (%@) contain illegal characters. "
  404. @"Only alphanumeric, dash, underscore and period characters are allowed.",
  405. library, version);
  406. }
  407. }
  408. + (NSString *)firebaseUserAgent {
  409. NSMutableArray<NSString *> *libraries =
  410. [[NSMutableArray<NSString *> alloc] initWithCapacity:sLibraryVersions.count];
  411. for (NSString *libraryName in sLibraryVersions) {
  412. [libraries
  413. addObject:[NSString stringWithFormat:@"%@/%@", libraryName, sLibraryVersions[libraryName]]];
  414. }
  415. [libraries sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
  416. return [libraries componentsJoinedByString:@" "];
  417. }
  418. - (void)checkExpectedBundleID {
  419. NSArray *bundles = [FIRBundleUtil relevantBundles];
  420. NSString *expectedBundleID = [self expectedBundleID];
  421. // The checking is only done when the bundle ID is provided in the serviceInfo dictionary for
  422. // backward compatibility.
  423. if (expectedBundleID != nil &&
  424. ![FIRBundleUtil hasBundleIdentifier:expectedBundleID inBundles:bundles]) {
  425. FIRLogError(kFIRLoggerCore, @"I-COR000008",
  426. @"The project's Bundle ID is inconsistent with "
  427. @"either the Bundle ID in '%@.%@', or the Bundle ID in the options if you are "
  428. @"using a customized options. To ensure that everything can be configured "
  429. @"correctly, you may need to make the Bundle IDs consistent. To continue with this "
  430. @"plist file, you may change your app's bundle identifier to '%@'. Or you can "
  431. @"download a new configuration file that matches your bundle identifier from %@ "
  432. @"and replace the current one.",
  433. kServiceInfoFileName, kServiceInfoFileType, expectedBundleID, kPlistURL);
  434. }
  435. }
  436. - (nullable NSString *)getUID {
  437. if (!_getUIDImplementation) {
  438. FIRLogWarning(kFIRLoggerCore, @"I-COR000025", @"FIRAuth getUID implementation wasn't set.");
  439. return nil;
  440. }
  441. return _getUIDImplementation();
  442. }
  443. #pragma mark - private - App ID Validation
  444. /**
  445. * Validates the format and fingerprint of the app ID contained in GOOGLE_APP_ID in the plist file.
  446. * This is the main method for validating app ID.
  447. *
  448. * @return YES if the app ID fulfills the expected format and fingerprint, NO otherwise.
  449. */
  450. - (BOOL)isAppIDValid {
  451. NSString *appID = _options.googleAppID;
  452. BOOL isValid = [FIRApp validateAppID:appID];
  453. if (!isValid) {
  454. NSString *expectedBundleID = [self expectedBundleID];
  455. FIRLogError(kFIRLoggerCore, @"I-COR000009",
  456. @"The GOOGLE_APP_ID either in the plist file "
  457. @"'%@.%@' or the one set in the customized options is invalid. If you are using "
  458. @"the plist file, use the iOS version of bundle identifier to download the file, "
  459. @"and do not manually edit the GOOGLE_APP_ID. You may change your app's bundle "
  460. @"identifier to '%@'. Or you can download a new configuration file that matches "
  461. @"your bundle identifier from %@ and replace the current one.",
  462. kServiceInfoFileName, kServiceInfoFileType, expectedBundleID, kPlistURL);
  463. };
  464. return isValid;
  465. }
  466. + (BOOL)validateAppID:(NSString *)appID {
  467. // Failing validation only occurs when we are sure we are looking at a V2 app ID and it does not
  468. // have a valid fingerprint, otherwise we just warn about the potential issue.
  469. if (!appID.length) {
  470. return NO;
  471. }
  472. // All app IDs must start with at least "<version number>:".
  473. NSString *const versionPattern = @"^\\d+:";
  474. NSRegularExpression *versionRegex =
  475. [NSRegularExpression regularExpressionWithPattern:versionPattern options:0 error:NULL];
  476. if (!versionRegex) {
  477. return NO;
  478. }
  479. NSRange appIDRange = NSMakeRange(0, appID.length);
  480. NSArray *versionMatches = [versionRegex matchesInString:appID options:0 range:appIDRange];
  481. if (versionMatches.count != 1) {
  482. return NO;
  483. }
  484. NSRange versionRange = [(NSTextCheckingResult *)versionMatches.firstObject range];
  485. NSString *appIDVersion = [appID substringWithRange:versionRange];
  486. NSArray *knownVersions = @[ @"1:" ];
  487. if (![knownVersions containsObject:appIDVersion]) {
  488. // Permit unknown yet properly formatted app ID versions.
  489. return YES;
  490. }
  491. if (![FIRApp validateAppIDFormat:appID withVersion:appIDVersion]) {
  492. return NO;
  493. }
  494. if (![FIRApp validateAppIDFingerprint:appID withVersion:appIDVersion]) {
  495. return NO;
  496. }
  497. return YES;
  498. }
  499. + (NSString *)actualBundleID {
  500. return [[NSBundle mainBundle] bundleIdentifier];
  501. }
  502. /**
  503. * Validates that the format of the app ID string is what is expected based on the supplied version.
  504. * The version must end in ":".
  505. *
  506. * For v1 app ids the format is expected to be
  507. * '<version #>:<project number>:ios:<fingerprint of bundle id>'.
  508. *
  509. * This method does not verify that the contents of the app id are correct, just that they fulfill
  510. * the expected format.
  511. *
  512. * @param appID Contents of GOOGLE_APP_ID from the plist file.
  513. * @param version Indicates what version of the app id format this string should be.
  514. * @return YES if provided string fufills the expected format, NO otherwise.
  515. */
  516. + (BOOL)validateAppIDFormat:(NSString *)appID withVersion:(NSString *)version {
  517. if (!appID.length || !version.length) {
  518. return NO;
  519. }
  520. if (![version hasSuffix:@":"]) {
  521. return NO;
  522. }
  523. if (![appID hasPrefix:version]) {
  524. return NO;
  525. }
  526. NSString *const pattern = @"^\\d+:ios:[a-f0-9]+$";
  527. NSRegularExpression *regex =
  528. [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
  529. if (!regex) {
  530. return NO;
  531. }
  532. NSRange localRange = NSMakeRange(version.length, appID.length - version.length);
  533. NSUInteger numberOfMatches = [regex numberOfMatchesInString:appID options:0 range:localRange];
  534. if (numberOfMatches != 1) {
  535. return NO;
  536. }
  537. return YES;
  538. }
  539. /**
  540. * Validates that the fingerprint of the app ID string is what is expected based on the supplied
  541. * version. The version must end in ":".
  542. *
  543. * Note that the v1 hash algorithm is not permitted on the client and cannot be fully validated.
  544. *
  545. * @param appID Contents of GOOGLE_APP_ID from the plist file.
  546. * @param version Indicates what version of the app id format this string should be.
  547. * @return YES if provided string fufills the expected fingerprint and the version is known, NO
  548. * otherwise.
  549. */
  550. + (BOOL)validateAppIDFingerprint:(NSString *)appID withVersion:(NSString *)version {
  551. if (!appID.length || !version.length) {
  552. return NO;
  553. }
  554. if (![version hasSuffix:@":"]) {
  555. return NO;
  556. }
  557. if (![appID hasPrefix:version]) {
  558. return NO;
  559. }
  560. // Extract the supplied fingerprint from the supplied app ID.
  561. // This assumes the app ID format is the same for all known versions below. If the app ID format
  562. // changes in future versions, the tokenizing of the app ID format will need to take into account
  563. // the version of the app ID.
  564. NSArray *components = [appID componentsSeparatedByString:@":"];
  565. if (components.count != 4) {
  566. return NO;
  567. }
  568. NSString *suppliedFingerprintString = components[3];
  569. if (!suppliedFingerprintString.length) {
  570. return NO;
  571. }
  572. uint64_t suppliedFingerprint;
  573. NSScanner *scanner = [NSScanner scannerWithString:suppliedFingerprintString];
  574. if (![scanner scanHexLongLong:&suppliedFingerprint]) {
  575. return NO;
  576. }
  577. if ([version isEqual:@"1:"]) {
  578. // The v1 hash algorithm is not permitted on the client so the actual hash cannot be validated.
  579. return YES;
  580. }
  581. // Unknown version.
  582. return NO;
  583. }
  584. - (NSString *)expectedBundleID {
  585. return _options.bundleID;
  586. }
  587. // end App ID validation
  588. #pragma mark - Reading From Plist & User Defaults
  589. /**
  590. * Clears the data collection switch from the standard NSUserDefaults for easier testing and
  591. * readability.
  592. */
  593. - (void)clearDataCollectionSwitchFromUserDefaults {
  594. NSString *key =
  595. [NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, self.name];
  596. [[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
  597. }
  598. /**
  599. * Reads the data collection switch from the standard NSUserDefaults for easier testing and
  600. * readability.
  601. */
  602. + (nullable NSNumber *)readDataCollectionSwitchFromUserDefaultsForApp:(FIRApp *)app {
  603. // Read the object in user defaults, and only return if it's an NSNumber.
  604. NSString *key =
  605. [NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, app.name];
  606. id collectionEnabledDefaultsObject = [[NSUserDefaults standardUserDefaults] objectForKey:key];
  607. if ([collectionEnabledDefaultsObject isKindOfClass:[NSNumber class]]) {
  608. return collectionEnabledDefaultsObject;
  609. }
  610. return nil;
  611. }
  612. /**
  613. * Reads the data collection switch from the Info.plist for easier testing and readability. Will
  614. * only read once from the plist and return the cached value.
  615. */
  616. + (nullable NSNumber *)readDataCollectionSwitchFromPlist {
  617. static NSNumber *collectionEnabledPlistObject;
  618. static dispatch_once_t onceToken;
  619. dispatch_once(&onceToken, ^{
  620. // Read the data from the `Info.plist`, only assign it if it's there and an NSNumber.
  621. id plistValue = [[NSBundle mainBundle]
  622. objectForInfoDictionaryKey:kFIRGlobalAppDataCollectionEnabledPlistKey];
  623. if (plistValue && [plistValue isKindOfClass:[NSNumber class]]) {
  624. collectionEnabledPlistObject = (NSNumber *)plistValue;
  625. }
  626. });
  627. return collectionEnabledPlistObject;
  628. }
  629. #pragma mark - Sending Logs
  630. - (void)sendLogsWithServiceName:(NSString *)serviceName
  631. version:(NSString *)version
  632. error:(NSError *)error {
  633. // If the user has manually turned off data collection, return and don't send logs.
  634. if (![self isAutomaticDataCollectionEnabled]) {
  635. return;
  636. }
  637. NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithDictionary:@{
  638. kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeSDK),
  639. kFIRAppDiagnosticsSDKNameKey : serviceName,
  640. kFIRAppDiagnosticsSDKVersionKey : version,
  641. kFIRAppDiagnosticsFIRAppKey : self
  642. }];
  643. if (error) {
  644. userInfo[kFIRAppDiagnosticsErrorKey] = error;
  645. }
  646. [[NSNotificationCenter defaultCenter] postNotificationName:kFIRAppDiagnosticsNotification
  647. object:nil
  648. userInfo:userInfo];
  649. }
  650. @end