diff --git a/core/server/api/shared/frame.js b/core/server/api/shared/frame.js index 1ec0d73778..709fa47781 100644 --- a/core/server/api/shared/frame.js +++ b/core/server/api/shared/frame.js @@ -41,7 +41,7 @@ class Frame { this.options.context = this.original.context; if (this.original.body && Object.keys(this.original.body).length) { - this.data = this.original.body; + this.data = _.cloneDeep(this.original.body); } else { if (apiConfig.data) { if (typeof apiConfig.data === 'function') { diff --git a/core/server/api/v2/settings.js b/core/server/api/v2/settings.js index e37598c7fc..d78ed58456 100644 --- a/core/server/api/v2/settings.js +++ b/core/server/api/v2/settings.js @@ -46,15 +46,6 @@ module.exports = { } }, permissions: { - before(frame) { - let setting = settingsCache.get(frame.options.key, {resolve: false}); - - if (setting.type === 'core' && !(frame.options.context && frame.options.context.internal)) { - return Promise.reject(new common.errors.NoPermissionError({ - message: common.i18n.t('errors.api.settings.accessCoreSettingFromExtReq') - })); - } - }, identifier(frame) { return frame.options.key; } @@ -62,6 +53,21 @@ module.exports = { query(frame) { let setting = settingsCache.get(frame.options.key, {resolve: false}); + if (!setting) { + return Promise.reject(new common.errors.NotFoundError({ + message: common.i18n.t('errors.api.settings.problemFindingSetting', { + key: frame.options.key + }) + })); + } + + // @TODO: handle in settings model permissible fn + if (setting.type === 'core' && !(frame.options.context && frame.options.context.internal)) { + return Promise.reject(new common.errors.NoPermissionError({ + message: common.i18n.t('errors.api.settings.accessCoreSettingFromExtReq') + })); + } + return { [frame.options.key]: setting }; @@ -102,6 +108,29 @@ module.exports = { return setting.key === 'type'; }); + const errors = []; + + _.each(frame.data.settings, (setting) => { + const settingFromCache = settingsCache.get(setting.key, {resolve: false}); + + if (!settingFromCache) { + errors.push(new common.errors.NotFoundError({ + message: common.i18n.t('errors.api.settings.problemFindingSetting', { + key: setting.key + }) + })); + } else if (settingFromCache.type === 'core' && !(frame.options.context && frame.options.context.internal)) { + // @TODO: handle in settings model permissible fn + errors.push(new common.errors.NoPermissionError({ + message: common.i18n.t('errors.api.settings.accessCoreSettingFromExtReq') + })); + } + }); + + if (errors.length) { + return Promise.reject(errors[0]); + } + return models.Settings.edit(frame.data.settings, frame.options); } }, diff --git a/core/server/api/v2/utils/serializers/input/settings.js b/core/server/api/v2/utils/serializers/input/settings.js index fc2c036a52..555cc1306e 100644 --- a/core/server/api/v2/utils/serializers/input/settings.js +++ b/core/server/api/v2/utils/serializers/input/settings.js @@ -1,6 +1,16 @@ const _ = require('lodash'); module.exports = { + read(apiConfig, frame) { + if (frame.options.key === 'codeinjection_head') { + frame.options.key = 'ghost_head'; + } + + if (frame.options.key === 'codeinjection_foot') { + frame.options.key = 'ghost_foot'; + } + }, + edit(apiConfig, frame) { // CASE: allow shorthand syntax where a single key and value are passed to edit instead of object and options if (_.isString(frame.data)) { @@ -8,11 +18,19 @@ module.exports = { } // CASE: transform objects/arrays into string (we store stringified objects in the db) - // @TODO: This belongs into the model layer? frame.data.settings.forEach((setting) => { + // @TODO: This belongs into the model layer? if (_.isObject(setting.value)) { setting.value = JSON.stringify(setting.value); } + + if (setting.key === 'codeinjection_head') { + setting.key = 'ghost_head'; + } + + if (setting.key === 'codeinjection_foot') { + setting.key = 'ghost_foot'; + } }); } }; diff --git a/core/server/api/v2/utils/serializers/output/settings.js b/core/server/api/v2/utils/serializers/output/settings.js index 21cc456167..f750a92e9e 100644 --- a/core/server/api/v2/utils/serializers/output/settings.js +++ b/core/server/api/v2/utils/serializers/output/settings.js @@ -35,7 +35,7 @@ module.exports = { } frame.response = { - settings: mapper.mapSettings(filteredSettings), + settings: mapper.mapSettings(filteredSettings, frame), meta: {} }; diff --git a/core/server/api/v2/utils/serializers/output/utils/extra-attrs.js b/core/server/api/v2/utils/serializers/output/utils/extra-attrs.js index 5f3ace9fba..fed7e44eeb 100644 --- a/core/server/api/v2/utils/serializers/output/utils/extra-attrs.js +++ b/core/server/api/v2/utils/serializers/output/utils/extra-attrs.js @@ -18,12 +18,49 @@ module.exports.forPost = (frame, model, attrs) => { }; // @NOTE: ghost_head & ghost_foot are deprecated, remove in Ghost 3.0 -module.exports.forSettings = (attrs) => { +module.exports.forSettings = (attrs, frame) => { const _ = require('lodash'); // @TODO: https://github.com/TryGhost/Ghost/issues/10106 // @NOTE: Admin & Content API return a different format, need to mappers if (_.isArray(attrs)) { + // CASE: read single setting + if (frame.original.params && frame.original.params.key) { + if (frame.original.params.key === 'ghost_head') { + return; + } + + if (frame.original.params.key === 'ghost_foot') { + return; + } + + if (frame.original.params.key === 'codeinjection_head') { + attrs[0].key = 'codeinjection_head'; + return; + } + + if (frame.original.params.key === 'codeinjection_foot') { + attrs[0].key = 'codeinjection_foot'; + return; + } + } + + // CASE: edit + if (frame.original.body && frame.original.body.settings) { + frame.original.body.settings.forEach((setting, index) => { + if (setting.key === 'codeinjection_head') { + attrs[index].key = 'codeinjection_head'; + } + + if (setting.key === 'codeinjection_foot') { + attrs[index].key = 'codeinjection_foot'; + } + }); + + return; + } + + // CASE: browse all settings, add extra keys and keep deprecated const ghostHead = _.cloneDeep(_.find(attrs, {key: 'ghost_head'})); const ghostFoot = _.cloneDeep(_.find(attrs, {key: 'ghost_foot'})); diff --git a/core/server/api/v2/utils/serializers/output/utils/mapper.js b/core/server/api/v2/utils/serializers/output/utils/mapper.js index 2d6e382dc4..af4cbefcf8 100644 --- a/core/server/api/v2/utils/serializers/output/utils/mapper.js +++ b/core/server/api/v2/utils/serializers/output/utils/mapper.js @@ -55,9 +55,9 @@ const mapPost = (model, frame) => { return jsonModel; }; -const mapSettings = (attrs) => { +const mapSettings = (attrs, frame) => { url.forSettings(attrs); - extraAttrs.forSettings(attrs); + extraAttrs.forSettings(attrs, frame); return attrs; }; diff --git a/core/server/api/v2/utils/validators/input/settings.js b/core/server/api/v2/utils/validators/input/settings.js index f9c9f1fcf2..a65e05723c 100644 --- a/core/server/api/v2/utils/validators/input/settings.js +++ b/core/server/api/v2/utils/validators/input/settings.js @@ -1,20 +1,11 @@ const Promise = require('bluebird'); const _ = require('lodash'); const common = require('../../../../../lib/common'); -const settingsCache = require('../../../../../services/settings/cache'); module.exports = { read(apiConfig, frame) { - let setting = settingsCache.get(frame.options.key, {resolve: false}); - - if (!setting) { - return Promise.reject(new common.errors.NotFoundError({ - message: common.i18n.t('errors.api.settings.problemFindingSetting', {key: frame.options.key}) - })); - } - // @NOTE: was removed (https://github.com/TryGhost/Ghost/commit/8bb7088ba026efd4a1c9cf7d6f1a5e9b4fa82575) - if (setting.key === 'permalinks') { + if (frame.options.key === 'permalinks') { return Promise.reject(new common.errors.NotFoundError({ message: common.i18n.t('errors.errors.resourceNotFound') })); @@ -25,13 +16,7 @@ module.exports = { const errors = []; _.each(frame.data.settings, (setting) => { - const settingFromCache = settingsCache.get(setting.key, {resolve: false}); - - if (!settingFromCache) { - errors.push(new common.errors.NotFoundError({ - message: common.i18n.t('errors.api.settings.problemFindingSetting', {key: setting.key}) - })); - } else if (settingFromCache.key === 'active_theme') { + if (setting.key === 'active_theme') { // @NOTE: active theme has to be changed via theme endpoints errors.push( new common.errors.BadRequestError({ @@ -39,7 +24,7 @@ module.exports = { help: common.i18n.t('errors.api.settings.activeThemeSetViaAPI.help') }) ); - } else if (settingFromCache.key === 'permalinks') { + } else if (setting.key === 'permalinks') { // @NOTE: was removed (https://github.com/TryGhost/Ghost/commit/8bb7088ba026efd4a1c9cf7d6f1a5e9b4fa82575) errors.push(new common.errors.NotFoundError({ message: common.i18n.t('errors.api.settings.problemFindingSetting', {key: setting.key}) diff --git a/core/test/acceptance/old/admin/settings_spec.js b/core/test/acceptance/old/admin/settings_spec.js index bd1afd43cb..5f36547ab3 100644 --- a/core/test/acceptance/old/admin/settings_spec.js +++ b/core/test/acceptance/old/admin/settings_spec.js @@ -60,7 +60,7 @@ describe('Settings API', function () { }); it('Can read a setting', function (done) { - request.get(localUtils.API.getApiQuery('settings/title/')) + request.get(localUtils.API.getApiQuery('settings/ghost_head/')) .set('Origin', config.get('url')) .expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) @@ -71,13 +71,15 @@ describe('Settings API', function () { } should.not.exist(res.headers['x-cache-invalidate']); - var jsonResponse = res.body; + const jsonResponse = res.body; should.exist(jsonResponse); should.exist(jsonResponse.settings); + jsonResponse.settings.length.should.eql(1); + testUtils.API.checkResponseValue(jsonResponse.settings[0], ['id', 'key', 'value', 'type', 'created_at', 'updated_at']); - jsonResponse.settings[0].key.should.eql('title'); + jsonResponse.settings[0].key.should.eql('ghost_head'); testUtils.API.isISO8601(jsonResponse.settings[0].created_at).should.be.true(); done(); }); @@ -102,7 +104,7 @@ describe('Settings API', function () { value: changedValue }, { - key: 'ghost_head', + key: 'codeinjection_head', value: null }, { @@ -133,10 +135,19 @@ describe('Settings API', function () { const putBody = res.body; res.headers['x-cache-invalidate'].should.eql('/*'); should.exist(putBody); + + putBody.settings[0].key.should.eql('title'); putBody.settings[0].value.should.eql(JSON.stringify(changedValue)); + + putBody.settings[1].key.should.eql('codeinjection_head'); should.equal(putBody.settings[1].value, null); + + putBody.settings[2].key.should.eql('navigation'); should.equal(putBody.settings[2].value, JSON.stringify({label: 'label1'})); + + putBody.settings[3].key.should.eql('slack'); should.equal(putBody.settings[3].value, JSON.stringify({username: 'username'})); + localUtils.API.checkResponse(putBody, 'settings'); done(); });