Refactor config, move ipc calls to app module, some cleanup (#2669)

This commit is contained in:
Devin Binnie
2023-04-06 11:24:57 -04:00
committed by GitHub
parent 88eb2e2c70
commit 741087cb55
29 changed files with 551 additions and 638 deletions

View File

@@ -4,7 +4,7 @@
import Joi from 'joi';
import {Args} from 'types/args';
import {AnyConfig, ConfigV0, ConfigV1, ConfigV2, ConfigV3, TeamWithTabs} from 'types/config';
import {AnyConfig, ConfigV0, ConfigV1, ConfigV2, ConfigV3, ConfigServer} from 'types/config';
import {DownloadedItems} from 'types/downloads';
import {SavedWindowState} from 'types/mainWindow';
import {AppState} from 'types/appState';
@@ -213,7 +213,7 @@ function cleanTeam<T extends {name: string; url: string}>(team: T) {
};
}
function cleanTeamWithTabs(team: TeamWithTabs) {
function cleanTeamWithTabs(team: ConfigServer) {
return {
...cleanTeam(team),
tabs: team.tabs.map((tab) => {

View File

@@ -67,7 +67,6 @@ describe('common/config/RegistryConfig', () => {
expect(registryConfig.data.teams).toContainEqual({
name: 'server-1',
url: 'http://server-1.com',
order: 0,
});
expect(registryConfig.data.enableAutoUpdater).toBe(true);
expect(registryConfig.data.enableServerManagement).toBe(true);

View File

@@ -6,7 +6,7 @@ import {EventEmitter} from 'events';
import WindowsRegistry from 'winreg';
import WindowsRegistryUTF8 from 'winreg-utf8';
import {RegistryConfig as RegistryConfigType, FullTeam} from 'types/config';
import {RegistryConfig as RegistryConfigType, Team} from 'types/config';
import {Logger} from 'common/log';
@@ -78,12 +78,11 @@ export default class RegistryConfig extends EventEmitter {
*/
async getServersListFromRegistry() {
const defaultServers = await this.getRegistryEntry(`${BASE_REGISTRY_KEY_PATH}\\DefaultServerList`);
return defaultServers.flat(2).reduce((servers: FullTeam[], server, index) => {
return defaultServers.flat(2).reduce((servers: Team[], server) => {
if (server) {
servers.push({
name: (server as WindowsRegistry.RegistryItem).name,
url: (server as WindowsRegistry.RegistryItem).value,
order: index,
});
}
return servers;

View File

@@ -1,22 +1,19 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import fs from 'fs';
import {Config} from 'common/config';
const configPath = '/fake/config/path';
const appName = 'app-name';
const appPath = '/my/app/path';
jest.mock('electron', () => ({
app: {
name: 'Mattermost',
getPath: jest.fn(),
getAppPath: () => '/path/to/app',
},
ipcMain: {
on: jest.fn(),
},
nativeTheme: {
shouldUseDarkColors: false,
},
jest.mock('fs', () => ({
readFileSync: jest.fn(),
writeFileSync: jest.fn(),
existsSync: jest.fn(),
mkdirSync: jest.fn(),
}));
jest.mock('common/Validator', () => ({
@@ -28,7 +25,7 @@ jest.mock('common/Validator', () => ({
}));
jest.mock('common/tabs/TabView', () => ({
getDefaultTeamWithTabsFromTeam: (value) => ({
getDefaultConfigTeamFromTeam: (value) => ({
...value,
tabs: [
{
@@ -99,15 +96,18 @@ jest.mock('common/config/RegistryConfig', () => {
describe('common/config', () => {
it('should load buildConfig', () => {
const config = new Config(configPath);
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
expect(config.predefinedTeams).toContainEqual(buildTeamWithTabs);
});
describe('loadRegistry', () => {
it('should load the registry items and reload the config', () => {
const config = new Config(configPath);
const config = new Config();
config.reload = jest.fn();
config.loadRegistry({teams: [registryTeam]});
config.init(configPath, appName, appPath);
config.onLoadRegistry({teams: [registryTeam]});
expect(config.reload).toHaveBeenCalled();
expect(config.predefinedTeams).toContainEqual({
...registryTeam,
@@ -125,7 +125,8 @@ describe('common/config', () => {
describe('reload', () => {
it('should emit update event', () => {
const config = new Config(configPath);
const config = new Config();
config.init(configPath, appName, appPath);
config.loadDefaultConfigData = jest.fn();
config.loadBuildConfigData = jest.fn();
config.loadLocalConfigFile = jest.fn();
@@ -134,6 +135,7 @@ describe('common/config', () => {
config.combinedData = {test: 'test'};
});
config.emit = jest.fn();
fs.existsSync.mockReturnValue(true);
config.reload();
expect(config.emit).toHaveBeenNthCalledWith(1, 'update', {test: 'test'});
@@ -142,7 +144,9 @@ describe('common/config', () => {
describe('set', () => {
it('should set an arbitrary value and save to local config data', () => {
const config = new Config(configPath);
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {};
config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => {
config.combinedData = {...config.localConfigData};
@@ -155,24 +159,46 @@ describe('common/config', () => {
expect(config.saveLocalConfigData).toHaveBeenCalled();
});
it('should set teams without including predefined', () => {
const config = new Config(configPath);
it('should not allow teams to be set using this method', () => {
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {teams: [team]};
config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => {
config.combinedData = {...config.localConfigData};
});
config.saveLocalConfigData = jest.fn();
config.set('teams', [{...buildTeamWithTabs, name: 'build-team-2'}]);
expect(config.localConfigData.teams).not.toContainEqual({...buildTeamWithTabs, name: 'build-team-2'});
expect(config.localConfigData.teams).toContainEqual(team);
});
});
describe('setServers', () => {
it('should set only local servers', () => {
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {};
config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => {
config.combinedData = {...config.localConfigData};
});
config.saveLocalConfigData = jest.fn();
config.set('teams', [{...buildTeamWithTabs, name: 'build-team-2'}, team]);
expect(config.localConfigData.teams).not.toContainEqual({...buildTeamWithTabs, name: 'build-team-2'});
expect(config.localConfigData.teams).toContainEqual(team);
expect(config.predefinedTeams).toContainEqual({...buildTeamWithTabs, name: 'build-team-2'});
config.setServers([{...buildTeamWithTabs, name: 'build-team-2'}, team], 0);
expect(config.localConfigData.teams).toContainEqual({...buildTeamWithTabs, name: 'build-team-2'});
expect(config.localConfigData.lastActiveTeam).toBe(0);
expect(config.regenerateCombinedConfigData).toHaveBeenCalled();
expect(config.saveLocalConfigData).toHaveBeenCalled();
});
});
describe('saveLocalConfigData', () => {
it('should emit update event on save', () => {
const config = new Config(configPath);
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData};
config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => {
@@ -185,7 +211,9 @@ describe('common/config', () => {
});
it('should emit error when fs.writeSync throws an error', () => {
const config = new Config(configPath);
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData};
config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => {
@@ -198,7 +226,9 @@ describe('common/config', () => {
});
it('should emit error when writeFile throws an error', () => {
const config = new Config(configPath);
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData};
config.writeFile = jest.fn().mockImplementation(() => {
@@ -212,7 +242,9 @@ describe('common/config', () => {
it('should retry when file is locked', () => {
const testFunc = jest.fn();
const config = new Config(configPath);
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData};
config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => {
@@ -228,37 +260,41 @@ describe('common/config', () => {
describe('loadLocalConfigFile', () => {
it('should use defaults if readFileSync fails', () => {
const config = new Config(configPath);
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.defaultConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData};
config.readFileSync = jest.fn().mockImplementation(() => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockImplementation(() => {
throw new Error('Error message');
});
config.writeFileSync = jest.fn();
config.writeFile = jest.fn();
const configData = config.loadLocalConfigFile();
expect(configData).toStrictEqual({test: 'test'});
});
it('should use defaults if validation fails', () => {
const config = new Config(configPath);
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.defaultConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData};
config.readFileSync = jest.fn().mockImplementation(() => {
return {version: -1};
});
config.writeFileSync = jest.fn();
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValue('{"version": -1}');
config.writeFile = jest.fn();
const configData = config.loadLocalConfigFile();
expect(configData).toStrictEqual({test: 'test'});
});
it('should return config data if valid', () => {
const config = new Config(configPath);
config.readFileSync = jest.fn().mockImplementation(() => {
return {version: 3};
});
config.writeFileSync = jest.fn();
const config = new Config();
config.init(configPath, appName, appPath);
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValue('{"version": 3}');
config.writeFile = jest.fn();
const configData = config.loadLocalConfigFile();
expect(configData).toStrictEqual({version: 3});
@@ -267,7 +303,9 @@ describe('common/config', () => {
describe('checkForConfigUpdates', () => {
it('should upgrade to latest version', () => {
const config = new Config(configPath);
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.defaultConfigData = {version: 10};
config.writeFileSync = jest.fn();
@@ -276,111 +314,67 @@ describe('common/config', () => {
});
});
describe('regenerateCombinedConfigData', () => {
it('should combine config from all sources', () => {
const config = new Config(configPath);
config.predefinedTeams = [];
config.useNativeWindow = false;
config.defaultConfigData = {defaultSetting: 'default', otherDefaultSetting: 'default'};
config.localConfigData = {otherDefaultSetting: 'local', localSetting: 'local', otherLocalSetting: 'local'};
config.buildConfigData = {otherLocalSetting: 'build', buildSetting: 'build', otherBuildSetting: 'build'};
config.registryConfigData = {otherBuildSetting: 'registry', registrySetting: 'registry'};
// TODO: Re-enable when we migrate to ServerManager fully
// describe('regenerateCombinedConfigData', () => {
// it('should combine config from all sources', () => {
// const config = new Config();
// config.reload = jest.fn();
// config.init(configPath, appName, appPath);
// config.useNativeWindow = false;
// config.defaultConfigData = {defaultSetting: 'default', otherDefaultSetting: 'default'};
// config.localConfigData = {otherDefaultSetting: 'local', localSetting: 'local', otherLocalSetting: 'local'};
// config.buildConfigData = {otherLocalSetting: 'build', buildSetting: 'build', otherBuildSetting: 'build'};
// config.registryConfigData = {otherBuildSetting: 'registry', registrySetting: 'registry'};
config.regenerateCombinedConfigData();
config.combinedData.darkMode = false;
expect(config.combinedData).toStrictEqual({
teams: [],
registryTeams: [],
appName: 'Mattermost',
useNativeWindow: false,
darkMode: false,
otherBuildSetting: 'registry',
registrySetting: 'registry',
otherLocalSetting: 'build',
buildSetting: 'build',
otherDefaultSetting: 'local',
localSetting: 'local',
defaultSetting: 'default',
});
});
// config.regenerateCombinedConfigData();
// config.combinedData.darkMode = false;
// expect(config.combinedData).toStrictEqual({
// appName: 'app-name',
// useNativeWindow: false,
// darkMode: false,
// otherBuildSetting: 'registry',
// registrySetting: 'registry',
// otherLocalSetting: 'build',
// buildSetting: 'build',
// otherDefaultSetting: 'local',
// localSetting: 'local',
// defaultSetting: 'default',
// });
// });
it('should combine teams from all sources and filter duplicates', () => {
const config = new Config(configPath);
config.defaultConfigData = {};
config.localConfigData = {};
config.buildConfigData = {enableServerManagement: true};
config.registryConfigData = {};
config.predefinedTeams = [team, team];
config.useNativeWindow = false;
config.localConfigData = {teams: [
team,
{
...team,
name: 'local-team-2',
url: 'http://local-team-2.com',
},
{
...team,
name: 'local-team-1',
order: 1,
url: 'http://local-team-1.com',
},
]};
// it('should not include any teams in the combined config', () => {
// const config = new Config();
// config.reload = jest.fn();
// config.init(configPath, appName, appPath);
// config.defaultConfigData = {};
// config.localConfigData = {};
// config.buildConfigData = {enableServerManagement: true};
// config.registryConfigData = {};
// config.predefinedTeams.push(team, team);
// config.useNativeWindow = false;
// config.localConfigData = {teams: [
// team,
// {
// ...team,
// name: 'local-team-2',
// url: 'http://local-team-2.com',
// },
// {
// ...team,
// name: 'local-team-1',
// order: 1,
// url: 'http://local-team-1.com',
// },
// ]};
config.regenerateCombinedConfigData();
config.combinedData.darkMode = false;
expect(config.combinedData).toStrictEqual({
teams: [
team,
{
...team,
name: 'local-team-2',
order: 1,
url: 'http://local-team-2.com',
},
{
...team,
name: 'local-team-1',
order: 2,
url: 'http://local-team-1.com',
},
],
registryTeams: [],
appName: 'Mattermost',
useNativeWindow: false,
darkMode: false,
enableServerManagement: true,
});
});
it('should not include local teams if enableServerManagement is false', () => {
const config = new Config(configPath);
config.defaultConfigData = {};
config.localConfigData = {};
config.buildConfigData = {enableServerManagement: false};
config.registryConfigData = {};
config.predefinedTeams = [team, team];
config.useNativeWindow = false;
config.localConfigData = {teams: [
team,
{
...team,
name: 'local-team-1',
order: 1,
url: 'http://local-team-1.com',
},
]};
config.regenerateCombinedConfigData();
config.combinedData.darkMode = false;
expect(config.combinedData).toStrictEqual({
teams: [team],
registryTeams: [],
appName: 'Mattermost',
useNativeWindow: false,
darkMode: false,
enableServerManagement: false,
});
});
});
// config.regenerateCombinedConfigData();
// config.combinedData.darkMode = false;
// expect(config.combinedData).toStrictEqual({
// appName: 'app-name',
// useNativeWindow: false,
// darkMode: false,
// enableServerManagement: true,
// });
// });
// });
});

View File

@@ -7,7 +7,6 @@ import os from 'os';
import path from 'path';
import {EventEmitter} from 'events';
import {ipcMain, nativeTheme, app} from 'electron';
import {
AnyConfig,
@@ -15,18 +14,13 @@ import {
CombinedConfig,
ConfigServer,
Config as ConfigType,
LocalConfiguration,
RegistryConfig as RegistryConfigType,
TeamWithTabs,
} from 'types/config';
import {UPDATE_TEAMS, GET_CONFIGURATION, UPDATE_CONFIGURATION, GET_LOCAL_CONFIGURATION, UPDATE_PATHS} from 'common/communication';
import * as Validator from 'common/Validator';
import {Logger} from 'common/log';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
import Utils from 'common/utils/util';
import {configPath} from 'main/constants';
import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import Utils, {copy} from 'common/utils/util';
import * as Validator from 'common/Validator';
import defaultPreferences, {getDefaultDownloadLocation} from './defaultPreferences';
import upgradeConfigData from './upgradePreferences';
@@ -36,61 +30,28 @@ import migrateConfigItems from './migrationPreferences';
const log = new Logger('Config');
/**
* Handles loading and merging all sources of configuration as well as saving user provided config
*/
function checkWriteableApp() {
if (process.platform === 'win32') {
try {
fs.accessSync(path.join(path.dirname(app.getAppPath()), '../../'), fs.constants.W_OK);
// check to make sure that app-update.yml exists
if (!fs.existsSync(path.join(process.resourcesPath, 'app-update.yml'))) {
log.warn('app-update.yml does not exist, disabling auto-updates');
return false;
}
} catch (error) {
log.info(`${app.getAppPath()}: ${error}`);
log.warn('autoupgrade disabled');
return false;
}
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return __CAN_UPGRADE__; // prevent showing the option if the path is not writeable, like in a managed environment.
}
// temporarily disabling auto updater for macOS due to security issues
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return process.platform !== 'darwin' && __CAN_UPGRADE__;
}
export class Config extends EventEmitter {
configFilePath: string;
private configFilePath?: string;
private appName?: string;
private appPath?: string;
registryConfig: RegistryConfig;
private registryConfig: RegistryConfig;
private predefinedServers: ConfigServer[];
private useNativeWindow: boolean;
combinedData?: CombinedConfig;
registryConfigData?: Partial<RegistryConfigType>;
defaultConfigData?: ConfigType;
buildConfigData?: BuildConfig;
localConfigData?: ConfigType;
useNativeWindow: boolean;
canUpgradeValue?: boolean
private combinedData?: CombinedConfig;
private localConfigData?: ConfigType;
private registryConfigData?: Partial<RegistryConfigType>;
private defaultConfigData?: ConfigType;
private buildConfigData?: BuildConfig;
private canUpgradeValue?: boolean;
predefinedTeams: TeamWithTabs[];
constructor(configFilePath: string) {
constructor() {
super();
this.configFilePath = configFilePath;
this.canUpgradeValue = checkWriteableApp();
this.registryConfig = new RegistryConfig();
this.predefinedTeams = [];
this.predefinedServers = [];
if (buildConfig.defaultTeams) {
this.predefinedTeams.push(...buildConfig.defaultTeams.map((team) => getDefaultTeamWithTabsFromTeam(team)));
this.predefinedServers.push(...buildConfig.defaultTeams.map((team, index) => getDefaultConfigTeamFromTeam({...team, order: index})));
}
try {
this.useNativeWindow = os.platform() === 'win32' && !Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.2');
@@ -99,45 +60,30 @@ export class Config extends EventEmitter {
}
}
// separating constructor from init so main can setup event listeners
init = (): void => {
init = (configFilePath: string, appName: string, appPath: string) => {
this.configFilePath = configFilePath;
this.appName = appName;
this.appPath = appPath;
this.canUpgradeValue = this.checkWriteableApp();
this.reload();
ipcMain.handle(GET_CONFIGURATION, this.handleGetConfiguration);
ipcMain.handle(GET_LOCAL_CONFIGURATION, this.handleGetLocalConfiguration);
ipcMain.handle(UPDATE_TEAMS, this.handleUpdateTeams);
ipcMain.on(UPDATE_CONFIGURATION, this.updateConfiguration);
if (process.platform === 'darwin' || process.platform === 'win32') {
nativeTheme.on('updated', this.handleUpdateTheme);
}
}
initRegistry = () => {
if (process.platform !== 'win32') {
return Promise.resolve();
}
return new Promise<void>((resolve) => {
this.registryConfig = new RegistryConfig();
this.registryConfig.once(REGISTRY_READ_EVENT, (data) => {
this.loadRegistry(data);
this.onLoadRegistry(data);
resolve();
});
this.registryConfig.init();
});
}
/**
* Gets the teams from registry into the config object and reload
*
* @param {object} registryData Team configuration from the registry and if teams can be managed by user
*/
loadRegistry = (registryData: Partial<RegistryConfigType>): void => {
log.verbose('Config.loadRegistry', {registryData});
this.registryConfigData = registryData;
if (this.registryConfigData.teams) {
this.predefinedTeams.push(...this.registryConfigData.teams.map((team) => getDefaultTeamWithTabsFromTeam(team)));
}
this.reload();
}
/**
* Reload all sources of config data
*
@@ -146,15 +92,21 @@ export class Config extends EventEmitter {
* @emits {synchronize} emitted when requested by a call to method; used to notify other config instances of changes
*/
reload = (): void => {
this.defaultConfigData = this.loadDefaultConfigData();
this.buildConfigData = this.loadBuildConfigData();
this.defaultConfigData = copy(defaultPreferences);
this.buildConfigData = copy(buildConfig);
const loadedConfig = this.loadLocalConfigFile();
this.localConfigData = this.checkForConfigUpdates(loadedConfig);
this.regenerateCombinedConfigData();
this.emit('update', this.combinedData);
}
/*********************
* Setters and Getters
*********************/
/**
* Used to save a single config property
*
@@ -166,18 +118,8 @@ export class Config extends EventEmitter {
this.setMultiple({[key]: data});
}
updateConfiguration = (event: Electron.IpcMainEvent, properties: Array<{key: keyof ConfigType; data: ConfigType[keyof ConfigType]}> = []): Partial<ConfigType> | undefined => {
log.debug('Config.updateConfiguration', properties);
if (properties.length) {
const newData = properties.reduce((obj, data) => {
(obj as any)[data.key] = data.data;
return obj;
}, {} as Partial<ConfigType>);
this.setMultiple(newData);
}
return this.localConfigData;
setConfigPath = (configPath: string) => {
this.configFilePath = configPath;
}
/**
@@ -188,20 +130,12 @@ export class Config extends EventEmitter {
setMultiple = (newData: Partial<ConfigType>) => {
log.debug('setMultiple', newData);
this.localConfigData = Object.assign({}, this.localConfigData, newData);
if (newData.teams && this.localConfigData) {
this.localConfigData.teams = this.filterOutPredefinedTeams(newData.teams as TeamWithTabs[]);
this.predefinedTeams = this.filterInPredefinedTeams(newData.teams as TeamWithTabs[]);
if (newData.darkMode && newData.darkMode !== this.darkMode) {
this.emit('darkModeChange', newData.darkMode);
}
this.localConfigData = Object.assign({}, this.localConfigData, {...newData, teams: this.localConfigData?.teams});
this.regenerateCombinedConfigData();
this.saveLocalConfigData();
return this.localConfigData; //this is the only part that changes
}
setRegistryConfigData = (registryConfigData = {teams: []}): void => {
this.registryConfigData = Object.assign({}, registryConfigData);
this.reload();
}
setServers = (servers: ConfigServer[], lastActiveTeam?: number) => {
@@ -212,50 +146,6 @@ export class Config extends EventEmitter {
this.saveLocalConfigData();
}
/**
* Used to replace the existing config data with new config data
*
* @param {object} configData a new, config data object to completely replace the existing config data
*/
replace = (configData: ConfigType) => {
const newConfigData = configData;
this.localConfigData = Object.assign({}, this.localConfigData, newConfigData);
this.regenerateCombinedConfigData();
this.saveLocalConfigData();
}
/**
* Used to save the current set of local config data to disk
*
* @emits {update} emitted once all data has been saved
* @emits {synchronize} emitted once all data has been saved; used to notify other config instances of changes
* @emits {error} emitted if saving local config data to file fails
*/
saveLocalConfigData = (): void => {
if (!this.localConfigData) {
return;
}
log.info('Saving config data to file...');
try {
this.writeFile(this.configFilePath, this.localConfigData, (error: NodeJS.ErrnoException | null) => {
if (error) {
if (error.code === 'EBUSY') {
this.saveLocalConfigData();
} else {
this.emit('error', error);
}
}
this.emit('update', this.combinedData);
});
} catch (error) {
this.emit('error', error);
}
}
// getters for accessing the various config data inputs
get data() {
@@ -288,6 +178,9 @@ export class Config extends EventEmitter {
get localTeams() {
return this.localConfigData?.teams ?? defaultPreferences.teams;
}
get predefinedTeams() {
return this.predefinedServers;
}
get enableHardwareAcceleration() {
return this.combinedData?.enableHardwareAcceleration ?? defaultPreferences.enableHardwareAcceleration;
}
@@ -361,29 +254,67 @@ export class Config extends EventEmitter {
return this.combinedData?.appLanguage;
}
// initialization/processing methods
/**
* Returns a copy of the app's default config data
* Gets the teams from registry into the config object and reload
*
* @param {object} registryData Team configuration from the registry and if teams can be managed by user
*/
loadDefaultConfigData = () => {
return this.copy(defaultPreferences);
private onLoadRegistry = (registryData: Partial<RegistryConfigType>): void => {
log.debug('loadRegistry', {registryData});
this.registryConfigData = registryData;
if (this.registryConfigData.teams) {
this.predefinedTeams.push(...this.registryConfigData.teams.map((team, index) => getDefaultConfigTeamFromTeam({...team, order: index})));
}
this.reload();
}
/**
* Returns a copy of the app's build config data
* Config file loading methods
*/
loadBuildConfigData = () => {
return this.copy(buildConfig);
/**
* Used to save the current set of local config data to disk
*
* @emits {update} emitted once all data has been saved
* @emits {synchronize} emitted once all data has been saved; used to notify other config instances of changes
* @emits {error} emitted if saving local config data to file fails
*/
private saveLocalConfigData = (): void => {
if (!(this.configFilePath && this.localConfigData)) {
return;
}
log.verbose('Saving config data to file...');
try {
this.writeFile(this.configFilePath, this.localConfigData, (error: NodeJS.ErrnoException | null) => {
if (error) {
if (error.code === 'EBUSY') {
this.saveLocalConfigData();
} else {
this.emit('error', error);
}
}
this.emit('update', this.combinedData);
});
} catch (error) {
this.emit('error', error);
}
}
/**
* Loads and returns locally stored config data from the filesystem or returns app defaults if no file is found
*/
loadLocalConfigFile = (): AnyConfig => {
private loadLocalConfigFile = (): AnyConfig => {
if (!this.configFilePath) {
throw new Error('Unable to read from config, no path specified');
}
let configData: AnyConfig;
try {
configData = this.readFileSync(this.configFilePath);
configData = JSON.parse(fs.readFileSync(this.configFilePath, 'utf8'));
// validate based on config file version
configData = Validator.validateConfigData(configData);
@@ -393,9 +324,9 @@ export class Config extends EventEmitter {
}
} catch (e) {
log.warn('Failed to load configuration file from the filesystem. Using defaults.');
configData = this.copy(this.defaultConfigData);
configData = copy(this.defaultConfigData);
this.writeFileSync(this.configFilePath, configData);
this.writeFile(this.configFilePath, configData);
}
return configData;
}
@@ -405,18 +336,22 @@ export class Config extends EventEmitter {
*
* @param {*} data locally stored data
*/
checkForConfigUpdates = (data: AnyConfig) => {
private checkForConfigUpdates = (data: AnyConfig) => {
if (!this.configFilePath) {
throw new Error('Config not initialized');
}
let configData = data;
if (this.defaultConfigData) {
try {
if (configData.version !== this.defaultConfigData.version) {
configData = upgradeConfigData(configData);
this.writeFileSync(this.configFilePath, configData);
this.writeFile(this.configFilePath, configData);
log.info(`Configuration updated to version ${this.defaultConfigData.version} successfully.`);
}
const didMigrate = migrateConfigItems(configData);
if (didMigrate) {
this.writeFileSync(this.configFilePath, configData);
this.writeFile(this.configFilePath, configData);
log.info('Migrating config items successfully.');
}
} catch (error) {
@@ -430,43 +365,45 @@ export class Config extends EventEmitter {
/**
* Properly combines all sources of data into a single, manageable set of all config data
*/
regenerateCombinedConfigData = () => {
// combine all config data in the correct order
this.combinedData = Object.assign({}, this.defaultConfigData, this.localConfigData, this.buildConfigData, this.registryConfigData, {useNativeWindow: this.useNativeWindow});
// remove unecessary data pulled from default and build config
delete this.combinedData!.defaultTeams;
// IMPORTANT: properly combine teams from all sources
let combinedTeams: TeamWithTabs[] = [];
combinedTeams.push(...this.predefinedTeams);
// - add locally defined teams only if server management is enabled
if (this.localConfigData && this.enableServerManagement) {
combinedTeams.push(...this.localConfigData.teams || []);
private regenerateCombinedConfigData = () => {
if (!this.appName) {
throw new Error('Config not initialized, cannot regenerate');
}
this.predefinedTeams = this.filterOutDuplicateTeams(this.predefinedTeams);
combinedTeams = this.filterOutDuplicateTeams(combinedTeams);
combinedTeams = this.sortUnorderedTeams(combinedTeams);
// combine all config data in the correct order
this.combinedData = Object.assign({},
this.defaultConfigData,
this.localConfigData,
this.buildConfigData,
this.registryConfigData,
{useNativeWindow: this.useNativeWindow},
);
// We don't want to include the servers in the combined config, they should only be accesible via the ServerManager
//delete (this.combinedData as any).teams;
delete (this.combinedData as any).defaultTeams;
if (this.combinedData) {
this.combinedData.teams = combinedTeams;
this.combinedData.registryTeams = this.registryConfigData?.teams || [];
if (process.platform === 'darwin' || process.platform === 'win32') {
this.combinedData.darkMode = nativeTheme.shouldUseDarkColors;
// TODO: This can be removed after we fully migrate to ServerManager
let combinedTeams: ConfigServer[] = [];
combinedTeams.push(...this.predefinedTeams);
if (this.localConfigData && this.enableServerManagement) {
combinedTeams.push(...this.localConfigData.teams || []);
}
this.combinedData.appName = app.name;
combinedTeams = this.filterOutDuplicateTeams(combinedTeams);
combinedTeams = this.sortUnorderedTeams(combinedTeams);
this.combinedData.teams = combinedTeams;
this.combinedData.appName = this.appName;
}
}
/**
* Returns the provided list of teams with duplicates filtered out
*
* TODO: This can be removed after we fully migrate to ServerManager
* @param {array} teams array of teams to check for duplicates
*/
filterOutDuplicateTeams = (teams: TeamWithTabs[]) => {
private filterOutDuplicateTeams = (teams: ConfigServer[]) => {
let newTeams = teams;
const uniqueURLs = new Set();
newTeams = newTeams.filter((team) => {
@@ -475,41 +412,12 @@ export class Config extends EventEmitter {
return newTeams;
}
/**
* Returns the provided array fo teams with existing teams filtered out
* @param {array} teams array of teams to check for already defined teams
*/
filterOutPredefinedTeams = (teams: TeamWithTabs[]) => {
let newTeams = teams;
// filter out predefined teams
newTeams = newTeams.filter((newTeam) => {
return this.predefinedTeams.findIndex((existingTeam) => newTeam.url === existingTeam.url) === -1; // eslint-disable-line max-nested-callbacks
});
return newTeams;
}
/**
* Returns the provided array fo teams with existing teams includes
* @param {array} teams array of teams to check for already defined teams
*/
filterInPredefinedTeams = (teams: TeamWithTabs[]) => {
let newTeams = teams;
// filter out predefined teams
newTeams = newTeams.filter((newTeam) => {
return this.predefinedTeams.findIndex((existingTeam) => newTeam.url === existingTeam.url) >= 0; // eslint-disable-line max-nested-callbacks
});
return newTeams;
}
/**
* Apply a default sort order to the team list, if no order is specified.
* @param {array} teams to sort
* TODO: This can be removed after we fully migrate to ServerManager
*/
sortUnorderedTeams = (teams: TeamWithTabs[]) => {
private sortUnorderedTeams = (teams: ConfigServer[]) => {
// We want to preserve the array order of teams in the config, otherwise a lot of bugs will occur
const mappedTeams = teams.map((team, index) => ({team, originalOrder: index}));
@@ -538,12 +446,7 @@ export class Config extends EventEmitter {
}
// helper functions
readFileSync = (filePath: string) => {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
writeFile = (filePath: string, configData: Partial<ConfigType>, callback: fs.NoParamCallback) => {
private writeFile = (filePath: string, configData: Partial<ConfigType>, callback?: fs.NoParamCallback) => {
if (!this.defaultConfigData) {
return;
}
@@ -552,101 +455,52 @@ export class Config extends EventEmitter {
throw new Error('version ' + configData.version + ' is not equal to ' + this.defaultConfigData.version);
}
const json = JSON.stringify(configData, null, ' ');
fs.writeFile(filePath, json, 'utf8', callback);
}
writeFileSync = (filePath: string, config: Partial<ConfigType>) => {
if (!this.defaultConfigData) {
return;
}
if (callback) {
fs.writeFile(filePath, json, 'utf8', callback);
} else {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
if (config.version !== this.defaultConfigData.version) {
throw new Error('version ' + config.version + ' is not equal to ' + this.defaultConfigData.version);
}
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
const json = JSON.stringify(config, null, ' ');
fs.writeFileSync(filePath, json, 'utf8');
}
merge = <T, T2>(base: T, target: T2) => {
return Object.assign({}, base, target);
}
copy = <T>(data: T) => {
return Object.assign({}, data);
}
handleGetConfiguration = (event: Electron.IpcMainInvokeEvent, option: keyof CombinedConfig) => {
log.debug('Config.handleGetConfiguration', option);
const config = {...this.combinedData};
if (option) {
return config[option];
}
return config;
}
handleGetLocalConfiguration = (event: Electron.IpcMainInvokeEvent, option: keyof ConfigType) => {
log.debug('Config.handleGetLocalConfiguration', option);
const config: Partial<LocalConfiguration> = {...this.localConfigData};
config.appName = app.name;
config.enableServerManagement = this.combinedData?.enableServerManagement;
config.canUpgrade = this.canUpgrade;
if (option) {
return config[option];
}
return config;
}
handleUpdateTeams = (event: Electron.IpcMainInvokeEvent, newTeams: TeamWithTabs[]) => {
log.debug('Config.handleUpdateTeams');
log.silly('Config.handleUpdateTeams', newTeams);
this.set('teams', newTeams);
return this.combinedData!.teams;
}
/**
* Detects changes in darkmode if it is windows or osx, updates the config and propagates the changes
* @emits 'darkModeChange'
*/
handleUpdateTheme = () => {
log.debug('Config.handleUpdateTheme');
if (this.combinedData && this.combinedData.darkMode !== nativeTheme.shouldUseDarkColors) {
this.combinedData.darkMode = nativeTheme.shouldUseDarkColors;
this.emit('darkModeChange', this.combinedData.darkMode);
fs.writeFileSync(filePath, json, 'utf8');
}
}
/**
* Manually toggles dark mode for OSes that don't have a native dark mode setting
* @emits 'darkModeChange'
*/
toggleDarkModeManually = () => {
if (!this.combinedData) {
return;
private checkWriteableApp = () => {
if (!this.appPath) {
throw new Error('Config not initialized, cannot regenerate');
}
this.set('darkMode', !this.combinedData.darkMode);
this.emit('darkModeChange', this.combinedData.darkMode);
if (process.platform === 'win32') {
try {
fs.accessSync(path.join(path.dirname(this.appPath), '../../'), fs.constants.W_OK);
// check to make sure that app-update.yml exists
if (!fs.existsSync(path.join(process.resourcesPath, 'app-update.yml'))) {
log.warn('app-update.yml does not exist, disabling auto-updates');
return false;
}
} catch (error) {
log.info(`${this.appPath}: ${error}`);
log.warn('autoupgrade disabled');
return false;
}
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return __CAN_UPGRADE__; // prevent showing the option if the path is not writeable, like in a managed environment.
}
// temporarily disabling auto updater for macOS due to security issues
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return process.platform !== 'darwin' && __CAN_UPGRADE__;
}
}
const config = new Config(configPath);
const config = new Config();
export default config;
ipcMain.on(UPDATE_PATHS, () => {
log.debug('Config.UPDATE_PATHS');
config.configFilePath = configPath;
if (config.combinedData) {
config.reload();
}
});

View File

@@ -5,7 +5,7 @@ import {upgradeV0toV1, upgradeV1toV2, upgradeV2toV3} from 'common/config/upgrade
import pastDefaultPreferences from 'common/config/pastDefaultPreferences';
jest.mock('common/tabs/TabView', () => ({
getDefaultTeamWithTabsFromTeam: (value) => ({
getDefaultConfigTeamFromTeam: (value) => ({
...value,
tabs: [
{
@@ -122,7 +122,6 @@ describe('common/config/upgradePreferences', () => {
name: 'tab2',
},
],
lastActiveTab: 0,
}, {
name: 'Secondary team',
url: 'http://server-2.com',
@@ -135,7 +134,6 @@ describe('common/config/upgradePreferences', () => {
name: 'tab2',
},
],
lastActiveTab: 0,
}],
});
});

View File

@@ -4,7 +4,7 @@
import {ConfigV3, ConfigV2, ConfigV1, ConfigV0, AnyConfig} from 'types/config';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import pastDefaultPreferences from './pastDefaultPreferences';
@@ -37,10 +37,7 @@ export function upgradeV2toV3(configV2: ConfigV2) {
const config: ConfigV3 = Object.assign({}, deepCopy<ConfigV3>(pastDefaultPreferences[3]), configV2);
config.version = 3;
config.teams = configV2.teams.map((value) => {
return {
...getDefaultTeamWithTabsFromTeam(value),
lastActiveTab: 0,
};
return getDefaultConfigTeamFromTeam(value);
});
config.lastActiveTeam = 0;
config.spellCheckerLocales = [];

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {FullTeam} from 'types/config';
import {Team} from 'types/config';
import {MattermostServer} from 'common/servers/MattermostServer';
@@ -21,7 +21,7 @@ export interface TabView {
get shouldNotify(): boolean;
}
export function getDefaultTeamWithTabsFromTeam(team: FullTeam) {
export function getDefaultConfigTeamFromTeam(team: Team & {order: number; lastActiveTab?: number}) {
return {
...team,
tabs: getDefaultTabs(),

View File

@@ -66,6 +66,10 @@ export const escapeRegex = (s?: string) => {
return s.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
};
export function copy<T>(data: T) {
return Object.assign({}, data);
}
export default {
runMode,
shorten,