From 7851e12e9e9c52dba36ad2c2f3c362a07b95288b Mon Sep 17 00:00:00 2001 From: Mo Date: Thu, 30 Jun 2022 09:20:00 -0500 Subject: [PATCH] fix: clear desktop web cache on first version launch (#1184) --- packages/desktop/.eslintrc | 2 +- packages/desktop/app/AppState.ts | 52 +++++ packages/desktop/app/application.ts | 60 ++---- packages/desktop/app/index.ts | 3 +- .../Main/Backups/BackupsManager.ts | 4 +- .../app/javascripts/Main/ExtensionsServer.ts | 4 +- .../Main/FileBackups/FileBackupsManager.ts | 4 +- .../app/javascripts/Main/Keychain/Keychain.ts | 3 +- .../Main/Keychain/KeychainInterface.ts | 2 +- .../app/javascripts/Main/Menus/Menus.ts | 5 +- .../Main/Packages/PackageManager.ts | 11 +- .../javascripts/Main/Remote/RemoteBridge.ts | 3 +- .../javascripts/Main/SpellcheckerManager.ts | 3 +- .../desktop/app/javascripts/Main/Store.ts | 185 ------------------ .../app/javascripts/Main/Store/Store.ts | 52 +++++ .../app/javascripts/Main/Store/StoreKeys.ts | 33 ++++ .../Main/Store/createSanitizedStoreData.ts | 101 ++++++++++ .../app/javascripts/Main/TrayManager.ts | 3 +- .../app/javascripts/Main/UpdateManager.ts | 4 +- .../desktop/app/javascripts/Main/Window.ts | 5 +- .../app/javascripts/Main/ZoomManager.ts | 3 +- 21 files changed, 286 insertions(+), 256 deletions(-) create mode 100644 packages/desktop/app/AppState.ts delete mode 100644 packages/desktop/app/javascripts/Main/Store.ts create mode 100644 packages/desktop/app/javascripts/Main/Store/Store.ts create mode 100644 packages/desktop/app/javascripts/Main/Store/StoreKeys.ts create mode 100644 packages/desktop/app/javascripts/Main/Store/createSanitizedStoreData.ts diff --git a/packages/desktop/.eslintrc b/packages/desktop/.eslintrc index cd1388445..726947f29 100644 --- a/packages/desktop/.eslintrc +++ b/packages/desktop/.eslintrc @@ -5,7 +5,7 @@ }, "extends": ["../../.eslintrc.json"], "rules": { - "no-console": "warn", + "no-console": "off", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-var-requires": "off" }, diff --git a/packages/desktop/app/AppState.ts b/packages/desktop/app/AppState.ts new file mode 100644 index 000000000..a3386042f --- /dev/null +++ b/packages/desktop/app/AppState.ts @@ -0,0 +1,52 @@ +import { action, makeObservable, observable } from 'mobx' +import { MessageType } from '../test/TestIpcMessage' +import { Store } from './javascripts/Main/Store/Store' +import { StoreKeys } from './javascripts/Main/Store/StoreKeys' +import { Paths, Urls } from './javascripts/Main/Types/Paths' +import { UpdateState } from './javascripts/Main/UpdateManager' +import { handleTestMessage } from './javascripts/Main/Utils/Testing' +import { isTesting } from './javascripts/Main/Utils/Utils' +import { WindowState } from './javascripts/Main/Window' + +export class AppState { + readonly version: string + readonly store: Store + readonly startUrl = Urls.indexHtml + readonly isPrimaryInstance: boolean + public willQuitApp = false + public lastBackupDate: number | null = null + public windowState?: WindowState + public deepLinkUrl?: string + public readonly updates: UpdateState + public lastRunVersion: string + + constructor(app: Electron.App) { + this.version = app.getVersion() + this.store = new Store(Paths.userDataDir) + this.isPrimaryInstance = app.requestSingleInstanceLock() + + this.lastRunVersion = this.store.get(StoreKeys.LastRunVersion) || 'unknown' + this.store.set(StoreKeys.LastRunVersion, this.version) + + makeObservable(this, { + lastBackupDate: observable, + setBackupCreationDate: action, + }) + + this.updates = new UpdateState(this) + + if (isTesting()) { + handleTestMessage(MessageType.AppStateCall, (method, ...args) => { + ;(this as any)[method](...args) + }) + } + } + + public isRunningVersionForFirstTime(): boolean { + return this.lastRunVersion !== this.version + } + + setBackupCreationDate(date: number | null): void { + this.lastBackupDate = date + } +} diff --git a/packages/desktop/app/application.ts b/packages/desktop/app/application.ts index 46ce59719..0c5693d1e 100644 --- a/packages/desktop/app/application.ts +++ b/packages/desktop/app/application.ts @@ -1,52 +1,15 @@ import { App, Shell } from 'electron' -import { action, makeObservable, observable } from 'mobx' -import { MessageType } from '../test/TestIpcMessage' +import { AppState } from './AppState' import { createExtensionsServer } from './javascripts/Main/ExtensionsServer' import { Keychain } from './javascripts/Main/Keychain/Keychain' -import { Store, StoreKeys } from './javascripts/Main/Store' +import { StoreKeys } from './javascripts/Main/Store/StoreKeys' import { AppName, initializeStrings } from './javascripts/Main/Strings' -import { Paths, Urls } from './javascripts/Main/Types/Paths' import { isLinux, isMac, isWindows } from './javascripts/Main/Types/Platforms' -import { UpdateState } from './javascripts/Main/UpdateManager' -import { handleTestMessage } from './javascripts/Main/Utils/Testing' -import { isDev, isTesting } from './javascripts/Main/Utils/Utils' -import { createWindowState, WindowState } from './javascripts/Main/Window' +import { isDev } from './javascripts/Main/Utils/Utils' +import { createWindowState } from './javascripts/Main/Window' const deepLinkScheme = 'standardnotes' -export class AppState { - readonly version: string - readonly store: Store - readonly startUrl = Urls.indexHtml - readonly isPrimaryInstance: boolean - public willQuitApp = false - public lastBackupDate: number | null = null - public windowState?: WindowState - public deepLinkUrl?: string - public readonly updates: UpdateState - - constructor(app: Electron.App) { - this.version = app.getVersion() - this.store = new Store(Paths.userDataDir) - this.isPrimaryInstance = app.requestSingleInstanceLock() - makeObservable(this, { - lastBackupDate: observable, - setBackupCreationDate: action, - }) - this.updates = new UpdateState(this) - - if (isTesting()) { - handleTestMessage(MessageType.AppStateCall, (method, ...args) => { - ;(this as any)[method](...args) - }) - } - } - - setBackupCreationDate(date: number | null): void { - this.lastBackupDate = date - } -} - export function initializeApplication(args: { app: Electron.App; ipcMain: Electron.IpcMain; shell: Shell }): void { const { app } = args @@ -146,7 +109,6 @@ async function finishApplicationInitialization({ app, shell, state }: { app: App const keychainWindow = await Keychain.ensureKeychainAccess(state.store) initializeStrings(app.getLocale()) - initializeExtensionsServer(state.store) const windowState = await createWindowState({ shell, @@ -157,6 +119,14 @@ async function finishApplicationInitialization({ app, shell, state }: { app: App }, }) + if (state.isRunningVersionForFirstTime()) { + console.log('Clearing window cache') + await windowState.window.webContents.session.clearCache() + } + + const host = createExtensionsServer() + state.store.set(StoreKeys.ExtServerHost, host) + /** * Close the keychain window after the main window is created, otherwise the * app will quit automatically @@ -171,9 +141,3 @@ async function finishApplicationInitialization({ app, shell, state }: { app: App void windowState.window.loadURL(state.startUrl) } - -function initializeExtensionsServer(store: Store) { - const host = createExtensionsServer() - - store.set(StoreKeys.ExtServerHost, host) -} diff --git a/packages/desktop/app/index.ts b/packages/desktop/app/index.ts index f62c03cc4..574aeddd4 100644 --- a/packages/desktop/app/index.ts +++ b/packages/desktop/app/index.ts @@ -5,7 +5,8 @@ import path from 'path' import './@types/modules' import { initializeApplication } from './application' import { enableExperimentalFeaturesForFileAccessFix } from './enableExperimentalWebFeatures' -import { Store, StoreKeys } from './javascripts/Main/Store' +import { Store } from './javascripts/Main/Store/Store' +import { StoreKeys } from './javascripts/Main/Store/StoreKeys' import { isSnap } from './javascripts/Main/Types/Constants' import { Paths } from './javascripts/Main/Types/Paths' import { setupTesting } from './javascripts/Main/Utils/Testing' diff --git a/packages/desktop/app/javascripts/Main/Backups/BackupsManager.ts b/packages/desktop/app/javascripts/Main/Backups/BackupsManager.ts index 72c770baa..2bcd863f9 100644 --- a/packages/desktop/app/javascripts/Main/Backups/BackupsManager.ts +++ b/packages/desktop/app/javascripts/Main/Backups/BackupsManager.ts @@ -2,9 +2,9 @@ import { dialog, shell, WebContents } from 'electron' import { promises as fs } from 'fs' import path from 'path' import { AppMessageType, MessageType } from '../../../../test/TestIpcMessage' -import { AppState } from '../../../application' +import { AppState } from '../../../AppState' import { MessageToWebApp } from '../../Shared/IpcMessages' -import { StoreKeys } from '../Store' +import { StoreKeys } from '../Store/StoreKeys' import { backups as str } from '../Strings' import { Paths } from '../Types/Paths' import { diff --git a/packages/desktop/app/javascripts/Main/ExtensionsServer.ts b/packages/desktop/app/javascripts/Main/ExtensionsServer.ts index 9525258b1..6b4844d6f 100644 --- a/packages/desktop/app/javascripts/Main/ExtensionsServer.ts +++ b/packages/desktop/app/javascripts/Main/ExtensionsServer.ts @@ -6,6 +6,7 @@ import { URL } from 'url' import { extensions as str } from './Strings' import { Paths } from './Types/Paths' import { FileDoesNotExist } from './Utils/FileUtils' +import { app } from 'electron' const Protocol = 'http' @@ -61,7 +62,8 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse) const mimeType = mime.lookup(path.parse(filePath).ext) response.setHeader('Access-Control-Allow-Origin', '*') - response.setHeader('Cache-Control', 'max-age=604800') + response.setHeader('Cache-Control', 'no-cache') + response.setHeader('ETag', app.getVersion()) response.setHeader('Content-Type', `${mimeType}; charset=utf-8`) const data = fs.readFileSync(filePath) diff --git a/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts b/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts index 7f98363f5..31d9656ff 100644 --- a/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts +++ b/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts @@ -1,7 +1,7 @@ import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports' -import { AppState } from 'app/application' +import { AppState } from 'app/AppState' import { shell } from 'electron' -import { StoreKeys } from '../Store' +import { StoreKeys } from '../Store/StoreKeys' import path from 'path' import { deleteFile, diff --git a/packages/desktop/app/javascripts/Main/Keychain/Keychain.ts b/packages/desktop/app/javascripts/Main/Keychain/Keychain.ts index 2a6597724..c1289b906 100644 --- a/packages/desktop/app/javascripts/Main/Keychain/Keychain.ts +++ b/packages/desktop/app/javascripts/Main/Keychain/Keychain.ts @@ -1,7 +1,8 @@ import { app, BrowserWindow, ipcMain } from 'electron' import keytar from 'keytar' import { MessageToMainProcess } from '../../Shared/IpcMessages' -import { Store, StoreKeys } from '../Store' +import { Store } from '../Store/Store' +import { StoreKeys } from '../Store/StoreKeys' import { AppName } from '../Strings' import { keychainAccessIsUserConfigurable } from '../Types/Constants' import { Paths, Urls } from '../Types/Paths' diff --git a/packages/desktop/app/javascripts/Main/Keychain/KeychainInterface.ts b/packages/desktop/app/javascripts/Main/Keychain/KeychainInterface.ts index 6ff11e362..0b3ccd086 100644 --- a/packages/desktop/app/javascripts/Main/Keychain/KeychainInterface.ts +++ b/packages/desktop/app/javascripts/Main/Keychain/KeychainInterface.ts @@ -1,5 +1,5 @@ import { BrowserWindow } from 'electron' -import { Store } from '../Store' +import { Store } from '../Store/Store' export interface KeychainInterface { ensureKeychainAccess(store: Store): Promise diff --git a/packages/desktop/app/javascripts/Main/Menus/Menus.ts b/packages/desktop/app/javascripts/Main/Menus/Menus.ts index 474428712..d68384f33 100644 --- a/packages/desktop/app/javascripts/Main/Menus/Menus.ts +++ b/packages/desktop/app/javascripts/Main/Menus/Menus.ts @@ -1,4 +1,4 @@ -import { AppState } from 'app/application' +import { AppState } from 'app/AppState' import { app, BrowserWindow, @@ -10,7 +10,8 @@ import { WebContents, } from 'electron' import { autorun } from 'mobx' -import { Store, StoreKeys } from '../Store' +import { Store } from '../Store/Store' +import { StoreKeys } from '../Store/StoreKeys' import { appMenu as str, contextMenu } from '../Strings' import { TrayManager } from '../TrayManager' import { autoUpdatingAvailable } from '../Types/Constants' diff --git a/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts b/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts index 900b55f0b..11265b932 100644 --- a/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts +++ b/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts @@ -19,11 +19,11 @@ import { downloadFile, getJSON } from './Networking' import { Component, MappingFile, PackageInfo, PackageManagerInterface, SyncTask } from './PackageManagerInterface' function logMessage(...message: any) { - log.info('PackageManager:', ...message) + log.info('PackageManager[Info]:', ...message) } function logError(...message: any) { - console.error('PackageManager:', ...message) + console.error('PackageManager[Error]:', ...message) } /** @@ -357,10 +357,13 @@ async function installComponent( function pathsForComponent(component: Pick) { const relativePath = path.join(Paths.extensionsDirRelative, component.content!.package_info.identifier) + const absolutePath = path.join(Paths.userDataDir, relativePath) + const downloadPath = path.join(Paths.tempDir, AppName, 'downloads', component.content!.name + '.zip') + return { relativePath, - absolutePath: path.join(Paths.userDataDir, relativePath), - downloadPath: path.join(Paths.tempDir, AppName, 'downloads', component.content!.name + '.zip'), + absolutePath, + downloadPath, } } diff --git a/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts b/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts index b93396a15..1eb6cc9da 100644 --- a/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts +++ b/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts @@ -1,5 +1,6 @@ import { CrossProcessBridge } from '../../Renderer/CrossProcessBridge' -import { Store, StoreKeys } from '../Store' +import { Store } from '../Store/Store' +import { StoreKeys } from '../Store/StoreKeys' const path = require('path') const rendererPath = path.join('file://', __dirname, '/renderer.js') diff --git a/packages/desktop/app/javascripts/Main/SpellcheckerManager.ts b/packages/desktop/app/javascripts/Main/SpellcheckerManager.ts index 685ef79e6..5267968c2 100644 --- a/packages/desktop/app/javascripts/Main/SpellcheckerManager.ts +++ b/packages/desktop/app/javascripts/Main/SpellcheckerManager.ts @@ -1,5 +1,6 @@ /* eslint-disable no-inline-comments */ -import { Store, StoreKeys } from './Store' +import { Store } from './Store/Store' +import { StoreKeys } from './Store/StoreKeys' import { isMac } from './Types/Platforms' import { isDev } from './Utils/Utils' diff --git a/packages/desktop/app/javascripts/Main/Store.ts b/packages/desktop/app/javascripts/Main/Store.ts deleted file mode 100644 index b023f4daf..000000000 --- a/packages/desktop/app/javascripts/Main/Store.ts +++ /dev/null @@ -1,185 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { MessageType } from '../../../test/TestIpcMessage' -import { BackupsDirectoryName } from './Backups/BackupsManager' -import { Language } from './SpellcheckerManager' -import { FileDoesNotExist } from './Utils/FileUtils' -import { handleTestMessage } from './Utils/Testing' -import { ensureIsBoolean, isBoolean, isDev, isTesting } from './Utils/Utils' - -const app = process.type === 'browser' ? require('electron').app : require('@electron/remote').app - -function logError(...message: any) { - console.error('store:', ...message) -} - -export enum StoreKeys { - ExtServerHost = 'extServerHost', - UseSystemMenuBar = 'useSystemMenuBar', - MenuBarVisible = 'isMenuBarVisible', - BackupsLocation = 'backupsLocation', - BackupsDisabled = 'backupsDisabled', - MinimizeToTray = 'minimizeToTray', - EnableAutoUpdate = 'enableAutoUpdates', - ZoomFactor = 'zoomFactor', - SelectedSpellCheckerLanguageCodes = 'selectedSpellCheckerLanguageCodes', - UseNativeKeychain = 'useNativeKeychain', - - FileBackupsEnabled = 'fileBackupsEnabled', - FileBackupsLocation = 'fileBackupsLocation', -} - -interface StoreData { - [StoreKeys.ExtServerHost]: string - [StoreKeys.UseSystemMenuBar]: boolean - [StoreKeys.MenuBarVisible]: boolean - [StoreKeys.BackupsLocation]: string - [StoreKeys.BackupsDisabled]: boolean - [StoreKeys.MinimizeToTray]: boolean - [StoreKeys.EnableAutoUpdate]: boolean - [StoreKeys.UseNativeKeychain]: boolean | null - [StoreKeys.ZoomFactor]: number - [StoreKeys.SelectedSpellCheckerLanguageCodes]: Set | null - [StoreKeys.FileBackupsEnabled]: boolean - [StoreKeys.FileBackupsLocation]: string -} - -function createSanitizedStoreData(data: any = {}): StoreData { - return { - [StoreKeys.MenuBarVisible]: ensureIsBoolean(data[StoreKeys.MenuBarVisible], true), - [StoreKeys.UseSystemMenuBar]: ensureIsBoolean(data[StoreKeys.UseSystemMenuBar], false), - [StoreKeys.BackupsDisabled]: ensureIsBoolean(data[StoreKeys.BackupsDisabled], false), - [StoreKeys.MinimizeToTray]: ensureIsBoolean(data[StoreKeys.MinimizeToTray], false), - [StoreKeys.EnableAutoUpdate]: ensureIsBoolean(data[StoreKeys.EnableAutoUpdate], true), - [StoreKeys.UseNativeKeychain]: isBoolean(data[StoreKeys.UseNativeKeychain]) - ? data[StoreKeys.UseNativeKeychain] - : null, - [StoreKeys.ExtServerHost]: data[StoreKeys.ExtServerHost], - [StoreKeys.BackupsLocation]: sanitizeBackupsLocation(data[StoreKeys.BackupsLocation]), - [StoreKeys.ZoomFactor]: sanitizeZoomFactor(data[StoreKeys.ZoomFactor]), - [StoreKeys.SelectedSpellCheckerLanguageCodes]: sanitizeSpellCheckerLanguageCodes( - data[StoreKeys.SelectedSpellCheckerLanguageCodes], - ), - [StoreKeys.FileBackupsEnabled]: ensureIsBoolean(data[StoreKeys.FileBackupsEnabled], false), - [StoreKeys.FileBackupsLocation]: data[StoreKeys.FileBackupsLocation], - } -} - -function sanitizeZoomFactor(factor?: any): number { - if (typeof factor === 'number' && factor > 0) { - return factor - } else { - return 1 - } -} - -function sanitizeBackupsLocation(location?: unknown): string { - const defaultPath = path.join( - isTesting() ? app.getPath('userData') : isDev() ? app.getPath('documents') : app.getPath('home'), - BackupsDirectoryName, - ) - - if (typeof location !== 'string') { - return defaultPath - } - - try { - const stat = fs.lstatSync(location) - if (stat.isDirectory()) { - return location - } - /** Path points to something other than a directory */ - return defaultPath - } catch (e) { - /** Path does not point to a valid directory */ - logError(e) - return defaultPath - } -} - -function sanitizeSpellCheckerLanguageCodes(languages?: unknown): Set | null { - if (!languages) { - return null - } - if (!Array.isArray(languages)) { - return null - } - - const set = new Set() - const validLanguages = Object.values(Language) - for (const language of languages) { - if (validLanguages.includes(language)) { - set.add(language) - } - } - return set -} - -export function serializeStoreData(data: StoreData): string { - return JSON.stringify(data, (_key, value) => { - if (value instanceof Set) { - return Array.from(value) - } - return value - }) -} - -function parseDataFile(filePath: string) { - try { - const fileData = fs.readFileSync(filePath) - const userData = JSON.parse(fileData.toString()) - return createSanitizedStoreData(userData) - } catch (error: any) { - console.log('Error reading store file', error) - if (error.code !== FileDoesNotExist) { - logError(error) - } - - return createSanitizedStoreData({}) - } -} - -export class Store { - static instance: Store - readonly path: string - readonly data: StoreData - - static getInstance(): Store { - if (!this.instance) { - /** - * Renderer process has to get `app` module via `remote`, whereas the main process - * can get it directly app.getPath('userData') will return a string of the user's - * app data directory path. - * TODO(baptiste): stop using Store in the renderer process. - */ - const userDataPath = app.getPath('userData') - this.instance = new Store(userDataPath) - } - return this.instance - } - - static get(key: T): StoreData[T] { - return this.getInstance().get(key) - } - - constructor(userDataPath: string) { - this.path = path.join(userDataPath, 'user-preferences.json') - this.data = parseDataFile(this.path) - - if (isTesting()) { - handleTestMessage(MessageType.StoreSettingsLocation, () => this.path) - handleTestMessage(MessageType.StoreSet, (key, value) => { - this.set(key, value) - }) - } - } - - get(key: T): StoreData[T] { - return this.data[key] - } - - set(key: T, val: StoreData[T]): void { - this.data[key] = val - fs.writeFileSync(this.path, serializeStoreData(this.data)) - } -} diff --git a/packages/desktop/app/javascripts/Main/Store/Store.ts b/packages/desktop/app/javascripts/Main/Store/Store.ts new file mode 100644 index 000000000..e4b3031ff --- /dev/null +++ b/packages/desktop/app/javascripts/Main/Store/Store.ts @@ -0,0 +1,52 @@ +import fs from 'fs' +import path from 'path' +import { MessageType } from '../../../../test/TestIpcMessage' +import { handleTestMessage } from '../Utils/Testing' +import { isTesting } from '../Utils/Utils' +import { parseDataFile, serializeStoreData } from './createSanitizedStoreData' +import { StoreData } from './StoreKeys' + +export const app = process.type === 'browser' ? require('electron').app : require('@electron/remote').app + +export function logError(...message: any) { + console.error('store:', ...message) +} + +export class Store { + static instance: Store + readonly path: string + readonly data: StoreData + + static getInstance(): Store { + if (!this.instance) { + const userDataPath = app.getPath('userData') + this.instance = new Store(userDataPath) + } + return this.instance + } + + static get(key: T): StoreData[T] { + return this.getInstance().get(key) + } + + constructor(userDataPath: string) { + this.path = path.join(userDataPath, 'user-preferences.json') + this.data = parseDataFile(this.path) + + if (isTesting()) { + handleTestMessage(MessageType.StoreSettingsLocation, () => this.path) + handleTestMessage(MessageType.StoreSet, (key, value) => { + this.set(key, value) + }) + } + } + + get(key: T): StoreData[T] { + return this.data[key] + } + + set(key: T, val: StoreData[T]): void { + this.data[key] = val + fs.writeFileSync(this.path, serializeStoreData(this.data)) + } +} diff --git a/packages/desktop/app/javascripts/Main/Store/StoreKeys.ts b/packages/desktop/app/javascripts/Main/Store/StoreKeys.ts new file mode 100644 index 000000000..92ee2e8e7 --- /dev/null +++ b/packages/desktop/app/javascripts/Main/Store/StoreKeys.ts @@ -0,0 +1,33 @@ +import { Language } from '../SpellcheckerManager' + +export enum StoreKeys { + ExtServerHost = 'extServerHost', + UseSystemMenuBar = 'useSystemMenuBar', + MenuBarVisible = 'isMenuBarVisible', + BackupsLocation = 'backupsLocation', + BackupsDisabled = 'backupsDisabled', + MinimizeToTray = 'minimizeToTray', + EnableAutoUpdate = 'enableAutoUpdates', + ZoomFactor = 'zoomFactor', + SelectedSpellCheckerLanguageCodes = 'selectedSpellCheckerLanguageCodes', + UseNativeKeychain = 'useNativeKeychain', + FileBackupsEnabled = 'fileBackupsEnabled', + FileBackupsLocation = 'fileBackupsLocation', + LastRunVersion = 'LastRunVersion', +} + +export interface StoreData { + [StoreKeys.ExtServerHost]: string + [StoreKeys.UseSystemMenuBar]: boolean + [StoreKeys.MenuBarVisible]: boolean + [StoreKeys.BackupsLocation]: string + [StoreKeys.BackupsDisabled]: boolean + [StoreKeys.MinimizeToTray]: boolean + [StoreKeys.EnableAutoUpdate]: boolean + [StoreKeys.UseNativeKeychain]: boolean | null + [StoreKeys.ZoomFactor]: number + [StoreKeys.SelectedSpellCheckerLanguageCodes]: Set | null + [StoreKeys.FileBackupsEnabled]: boolean + [StoreKeys.FileBackupsLocation]: string + [StoreKeys.LastRunVersion]: string +} diff --git a/packages/desktop/app/javascripts/Main/Store/createSanitizedStoreData.ts b/packages/desktop/app/javascripts/Main/Store/createSanitizedStoreData.ts new file mode 100644 index 000000000..1be162284 --- /dev/null +++ b/packages/desktop/app/javascripts/Main/Store/createSanitizedStoreData.ts @@ -0,0 +1,101 @@ +import fs from 'fs' +import path from 'path' +import { BackupsDirectoryName } from '../Backups/BackupsManager' +import { Language } from '../SpellcheckerManager' +import { FileDoesNotExist } from '../Utils/FileUtils' +import { ensureIsBoolean, isBoolean, isDev, isTesting } from '../Utils/Utils' +import { StoreData, StoreKeys } from './StoreKeys' +import { app, logError } from './Store' + +export function createSanitizedStoreData(data: any = {}): StoreData { + return { + [StoreKeys.MenuBarVisible]: ensureIsBoolean(data[StoreKeys.MenuBarVisible], true), + [StoreKeys.UseSystemMenuBar]: ensureIsBoolean(data[StoreKeys.UseSystemMenuBar], false), + [StoreKeys.BackupsDisabled]: ensureIsBoolean(data[StoreKeys.BackupsDisabled], false), + [StoreKeys.MinimizeToTray]: ensureIsBoolean(data[StoreKeys.MinimizeToTray], false), + [StoreKeys.EnableAutoUpdate]: ensureIsBoolean(data[StoreKeys.EnableAutoUpdate], true), + [StoreKeys.UseNativeKeychain]: isBoolean(data[StoreKeys.UseNativeKeychain]) + ? data[StoreKeys.UseNativeKeychain] + : null, + [StoreKeys.ExtServerHost]: data[StoreKeys.ExtServerHost], + [StoreKeys.BackupsLocation]: sanitizeBackupsLocation(data[StoreKeys.BackupsLocation]), + [StoreKeys.ZoomFactor]: sanitizeZoomFactor(data[StoreKeys.ZoomFactor]), + [StoreKeys.SelectedSpellCheckerLanguageCodes]: sanitizeSpellCheckerLanguageCodes( + data[StoreKeys.SelectedSpellCheckerLanguageCodes], + ), + [StoreKeys.FileBackupsEnabled]: ensureIsBoolean(data[StoreKeys.FileBackupsEnabled], false), + [StoreKeys.FileBackupsLocation]: data[StoreKeys.FileBackupsLocation], + [StoreKeys.LastRunVersion]: data[StoreKeys.LastRunVersion], + } +} +function sanitizeZoomFactor(factor?: any): number { + if (typeof factor === 'number' && factor > 0) { + return factor + } else { + return 1 + } +} +function sanitizeBackupsLocation(location?: unknown): string { + const defaultPath = path.join( + isTesting() ? app.getPath('userData') : isDev() ? app.getPath('documents') : app.getPath('home'), + BackupsDirectoryName, + ) + + if (typeof location !== 'string') { + return defaultPath + } + + try { + const stat = fs.lstatSync(location) + if (stat.isDirectory()) { + return location + } + /** Path points to something other than a directory */ + return defaultPath + } catch (e) { + /** Path does not point to a valid directory */ + logError(e) + return defaultPath + } +} +function sanitizeSpellCheckerLanguageCodes(languages?: unknown): Set | null { + if (!languages) { + return null + } + if (!Array.isArray(languages)) { + return null + } + + const set = new Set() + const validLanguages = Object.values(Language) + for (const language of languages) { + if (validLanguages.includes(language)) { + set.add(language) + } + } + return set +} + +export function serializeStoreData(data: StoreData): string { + return JSON.stringify(data, (_key, value) => { + if (value instanceof Set) { + return Array.from(value) + } + return value + }) +} + +export function parseDataFile(filePath: string) { + try { + const fileData = fs.readFileSync(filePath) + const userData = JSON.parse(fileData.toString()) + return createSanitizedStoreData(userData) + } catch (error: any) { + console.log('Error reading store file', error) + if (error.code !== FileDoesNotExist) { + logError(error) + } + + return createSanitizedStoreData({}) + } +} diff --git a/packages/desktop/app/javascripts/Main/TrayManager.ts b/packages/desktop/app/javascripts/Main/TrayManager.ts index 11f76d41d..02b947682 100644 --- a/packages/desktop/app/javascripts/Main/TrayManager.ts +++ b/packages/desktop/app/javascripts/Main/TrayManager.ts @@ -1,6 +1,7 @@ import { Menu, Tray } from 'electron' import path from 'path' -import { Store, StoreKeys } from './Store' +import { Store } from './Store/Store' +import { StoreKeys } from './Store/StoreKeys' import { AppName, tray as str } from './Strings' import { isLinux, isWindows } from './Types/Platforms' import { isDev } from './Utils/Utils' diff --git a/packages/desktop/app/javascripts/Main/UpdateManager.ts b/packages/desktop/app/javascripts/Main/UpdateManager.ts index 8c2bf799b..b7a00ccf3 100644 --- a/packages/desktop/app/javascripts/Main/UpdateManager.ts +++ b/packages/desktop/app/javascripts/Main/UpdateManager.ts @@ -4,9 +4,9 @@ import electronLog from 'electron-log' import { autoUpdater } from 'electron-updater' import { action, autorun, computed, makeObservable, observable } from 'mobx' import { MessageType } from '../../../test/TestIpcMessage' -import { AppState } from '../../application' +import { AppState } from '../../AppState' import { BackupsManagerInterface } from './Backups/BackupsManagerInterface' -import { StoreKeys } from './Store' +import { StoreKeys } from './Store/StoreKeys' import { updates as str } from './Strings' import { autoUpdatingAvailable } from './Types/Constants' import { handleTestMessage } from './Utils/Testing' diff --git a/packages/desktop/app/javascripts/Main/Window.ts b/packages/desktop/app/javascripts/Main/Window.ts index c52fd6288..e4dd46b70 100644 --- a/packages/desktop/app/javascripts/Main/Window.ts +++ b/packages/desktop/app/javascripts/Main/Window.ts @@ -4,7 +4,7 @@ import fs from 'fs' import { debounce } from 'lodash' import path from 'path' import { AppMessageType, MessageType } from '../../../test/TestIpcMessage' -import { AppState } from '../../application' +import { AppState } from '../../AppState' import { MessageToWebApp } from '../Shared/IpcMessages' import { createBackupsManager } from './Backups/BackupsManager' import { BackupsManagerInterface } from './Backups/BackupsManagerInterface' @@ -16,7 +16,8 @@ import { initializePackageManager } from './Packages/PackageManager' import { RemoteBridge } from './Remote/RemoteBridge' import { initializeSearchManager } from './Search/SearchManager' import { createSpellcheckerManager } from './SpellcheckerManager' -import { Store, StoreKeys } from './Store' +import { Store } from './Store/Store' +import { StoreKeys } from './Store/StoreKeys' import { createTrayManager, TrayManager } from './TrayManager' import { Paths } from './Types/Paths' import { isMac, isWindows } from './Types/Platforms' diff --git a/packages/desktop/app/javascripts/Main/ZoomManager.ts b/packages/desktop/app/javascripts/Main/ZoomManager.ts index 1ff242f61..623924583 100644 --- a/packages/desktop/app/javascripts/Main/ZoomManager.ts +++ b/packages/desktop/app/javascripts/Main/ZoomManager.ts @@ -1,5 +1,6 @@ import { BrowserWindow } from 'electron' -import { Store, StoreKeys } from './Store' +import { Store } from './Store/Store' +import { StoreKeys } from './Store/StoreKeys' export function initializeZoomManager(window: BrowserWindow, store: Store): void { window.webContents.on('dom-ready', () => {