/* * 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 #import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" #import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" /// Using macro for securely preprocessing string concatenation in query before runtime. #define RCNTableNameMain "main" #define RCNTableNameMainActive "main_active" #define RCNTableNameMainDefault "main_default" #define RCNTableNameMetadataDeprecated "fetch_metadata" #define RCNTableNameMetadata "fetch_metadata_v2" #define RCNTableNameInternalMetadata "internal_metadata" #define RCNTableNameExperiment "experiment" #define RCNTableNamePersonalization "personalization" static BOOL gIsNewDatabase; /// SQLite file name in versions 0, 1 and 2. static NSString *const RCNDatabaseName = @"RemoteConfig.sqlite3"; /// The storage sub-directory that the Remote Config database resides in. static NSString *const RCNRemoteConfigStorageSubDirectory = @"Google/RemoteConfig"; /// Remote Config database path for deprecated V0 version. static NSString *RemoteConfigPathForOldDatabaseV0() { NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *docPath = dirPaths.firstObject; return [docPath stringByAppendingPathComponent:RCNDatabaseName]; } /// Remote Config database path for current database. static NSString *RemoteConfigPathForDatabase(void) { #if TARGET_OS_TV NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); #else NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); #endif NSString *storageDirPath = dirPaths.firstObject; NSArray *components = @[ storageDirPath, RCNRemoteConfigStorageSubDirectory, RCNDatabaseName ]; return [NSString pathWithComponents:components]; } static BOOL RemoteConfigAddSkipBackupAttributeToItemAtPath(NSString *filePathString) { NSURL *URL = [NSURL fileURLWithPath:filePathString]; assert([[NSFileManager defaultManager] fileExistsAtPath:[URL path]]); NSError *error = nil; BOOL success = [URL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:&error]; if (!success) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000017", @"Error excluding %@ from backup %@.", [URL lastPathComponent], error); } return success; } static BOOL RemoteConfigCreateFilePathIfNotExist(NSString *filePath) { if (!filePath || !filePath.length) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000018", @"Failed to create subdirectory for an empty file path."); return NO; } NSFileManager *fileManager = [NSFileManager defaultManager]; if (![fileManager fileExistsAtPath:filePath]) { gIsNewDatabase = YES; NSError *error; [fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:&error]; if (error) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000019", @"Failed to create subdirectory for database file: %@.", error); return NO; } } return YES; } static NSArray *RemoteConfigMetadataTableColumnsInOrder() { return @[ RCNKeyBundleIdentifier, RCNKeyNamespace, RCNKeyFetchTime, RCNKeyDigestPerNamespace, RCNKeyDeviceContext, RCNKeyAppContext, RCNKeySuccessFetchTime, RCNKeyFailureFetchTime, RCNKeyLastFetchStatus, RCNKeyLastFetchError, RCNKeyLastApplyTime, RCNKeyLastSetDefaultsTime ]; } @interface RCNConfigDBManager () { /// Database storing all the config information. sqlite3 *_database; /// Serial queue for database read/write operations. dispatch_queue_t _databaseOperationQueue; } @end @implementation RCNConfigDBManager + (instancetype)sharedInstance { static dispatch_once_t onceToken; static RCNConfigDBManager *sharedInstance; dispatch_once(&onceToken, ^{ sharedInstance = [[RCNConfigDBManager alloc] init]; }); return sharedInstance; } /// Returns the current version of the Remote Config database. + (NSString *)remoteConfigPathForDatabase { return RemoteConfigPathForDatabase(); } - (instancetype)init { self = [super init]; if (self) { _databaseOperationQueue = dispatch_queue_create("com.google.GoogleConfigService.database", DISPATCH_QUEUE_SERIAL); [self createOrOpenDatabase]; } return self; } #pragma mark - database - (void)migrateV1NamespaceToV2Namespace { for (int table = 0; table < 3; table++) { NSString *tableName = @"" RCNTableNameMain; switch (table) { case 1: tableName = @"" RCNTableNameMainActive; break; case 2: tableName = @"" RCNTableNameMainDefault; break; default: break; } NSString *SQLString = [NSString stringWithFormat:@"SELECT namespace FROM %@ WHERE namespace NOT LIKE '%%:%%'", tableName]; const char *SQL = [SQLString UTF8String]; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return; } NSMutableArray *namespaceArray = [[NSMutableArray alloc] init]; while (sqlite3_step(statement) == SQLITE_ROW) { NSString *configNamespace = [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)]; [namespaceArray addObject:configNamespace]; } sqlite3_finalize(statement); // Update. for (NSString *namespaceToUpdate in namespaceArray) { NSString *newNamespace = [NSString stringWithFormat:@"%@:%@", namespaceToUpdate, kFIRDefaultAppName]; NSString *updateSQLString = [NSString stringWithFormat:@"UPDATE %@ SET namespace = ? WHERE namespace = ?", tableName]; const char *updateSQL = [updateSQLString UTF8String]; sqlite3_stmt *updateStatement = [self prepareSQL:updateSQL]; if (!updateStatement) { return; } NSArray *updateParams = @[ newNamespace, namespaceToUpdate ]; [self bindStringsToStatement:updateStatement stringArray:updateParams]; int result = sqlite3_step(updateStatement); if (result != SQLITE_DONE) { [self logErrorWithSQL:SQL finalizeStatement:updateStatement returnValue:NO]; return; } sqlite3_finalize(updateStatement); } } } - (void)createOrOpenDatabase { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { return; } NSString *oldV0DBPath = RemoteConfigPathForOldDatabaseV0(); // Backward Compatibility if ([[NSFileManager defaultManager] fileExistsAtPath:oldV0DBPath]) { FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000009", @"Old database V0 exists, removed it and replace with the new one."); [strongSelf removeDatabase:oldV0DBPath]; } NSString *dbPath = [RCNConfigDBManager remoteConfigPathForDatabase]; FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000062", @"Loading database at path %@", dbPath); const char *databasePath = dbPath.UTF8String; // Create or open database path. if (!RemoteConfigCreateFilePathIfNotExist(dbPath)) { return; } int flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION | SQLITE_OPEN_FULLMUTEX; if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) { // Always try to create table if not exists for backward compatibility. if (![strongSelf createTableSchema]) { // Remove database before fail. [strongSelf removeDatabase:dbPath]; FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010", @"Failed to create table."); // Create a new database if existing database file is corrupted. if (!RemoteConfigCreateFilePathIfNotExist(dbPath)) { return; } if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) { if (![strongSelf createTableSchema]) { // Remove database before fail. [strongSelf removeDatabase:dbPath]; // If it failed again, there's nothing we can do here. FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010", @"Failed to create table."); } else { // Exclude the app data used from iCloud backup. RemoteConfigAddSkipBackupAttributeToItemAtPath(dbPath); } } else { [strongSelf logDatabaseError]; } } else { // DB file already exists. Migrate any V1 namespace column entries to V2 fully qualified // 'namespace:FIRApp' entries. [self migrateV1NamespaceToV2Namespace]; // Exclude the app data used from iCloud backup. RemoteConfigAddSkipBackupAttributeToItemAtPath(dbPath); } } else { [strongSelf logDatabaseError]; } }); } - (BOOL)createTableSchema { RCN_MUST_NOT_BE_MAIN_THREAD(); static const char *createTableMain = "create TABLE IF NOT EXISTS " RCNTableNameMain " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)"; static const char *createTableMainActive = "create TABLE IF NOT EXISTS " RCNTableNameMainActive " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)"; static const char *createTableMainDefault = "create TABLE IF NOT EXISTS " RCNTableNameMainDefault " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)"; static const char *createTableMetadata = "create TABLE IF NOT EXISTS " RCNTableNameMetadata " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT," " fetch_time INTEGER, digest_per_ns BLOB, device_context BLOB, app_context BLOB, " "success_fetch_time BLOB, failure_fetch_time BLOB, last_fetch_status INTEGER, " "last_fetch_error INTEGER, last_apply_time INTEGER, last_set_defaults_time INTEGER)"; static const char *createTableInternalMetadata = "create TABLE IF NOT EXISTS " RCNTableNameInternalMetadata " (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)"; static const char *createTableExperiment = "create TABLE IF NOT EXISTS " RCNTableNameExperiment " (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)"; static const char *createTablePersonalization = "create TABLE IF NOT EXISTS " RCNTableNamePersonalization " (_id INTEGER PRIMARY KEY, key INTEGER, value BLOB)"; return [self executeQuery:createTableMain] && [self executeQuery:createTableMainActive] && [self executeQuery:createTableMainDefault] && [self executeQuery:createTableMetadata] && [self executeQuery:createTableInternalMetadata] && [self executeQuery:createTableExperiment] && [self executeQuery:createTablePersonalization]; } - (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path { __weak RCNConfigDBManager *weakSelf = self; dispatch_sync(_databaseOperationQueue, ^{ RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { return; } if (sqlite3_close(strongSelf->_database) != SQLITE_OK) { [self logDatabaseError]; } strongSelf->_database = nil; NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error; if (![fileManager removeItemAtPath:path error:&error]) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011", @"Failed to remove database at path %@ for error %@.", path, error); } }); } - (void)removeDatabase:(NSString *)path { if (sqlite3_close(_database) != SQLITE_OK) { [self logDatabaseError]; } _database = nil; NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error; if (![fileManager removeItemAtPath:path error:&error]) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011", @"Failed to remove database at path %@ for error %@.", path, error); } } #pragma mark - execute - (BOOL)executeQuery:(const char *)SQL { RCN_MUST_NOT_BE_MAIN_THREAD(); char *error; if (sqlite3_exec(_database, SQL, nil, nil, &error) != SQLITE_OK) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000012", @"Failed to execute query with error %s.", error); return NO; } return YES; } #pragma mark - insert - (void)insertMetadataTableWithValues:(NSDictionary *)columnNameToValue completionHandler:(RCNDBCompletion)handler { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ BOOL success = [weakSelf insertMetadataTableWithValues:columnNameToValue]; if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ handler(success, nil); }); } }); } - (BOOL)insertMetadataTableWithValues:(NSDictionary *)columnNameToValue { RCN_MUST_NOT_BE_MAIN_THREAD(); static const char *SQL = "INSERT INTO " RCNTableNameMetadata " (bundle_identifier, namespace, fetch_time, digest_per_ns, device_context, " "app_context, success_fetch_time, failure_fetch_time, last_fetch_status, " "last_fetch_error, last_apply_time, last_set_defaults_time) values (?, ?, ?, ?, ?, ?, " "?, ?, ?, ?, ?, ?)"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { [self logErrorWithSQL:SQL finalizeStatement:nil returnValue:NO]; return NO; } NSArray *columns = RemoteConfigMetadataTableColumnsInOrder(); int index = 0; for (NSString *columnName in columns) { if ([columnName isEqualToString:RCNKeyBundleIdentifier] || [columnName isEqualToString:RCNKeyNamespace]) { NSString *value = columnNameToValue[columnName]; if (![self bindStringToStatement:statement index:++index string:value]) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } } else if ([columnName isEqualToString:RCNKeyFetchTime] || [columnName isEqualToString:RCNKeyLastApplyTime] || [columnName isEqualToString:RCNKeyLastSetDefaultsTime]) { double value = [columnNameToValue[columnName] doubleValue]; if (sqlite3_bind_double(statement, ++index, value) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } } else if ([columnName isEqualToString:RCNKeyLastFetchStatus] || [columnName isEqualToString:RCNKeyLastFetchError]) { int value = [columnNameToValue[columnName] intValue]; if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } } else { NSData *data = columnNameToValue[columnName]; if (sqlite3_bind_blob(statement, ++index, data.bytes, (int)data.length, NULL) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } } } if (sqlite3_step(statement) != SQLITE_DONE) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } sqlite3_finalize(statement); return YES; } - (void)insertMainTableWithValues:(NSArray *)values fromSource:(RCNDBSource)source completionHandler:(RCNDBCompletion)handler { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ BOOL success = [weakSelf insertMainTableWithValues:values fromSource:source]; if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ handler(success, nil); }); } }); } - (BOOL)insertMainTableWithValues:(NSArray *)values fromSource:(RCNDBSource)source { RCN_MUST_NOT_BE_MAIN_THREAD(); if (values.count != 4) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000013", @"Failed to insert config record. Wrong number of give parameters, current " @"number is %ld, correct number is 4.", (long)values.count); return NO; } const char *SQL = "INSERT INTO " RCNTableNameMain " (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)"; if (source == RCNDBSourceDefault) { SQL = "INSERT INTO " RCNTableNameMainDefault " (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)"; } else if (source == RCNDBSourceActive) { SQL = "INSERT INTO " RCNTableNameMainActive " (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)"; } sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return NO; } NSString *aString = values[0]; if (![self bindStringToStatement:statement index:1 string:aString]) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } aString = values[1]; if (![self bindStringToStatement:statement index:2 string:aString]) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } aString = values[2]; if (![self bindStringToStatement:statement index:3 string:aString]) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } NSData *blobData = values[3]; if (sqlite3_bind_blob(statement, 4, blobData.bytes, (int)blobData.length, NULL) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } if (sqlite3_step(statement) != SQLITE_DONE) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } sqlite3_finalize(statement); return YES; } - (void)insertInternalMetadataTableWithValues:(NSArray *)values completionHandler:(RCNDBCompletion)handler { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ BOOL success = [weakSelf insertInternalMetadataWithValues:values]; if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ handler(success, nil); }); } }); } - (BOOL)insertInternalMetadataWithValues:(NSArray *)values { RCN_MUST_NOT_BE_MAIN_THREAD(); if (values.count != 2) { return NO; } const char *SQL = "INSERT OR REPLACE INTO " RCNTableNameInternalMetadata " (key, value) values (?, ?)"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return NO; } NSString *aString = values[0]; if (![self bindStringToStatement:statement index:1 string:aString]) { [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; return NO; } NSData *blobData = values[1]; if (sqlite3_bind_blob(statement, 2, blobData.bytes, (int)blobData.length, NULL) != SQLITE_OK) { [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; return NO; } if (sqlite3_step(statement) != SQLITE_DONE) { [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; return NO; } sqlite3_finalize(statement); return YES; } - (void)insertExperimentTableWithKey:(NSString *)key value:(NSData *)serializedValue completionHandler:(RCNDBCompletion)handler { dispatch_async(_databaseOperationQueue, ^{ BOOL success = [self insertExperimentTableWithKey:key value:serializedValue]; if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ handler(success, nil); }); } }); } - (BOOL)insertExperimentTableWithKey:(NSString *)key value:(NSData *)dataValue { if ([key isEqualToString:@RCNExperimentTableKeyMetadata]) { return [self updateExperimentMetadata:dataValue]; } RCN_MUST_NOT_BE_MAIN_THREAD(); const char *SQL = "INSERT INTO " RCNTableNameExperiment " (key, value) values (?, ?)"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return NO; } if (![self bindStringToStatement:statement index:1 string:key]) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } if (sqlite3_bind_blob(statement, 2, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } if (sqlite3_step(statement) != SQLITE_DONE) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } sqlite3_finalize(statement); return YES; } - (BOOL)updateExperimentMetadata:(NSData *)dataValue { RCN_MUST_NOT_BE_MAIN_THREAD(); const char *SQL = "INSERT OR REPLACE INTO " RCNTableNameExperiment " (_id, key, value) values ((SELECT _id from " RCNTableNameExperiment " WHERE key = ?), ?, ?)"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return NO; } if (![self bindStringToStatement:statement index:1 string:@RCNExperimentTableKeyMetadata]) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } if (![self bindStringToStatement:statement index:2 string:@RCNExperimentTableKeyMetadata]) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } if (sqlite3_bind_blob(statement, 3, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } if (sqlite3_step(statement) != SQLITE_DONE) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } sqlite3_finalize(statement); return YES; } - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)dataValue fromSource:(RCNDBSource)source { RCN_MUST_NOT_BE_MAIN_THREAD(); NSError *error; NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:dataValue options:NSJSONWritingPrettyPrinted error:&error]; if (!JSONPayload || error) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000075", @"Invalid Personalization payload to be serialized."); } const char *SQL = "INSERT OR REPLACE INTO " RCNTableNamePersonalization " (_id, key, value) values ((SELECT _id from " RCNTableNamePersonalization " WHERE key = ?), ?, ?)"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return NO; } if (sqlite3_bind_int(statement, 1, (int)source) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } if (sqlite3_bind_int(statement, 2, (int)source) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } if (sqlite3_bind_blob(statement, 3, JSONPayload.bytes, (int)JSONPayload.length, NULL) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } if (sqlite3_step(statement) != SQLITE_DONE) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } sqlite3_finalize(statement); return YES; } #pragma mark - update - (void)updateMetadataWithOption:(RCNUpdateOption)option namespace:(NSString *)namespace values:(NSArray *)values completionHandler:(RCNDBCompletion)handler { dispatch_async(_databaseOperationQueue, ^{ BOOL success = [self updateMetadataTableWithOption:option namespace:namespace andValues:values]; if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ handler(success, nil); }); } }); } - (BOOL)updateMetadataTableWithOption:(RCNUpdateOption)option namespace:(NSString *)namespace andValues:(NSArray *)values { RCN_MUST_NOT_BE_MAIN_THREAD(); static const char *SQL = "UPDATE " RCNTableNameMetadata " (last_fetch_status, last_fetch_error, last_apply_time, " "last_set_defaults_time) values (?, ?, ?, ?) WHERE namespace = ?"; if (option == RCNUpdateOptionFetchStatus) { SQL = "UPDATE " RCNTableNameMetadata " SET last_fetch_status = ?, last_fetch_error = ? WHERE namespace = ?"; } else if (option == RCNUpdateOptionApplyTime) { SQL = "UPDATE " RCNTableNameMetadata " SET last_apply_time = ? WHERE namespace = ?"; } else if (option == RCNUpdateOptionDefaultTime) { SQL = "UPDATE " RCNTableNameMetadata " SET last_set_defaults_time = ? WHERE namespace = ?"; } else { return NO; } sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return NO; } int index = 0; if ((option == RCNUpdateOptionApplyTime || option == RCNUpdateOptionDefaultTime) && values.count == 1) { double value = [values[0] doubleValue]; if (sqlite3_bind_double(statement, ++index, value) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } } else if (option == RCNUpdateOptionFetchStatus && values.count == 2) { int value = [values[0] intValue]; if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } value = [values[1] intValue]; if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } } // bind namespace to query if (sqlite3_bind_text(statement, ++index, [namespace UTF8String], -1, SQLITE_TRANSIENT) != SQLITE_OK) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } if (sqlite3_step(statement) != SQLITE_DONE) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } sqlite3_finalize(statement); return YES; } #pragma mark - read from DB - (NSDictionary *)loadMetadataWithBundleIdentifier:(NSString *)bundleIdentifier namespace:(NSString *)namespace { __block NSDictionary *metadataTableResult; __weak RCNConfigDBManager *weakSelf = self; dispatch_sync(_databaseOperationQueue, ^{ metadataTableResult = [weakSelf loadMetadataTableWithBundleIdentifier:bundleIdentifier namespace:namespace]; }); if (metadataTableResult) { return metadataTableResult; } return [[NSDictionary alloc] init]; } - (NSMutableDictionary *)loadMetadataTableWithBundleIdentifier:(NSString *)bundleIdentifier namespace:(NSString *)namespace { NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; const char *SQL = "SELECT bundle_identifier, fetch_time, digest_per_ns, device_context, app_context, " "success_fetch_time, failure_fetch_time , last_fetch_status, " "last_fetch_error, last_apply_time, last_set_defaults_time FROM " RCNTableNameMetadata " WHERE bundle_identifier = ? and namespace = ?"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return nil; } NSArray *params = @[ bundleIdentifier, namespace ]; [self bindStringsToStatement:statement stringArray:params]; while (sqlite3_step(statement) == SQLITE_ROW) { NSString *dbBundleIdentifier = [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)]; if (dbBundleIdentifier && ![dbBundleIdentifier isEqualToString:bundleIdentifier]) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000014", @"Load Metadata from table error: Wrong package name %@, should be %@.", dbBundleIdentifier, bundleIdentifier); return nil; } double fetchTime = sqlite3_column_double(statement, 1); NSData *digestPerNamespace = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 2) length:sqlite3_column_bytes(statement, 2)]; NSData *deviceContext = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 3) length:sqlite3_column_bytes(statement, 3)]; NSData *appContext = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 4) length:sqlite3_column_bytes(statement, 4)]; NSData *successTimeDigest = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 5) length:sqlite3_column_bytes(statement, 5)]; NSData *failureTimeDigest = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 6) length:sqlite3_column_bytes(statement, 6)]; int lastFetchStatus = sqlite3_column_int(statement, 7); int lastFetchFailReason = sqlite3_column_int(statement, 8); double lastApplyTimestamp = sqlite3_column_double(statement, 9); double lastSetDefaultsTimestamp = sqlite3_column_double(statement, 10); NSError *error; NSMutableDictionary *deviceContextDict = nil; if (deviceContext) { deviceContextDict = [NSJSONSerialization JSONObjectWithData:deviceContext options:NSJSONReadingMutableContainers error:&error]; } NSMutableDictionary *appContextDict = nil; if (appContext) { appContextDict = [NSJSONSerialization JSONObjectWithData:appContext options:NSJSONReadingMutableContainers error:&error]; } NSMutableDictionary *digestPerNamespaceDictionary = nil; if (digestPerNamespace) { digestPerNamespaceDictionary = [NSJSONSerialization JSONObjectWithData:digestPerNamespace options:NSJSONReadingMutableContainers error:&error]; } NSMutableArray *successTimes = nil; if (successTimeDigest) { successTimes = [NSJSONSerialization JSONObjectWithData:successTimeDigest options:NSJSONReadingMutableContainers error:&error]; } NSMutableArray *failureTimes = nil; if (failureTimeDigest) { failureTimes = [NSJSONSerialization JSONObjectWithData:failureTimeDigest options:NSJSONReadingMutableContainers error:&error]; } dict[RCNKeyBundleIdentifier] = bundleIdentifier; dict[RCNKeyFetchTime] = @(fetchTime); dict[RCNKeyDigestPerNamespace] = digestPerNamespaceDictionary; dict[RCNKeyDeviceContext] = deviceContextDict; dict[RCNKeyAppContext] = appContextDict; dict[RCNKeySuccessFetchTime] = successTimes; dict[RCNKeyFailureFetchTime] = failureTimes; dict[RCNKeyLastFetchStatus] = @(lastFetchStatus); dict[RCNKeyLastFetchError] = @(lastFetchFailReason); dict[RCNKeyLastApplyTime] = @(lastApplyTimestamp); dict[RCNKeyLastSetDefaultsTime] = @(lastSetDefaultsTimestamp); break; } sqlite3_finalize(statement); return dict; } - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { return; } NSMutableArray *experimentPayloads = [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyPayload]; if (!experimentPayloads) { experimentPayloads = [[NSMutableArray alloc] init]; } NSMutableDictionary *experimentMetadata; NSMutableArray *experiments = [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyMetadata]; // There should be only one entry for experiment metadata. if (experiments.count > 0) { NSError *error; experimentMetadata = [NSJSONSerialization JSONObjectWithData:experiments[0] options:NSJSONReadingMutableContainers error:&error]; } if (!experimentMetadata) { experimentMetadata = [[NSMutableDictionary alloc] init]; } if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ handler( YES, @{ @RCNExperimentTableKeyPayload : [experimentPayloads copy], @RCNExperimentTableKeyMetadata : [experimentMetadata copy] }); }); } }); } - (NSMutableArray *)loadExperimentTableFromKey:(NSString *)key { RCN_MUST_NOT_BE_MAIN_THREAD(); NSMutableArray *results = [[NSMutableArray alloc] init]; const char *SQL = "SELECT value FROM " RCNTableNameExperiment " WHERE key = ?"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return nil; } NSArray *params = @[ key ]; [self bindStringsToStatement:statement stringArray:params]; NSData *experimentData; while (sqlite3_step(statement) == SQLITE_ROW) { experimentData = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0) length:sqlite3_column_bytes(statement, 0)]; if (experimentData) { [results addObject:experimentData]; } } sqlite3_finalize(statement); return results; } - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ handler(NO, [NSMutableDictionary new], [NSMutableDictionary new], nil); }); return; } NSDictionary *activePersonalization; NSData *personalizationResult = [strongSelf loadPersonalizationTableFromKey:RCNDBSourceActive]; // There should be only one entry for Personalization metadata. if (personalizationResult) { NSError *error; activePersonalization = [NSJSONSerialization JSONObjectWithData:personalizationResult options:0 error:&error]; } if (!activePersonalization) { activePersonalization = [[NSMutableDictionary alloc] init]; } NSDictionary *fetchedPersonalization; personalizationResult = [strongSelf loadPersonalizationTableFromKey:RCNDBSourceFetched]; // There should be only one entry for Personalization metadata. if (personalizationResult) { NSError *error; fetchedPersonalization = [NSJSONSerialization JSONObjectWithData:personalizationResult options:0 error:&error]; } if (!fetchedPersonalization) { fetchedPersonalization = [[NSMutableDictionary alloc] init]; } if (handler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ handler(YES, fetchedPersonalization, activePersonalization, nil); }); } }); } - (NSData *)loadPersonalizationTableFromKey:(int)key { RCN_MUST_NOT_BE_MAIN_THREAD(); NSMutableArray *results = [[NSMutableArray alloc] init]; const char *SQL = "SELECT value FROM " RCNTableNamePersonalization " WHERE key = ?"; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return nil; } if (sqlite3_bind_int(statement, 1, key) != SQLITE_OK) { [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; return nil; } NSData *personalizationData; while (sqlite3_step(statement) == SQLITE_ROW) { personalizationData = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0) length:sqlite3_column_bytes(statement, 0)]; if (personalizationData) { [results addObject:personalizationData]; } } sqlite3_finalize(statement); // There should be only one entry in this table. if (results.count != 1) { return nil; } return results[0]; } - (NSDictionary *)loadInternalMetadataTable { __block NSMutableDictionary *internalMetadataTableResult; __weak RCNConfigDBManager *weakSelf = self; dispatch_sync(_databaseOperationQueue, ^{ internalMetadataTableResult = [weakSelf loadInternalMetadataTableInternal]; }); return internalMetadataTableResult; } - (NSMutableDictionary *)loadInternalMetadataTableInternal { NSMutableDictionary *internalMetadata = [[NSMutableDictionary alloc] init]; const char *SQL = "SELECT key, value FROM " RCNTableNameInternalMetadata; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return nil; } while (sqlite3_step(statement) == SQLITE_ROW) { NSString *key = [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)]; NSData *dataValue = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 1) length:sqlite3_column_bytes(statement, 1)]; internalMetadata[key] = dataValue; } sqlite3_finalize(statement); return internalMetadata; } /// This method is only meant to be called at init time. The underlying logic will need to be /// revaluated if the assumption changes at a later time. - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier completionHandler:(RCNDBLoadCompletion)handler { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ handler(NO, [NSDictionary new], [NSDictionary new], [NSDictionary new]); }); return; } __block NSDictionary *fetchedConfig = [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier fromSource:RCNDBSourceFetched]; __block NSDictionary *activeConfig = [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier fromSource:RCNDBSourceActive]; __block NSDictionary *defaultConfig = [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier fromSource:RCNDBSourceDefault]; if (handler) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ fetchedConfig = fetchedConfig ? fetchedConfig : [[NSDictionary alloc] init]; activeConfig = activeConfig ? activeConfig : [[NSDictionary alloc] init]; defaultConfig = defaultConfig ? defaultConfig : [[NSDictionary alloc] init]; handler(YES, fetchedConfig, activeConfig, defaultConfig); }); } }); } - (NSMutableDictionary *)loadMainTableWithBundleIdentifier:(NSString *)bundleIdentifier fromSource:(RCNDBSource)source { NSMutableDictionary *namespaceToConfig = [[NSMutableDictionary alloc] init]; const char *SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMain " WHERE bundle_identifier = ?"; if (source == RCNDBSourceDefault) { SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMainDefault " WHERE bundle_identifier = ?"; } else if (source == RCNDBSourceActive) { SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMainActive " WHERE bundle_identifier = ?"; } NSArray *params = @[ bundleIdentifier ]; sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return nil; } [self bindStringsToStatement:statement stringArray:params]; while (sqlite3_step(statement) == SQLITE_ROW) { NSString *configNamespace = [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 1)]; NSString *key = [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 2)]; NSData *value = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 3) length:sqlite3_column_bytes(statement, 3)]; if (!namespaceToConfig[configNamespace]) { namespaceToConfig[configNamespace] = [[NSMutableDictionary alloc] init]; } if (source == RCNDBSourceDefault) { namespaceToConfig[configNamespace][key] = [[FIRRemoteConfigValue alloc] initWithData:value source:FIRRemoteConfigSourceDefault]; } else { namespaceToConfig[configNamespace][key] = [[FIRRemoteConfigValue alloc] initWithData:value source:FIRRemoteConfigSourceRemote]; } } sqlite3_finalize(statement); return namespaceToConfig; } #pragma mark - delete - (void)deleteRecordFromMainTableWithNamespace:(NSString *)namespace_p bundleIdentifier:(NSString *)bundleIdentifier fromSource:(RCNDBSource)source { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { return; } NSArray *params = @[ bundleIdentifier, namespace_p ]; const char *SQL = "DELETE FROM " RCNTableNameMain " WHERE bundle_identifier = ? and namespace = ?"; if (source == RCNDBSourceDefault) { SQL = "DELETE FROM " RCNTableNameMainDefault " WHERE bundle_identifier = ? and namespace = ?"; } else if (source == RCNDBSourceActive) { SQL = "DELETE FROM " RCNTableNameMainActive " WHERE bundle_identifier = ? and namespace = ?"; } [strongSelf executeQuery:SQL withParams:params]; }); } - (void)deleteRecordWithBundleIdentifier:(NSString *)bundleIdentifier namespace:(NSString *)namespace isInternalDB:(BOOL)isInternalDB { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { return; } const char *SQL = "DELETE FROM " RCNTableNameInternalMetadata " WHERE key LIKE ?"; NSArray *params = @[ bundleIdentifier ]; if (!isInternalDB) { SQL = "DELETE FROM " RCNTableNameMetadata " WHERE bundle_identifier = ? and namespace = ?"; params = @[ bundleIdentifier, namespace ]; } [strongSelf executeQuery:SQL withParams:params]; }); } - (void)deleteAllRecordsFromTableWithSource:(RCNDBSource)source { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { return; } const char *SQL = "DELETE FROM " RCNTableNameMain; if (source == RCNDBSourceDefault) { SQL = "DELETE FROM " RCNTableNameMainDefault; } else if (source == RCNDBSourceActive) { SQL = "DELETE FROM " RCNTableNameMainActive; } [strongSelf executeQuery:SQL]; }); } - (void)deleteExperimentTableForKey:(NSString *)key { __weak RCNConfigDBManager *weakSelf = self; dispatch_async(_databaseOperationQueue, ^{ RCNConfigDBManager *strongSelf = weakSelf; if (!strongSelf) { return; } NSArray *params = @[ key ]; const char *SQL = "DELETE FROM " RCNTableNameExperiment " WHERE key = ?"; [strongSelf executeQuery:SQL withParams:params]; }); } #pragma mark - helper - (BOOL)executeQuery:(const char *)SQL withParams:(NSArray *)params { RCN_MUST_NOT_BE_MAIN_THREAD(); sqlite3_stmt *statement = [self prepareSQL:SQL]; if (!statement) { return NO; } [self bindStringsToStatement:statement stringArray:params]; if (sqlite3_step(statement) != SQLITE_DONE) { return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; } sqlite3_finalize(statement); return YES; } /// Params only accept TEXT format string. - (BOOL)bindStringsToStatement:(sqlite3_stmt *)statement stringArray:(NSArray *)array { int index = 1; for (NSString *param in array) { if (![self bindStringToStatement:statement index:index string:param]) { return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO]; } index++; } return YES; } - (BOOL)bindStringToStatement:(sqlite3_stmt *)statement index:(int)index string:(NSString *)value { if (sqlite3_bind_text(statement, index, [value UTF8String], -1, SQLITE_TRANSIENT) != SQLITE_OK) { return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO]; } return YES; } - (sqlite3_stmt *)prepareSQL:(const char *)SQL { sqlite3_stmt *statement = nil; if (sqlite3_prepare_v2(_database, SQL, -1, &statement, NULL) != SQLITE_OK) { [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; return nil; } return statement; } - (NSString *)errorMessage { return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]; } - (int)errorCode { return sqlite3_errcode(_database); } - (void)logDatabaseError { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000015", @"Error message: %@. Error code: %d.", [self errorMessage], [self errorCode]); } - (BOOL)logErrorWithSQL:(const char *)SQL finalizeStatement:(sqlite3_stmt *)statement returnValue:(BOOL)returnValue { if (SQL) { FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000016", @"Failed with SQL: %s.", SQL); } [self logDatabaseError]; if (statement) { sqlite3_finalize(statement); } return returnValue; } - (BOOL)isNewDatabase { return gIsNewDatabase; } @end