Chapter 11 - Chat App/ChatAppFinished/Pods/FirebaseInstanceID/Firebase/InstanceID/FIRInstanceIDStore.m
/*
 * Copyright 2019 Google
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#import "FIRInstanceIDStore.h"

#import "FIRInstanceIDCheckinPreferences.h"
#import "FIRInstanceIDCheckinStore.h"
#import "FIRInstanceIDConstants.h"
#import "FIRInstanceIDLogger.h"
#import "FIRInstanceIDTokenStore.h"
#import "FIRInstanceIDVersionUtilities.h"

// NOTE: These values should be in sync with what InstanceID saves in as.
static NSString *const kCheckinFileName = @"g-checkin";

// APNS token (use the old key value i.e. with prefix GMS)
static NSString *const kFIRInstanceIDLibraryVersion = @"GMSInstanceID-version";

@interface FIRInstanceIDStore ()

@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinStore *checkinStore;
@property(nonatomic, readwrite, strong) FIRInstanceIDTokenStore *tokenStore;

@end

@implementation FIRInstanceIDStore

- (instancetype)initWithDelegate:(NSObject<FIRInstanceIDStoreDelegate> *)delegate {
  FIRInstanceIDCheckinStore *checkinStore = [[FIRInstanceIDCheckinStore alloc]
      initWithCheckinPlistFileName:kCheckinFileName
                  subDirectoryName:kFIRInstanceIDSubDirectoryName];

  FIRInstanceIDTokenStore *tokenStore = [FIRInstanceIDTokenStore defaultStore];

  return [self initWithCheckinStore:checkinStore tokenStore:tokenStore delegate:delegate];
}

- (instancetype)initWithCheckinStore:(FIRInstanceIDCheckinStore *)checkinStore
                          tokenStore:(FIRInstanceIDTokenStore *)tokenStore
                            delegate:(NSObject<FIRInstanceIDStoreDelegate> *)delegate {
  self = [super init];
  if (self) {
    _checkinStore = checkinStore;
    _tokenStore = tokenStore;
    _delegate = delegate;
    [self resetCredentialsIfNeeded];
  }
  return self;
}

#pragma mark - Upgrades

+ (BOOL)hasSubDirectory:(NSString *)subDirectoryName {
  NSString *subDirectoryPath = [self pathForSupportSubDirectory:subDirectoryName];
  BOOL isDirectory;
  if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
                                            isDirectory:&isDirectory]) {
    return NO;
  } else if (!isDirectory) {
    return NO;
  }
  return YES;
}

+ (NSSearchPathDirectory)supportedDirectory {
#if TARGET_OS_TV
  return NSCachesDirectory;
#else
  return NSApplicationSupportDirectory;
#endif
}

+ (NSString *)pathForSupportSubDirectory:(NSString *)subDirectoryName {
  NSArray *directoryPaths =
      NSSearchPathForDirectoriesInDomains([self supportedDirectory], NSUserDomainMask, YES);
  NSString *dirPath = directoryPaths.lastObject;
  NSArray *components = @[ dirPath, subDirectoryName ];
  return [NSString pathWithComponents:components];
}

+ (BOOL)createSubDirectory:(NSString *)subDirectoryName {
  NSString *subDirectoryPath = [self pathForSupportSubDirectory:subDirectoryName];
  BOOL hasSubDirectory;

  if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
                                            isDirectory:&hasSubDirectory]) {
    NSError *error;
    [[NSFileManager defaultManager] createDirectoryAtPath:subDirectoryPath
                              withIntermediateDirectories:YES
                                               attributes:nil
                                                    error:&error];
    if (error) {
      FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeStore000,
                               @"Cannot create directory %@, error: %@", subDirectoryPath, error);
      return NO;
    }
  } else {
    if (!hasSubDirectory) {
      FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeStore001,
                               @"Found file instead of directory at %@", subDirectoryPath);
      return NO;
    }
  }
  return YES;
}

+ (BOOL)removeSubDirectory:(NSString *)subDirectoryName error:(NSError **)error {
  if ([self hasSubDirectory:subDirectoryName]) {
    NSString *subDirectoryPath = [self pathForSupportSubDirectory:subDirectoryName];
    BOOL isDirectory;
    if ([[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
                                             isDirectory:&isDirectory]) {
      return [[NSFileManager defaultManager] removeItemAtPath:subDirectoryPath error:error];
    }
  }
  return YES;
}

/**
 *  Reset the keychain preferences if the app had been deleted earlier and then reinstalled.
 *  Keychain preferences are not cleared in the above scenario so explicitly clear them.
 *
 *  In case of an iCloud backup and restore the Keychain preferences should already be empty
 *  since the Keychain items are marked with `*BackupThisDeviceOnly`.
 */
