mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
✨ Added Settings endpoint to V2 Content API
refs #10318 - This settings endpoint returns the commonly used, public information from our settings. - The values are whitelisted each with a custom name for returning from the endpoint
This commit is contained in:
parent
80f9765a35
commit
5d977f23d4
8 changed files with 207 additions and 49 deletions
|
@ -93,5 +93,9 @@ module.exports = {
|
|||
|
||||
get configuration() {
|
||||
return shared.pipeline(require('./configuration'), localUtils);
|
||||
},
|
||||
|
||||
get publicSettings() {
|
||||
return shared.pipeline(require('./settings-public'), localUtils);
|
||||
}
|
||||
};
|
||||
|
|
12
core/server/api/v2/settings-public.js
Normal file
12
core/server/api/v2/settings-public.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
const settingsCache = require('../../services/settings/cache');
|
||||
|
||||
module.exports = {
|
||||
docName: 'settings',
|
||||
|
||||
browse: {
|
||||
permissions: true,
|
||||
query() {
|
||||
return settingsCache.getPublic();
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
const _ = require('lodash');
|
||||
const utils = require('../../index');
|
||||
const _private = {};
|
||||
const deprecatedSettings = ['force_i18n', 'permalinks'];
|
||||
|
||||
|
@ -23,7 +24,13 @@ _private.settingsFilter = (settings, filter) => {
|
|||
|
||||
module.exports = {
|
||||
browse(models, apiConfig, frame) {
|
||||
let filteredSettings = _.values(_private.settingsFilter(models, frame.options.type));
|
||||
let filteredSettings;
|
||||
// If this is public, we already have the right data, we just need to add an Array wrapper
|
||||
if (utils.isContentAPI(frame)) {
|
||||
filteredSettings = models;
|
||||
} else {
|
||||
filteredSettings = _.values(_private.settingsFilter(models, frame.options.type));
|
||||
}
|
||||
|
||||
frame.response = {
|
||||
settings: filteredSettings,
|
||||
|
|
|
@ -1,26 +1,50 @@
|
|||
// It's important to keep the requires absolutely minimal here,
|
||||
// As this cache is used in SO many other areas, we may open ourselves to
|
||||
// circular dependency bugs.
|
||||
var debug = require('ghost-ignition').debug('settings:cache'),
|
||||
_ = require('lodash'),
|
||||
common = require('../../lib/common'),
|
||||
/**
|
||||
* ## Cache
|
||||
* Holds cached settings
|
||||
* Keyed by setting.key
|
||||
* Contains the JSON version of the model
|
||||
* @type {{}} - object of objects
|
||||
*/
|
||||
settingsCache = {},
|
||||
_private = {};
|
||||
const debug = require('ghost-ignition').debug('settings:cache');
|
||||
const _ = require('lodash');
|
||||
const common = require('../../lib/common');
|
||||
const publicSettings = require('./public');
|
||||
|
||||
// Local function, only ever used for initialising
|
||||
// We deliberately call "set" on each model so that set is a consistent interface
|
||||
_private.updateSettingFromModel = function updateSettingFromModel(settingModel) {
|
||||
const updateSettingFromModel = function updateSettingFromModel(settingModel) {
|
||||
debug('Auto updating', settingModel.get('key'));
|
||||
module.exports.set(settingModel.get('key'), settingModel.toJSON());
|
||||
};
|
||||
|
||||
/**
|
||||
* ## Cache
|
||||
* Holds cached settings
|
||||
* Keyed by setting.key
|
||||
* Contains the JSON version of the model
|
||||
* @type {{}} - object of objects
|
||||
*/
|
||||
let settingsCache = {};
|
||||
|
||||
const doGet = (key, options) => {
|
||||
if (!settingsCache[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't try to resolve to the value of the setting
|
||||
if (options && options.resolve === false) {
|
||||
return settingsCache[key];
|
||||
}
|
||||
|
||||
// Default behaviour is to try to resolve the value and return that
|
||||
try {
|
||||
// CASE: if a string contains a number e.g. "1", JSON.parse will auto convert into integer
|
||||
if (!isNaN(Number(settingsCache[key].value))) {
|
||||
return settingsCache[key].value;
|
||||
}
|
||||
|
||||
return JSON.parse(settingsCache[key].value);
|
||||
} catch (err) {
|
||||
return settingsCache[key].value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* IMPORTANT:
|
||||
|
@ -45,27 +69,8 @@ module.exports = {
|
|||
* @param {object} options
|
||||
* @return {*}
|
||||
*/
|
||||
get: function get(key, options) {
|
||||
if (!settingsCache[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't try to resolve to the value of the setting
|
||||
if (options && options.resolve === false) {
|
||||
return settingsCache[key];
|
||||
}
|
||||
|
||||
// Default behaviour is to try to resolve the value and return that
|
||||
try {
|
||||
// CASE: if a string contains a number e.g. "1", JSON.parse will auto convert into integer
|
||||
if (!isNaN(Number(settingsCache[key].value))) {
|
||||
return settingsCache[key].value;
|
||||
}
|
||||
|
||||
return JSON.parse(settingsCache[key].value);
|
||||
} catch (err) {
|
||||
return settingsCache[key].value;
|
||||
}
|
||||
get(key, options) {
|
||||
return doGet(key, options);
|
||||
},
|
||||
/**
|
||||
* Set a key on the cache
|
||||
|
@ -74,7 +79,7 @@ module.exports = {
|
|||
* @param {string} key
|
||||
* @param {object} value json version of settings model
|
||||
*/
|
||||
set: function set(key, value) {
|
||||
set(key, value) {
|
||||
settingsCache[key] = _.cloneDeep(value);
|
||||
},
|
||||
/**
|
||||
|
@ -82,9 +87,24 @@ module.exports = {
|
|||
* Uses clone to prevent modifications from being reflected
|
||||
* @return {{}} cache
|
||||
*/
|
||||
getAll: function getAll() {
|
||||
getAll() {
|
||||
return _.cloneDeep(settingsCache);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all the publically accessible cache entries with their correct names
|
||||
* Uses clone to prevent modifications from being reflected
|
||||
* @return {{}} cache
|
||||
*/
|
||||
getPublic() {
|
||||
let settings = {};
|
||||
|
||||
_.each(publicSettings, (newKey, key) => {
|
||||
settings[newKey] = doGet(key) || '';
|
||||
});
|
||||
|
||||
return settings;
|
||||
},
|
||||
/**
|
||||
* Initialise the cache
|
||||
*
|
||||
|
@ -93,27 +113,27 @@ module.exports = {
|
|||
* @param {Bookshelf.Collection<Settings>} [settingsCollection]
|
||||
* @return {{}}
|
||||
*/
|
||||
init: function init(settingsCollection) {
|
||||
init(settingsCollection) {
|
||||
// First, reset the cache
|
||||
settingsCache = {};
|
||||
|
||||
// // if we have been passed a collection of settings, use this to populate the cache
|
||||
if (settingsCollection && settingsCollection.models) {
|
||||
_.each(settingsCollection.models, _private.updateSettingFromModel);
|
||||
_.each(settingsCollection.models, updateSettingFromModel);
|
||||
}
|
||||
|
||||
// Bind to events to automatically keep up-to-date
|
||||
common.events.on('settings.edited', _private.updateSettingFromModel);
|
||||
common.events.on('settings.added', _private.updateSettingFromModel);
|
||||
common.events.on('settings.deleted', _private.updateSettingFromModel);
|
||||
common.events.on('settings.edited', updateSettingFromModel);
|
||||
common.events.on('settings.added', updateSettingFromModel);
|
||||
common.events.on('settings.deleted', updateSettingFromModel);
|
||||
|
||||
return settingsCache;
|
||||
},
|
||||
|
||||
shutdown: function () {
|
||||
common.events.removeListener('settings.edited', _private.updateSettingFromModel);
|
||||
common.events.removeListener('settings.added', _private.updateSettingFromModel);
|
||||
common.events.removeListener('settings.deleted', _private.updateSettingFromModel);
|
||||
shutdown() {
|
||||
common.events.removeListener('settings.edited', updateSettingFromModel);
|
||||
common.events.removeListener('settings.added', updateSettingFromModel);
|
||||
common.events.removeListener('settings.deleted', updateSettingFromModel);
|
||||
},
|
||||
|
||||
reset() {
|
||||
|
|
22
core/server/services/settings/public.js
Normal file
22
core/server/services/settings/public.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* The settings with type "blog" were originally meant to be public
|
||||
* This has been misused - unsplash and slack are incorrectly stored there
|
||||
* https://github.com/TryGhost/Ghost/issues/10318
|
||||
*
|
||||
* This file acts as a new whitelist for "public" settings
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
logo: 'logo',
|
||||
icon: 'icon',
|
||||
cover_image: 'cover_image',
|
||||
facebook: 'facebook',
|
||||
twitter: 'twitter',
|
||||
default_locale: 'lang',
|
||||
active_timezone: 'timezone',
|
||||
ghost_head: 'ghost_head',
|
||||
ghost_foot: 'ghost_foot',
|
||||
navigation: 'navigation'
|
||||
};
|
|
@ -28,5 +28,8 @@ module.exports = function apiRoutes() {
|
|||
router.get('/tags/:id', mw.authenticatePublic, apiv2.http(apiv2.tagsPublic.read));
|
||||
router.get('/tags/slug/:slug', mw.authenticatePublic, apiv2.http(apiv2.tagsPublic.read));
|
||||
|
||||
// ## Settings
|
||||
router.get('/settings', mw.authenticatePublic, apiv2.http(apiv2.publicSettings.browse));
|
||||
|
||||
return router;
|
||||
};
|
||||
|
|
60
core/test/functional/api/v2/content/settings_spec.js
Normal file
60
core/test/functional/api/v2/content/settings_spec.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
const should = require('should');
|
||||
const supertest = require('supertest');
|
||||
const _ = require('lodash');
|
||||
const testUtils = require('../../../../utils');
|
||||
const localUtils = require('./utils');
|
||||
const config = require('../../../../../server/config');
|
||||
|
||||
// Values to test against
|
||||
const publicSettings = require('../../../../../server/services/settings/public');
|
||||
const defaultSettings = require('../../../../../server/data/schema').defaultSettings.blog;
|
||||
|
||||
const ghost = testUtils.startGhost;
|
||||
let request;
|
||||
|
||||
describe('Settings', function () {
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then(function () {
|
||||
request = supertest.agent(config.get('url'));
|
||||
}).then(function () {
|
||||
return testUtils.initFixtures('api_keys');
|
||||
});
|
||||
});
|
||||
|
||||
it('browse settings', function () {
|
||||
const key = localUtils.getValidKey();
|
||||
return request.get(localUtils.API.getApiQuery(`settings/?key=${key}`))
|
||||
.set('Origin', testUtils.API.getURL())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.headers.vary.should.eql('Accept-Encoding');
|
||||
should.exist(res.headers['access-control-allow-origin']);
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse.settings);
|
||||
should.exist(jsonResponse.meta);
|
||||
|
||||
jsonResponse.settings.should.be.an.Object();
|
||||
const settings = jsonResponse.settings;
|
||||
|
||||
// Verify we have the right keys for settings
|
||||
settings.should.have.properties(_.values(publicSettings));
|
||||
|
||||
// Verify that we are returning the defaults for each value
|
||||
_.forEach(settings, (value, key) => {
|
||||
let defaultKey = _.findKey(publicSettings, (v) => v === key);
|
||||
let defaultValue = _.find(defaultSettings, (setting) => setting.key === defaultKey).defaultValue;
|
||||
|
||||
if (defaultKey === 'navigation') {
|
||||
defaultValue = JSON.parse(defaultValue);
|
||||
}
|
||||
|
||||
value.should.eql(defaultValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,10 +1,16 @@
|
|||
var rewire = require('rewire'),
|
||||
should = require('should'),
|
||||
cache = rewire('../../../../server/services/settings/cache');
|
||||
const rewire = require('rewire');
|
||||
const should = require('should');
|
||||
const _ = require('lodash');
|
||||
const publicSettings = require('../../../../server/services/settings/public');
|
||||
let cache = rewire('../../../../server/services/settings/cache');
|
||||
|
||||
should.equal(true, true);
|
||||
|
||||
describe('UNIT: settings cache', function () {
|
||||
beforeEach(function () {
|
||||
cache = rewire('../../../../server/services/settings/cache');
|
||||
});
|
||||
|
||||
it('does not auto convert string into number', function () {
|
||||
cache.set('key1', {value: '1'});
|
||||
(typeof cache.get('key1')).should.eql('string');
|
||||
|
@ -23,4 +29,28 @@ describe('UNIT: settings cache', function () {
|
|||
cache.get('key2').c.should.eql({d: []});
|
||||
cache.get('key2').e.should.eql(2);
|
||||
});
|
||||
|
||||
it('can get all values', function () {
|
||||
cache.set('key1', {value: '1'});
|
||||
cache.get('key1').should.eql('1');
|
||||
cache.getAll().should.eql({key1: {value: '1'}});
|
||||
});
|
||||
|
||||
it('correctly filters and formats public values', function () {
|
||||
cache.set('key1', {value: 'something'});
|
||||
cache.set('title', {value: 'hello world'});
|
||||
cache.set('active_timezone', {value: 'PST'});
|
||||
|
||||
cache.getAll().should.eql({
|
||||
key1: {value: 'something'},
|
||||
title: {value: 'hello world'},
|
||||
active_timezone: {value: 'PST'}
|
||||
});
|
||||
|
||||
let values = _.zipObject(_.values(publicSettings), _.fill(Array(_.size(publicSettings)), ''));
|
||||
values.title = 'hello world';
|
||||
values.timezone = 'PST';
|
||||
|
||||
cache.getPublic().should.eql(values);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue