diff --git a/native/data/sqlite-data-handler.js b/native/data/sqlite-data-handler.js index a0a4ed4e2..aaba49469 100644 --- a/native/data/sqlite-data-handler.js +++ b/native/data/sqlite-data-handler.js @@ -1,272 +1,277 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { MediaCacheContext } from 'lib/components/media-cache-provider.react.js'; import type { CallKeyserverEndpoint } from 'lib/keyserver-conn/keyserver-conn-types.js'; import { useKeyserverRecoveryLogIn } from 'lib/keyserver-conn/recovery-utils.js'; import { communityStoreOpsHandlers } from 'lib/ops/community-store-ops.js'; import { integrityStoreOpsHandlers } from 'lib/ops/integrity-store-ops.js'; import { keyserverStoreOpsHandlers } from 'lib/ops/keyserver-store-ops.js'; import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js'; +import { syncedMetadataStoreOpsHandlers } from 'lib/ops/synced-metadata-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { userStoreOpsHandlers } from 'lib/ops/user-store-ops.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import { recoveryActionSources, type RecoveryActionSource, } from 'lib/types/account-types.js'; import type { CallSingleKeyserverEndpoint } from 'lib/utils/call-single-keyserver-endpoint.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { supportingMultipleKeyservers } from 'lib/utils/services-utils.js'; import { resolveKeyserverSessionInvalidationUsingNativeCredentials } from '../account/legacy-recover-keyserver-session.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { filesystemMediaCache } from '../media/media-cache.js'; import { commCoreModule } from '../native-modules.js'; import { setStoreLoadedActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import Alert from '../utils/alert.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; async function clearSensitiveData() { await commCoreModule.clearSensitiveData(); try { await filesystemMediaCache.clearCache(); } catch { throw new Error('clear_media_cache_failed'); } } const returnsFalseSinceDoesntNeedToSupportCancellation = () => false; function SQLiteDataHandler(): React.Node { const storeLoaded = useSelector(state => state.storeLoaded); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated), ); const staffCanSee = useStaffCanSee(); const loggedIn = useSelector(isLoggedIn); const currentLoggedInUserID = useSelector(state => state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id, ); const mediaCacheContext = React.useContext(MediaCacheContext); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); const keyserverRecoveryLogIn = useKeyserverRecoveryLogIn( authoritativeKeyserverID, ); const recoverDataFromAuthoritativeKeyserver = React.useCallback( async (source: RecoveryActionSource) => { const innerRecoverDataFromAuthoritativeKeyserver = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, ) => resolveKeyserverSessionInvalidationUsingNativeCredentials( callSingleKeyserverEndpoint, callKeyserverEndpoint, dispatchActionPromise, source, authoritativeKeyserverID, getInitialNotificationsEncryptedMessage, returnsFalseSinceDoesntNeedToSupportCancellation, ); try { await keyserverRecoveryLogIn( source, innerRecoverDataFromAuthoritativeKeyserver, returnsFalseSinceDoesntNeedToSupportCancellation, ); dispatch({ type: setStoreLoadedActionType }); } catch (fetchCookieException) { if (staffCanSee) { Alert.alert( `Error fetching new cookie from native credentials: ${ getMessageForException(fetchCookieException) ?? '{no exception message}' }. Please kill the app.`, ); } else { commCoreModule.terminate(); } } }, [ dispatch, dispatchActionPromise, keyserverRecoveryLogIn, staffCanSee, getInitialNotificationsEncryptedMessage, ], ); const recoverData = React.useCallback( (source: RecoveryActionSource) => { if (supportingMultipleKeyservers) { invariant( false, 'recoverData in SQLiteDataHandler is not yet implemented when ' + 'supportingMultipleKeyservers is enabled. It should recover ' + 'from broken SQLite state by restoring from backup service', ); } return recoverDataFromAuthoritativeKeyserver(source); }, [recoverDataFromAuthoritativeKeyserver], ); const callClearSensitiveData = React.useCallback( async (triggeredBy: string) => { await clearSensitiveData(); console.log(`SQLite database deletion was triggered by ${triggeredBy}`); }, [], ); const handleSensitiveData = React.useCallback(async () => { try { const databaseCurrentUserInfoID = await commCoreModule.getCurrentUserID(); if ( databaseCurrentUserInfoID && databaseCurrentUserInfoID !== currentLoggedInUserID ) { await callClearSensitiveData('change in logged-in user credentials'); } if (currentLoggedInUserID) { await commCoreModule.setCurrentUserID(currentLoggedInUserID); } } catch (e) { if (isTaskCancelledError(e)) { return; } if (__DEV__) { throw e; } console.log(e); if (e.message !== 'clear_media_cache_failed') { commCoreModule.terminate(); } } }, [callClearSensitiveData, currentLoggedInUserID]); React.useEffect(() => { if (!rehydrateConcluded) { return; } const databaseNeedsDeletion = commCoreModule.checkIfDatabaseNeedsDeletion(); if (databaseNeedsDeletion) { void (async () => { try { await callClearSensitiveData('detecting corrupted database'); } catch (e) { if (__DEV__) { throw e; } console.log(e); if (e.message !== 'clear_media_cache_failed') { commCoreModule.terminate(); } } await recoverData(recoveryActionSources.corruptedDatabaseDeletion); })(); return; } const sensitiveDataHandled = handleSensitiveData(); if (storeLoaded) { return; } if (!loggedIn) { dispatch({ type: setStoreLoadedActionType }); return; } void (async () => { await Promise.all([ sensitiveDataHandled, mediaCacheContext?.evictCache(), ]); try { const { threads, messages, drafts, messageStoreThreads, reports, users, keyservers, communities, integrityThreadHashes, + syncedMetadata, } = await commCoreModule.getClientDBStore(); const threadInfosFromDB = threadStoreOpsHandlers.translateClientDBData(threads); const reportsFromDB = reportStoreOpsHandlers.translateClientDBData(reports); const usersFromDB = userStoreOpsHandlers.translateClientDBData(users); const keyserverInfosFromDB = keyserverStoreOpsHandlers.translateClientDBData(keyservers); const communityInfosFromDB = communityStoreOpsHandlers.translateClientDBData(communities); const threadHashesFromDB = integrityStoreOpsHandlers.translateClientDBData( integrityThreadHashes, ); + const syncedMetadataFromDB = + syncedMetadataStoreOpsHandlers.translateClientDBData(syncedMetadata); dispatch({ type: setClientDBStoreActionType, payload: { drafts, messages, threadStore: { threadInfos: threadInfosFromDB }, currentUserID: currentLoggedInUserID, messageStoreThreads, reports: reportsFromDB, users: usersFromDB, keyserverInfos: keyserverInfosFromDB, communities: communityInfosFromDB, threadHashes: threadHashesFromDB, + syncedMetadata: syncedMetadataFromDB, }, }); } catch (setStoreException) { if (isTaskCancelledError(setStoreException)) { dispatch({ type: setStoreLoadedActionType }); return; } if (staffCanSee) { Alert.alert( 'Error setting threadStore or messageStore', getMessageForException(setStoreException) ?? '{no exception message}', ); } await recoverData(recoveryActionSources.sqliteLoadFailure); } })(); }, [ currentLoggedInUserID, handleSensitiveData, loggedIn, dispatch, rehydrateConcluded, staffCanSee, storeLoaded, recoverData, callClearSensitiveData, mediaCacheContext, ]); return null; } export { SQLiteDataHandler, clearSensitiveData }; diff --git a/web/shared-worker/utils/store.js b/web/shared-worker/utils/store.js index 6e4a77064..f3de11e32 100644 --- a/web/shared-worker/utils/store.js +++ b/web/shared-worker/utils/store.js @@ -1,160 +1,168 @@ // @flow import { communityStoreOpsHandlers } from 'lib/ops/community-store-ops.js'; import { integrityStoreOpsHandlers } from 'lib/ops/integrity-store-ops.js'; import { keyserverStoreOpsHandlers } from 'lib/ops/keyserver-store-ops.js'; import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js'; import { syncedMetadataStoreOpsHandlers } from 'lib/ops/synced-metadata-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js'; import type { ClientStore, StoreOperations, } from 'lib/types/store-ops-types.js'; import { defaultWebState } from '../../redux/default-state.js'; import { workerRequestMessageTypes } from '../../types/worker-types.js'; import { getCommSharedWorker } from '../shared-worker-provider.js'; async function getClientDBStore(): Promise { const sharedWorker = await getCommSharedWorker(); let result: ClientStore = { currentUserID: null, drafts: [], messages: null, threadStore: null, messageStoreThreads: null, reports: null, users: null, keyserverInfos: defaultWebState.keyserverStore.keyserverInfos, communityInfos: null, threadHashes: null, syncedMetadata: null, }; const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); if (data?.store?.drafts) { result = { ...result, drafts: data.store.drafts, }; } if (data?.store?.reports) { result = { ...result, reports: reportStoreOpsHandlers.translateClientDBData(data.store.reports), }; } if (data?.store?.threads && data.store.threads.length > 0) { result = { ...result, threadStore: { threadInfos: threadStoreOpsHandlers.translateClientDBData( data.store.threads, ), }, }; } if (data?.store?.keyservers?.length) { result = { ...result, keyserverInfos: keyserverStoreOpsHandlers.translateClientDBData( data.store.keyservers, ), }; } if (data?.store?.communities) { result = { ...result, communityInfos: communityStoreOpsHandlers.translateClientDBData( data.store.communities, ), }; } if (data?.store?.integrityThreadHashes) { result = { ...result, threadHashes: integrityStoreOpsHandlers.translateClientDBData( data.store.integrityThreadHashes, ), }; } + if (data?.store?.syncedMetadata) { + result = { + ...result, + syncedMetadata: syncedMetadataStoreOpsHandlers.translateClientDBData( + data.store.syncedMetadata, + ), + }; + } return result; } async function processDBStoreOperations( storeOperations: StoreOperations, userID: null | string, ): Promise { const { draftStoreOperations, threadStoreOperations, reportStoreOperations, keyserverStoreOperations, communityStoreOperations, integrityStoreOperations, syncedMetadataStoreOperations, } = storeOperations; const canUseDatabase = canUseDatabaseOnWeb(userID); const convertedThreadStoreOperations = canUseDatabase ? threadStoreOpsHandlers.convertOpsToClientDBOps(threadStoreOperations) : []; const convertedReportStoreOperations = reportStoreOpsHandlers.convertOpsToClientDBOps(reportStoreOperations); const convertedKeyserverStoreOperations = keyserverStoreOpsHandlers.convertOpsToClientDBOps(keyserverStoreOperations); const convertedCommunityStoreOperations = communityStoreOpsHandlers.convertOpsToClientDBOps(communityStoreOperations); const convertedIntegrityStoreOperations = integrityStoreOpsHandlers.convertOpsToClientDBOps(integrityStoreOperations); const convertedSyncedMetadataStoreOperations = syncedMetadataStoreOpsHandlers.convertOpsToClientDBOps( syncedMetadataStoreOperations, ); if ( convertedThreadStoreOperations.length === 0 && convertedReportStoreOperations.length === 0 && draftStoreOperations.length === 0 && convertedKeyserverStoreOperations.length === 0 && convertedCommunityStoreOperations.length === 0 && convertedIntegrityStoreOperations.length === 0 && convertedSyncedMetadataStoreOperations.length === 0 ) { return; } const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return; } try { await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations, reportStoreOperations: convertedReportStoreOperations, threadStoreOperations: convertedThreadStoreOperations, keyserverStoreOperations: convertedKeyserverStoreOperations, communityStoreOperations: convertedCommunityStoreOperations, integrityStoreOperations: convertedIntegrityStoreOperations, syncedMetadataStoreOperations: convertedSyncedMetadataStoreOperations, }, }); } catch (e) { console.log(e); if (canUseDatabase) { window.alert(e.message); if (threadStoreOperations.length > 0) { await sharedWorker.init({ clearDatabase: true }); location.reload(); } } } } export { getClientDBStore, processDBStoreOperations };