Ghost/core/server/models/settings.js
Katharina Irrgang 04c60b4ce1
🐛 Fixed private blogging getting enabled when saving any setting (#10576)
no issue

- Reported here: https://forum.ghost.org/t/in-version-2-16-3-found-bug/6065/3

---

Admin Client sends false or true booleans for `is_private` key.

The settings table has two columns "key" and "value". And "value" is always type TEXT.

If you pass value=false, the db will transform this value into "0".
`settingsCache.get('is_private')` is then always true, even though the value is meant to be false.

We should add a migration in v3 and normalize all setting values to ensure consistent database values. Furthermore, we should improve the handling around settings values in general.

For now, we protect parsing values from DB, which we anyway need to transform the values into the correct data type, because we always save strings. This will protect values being stored as "false" or "1" or whatever.
2019-03-06 12:56:26 +01:00

223 lines
7.5 KiB
JavaScript

const Promise = require('bluebird'),
_ = require('lodash'),
uuid = require('uuid'),
crypto = require('crypto'),
keypair = require('keypair'),
ghostBookshelf = require('./base'),
common = require('../lib/common'),
validation = require('../data/validation'),
internalContext = {context: {internal: true}};
let Settings, defaultSettings;
// For neatness, the defaults file is split into categories.
// It's much easier for us to work with it as a single level
// instead of iterating those categories every time
function parseDefaultSettings() {
var defaultSettingsInCategories = require('../data/schema/').defaultSettings,
defaultSettingsFlattened = {},
dynamicDefault = {
db_hash: uuid.v4(),
public_hash: crypto.randomBytes(15).toString('hex'),
session_secret: crypto.randomBytes(32).toString('hex'),
members_session_secret: crypto.randomBytes(32).toString('hex')
};
const membersKeypair = keypair({
bits: 1024
});
dynamicDefault.members_public_key = membersKeypair.public;
dynamicDefault.members_private_key = membersKeypair.private;
_.each(defaultSettingsInCategories, function each(settings, categoryName) {
_.each(settings, function each(setting, settingName) {
setting.type = categoryName;
setting.key = settingName;
if (dynamicDefault[setting.key]) {
setting.defaultValue = dynamicDefault[setting.key];
}
defaultSettingsFlattened[settingName] = setting;
});
});
return defaultSettingsFlattened;
}
function getDefaultSettings() {
if (!defaultSettings) {
defaultSettings = parseDefaultSettings();
}
return defaultSettings;
}
// Each setting is saved as a separate row in the database,
// but the overlying API treats them as a single key:value mapping
Settings = ghostBookshelf.Model.extend({
tableName: 'settings',
defaults: function defaults() {
return {
type: 'core'
};
},
emitChange: function emitChange(event, options) {
const eventToTrigger = 'settings' + '.' + event;
ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
},
onDestroyed: function onDestroyed(model, options) {
ghostBookshelf.Model.prototype.onDestroyed.apply(this, arguments);
model.emitChange('deleted', options);
model.emitChange(model._previousAttributes.key + '.' + 'deleted', options);
},
onCreated: function onCreated(model, response, options) {
ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
model.emitChange('added', options);
model.emitChange(model.attributes.key + '.' + 'added', options);
},
onUpdated: function onUpdated(model, response, options) {
ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
model.emitChange('edited', options);
model.emitChange(model.attributes.key + '.' + 'edited', options);
},
onValidate: function onValidate() {
var self = this;
return ghostBookshelf.Model.prototype.onValidate.apply(this, arguments)
.then(function then() {
return validation.validateSettings(getDefaultSettings(), self);
});
},
parse() {
const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);
// transform "0" to false
// transform "false" to false
// transform "null" to null
if (attrs.value === '0' || attrs.value === '1') {
attrs.value = !!+attrs.value;
}
if (attrs.value === 'false' || attrs.value === 'true') {
attrs.value = JSON.parse(attrs.value);
}
if (attrs.value === 'null') {
attrs.value = null;
}
return attrs;
}
}, {
findOne: function (data, options) {
if (_.isEmpty(data)) {
options = data;
}
// Allow for just passing the key instead of attributes
if (!_.isObject(data)) {
data = {key: data};
}
return Promise.resolve(ghostBookshelf.Model.findOne.call(this, data, options));
},
edit: function (data, unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'edit'),
self = this;
if (!Array.isArray(data)) {
data = [data];
}
return Promise.map(data, function (item) {
// Accept an array of models as input
if (item.toJSON) {
item = item.toJSON();
}
if (!(_.isString(item.key) && item.key.length > 0)) {
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.models.settings.valueCannotBeBlank')}));
}
item = self.filterData(item);
return Settings.forge({key: item.key}).fetch(options).then(function then(setting) {
if (setting) {
// it's allowed to edit all attributes in case of importing/migrating
if (options.importing) {
return setting.save(item, options);
} else {
// If we have a value, set it.
if (item.hasOwnProperty('value')) {
setting.set('value', item.value);
}
// Internal context can overwrite type (for fixture migrations)
if (options.context && options.context.internal && item.hasOwnProperty('type')) {
setting.set('type', item.type);
}
// If anything has changed, save the updated model
if (setting.hasChanged()) {
return setting.save(null, options);
}
return setting;
}
}
return Promise.reject(new common.errors.NotFoundError({message: common.i18n.t('errors.models.settings.unableToFindSetting', {key: item.key})}));
});
});
},
populateDefaults: function populateDefaults(unfilteredOptions) {
var options = this.filterOptions(unfilteredOptions, 'populateDefaults'),
self = this;
if (!options.context) {
options.context = internalContext.context;
}
return this
.findAll(options)
.then(function checkAllSettings(allSettings) {
var usedKeys = allSettings.models.map(function mapper(setting) {
return setting.get('key');
}),
insertOperations = [];
_.each(getDefaultSettings(), function forEachDefault(defaultSetting, defaultSettingKey) {
var isMissingFromDB = usedKeys.indexOf(defaultSettingKey) === -1;
if (isMissingFromDB) {
defaultSetting.value = defaultSetting.defaultValue;
insertOperations.push(Settings.forge(defaultSetting).save(null, options));
}
});
if (insertOperations.length > 0) {
return Promise.all(insertOperations).then(function fetchAllToReturn() {
return self.findAll(options);
});
}
return allSettings;
});
}
});
module.exports = {
Settings: ghostBookshelf.model('Settings', Settings)
};