- (void)resetCredentialsIfNeeded {
  BOOL checkinPlistExists = [self.checkinStore hasCheckinPlist];
  // Checkin info existed in backup excluded plist. Should not be a fresh install.
  if (checkinPlistExists) {
    // FCM user can still have the old version of checkin, migration should only happen once.
    [self.checkinStore migrateCheckinItemIfNeeded];
    return;
  }

  // reset checkin in keychain if a fresh install.
  // set the old checkin preferences to unregister pre-registered tokens
  FIRInstanceIDCheckinPreferences *oldCheckinPreferences =
      [self.checkinStore cachedCheckinPreferences];

  if (oldCheckinPreferences) {
    [self.checkinStore removeCheckinPreferencesWithHandler:^(NSError *error) {
      if (!error) {
        FIRInstanceIDLoggerDebug(
            kFIRInstanceIDMessageCodeStore002,
            @"Removed cached checkin preferences from Keychain because this is a fresh install.");
      } else {
        FIRInstanceIDLoggerError(
            kFIRInstanceIDMessageCodeStore003,
            @"Couldn't remove cached checkin preferences for a fresh install. Error: %@", error);
      }
      if (oldCheckinPreferences.deviceID.length && oldCheckinPreferences.secretToken.length) {
        FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeStore006,
                                 @"App reset detected. Will delete server registrations.");
        // We don't really need to delete old FCM tokens created via IID auth tokens since
        // those tokens are already hashed by APNS token as the has so creating a new
        // token should automatically delete the old-token.
        [self.delegate store:self didDeleteFCMScopedTokensForCheckin:oldCheckinPreferences];
      } else {
        FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeStore009,
                                 @"App reset detected but no valid checkin auth preferences found."
                                 @" Will not delete server registrations.");
      }
    }];
  }
}

#pragma mark - Get

- (FIRInstanceIDTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity
                                                    scope:(NSString *)scope {
  // TODO(chliangGoogle): If we don't have the token plist we should delete all the tokens from
  // the keychain. This is because not having the plist signifies a backup and restore operation.
  // In case the keychain has any tokens these would now be stale and therefore should be
  // deleted.
  if (![authorizedEntity length] || ![scope length]) {
    return nil;
  }
  FIRInstanceIDTokenInfo *info = [self.tokenStore tokenInfoWithAuthorizedEntity:authorizedEntity
                                                                          scope:scope];
  return info;
}

- (NSArray<FIRInstanceIDTokenInfo *> *)cachedTokenInfos {
  return [self.tokenStore cachedTokenInfos];
}

#pragma mark - Save

- (void)saveTokenInfo:(FIRInstanceIDTokenInfo *)tokenInfo
              handler:(void (^)(NSError *error))handler {
  [self.tokenStore saveTokenInfo:tokenInfo handler:handler];
}

#pragma mark - Delete

- (void)removeCachedTokenWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope {
  if (![authorizedEntity length] || ![scope length]) {
    FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeStore012,
                             @"Will not delete token with invalid entity: %@, scope: %@",
                             authorizedEntity, scope);
    return;
  }
  [self.tokenStore removeTokenWithAuthorizedEntity:authorizedEntity scope:scope];
}

- (void)removeAllCachedTokensWithHandler:(void (^)(NSError *error))handler {
  [self.tokenStore removeAllTokensWithHandler:handler];
}

#pragma mark - FIRInstanceIDCheckinCache protocol

- (void)saveCheckinPreferences:(FIRInstanceIDCheckinPreferences *)preferences
                       handler:(void (^)(NSError *error))handler {
  [self.checkinStore saveCheckinPreferences:preferences handler:handler];
}

- (FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences {
  return [self.checkinStore cachedCheckinPreferences];
}

- (void)removeCheckinPreferencesWithHandler:(void (^)(NSError *))handler {
  [self.checkinStore removeCheckinPreferencesWithHandler:^(NSError *error) {
    if (handler) {
      handler(error);
    }
  }];
}

@end
