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