diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index bf88151eca..65ac211e15 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -48,6 +48,9 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) // Load data from other servers syncOtherServers(serverUrl); } + + verifyPushProxy(serverUrl); + return result; } @@ -95,8 +98,6 @@ async function restAppEntry(serverUrl: string, since = 0, isUpgrade = false) { const {config, license} = await getCommonSystemValues(database); await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined); - verifyPushProxy(serverUrl); - return {userId: currentUserId}; } diff --git a/app/database/manager/__mocks__/index.ts b/app/database/manager/__mocks__/index.ts index 1aa13afcd2..03085aa681 100644 --- a/app/database/manager/__mocks__/index.ts +++ b/app/database/manager/__mocks__/index.ts @@ -23,7 +23,7 @@ import {schema as appSchema} from '@database/schema/app'; import {serverSchema} from '@database/schema/server'; import {queryActiveServer, queryServer, queryServerByIdentifier} from '@queries/app/servers'; import {deleteIOSDatabase} from '@utils/mattermost_managed'; -import {hashCode} from '@utils/security'; +import {urlSafeBase64Encode} from '@utils/security'; import {removeProtocol} from '@utils/url'; import type {AppDatabase, CreateServerDatabaseArgs, Models, RegisterServerDatabaseArgs, ServerDatabase, ServerDatabases} from '@typings/database/database'; @@ -132,7 +132,7 @@ class DatabaseManager { private initServerDatabase = async (serverUrl: string): Promise => { await this.createServerDatabase({ config: { - dbName: hashCode(serverUrl), + dbName: urlSafeBase64Encode(serverUrl), dbType: DatabaseType.SERVER, serverUrl, }, @@ -306,7 +306,7 @@ class DatabaseManager { }; private deleteServerDatabaseFiles = async (serverUrl: string): Promise => { - const databaseName = hashCode(serverUrl); + const databaseName = urlSafeBase64Encode(serverUrl); if (Platform.OS === 'ios') { // On iOS, we'll delete the *.db file under the shared app-group/databases folder diff --git a/app/database/manager/index.ts b/app/database/manager/index.ts index d3c63ba8b7..7599a09f9e 100644 --- a/app/database/manager/index.ts +++ b/app/database/manager/index.ts @@ -23,10 +23,11 @@ import ServerDataOperator from '@database/operator/server_data_operator'; import {schema as appSchema} from '@database/schema/app'; import {serverSchema} from '@database/schema/server'; import {queryActiveServer, queryServer, queryServerByIdentifier} from '@queries/app/servers'; +import {deleteLegacyFileCache} from '@utils/file'; import {emptyFunction} from '@utils/general'; import {logDebug, logError} from '@utils/log'; -import {deleteIOSDatabase, getIOSAppGroupDetails} from '@utils/mattermost_managed'; -import {hashCode} from '@utils/security'; +import {deleteIOSDatabase, getIOSAppGroupDetails, renameIOSDatabase} from '@utils/mattermost_managed'; +import {hashCode_DEPRECATED, urlSafeBase64Encode} from '@utils/security'; import {removeProtocol} from '@utils/url'; import type {AppDatabase, CreateServerDatabaseArgs, RegisterServerDatabaseArgs, Models, ServerDatabase, ServerDatabases} from '@typings/database/database'; @@ -126,7 +127,13 @@ class DatabaseManager { if (serverUrl) { try { - const databaseName = hashCode(serverUrl); + const databaseName = urlSafeBase64Encode(serverUrl); + const oldDatabaseName = hashCode_DEPRECATED(serverUrl); + + // Remove any legacy database we may already have. + await this.renameDatabase(oldDatabaseName, databaseName); + deleteLegacyFileCache(serverUrl); + const databaseFilePath = this.getDatabaseFilePath(databaseName); const migrations = ServerDatabaseMigrations; const modelClasses = this.serverModels; @@ -192,9 +199,9 @@ class DatabaseManager { try { const appDatabase = this.appDatabase?.database; if (appDatabase) { - const isServerPresent = await this.isServerPresent(serverUrl); + const serverModel = await queryServer(appDatabase, serverUrl); - if (!isServerPresent) { + if (!serverModel) { await appDatabase.write(async () => { const serversCollection = appDatabase.collections.get(SERVERS); await serversCollection.create((server: ServersModel) => { @@ -205,6 +212,12 @@ class DatabaseManager { server.lastActiveAt = 0; }); }); + } else if (serverModel.dbPath !== databaseFilePath) { + await appDatabase.write(async () => { + await serverModel.update((s) => { + s.dbPath = databaseFilePath; + }); + }); } else if (identifier) { await this.updateServerIdentifier(serverUrl, identifier, displayName); } @@ -412,8 +425,16 @@ class DatabaseManager { * @returns {Promise} */ private deleteServerDatabaseFiles = async (serverUrl: string): Promise => { - const databaseName = hashCode(serverUrl); + const databaseName = urlSafeBase64Encode(serverUrl); + this.deleteServerDatabaseFilesByName(databaseName); + }; + /** + * deleteServerDatabaseFilesByName: Removes the *.db file from the App-Group directory for iOS or the files directory for Android, given the database name + * @param {string} databaseName + * @returns {Promise} + */ + private deleteServerDatabaseFilesByName = async (databaseName: string): Promise => { if (Platform.OS === 'ios') { // On iOS, we'll delete the *.db file under the shared app-group/databases folder deleteIOSDatabase({databaseName}); @@ -431,6 +452,47 @@ class DatabaseManager { FileSystem.unlink(databaseWal).catch(emptyFunction); }; + /** + * deleteServerDatabaseFilesByName: Removes the *.db file from the App-Group directory for iOS or the files directory for Android, given the database name + * @param {string} databaseName + * @returns {Promise} + */ + private renameDatabase = async (databaseName: string, newDBName: string): Promise => { + if (Platform.OS === 'ios') { + // On iOS, we'll move the *.db file under the shared app-group/databases folder + renameIOSDatabase(databaseName, newDBName); + return; + } + + // On Android, we'll move the *.db, the *.db-shm and *.db-wal files + const androidFilesDir = this.databaseDirectory; + const databaseFile = `${androidFilesDir}${databaseName}.db`; + const databaseShm = `${androidFilesDir}${databaseName}.db-shm`; + const databaseWal = `${androidFilesDir}${databaseName}.db-wal`; + + const newDatabaseFile = `${androidFilesDir}${newDBName}.db`; + const newDatabaseShm = `${androidFilesDir}${newDBName}.db-shm`; + const newDatabaseWal = `${androidFilesDir}${newDBName}.db-wal`; + + if (await FileSystem.exists(newDatabaseFile)) { + // Already renamed, do not try + return; + } + + if (!await FileSystem.exists(databaseFile)) { + // Nothing to rename, do not try + return; + } + + try { + await FileSystem.moveFile(databaseFile, newDatabaseFile); + await FileSystem.moveFile(databaseShm, newDatabaseShm); + await FileSystem.moveFile(databaseWal, newDatabaseWal); + } catch (error) { + // Do nothing + } + }; + /** * factoryReset: Removes the databases directory and all its contents on the respective platform * @param {boolean} shouldRemoveDirectory @@ -497,7 +559,7 @@ class DatabaseManager { * @returns {string} */ private getDatabaseFilePath = (dbName: string): string => { - return Platform.OS === 'ios' ? `${this.databaseDirectory}/${dbName}.db` : `${this.databaseDirectory}/${dbName}.db`; + return Platform.OS === 'ios' ? `${this.databaseDirectory}/${dbName}.db` : `${this.databaseDirectory}${dbName}.db`; }; /** diff --git a/app/utils/file/index.ts b/app/utils/file/index.ts index 6406adf314..405aa6f69e 100644 --- a/app/utils/file/index.ts +++ b/app/utils/file/index.ts @@ -18,7 +18,7 @@ import {generateId} from '@utils/general'; import keyMirror from '@utils/key_mirror'; import {logError} from '@utils/log'; import {deleteEntititesFile, getIOSAppGroupDetails} from '@utils/mattermost_managed'; -import {hashCode} from '@utils/security'; +import {hashCode_DEPRECATED, urlSafeBase64Encode} from '@utils/security'; import type FileModel from '@typings/database/models/servers/file'; @@ -165,8 +165,17 @@ export async function deleteV1Data() { } export async function deleteFileCache(serverUrl: string) { - const serverDir = hashCode(serverUrl); - const cacheDir = `${FileSystem.CachesDirectoryPath}/${serverDir}`; + const serverDir = urlSafeBase64Encode(serverUrl); + deleteFileCacheByDir(serverDir); +} + +export async function deleteLegacyFileCache(serverUrl: string) { + const serverDir = hashCode_DEPRECATED(serverUrl); + deleteFileCacheByDir(serverDir); +} + +async function deleteFileCacheByDir(dir: string) { + const cacheDir = `${FileSystem.CachesDirectoryPath}/${dir}`; if (cacheDir) { const cacheDirInfo = await FileSystem.exists(cacheDir); if (cacheDirInfo) { @@ -340,9 +349,10 @@ export function getFileType(file: FileInfo): string { } export function getLocalFilePathFromFile(serverUrl: string, file: FileInfo | FileModel) { + const fileIdPath = file.id?.replace(/[^0-9a-z]/g, ''); if (serverUrl) { - const server = hashCode(serverUrl); - if (file?.name) { + const server = urlSafeBase64Encode(serverUrl); + if (file?.name && !file.name.includes('/')) { let extension: string | undefined = file.extension; let filename = file.name; @@ -362,9 +372,9 @@ export function getLocalFilePathFromFile(serverUrl: string, file: FileInfo | Fil } } - return `${FileSystem.CachesDirectoryPath}/${server}/${filename}-${hashCode(file.id!)}.${extension}`; + return `${FileSystem.CachesDirectoryPath}/${server}/${filename}-${fileIdPath}.${extension}`; } else if (file?.id && file?.extension) { - return `${FileSystem.CachesDirectoryPath}/${server}/${file.id}.${file.extension}`; + return `${FileSystem.CachesDirectoryPath}/${server}/${fileIdPath}.${file.extension}`; } } @@ -504,8 +514,9 @@ export const getAllFilesInCachesDirectory = async (serverUrl: string) => { try { const files: FileSystem.ReadDirItem[][] = []; - const directoryFiles = await FileSystem.readDir(`${FileSystem.CachesDirectoryPath}/${hashCode(serverUrl)}`); + const directoryFiles = await FileSystem.readDir(`${FileSystem.CachesDirectoryPath}/${urlSafeBase64Encode(serverUrl)}`); files.push(directoryFiles); + const flattenedFiles = files.flat(); const totalSize = flattenedFiles.reduce((acc, file) => acc + file.size, 0); return { diff --git a/app/utils/mattermost_managed.ts b/app/utils/mattermost_managed.ts index 781b5ace8c..c419930891 100644 --- a/app/utils/mattermost_managed.ts +++ b/app/utils/mattermost_managed.ts @@ -44,6 +44,15 @@ export const deleteIOSDatabase = ({ MattermostManaged.deleteDatabaseDirectory(databaseName, shouldRemoveDirectory, () => null); }; +/** + * renameIOSDatabase renames the .db and any other related file to the new name. + * @param {string} from original database name + * @param {string} to new database name + */ +export const renameIOSDatabase = (from: string, to: string) => { + MattermostManaged.renameDatabase(from, to, () => null); +}; + export const deleteEntititesFile = (callback?: (success: boolean) => void) => { if (Platform.OS === 'ios') { MattermostManaged.deleteEntititesFile((result: boolean) => { diff --git a/app/utils/security.ts b/app/utils/security.ts index f90874a84d..03a32bb1a4 100644 --- a/app/utils/security.ts +++ b/app/utils/security.ts @@ -2,13 +2,15 @@ // See LICENSE.txt for license information. import CookieManager from '@react-native-cookies/cookies'; +import base64 from 'base-64'; export async function getCSRFFromCookie(url: string) { const cookies = await CookieManager.get(url, false); return cookies.MMCSRF?.value; } -export const hashCode = (str: string): string => { +// This has been deprecated and is only used for migrations +export const hashCode_DEPRECATED = (str: string): string => { let hash = 0; let i; let chr; @@ -23,3 +25,7 @@ export const hashCode = (str: string): string => { } return hash.toString(); }; + +export const urlSafeBase64Encode = (str: string): string => { + return base64.encode(str).replace(/\+/g, '-').replace(/\//g, '_'); +}; diff --git a/ios/Mattermost/MattermostManaged.m b/ios/Mattermost/MattermostManaged.m index a5493ef462..d3edc7f324 100644 --- a/ios/Mattermost/MattermostManaged.m +++ b/ios/Mattermost/MattermostManaged.m @@ -95,6 +95,48 @@ RCT_EXPORT_METHOD(deleteDatabaseDirectory: (NSString *)databaseName shouldRemov } } +RCT_EXPORT_METHOD(renameDatabase: (NSString *)databaseName to: (NSString *) newDBName callback: (RCTResponseSenderBlock)callback){ + @try { + NSDictionary *appGroupDir = [self appGroupSharedDirectory]; + NSString *databaseDir; + NSString *newDBDir; + + + if(databaseName){ + databaseDir = [NSString stringWithFormat:@"%@/%@%@", appGroupDir[@"databasePath"], databaseName , @".db"]; + } + + if (newDBName){ + newDBDir = [NSString stringWithFormat:@"%@/%@%@", appGroupDir[@"databasePath"], newDBName , @".db"]; + } + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error = nil; + + BOOL destinationHasFile = [fileManager fileExistsAtPath:newDBDir]; + + if (!destinationHasFile && [fileManager fileExistsAtPath:[NSString stringWithFormat:@"%@-wal", databaseDir]]) { + [fileManager moveItemAtPath:[NSString stringWithFormat:@"%@-wal", databaseDir] toPath:[NSString stringWithFormat:@"%@-wal", newDBDir] error:nil]; + } + + if (!destinationHasFile && [fileManager fileExistsAtPath:[NSString stringWithFormat:@"%@-shm", databaseDir]]) { + [fileManager moveItemAtPath:[NSString stringWithFormat:@"%@-shm", databaseDir] toPath:[NSString stringWithFormat:@"%@-shm", newDBDir] error:nil]; + } + + BOOL successCode = destinationHasFile; + if (!destinationHasFile && [fileManager fileExistsAtPath:databaseDir]){ + successCode = [fileManager moveItemAtPath:databaseDir toPath: newDBDir error:&error]; + } + NSNumber *success= [NSNumber numberWithBool:successCode]; + + callback(@[(error ?: [NSNull null]), success]); + } + @catch (NSException *exception) { + NSLog(@"%@", exception.reason); + callback(@[exception.reason, @NO]); + } +} + RCT_EXPORT_METHOD(deleteEntititesFile: (RCTResponseSenderBlock) callback) { @try { NSDictionary *appGroupDir = [self appGroupSharedDirectory];