FIRInstallations.m 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. /*
  2. * Copyright 2019 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 "FirebaseInstallations/Source/Library/Public/FirebaseInstallations/FIRInstallations.h"
  17. #if __has_include(<FBLPromises/FBLPromises.h>)
  18. #import <FBLPromises/FBLPromises.h>
  19. #else
  20. #import "FBLPromises.h"
  21. #endif
  22. #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
  23. #import "FirebaseInstallations/Source/Library/FIRInstallationsAuthTokenResultInternal.h"
  24. #import "FirebaseInstallations/Source/Library/Errors/FIRInstallationsErrorUtil.h"
  25. #import "FirebaseInstallations/Source/Library/FIRInstallationsItem.h"
  26. #import "FirebaseInstallations/Source/Library/FIRInstallationsLogger.h"
  27. #import "FirebaseInstallations/Source/Library/InstallationsIDController/FIRInstallationsIDController.h"
  28. #import "FirebaseInstallations/Source/Library/InstallationsStore/FIRInstallationsStoredAuthToken.h"
  29. NS_ASSUME_NONNULL_BEGIN
  30. static const NSUInteger kExpectedAPIKeyLength = 39;
  31. @protocol FIRInstallationsInstanceProvider <FIRLibrary>
  32. @end
  33. @interface FIRInstallations () <FIRInstallationsInstanceProvider>
  34. @property(nonatomic, readonly) FIROptions *appOptions;
  35. @property(nonatomic, readonly) NSString *appName;
  36. @property(nonatomic, readonly) FIRInstallationsIDController *installationsIDController;
  37. @end
  38. @implementation FIRInstallations
  39. #pragma mark - Firebase component
  40. + (void)load {
  41. [FIRApp registerInternalLibrary:(Class<FIRLibrary>)self withName:@"fire-install"];
  42. }
  43. + (nonnull NSArray<FIRComponent *> *)componentsToRegister {
  44. FIRComponentCreationBlock creationBlock =
  45. ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
  46. *isCacheable = YES;
  47. FIRInstallations *installations = [[FIRInstallations alloc] initWithApp:container.app];
  48. return installations;
  49. };
  50. FIRComponent *installationsProvider =
  51. [FIRComponent componentWithProtocol:@protocol(FIRInstallationsInstanceProvider)
  52. instantiationTiming:FIRInstantiationTimingAlwaysEager
  53. dependencies:@[]
  54. creationBlock:creationBlock];
  55. return @[ installationsProvider ];
  56. }
  57. - (instancetype)initWithApp:(FIRApp *)app {
  58. return [self initWitAppOptions:app.options appName:app.name];
  59. }
  60. - (instancetype)initWitAppOptions:(FIROptions *)appOptions appName:(NSString *)appName {
  61. FIRInstallationsIDController *IDController =
  62. [[FIRInstallationsIDController alloc] initWithGoogleAppID:appOptions.googleAppID
  63. appName:appName
  64. APIKey:appOptions.APIKey
  65. projectID:appOptions.projectID
  66. GCMSenderID:appOptions.GCMSenderID
  67. accessGroup:appOptions.appGroupID];
  68. // `prefetchAuthToken` is disabled due to b/156746574.
  69. return [self initWithAppOptions:appOptions
  70. appName:appName
  71. installationsIDController:IDController
  72. prefetchAuthToken:NO];
  73. }
  74. /// The initializer is supposed to be used by tests to inject `installationsStore`.
  75. - (instancetype)initWithAppOptions:(FIROptions *)appOptions
  76. appName:(NSString *)appName
  77. installationsIDController:(FIRInstallationsIDController *)installationsIDController
  78. prefetchAuthToken:(BOOL)prefetchAuthToken {
  79. self = [super init];
  80. if (self) {
  81. [[self class] validateAppOptions:appOptions appName:appName];
  82. [[self class] assertCompatibleIIDVersion];
  83. _appOptions = [appOptions copy];
  84. _appName = [appName copy];
  85. _installationsIDController = installationsIDController;
  86. // Pre-fetch auth token.
  87. if (prefetchAuthToken) {
  88. [self authTokenWithCompletion:^(FIRInstallationsAuthTokenResult *_Nullable tokenResult,
  89. NSError *_Nullable error){
  90. }];
  91. }
  92. }
  93. return self;
  94. }
  95. + (void)validateAppOptions:(FIROptions *)appOptions appName:(NSString *)appName {
  96. NSMutableArray *missingFields = [NSMutableArray array];
  97. if (appName.length < 1) {
  98. [missingFields addObject:@"`FirebaseApp.name`"];
  99. }
  100. if (appOptions.APIKey.length < 1) {
  101. [missingFields addObject:@"`FirebaseOptions.APIKey`"];
  102. }
  103. if (appOptions.googleAppID.length < 1) {
  104. [missingFields addObject:@"`FirebaseOptions.googleAppID`"];
  105. }
  106. if (appOptions.projectID.length < 1) {
  107. [missingFields addObject:@"`FirebaseOptions.projectID`"];
  108. }
  109. if (missingFields.count > 0) {
  110. [NSException
  111. raise:kFirebaseInstallationsErrorDomain
  112. format:
  113. @"%@[%@] Could not configure Firebase Installations due to invalid FirebaseApp "
  114. @"options. The following parameters are nil or empty: %@. If you use "
  115. @"GoogleServices-Info.plist please download the most recent version from the Firebase "
  116. @"Console. If you configure Firebase in code, please make sure you specify all "
  117. @"required parameters.",
  118. kFIRLoggerInstallations, kFIRInstallationsMessageCodeInvalidFirebaseAppOptions,
  119. [missingFields componentsJoinedByString:@", "]];
  120. }
  121. [self validateAPIKey:appOptions.APIKey];
  122. }
  123. + (void)validateAPIKey:(nullable NSString *)APIKey {
  124. NSMutableArray<NSString *> *validationIssues = [[NSMutableArray alloc] init];
  125. if (APIKey.length != kExpectedAPIKeyLength) {
  126. [validationIssues addObject:[NSString stringWithFormat:@"API Key length must be %lu characters",
  127. (unsigned long)kExpectedAPIKeyLength]];
  128. }
  129. if (![[APIKey substringToIndex:1] isEqualToString:@"A"]) {
  130. [validationIssues addObject:@"API Key must start with `A`"];
  131. }
  132. NSMutableCharacterSet *allowedCharacters = [NSMutableCharacterSet alphanumericCharacterSet];
  133. [allowedCharacters
  134. formUnionWithCharacterSet:[NSCharacterSet characterSetWithCharactersInString:@"-_"]];
  135. NSCharacterSet *characters = [NSCharacterSet characterSetWithCharactersInString:APIKey];
  136. if (![allowedCharacters isSupersetOfSet:characters]) {
  137. [validationIssues addObject:@"API Key must contain only base64 url-safe characters characters"];
  138. }
  139. if (validationIssues.count > 0) {
  140. [NSException
  141. raise:kFirebaseInstallationsErrorDomain
  142. format:
  143. @"%@[%@] Could not configure Firebase Installations due to invalid FirebaseApp "
  144. @"options. `FirebaseOptions.APIKey` doesn't match the expected format: %@. If you use "
  145. @"GoogleServices-Info.plist please download the most recent version from the Firebase "
  146. @"Console. If you configure Firebase in code, please make sure you specify all "
  147. @"required parameters.",
  148. kFIRLoggerInstallations, kFIRInstallationsMessageCodeInvalidFirebaseAppOptions,
  149. [validationIssues componentsJoinedByString:@", "]];
  150. }
  151. }
  152. #pragma mark - Public
  153. + (FIRInstallations *)installations {
  154. FIRApp *defaultApp = [FIRApp defaultApp];
  155. if (!defaultApp) {
  156. [NSException raise:kFirebaseInstallationsErrorDomain
  157. format:@"The default FirebaseApp instance must be configured before the default"
  158. @"FirebaseApp instance can be initialized. One way to ensure that is to "
  159. @"call `[FIRApp configure];` (`FirebaseApp.configure()` in Swift) in the App"
  160. @" Delegate's `application:didFinishLaunchingWithOptions:` "
  161. @"(`application(_:didFinishLaunchingWithOptions:)` in Swift)."];
  162. }
  163. return [self installationsWithApp:defaultApp];
  164. }
  165. + (FIRInstallations *)installationsWithApp:(FIRApp *)app {
  166. id<FIRInstallationsInstanceProvider> installations =
  167. FIR_COMPONENT(FIRInstallationsInstanceProvider, app.container);
  168. return (FIRInstallations *)installations;
  169. }
  170. - (void)installationIDWithCompletion:(FIRInstallationsIDHandler)completion {
  171. [self.installationsIDController getInstallationItem]
  172. .then(^id(FIRInstallationsItem *installation) {
  173. completion(installation.firebaseInstallationID, nil);
  174. return nil;
  175. })
  176. .catch(^(NSError *error) {
  177. completion(nil, [FIRInstallationsErrorUtil publicDomainErrorWithError:error]);
  178. });
  179. }
  180. - (void)authTokenWithCompletion:(FIRInstallationsTokenHandler)completion {
  181. [self authTokenForcingRefresh:NO completion:completion];
  182. }
  183. - (void)authTokenForcingRefresh:(BOOL)forceRefresh
  184. completion:(FIRInstallationsTokenHandler)completion {
  185. [self.installationsIDController getAuthTokenForcingRefresh:forceRefresh]
  186. .then(^FIRInstallationsAuthTokenResult *(FIRInstallationsItem *installation) {
  187. FIRInstallationsAuthTokenResult *result = [[FIRInstallationsAuthTokenResult alloc]
  188. initWithToken:installation.authToken.token
  189. expirationDate:installation.authToken.expirationDate];
  190. return result;
  191. })
  192. .then(^id(FIRInstallationsAuthTokenResult *token) {
  193. completion(token, nil);
  194. return nil;
  195. })
  196. .catch(^void(NSError *error) {
  197. completion(nil, [FIRInstallationsErrorUtil publicDomainErrorWithError:error]);
  198. });
  199. }
  200. - (void)deleteWithCompletion:(void (^)(NSError *__nullable error))completion {
  201. [self.installationsIDController deleteInstallation]
  202. .then(^id(id result) {
  203. completion(nil);
  204. return nil;
  205. })
  206. .catch(^void(NSError *error) {
  207. completion([FIRInstallationsErrorUtil publicDomainErrorWithError:error]);
  208. });
  209. }
  210. #pragma mark - IID version compatibility
  211. + (void)assertCompatibleIIDVersion {
  212. // We use this flag to disable IID compatibility exception for unit tests.
  213. #ifdef FIR_INSTALLATIONS_ALLOWS_INCOMPATIBLE_IID_VERSION
  214. return;
  215. #else
  216. if (![self isIIDVersionCompatible]) {
  217. [NSException raise:kFirebaseInstallationsErrorDomain
  218. format:@"FirebaseInstallations will not work correctly with current version of "
  219. @"Firebase Instance ID. Please update your Firebase Instance ID version."];
  220. }
  221. #endif
  222. }
  223. + (BOOL)isIIDVersionCompatible {
  224. Class IIDClass = NSClassFromString(@"FIRInstanceID");
  225. if (IIDClass == nil) {
  226. // It is OK if there is no IID at all.
  227. return YES;
  228. }
  229. // We expect a compatible version having the method `+[FIRInstanceID usesFIS]` defined.
  230. BOOL isCompatibleVersion = [IIDClass respondsToSelector:NSSelectorFromString(@"usesFIS")];
  231. return isCompatibleVersion;
  232. }
  233. @end
  234. NS_ASSUME_NONNULL_END