You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
11 KiB
441 lines
11 KiB
import { EventEmitter } from 'events';
|
|
|
|
import { Meteor } from 'meteor/meteor';
|
|
import _ from 'underscore';
|
|
|
|
import { SettingsBase } from '../../lib/settings';
|
|
import SettingsModel from '../../../models/server/models/Settings';
|
|
import { updateValue } from '../raw';
|
|
import { ISetting, SettingValue } from '../../../../definition/ISetting';
|
|
import { SystemLogger } from '../../../../server/lib/logger/system';
|
|
|
|
const blockedSettings = new Set<string>();
|
|
const hiddenSettings = new Set<string>();
|
|
const wizardRequiredSettings = new Set<string>();
|
|
|
|
if (process.env.SETTINGS_BLOCKED) {
|
|
process.env.SETTINGS_BLOCKED.split(',').forEach((settingId) => blockedSettings.add(settingId.trim()));
|
|
}
|
|
|
|
if (process.env.SETTINGS_HIDDEN) {
|
|
process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => hiddenSettings.add(settingId.trim()));
|
|
}
|
|
|
|
if (process.env.SETTINGS_REQUIRED_ON_WIZARD) {
|
|
process.env.SETTINGS_REQUIRED_ON_WIZARD.split(',').forEach((settingId) => wizardRequiredSettings.add(settingId.trim()));
|
|
}
|
|
|
|
export const SettingsEvents = new EventEmitter();
|
|
|
|
const overrideSetting = (_id: string, value: SettingValue, options: ISettingAddOptions): SettingValue => {
|
|
const envValue = process.env[_id];
|
|
if (envValue) {
|
|
if (envValue.toLowerCase() === 'true') {
|
|
value = true;
|
|
} else if (envValue.toLowerCase() === 'false') {
|
|
value = false;
|
|
} else if (options.type === 'int') {
|
|
value = parseInt(envValue);
|
|
} else {
|
|
value = envValue;
|
|
}
|
|
options.processEnvValue = value;
|
|
options.valueSource = 'processEnvValue';
|
|
} else if (Meteor.settings[_id] != null && Meteor.settings[_id] !== value) {
|
|
value = Meteor.settings[_id];
|
|
options.meteorSettingsValue = value;
|
|
options.valueSource = 'meteorSettingsValue';
|
|
}
|
|
|
|
const overwriteValue = process.env[`OVERWRITE_SETTING_${ _id }`];
|
|
if (overwriteValue) {
|
|
if (overwriteValue.toLowerCase() === 'true') {
|
|
value = true;
|
|
} else if (overwriteValue.toLowerCase() === 'false') {
|
|
value = false;
|
|
} else if (options.type === 'int') {
|
|
value = parseInt(overwriteValue);
|
|
} else {
|
|
value = overwriteValue;
|
|
}
|
|
options.value = value;
|
|
options.processEnvValue = value;
|
|
options.valueSource = 'processEnvValue';
|
|
}
|
|
|
|
return value;
|
|
};
|
|
|
|
export interface ISettingAddOptions extends Partial<ISetting> {
|
|
force?: boolean;
|
|
actionText?: string;
|
|
code?: 'application/json';
|
|
}
|
|
|
|
export interface ISettingAddGroupOptions {
|
|
hidden?: boolean;
|
|
blocked?: boolean;
|
|
ts?: Date;
|
|
i18nLabel?: string;
|
|
i18nDescription?: string;
|
|
}
|
|
|
|
|
|
interface IUpdateOperator {
|
|
$set: ISettingAddOptions;
|
|
$setOnInsert: ISettingAddOptions & {
|
|
createdAt: Date;
|
|
};
|
|
$unset?: {
|
|
section?: 1;
|
|
tab?: 1;
|
|
};
|
|
}
|
|
|
|
type QueryExpression = {
|
|
$exists: boolean;
|
|
}
|
|
|
|
type Query<T> = {
|
|
[P in keyof T]?: T[P] | QueryExpression;
|
|
}
|
|
|
|
type addSectionCallback = (this: {
|
|
add(id: string, value: SettingValue, options: ISettingAddOptions): void;
|
|
set(options: ISettingAddOptions, cb: addSectionCallback): void;
|
|
}) => void;
|
|
|
|
type addGroupCallback = (this: {
|
|
add(id: string, value: SettingValue, options: ISettingAddOptions): void;
|
|
section(section: string, cb: addSectionCallback): void;
|
|
set(options: ISettingAddOptions, cb: addGroupCallback): void;
|
|
}) => void;
|
|
|
|
class Settings extends SettingsBase {
|
|
private afterInitialLoad: Array<(settings: Meteor.Settings) => void> = [];
|
|
|
|
private _sorter: {[key: string]: number} = {};
|
|
|
|
private initialLoad = false;
|
|
|
|
private validateOptions(_id: string, value: SettingValue, options: ISettingAddOptions): void {
|
|
const sorterKey = options.group && options.section ? `${ options.group }_${ options.section }` : options.group;
|
|
if (sorterKey && this._sorter[sorterKey] == null) {
|
|
if (options.group && options.section) {
|
|
const currentGroupValue = this._sorter[options.group] || 0;
|
|
this._sorter[sorterKey] = currentGroupValue * 1000;
|
|
} else {
|
|
this._sorter[sorterKey] = 0;
|
|
}
|
|
}
|
|
options.packageValue = value;
|
|
options.valueSource = 'packageValue';
|
|
options.hidden = options.hidden || false;
|
|
options.requiredOnWizard = options.requiredOnWizard || false;
|
|
options.secret = options.secret || false;
|
|
options.enterprise = options.enterprise || false;
|
|
|
|
if (options.enterprise && !('invalidValue' in options)) {
|
|
SystemLogger.error(`Enterprise setting ${ _id } is missing the invalidValue option`);
|
|
throw new Error(`Enterprise setting ${ _id } is missing the invalidValue option`);
|
|
}
|
|
|
|
if (sorterKey && options.sorter == null) {
|
|
options.sorter = this._sorter[sorterKey]++;
|
|
}
|
|
if (options.enableQuery != null) {
|
|
options.enableQuery = JSON.stringify(options.enableQuery);
|
|
}
|
|
if (options.displayQuery != null) {
|
|
options.displayQuery = JSON.stringify(options.displayQuery);
|
|
}
|
|
if (options.i18nDescription == null) {
|
|
options.i18nDescription = `${ _id }_Description`;
|
|
}
|
|
if (blockedSettings.has(_id)) {
|
|
options.blocked = true;
|
|
}
|
|
if (hiddenSettings.has(_id)) {
|
|
options.hidden = true;
|
|
}
|
|
if (wizardRequiredSettings.has(_id)) {
|
|
options.requiredOnWizard = true;
|
|
}
|
|
if (options.autocomplete == null) {
|
|
options.autocomplete = true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Add a setting
|
|
*/
|
|
add(_id: string, value: SettingValue, { editor, ...options }: ISettingAddOptions = {}): boolean {
|
|
if (!_id || value == null) {
|
|
return false;
|
|
}
|
|
|
|
this.validateOptions(_id, value, options);
|
|
options.blocked = options.blocked || false;
|
|
if (options.i18nLabel == null) {
|
|
options.i18nLabel = _id;
|
|
}
|
|
|
|
value = overrideSetting(_id, value, options);
|
|
|
|
const updateOperations: IUpdateOperator = {
|
|
$set: options,
|
|
$setOnInsert: {
|
|
createdAt: new Date(),
|
|
},
|
|
};
|
|
if (editor != null) {
|
|
updateOperations.$setOnInsert.editor = editor;
|
|
updateOperations.$setOnInsert.packageEditor = editor;
|
|
}
|
|
|
|
if (options.value == null) {
|
|
if (options.force === true) {
|
|
updateOperations.$set.value = options.packageValue;
|
|
} else {
|
|
updateOperations.$setOnInsert.value = value;
|
|
}
|
|
}
|
|
|
|
const query: Query<ISettingAddOptions> = {
|
|
_id,
|
|
...updateOperations.$set,
|
|
};
|
|
|
|
if (options.section == null) {
|
|
updateOperations.$unset = {
|
|
section: 1,
|
|
};
|
|
query.section = {
|
|
$exists: false,
|
|
};
|
|
}
|
|
|
|
if (!options.tab) {
|
|
updateOperations.$unset = {
|
|
tab: 1,
|
|
};
|
|
query.tab = {
|
|
$exists: false,
|
|
};
|
|
}
|
|
|
|
const existentSetting = SettingsModel.findOne(query);
|
|
if (existentSetting) {
|
|
if (existentSetting.editor || !updateOperations.$setOnInsert.editor) {
|
|
return true;
|
|
}
|
|
|
|
updateOperations.$set.editor = updateOperations.$setOnInsert.editor;
|
|
delete updateOperations.$setOnInsert.editor;
|
|
}
|
|
|
|
updateOperations.$set.ts = new Date();
|
|
|
|
SettingsModel.upsert({
|
|
_id,
|
|
}, updateOperations);
|
|
|
|
const record = {
|
|
_id,
|
|
value,
|
|
type: options.type || 'string',
|
|
env: options.env || false,
|
|
i18nLabel: options.i18nLabel,
|
|
public: options.public || false,
|
|
packageValue: options.packageValue,
|
|
blocked: options.blocked,
|
|
};
|
|
|
|
this.storeSettingValue(record, this.initialLoad);
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Add a setting group
|
|
*/
|
|
addGroup(_id: string, cb?: addGroupCallback): boolean;
|
|
|
|
// eslint-disable-next-line no-dupe-class-members
|
|
addGroup(_id: string, options: ISettingAddGroupOptions | addGroupCallback = {}, cb?: addGroupCallback): boolean {
|
|
if (!_id) {
|
|
return false;
|
|
}
|
|
if (_.isFunction(options)) {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
if (options.i18nLabel == null) {
|
|
options.i18nLabel = _id;
|
|
}
|
|
if (options.i18nDescription == null) {
|
|
options.i18nDescription = `${ _id }_Description`;
|
|
}
|
|
|
|
options.blocked = false;
|
|
options.hidden = false;
|
|
if (blockedSettings.has(_id)) {
|
|
options.blocked = true;
|
|
}
|
|
if (hiddenSettings.has(_id)) {
|
|
options.hidden = true;
|
|
}
|
|
|
|
const existentGroup = SettingsModel.findOne({
|
|
_id,
|
|
type: 'group',
|
|
...options,
|
|
});
|
|
|
|
if (!existentGroup) {
|
|
options.ts = new Date();
|
|
|
|
SettingsModel.upsert({
|
|
_id,
|
|
}, {
|
|
$set: options,
|
|
$setOnInsert: {
|
|
type: 'group',
|
|
createdAt: new Date(),
|
|
},
|
|
});
|
|
}
|
|
|
|
if (cb != null) {
|
|
const addWith = (preset: ISettingAddOptions) => (id: string, value: SettingValue, options: ISettingAddOptions = {}): void => {
|
|
const mergedOptions = Object.assign({}, preset, options);
|
|
this.add(id, value, mergedOptions);
|
|
};
|
|
const sectionSetWith = (preset: ISettingAddOptions) => (options: ISettingAddOptions, cb: addSectionCallback): void => {
|
|
const mergedOptions = Object.assign({}, preset, options);
|
|
cb.call({
|
|
add: addWith(mergedOptions),
|
|
set: sectionSetWith(mergedOptions),
|
|
});
|
|
};
|
|
const sectionWith = (preset: ISettingAddOptions) => (section: string, cb: addSectionCallback): void => {
|
|
const mergedOptions = Object.assign({}, preset, { section });
|
|
cb.call({
|
|
add: addWith(mergedOptions),
|
|
set: sectionSetWith(mergedOptions),
|
|
});
|
|
};
|
|
|
|
const groupSetWith = (preset: ISettingAddOptions) => (options: ISettingAddOptions, cb: addGroupCallback): void => {
|
|
const mergedOptions = Object.assign({}, preset, options);
|
|
|
|
cb.call({
|
|
add: addWith(mergedOptions),
|
|
section: sectionWith(mergedOptions),
|
|
set: groupSetWith(mergedOptions),
|
|
});
|
|
};
|
|
|
|
groupSetWith({ group: _id })({}, cb);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Remove a setting by id
|
|
*/
|
|
removeById(_id: string): boolean {
|
|
if (!_id) {
|
|
return false;
|
|
}
|
|
return SettingsModel.removeById(_id);
|
|
}
|
|
|
|
/*
|
|
* Update a setting by id
|
|
*/
|
|
updateById(_id: string, value: SettingValue, editor?: string): boolean {
|
|
if (!_id || value == null) {
|
|
return false;
|
|
}
|
|
if (editor != null) {
|
|
return SettingsModel.updateValueAndEditorById(_id, value, editor);
|
|
}
|
|
return SettingsModel.updateValueById(_id, value);
|
|
}
|
|
|
|
/*
|
|
* Update options of a setting by id
|
|
*/
|
|
updateOptionsById(_id: string, options: ISettingAddOptions): boolean {
|
|
if (!_id || options == null) {
|
|
return false;
|
|
}
|
|
|
|
return SettingsModel.updateOptionsById(_id, options);
|
|
}
|
|
|
|
/*
|
|
* Update a setting by id
|
|
*/
|
|
clearById(_id: string): boolean {
|
|
if (_id == null) {
|
|
return false;
|
|
}
|
|
return SettingsModel.updateValueById(_id, undefined);
|
|
}
|
|
|
|
/*
|
|
* Change a setting value on the Meteor.settings object
|
|
*/
|
|
storeSettingValue(record: ISetting, initialLoad: boolean): void {
|
|
const newData = {
|
|
value: record.value,
|
|
};
|
|
SettingsEvents.emit('store-setting-value', record, newData);
|
|
const { value } = newData;
|
|
|
|
Meteor.settings[record._id] = value;
|
|
if (record.env === true) {
|
|
process.env[record._id] = String(value);
|
|
}
|
|
|
|
this.load(record._id, value, initialLoad);
|
|
}
|
|
|
|
/*
|
|
* Remove a setting value on the Meteor.settings object
|
|
*/
|
|
removeSettingValue(record: ISetting, initialLoad: boolean): void {
|
|
SettingsEvents.emit('remove-setting-value', record);
|
|
|
|
delete Meteor.settings[record._id];
|
|
if (record.env === true) {
|
|
delete process.env[record._id];
|
|
}
|
|
|
|
this.load(record._id, undefined, initialLoad);
|
|
}
|
|
|
|
/*
|
|
* Update a setting by id
|
|
*/
|
|
init(): void {
|
|
this.initialLoad = true;
|
|
SettingsModel.find().fetch().forEach((record: ISetting) => {
|
|
this.storeSettingValue(record, this.initialLoad);
|
|
updateValue(record._id, { value: record.value });
|
|
});
|
|
this.initialLoad = false;
|
|
this.afterInitialLoad.forEach((fn) => fn(Meteor.settings));
|
|
}
|
|
|
|
onAfterInitialLoad(fn: (settings: Meteor.Settings) => void): void {
|
|
this.afterInitialLoad.push(fn);
|
|
if (this.initialLoad === false) {
|
|
fn(Meteor.settings);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const settings = new Settings();
|
|
|