1 // Copyright 2017 Google
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
7 // http://www.apache.org/licenses/LICENSE-2.0
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.
15 #include <sys/utsname.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"
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";
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";
52 NSString *const kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat =
53 @"/google/firebase/global_data_collection_enabled:%@";
54 NSString *const kFIRGlobalAppDataCollectionEnabledPlistKey =
55 @"FirebaseDataCollectionDefaultEnabled";
57 NSString *const kFIRAppDiagnosticsNotification = @"FIRAppDiagnosticsNotification";
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";
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";
76 * The URL to download plist files.
78 static NSString *const kPlistURL = @"https://console.firebase.google.com/";
81 * An array of all classes that registered as `FIRCoreConfigurable` in order to receive lifecycle
84 static NSMutableArray<Class<FIRCoreConfigurable>> *gRegisteredAsConfigurable;
88 @property(nonatomic) BOOL alreadySentConfigureNotification;
90 @property(nonatomic) BOOL alreadySentDeleteNotification;
93 @property(nonatomic) BOOL alreadyOutputDataCollectionFlag;
98 @implementation FIRApp
100 // This is necessary since our custom getter prevents `_options` from being created.
101 @synthesize options = _options;
103 static NSMutableDictionary *sAllApps;
104 static FIRApp *sDefaultApp;
105 static NSMutableDictionary *sLibraryVersions;
108 FIROptions *options = [FIROptions defaultOptions];
110 [[NSNotificationCenter defaultCenter]
111 postNotificationName:kFIRAppDiagnosticsNotification
114 kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
115 kFIRAppDiagnosticsErrorKey : [FIRApp errorForMissingOptions]
117 [NSException raise:kFirebaseCoreErrorDomain
119 @"`[FIRApp configure];` (`FirebaseApp.configure()` in Swift) could not find "
120 @"a valid GoogleService-Info.plist in your project. Please download one "
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.");
133 + (void)configureWithOptions:(FIROptions *)options {
135 [NSException raise:kFirebaseCoreErrorDomain
136 format:@"Options is nil. Please pass a valid options."];
138 [FIRApp configureDefaultAppWithOptions:options sendingNotifications:YES];
141 + (void)configureDefaultAppWithOptions:(FIROptions *)options
142 sendingNotifications:(BOOL)sendNotifications {
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."];
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;
163 + (void)configureWithName:(NSString *)name options:(FIROptions *)options {
164 if (!name || !options) {
165 [NSException raise:kFirebaseCoreErrorDomain format:@"Neither name nor options can be nil."];
167 if (name.length == 0) {
168 [NSException raise:kFirebaseCoreErrorDomain format:@"Name cannot be empty."];
170 if ([name isEqualToString:kFIRDefaultAppName]) {
171 [NSException raise:kFirebaseCoreErrorDomain format:@"Name cannot be __FIRAPP_DEFAULT."];
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
179 @"App name should only contain Letters, "
180 @"Numbers, Underscores, and Dashes."];
184 if (sAllApps && sAllApps[name]) {
185 [NSException raise:kFirebaseCoreErrorDomain
186 format:@"App named %@ has already been configured.", name];
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;
200 + (FIRApp *)defaultApp {
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.");
211 + (FIRApp *)appNamed:(NSString *)name {
212 @synchronized(self) {
214 FIRApp *app = sAllApps[name];
219 FIRLogError(kFIRLoggerCore, @"I-COR000004", @"App with name %@ does not exist.", name);
224 + (NSDictionary *)allApps {
225 @synchronized(self) {
227 FIRLogError(kFIRLoggerCore, @"I-COR000005", @"No app has been configured yet.");
229 NSDictionary *dict = [NSDictionary dictionaryWithDictionary:sAllApps];
234 // Public only for tests
237 [sAllApps removeAllObjects];
239 [sLibraryVersions removeAllObjects];
240 sLibraryVersions = nil;
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);
248 // Remove all cached instances from the container before deleting the app.
249 [self.container removeAllCachedInstances];
251 [sAllApps removeObjectForKey:self.name];
252 [self clearDataCollectionSwitchFromUserDefaults];
253 if ([self.name isEqualToString:kFIRDefaultAppName]) {
256 if (!self.alreadySentDeleteNotification) {
257 NSDictionary *appInfoDict = @{kFIRAppNameKey : self.name};
258 [[NSNotificationCenter defaultCenter] postNotificationName:kFIRAppDeleteNotification
260 userInfo:appInfoDict];
261 self.alreadySentDeleteNotification = YES;
265 FIRLogError(kFIRLoggerCore, @"I-COR000007", @"App does not exist.");
271 + (void)addAppToAppDictionary:(FIRApp *)app {
273 sAllApps = [NSMutableDictionary dictionary];
275 if ([app configureCore]) {
276 sAllApps[app.name] = app;
277 [[NSNotificationCenter defaultCenter]
278 postNotificationName:kFIRAppDiagnosticsNotification
281 kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
282 kFIRAppDiagnosticsFIRAppKey : app
285 [NSException raise:kFirebaseCoreErrorDomain
287 @"Configuration fails. It may be caused by an invalid GOOGLE_APP_ID in "
288 @"GoogleService-Info.plist or set in the customized options."];
292 - (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options {
296 _options = [options copy];
297 _options.editingLocked = YES;
298 _isDefaultApp = [name isEqualToString:kFIRDefaultAppName];
299 _container = [[FIRComponentContainer alloc] initWithApp:self];
301 FIRApp *app = sAllApps[name];
302 _alreadySentConfigureNotification = app.alreadySentConfigureNotification;
303 _alreadySentDeleteNotification = app.alreadySentDeleteNotification;
308 - (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(FIRTokenCallback)callback {
309 if (!_getTokenImplementation) {
314 _getTokenImplementation(forceRefresh, callback);
317 - (BOOL)configureCore {
318 [self checkExpectedBundleID];
319 if (![self isAppIDValid]) {
320 if (_options.usingOptionsFromDefaultPlist) {
321 [[NSNotificationCenter defaultCenter]
322 postNotificationName:kFIRAppDiagnosticsNotification
325 kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeCore),
326 kFIRAppDiagnosticsErrorKey : [FIRApp errorForInvalidAppID],
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");
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
361 - (FIROptions *)options {
362 return [_options copy];
365 - (void)setDataCollectionDefaultEnabled:(BOOL)dataCollectionDefaultEnabled {
367 FIRLogDebug(kFIRLoggerCore, @"I-COR000034", @"Explicitly %@ data collection flag.",
368 dataCollectionDefaultEnabled ? @"enabled" : @"disabled");
369 self.alreadyOutputDataCollectionFlag = YES;
373 [NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, self.name];
374 [[NSUserDefaults standardUserDefaults] setBool:dataCollectionDefaultEnabled forKey:key];
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) {
383 // Check if the Analytics flag is explicitly set. If so, no further actions are necessary.
384 if ([self.options isAnalyticsCollectionExpicitlySet]) {
388 // The Analytics flag has not been explicitly set, so update with the value being set.
389 [[FIRAnalyticsConfiguration sharedInstance]
390 setAnalyticsCollectionEnabled:dataCollectionDefaultEnabled
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) {
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;
405 return [defaultsObject boolValue];
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) {
414 if (!self.alreadyOutputDataCollectionFlag) {
415 FIRLogDebug(kFIRLoggerCore, @"I-COR000032", @"Data Collection flag is %@ in plist.",
416 [collectionEnabledPlistValue boolValue] ? @"enabled" : @"disabled");
417 self.alreadyOutputDataCollectionFlag = YES;
420 return [collectionEnabledPlistValue boolValue];
424 if (!self.alreadyOutputDataCollectionFlag) {
425 FIRLogDebug(kFIRLoggerCore, @"I-COR000033", @"Data Collection flag is not set.");
426 self.alreadyOutputDataCollectionFlag = YES;
432 #pragma mark - private
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
442 [[NSNotificationCenter defaultCenter] postNotificationName:kFIRAppReadyToConfigureSDKNotification
444 userInfo:appInfoDict];
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];
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."
460 return [NSError errorWithDomain:kFirebaseCoreErrorDomain
461 code:FIRErrorCodeInvalidPlistFile
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];
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."
483 return [NSError errorWithDomain:kFirebaseCoreErrorDomain
484 code:FIRErrorCodeInvalidAppID
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];
495 NSAssert([(Class)klass conformsToProtocol:@protocol(FIRCoreConfigurable)],
496 @"The class being registered (%@) must conform to `FIRCoreConfigurable`.", klass);
497 [gRegisteredAsConfigurable addObject:klass];
500 + (BOOL)isDefaultAppConfigured {
501 return (sDefaultApp != nil);
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];
516 sLibraryVersions[library] = version;
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.",
525 + (NSString *)firebaseUserAgent {
526 NSMutableArray<NSString *> *libraries =
527 [[NSMutableArray<NSString *> alloc] initWithCapacity:sLibraryVersions.count];
528 for (NSString *libraryName in sLibraryVersions) {
530 addObject:[NSString stringWithFormat:@"%@/%@", libraryName, sLibraryVersions[libraryName]]];
532 [libraries sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
533 return [libraries componentsJoinedByString:@" "];
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);
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.");
561 return _getUIDImplementation();
564 #pragma mark - private - App ID Validation
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.
570 * @return YES if the app ID fulfills the expected format and fingerprint, NO otherwise.
572 - (BOOL)isAppIDValid {
573 NSString *appID = _options.googleAppID;
574 BOOL isValid = [FIRApp validateAppID:appID];
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);
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.
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];
604 NSRange appIDRange = NSMakeRange(0, appID.length);
605 NSArray *versionMatches = [versionRegex matchesInString:appID options:0 range:appIDRange];
606 if (versionMatches.count != 1) {
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.
618 if (![FIRApp validateAppIDFormat:appID withVersion:appIDVersion]) {
622 if (![FIRApp validateAppIDFingerprint:appID withVersion:appIDVersion]) {
629 + (NSString *)actualBundleID {
630 return [[NSBundle mainBundle] bundleIdentifier];
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 ":".
637 * For v1 app ids the format is expected to be
638 * '<version #>:<project number>:ios:<fingerprint of bundle id>'.
640 * This method does not verify that the contents of the app id are correct, just that they fulfill
641 * the expected format.
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.
647 + (BOOL)validateAppIDFormat:(NSString *)appID withVersion:(NSString *)version {
648 if (!appID.length || !version.length) {
652 if (![version hasSuffix:@":"]) {
656 if (![appID hasPrefix:version]) {
660 NSString *const pattern = @"^\\d+:ios:[a-f0-9]+$";
661 NSRegularExpression *regex =
662 [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
667 NSRange localRange = NSMakeRange(version.length, appID.length - version.length);
668 NSUInteger numberOfMatches = [regex numberOfMatchesInString:appID options:0 range:localRange];
669 if (numberOfMatches != 1) {
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 ":".
679 * Note that the v1 hash algorithm is not permitted on the client and cannot be fully validated.
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
686 + (BOOL)validateAppIDFingerprint:(NSString *)appID withVersion:(NSString *)version {
687 if (!appID.length || !version.length) {
691 if (![version hasSuffix:@":"]) {
695 if (![appID hasPrefix:version]) {
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) {
708 NSString *suppliedFingerprintString = components[3];
709 if (!suppliedFingerprintString.length) {
713 uint64_t suppliedFingerprint;
714 NSScanner *scanner = [NSScanner scannerWithString:suppliedFingerprintString];
715 if (![scanner scanHexLongLong:&suppliedFingerprint]) {
719 if ([version isEqual:@"1:"]) {
720 // The v1 hash algorithm is not permitted on the client so the actual hash cannot be validated.
728 - (NSString *)expectedBundleID {
729 return _options.bundleID;
732 // end App ID validation
734 #pragma mark - Reading From Plist & User Defaults
737 * Clears the data collection switch from the standard NSUserDefaults for easier testing and
740 - (void)clearDataCollectionSwitchFromUserDefaults {
742 [NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, self.name];
743 [[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
747 * Reads the data collection switch from the standard NSUserDefaults for easier testing and
750 + (nullable NSNumber *)readDataCollectionSwitchFromUserDefaultsForApp:(FIRApp *)app {
751 // Read the object in user defaults, and only return if it's an NSNumber.
753 [NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, app.name];
754 id collectionEnabledDefaultsObject = [[NSUserDefaults standardUserDefaults] objectForKey:key];
755 if ([collectionEnabledDefaultsObject isKindOfClass:[NSNumber class]]) {
756 return collectionEnabledDefaultsObject;
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.
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;
778 return collectionEnabledPlistObject;
781 #pragma mark - Sending Logs
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]) {
791 NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithDictionary:@{
792 kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeSDK),
793 kFIRAppDiagnosticsSDKNameKey : serviceName,
794 kFIRAppDiagnosticsSDKVersionKey : version,
795 kFIRAppDiagnosticsFIRAppKey : self
798 userInfo[kFIRAppDiagnosticsErrorKey] = error;
800 [[NSNotificationCenter defaultCenter] postNotificationName:kFIRAppDiagnosticsNotification