0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added settings ctrl to v2

refs #9866
This commit is contained in:
kirrg001 2018-10-12 19:44:02 +02:00 committed by Katharina Irrgang
parent 8b54cfea81
commit b899a6fec8
11 changed files with 583 additions and 7 deletions

View file

@ -29,5 +29,9 @@ module.exports = {
get posts() {
return shared.pipeline(require('./posts'), localUtils);
},
get settings() {
return shared.pipeline(require('./settings'), localUtils);
}
};

View file

@ -0,0 +1,177 @@
const Promise = require('bluebird');
const _ = require('lodash');
const moment = require('moment-timezone');
const fs = require('fs-extra');
const path = require('path');
const config = require('../../config');
const models = require('../../models');
const urlService = require('../../services/url');
const common = require('../../lib/common');
const settingsCache = require('../../services/settings/cache');
module.exports = {
docName: 'settings',
browse: {
options: ['type'],
permissions: true,
query(frame) {
let settings = settingsCache.getAll();
// CASE: no context passed (functional call)
if (!frame.options.context) {
return Promise.resolve(settings.filter((setting) => {
return setting.type === 'blog';
}));
}
// CASE: omit core settings unless internal request
if (!frame.options.context.internal) {
settings = _.filter(settings, (setting) => {
return setting.type !== 'core' && setting.key !== 'permalinks';
});
}
return settings;
}
},
read: {
options: ['key'],
validation: {
options: {
key: {
required: true
}
}
},
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;
}
},
query(frame) {
let setting = settingsCache.get(frame.options.key, {resolve: false});
return {
[frame.options.key]: setting
};
}
},
edit: {
headers: {
cacheInvalidate: true
},
permissions: {
before(frame) {
const errors = [];
frame.data.settings.map((setting) => {
if (setting.type === 'core' && !(frame.options.context && frame.options.context.internal)) {
errors.push(new common.errors.NoPermissionError({
message: common.i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
}));
}
});
if (errors.length) {
return Promise.reject(errors[0]);
}
}
},
query(frame) {
let type = frame.data.settings.find((setting) => {
return setting.key === 'type';
});
if (_.isObject(type)) {
type = type.value;
}
frame.data.settings = _.reject(frame.data.settings, (setting) => {
return setting.key === 'type';
});
return models.Settings.edit(frame.data.settings, frame.options);
}
},
upload: {
headers: {
cacheInvalidate: true
},
permissions: {
method: 'edit'
},
query(frame) {
const backupRoutesPath = path.join(config.getContentPath('settings'), `routes-${moment().format('YYYY-MM-DD-HH-mm-ss')}.yaml`);
return fs.copy(`${config.getContentPath('settings')}/routes.yaml`, backupRoutesPath)
.then(() => {
return fs.copy(frame.file.path, `${config.getContentPath('settings')}/routes.yaml`);
})
.then(() => {
urlService.resetGenerators({releaseResourcesOnly: true});
})
.then(() => {
const siteApp = require('../../web/site/app');
try {
return siteApp.reload();
} catch (err) {
// bring back backup, otherwise your Ghost blog is broken
return fs.copy(backupRoutesPath, `${config.getContentPath('settings')}/routes.yaml`)
.then(() => {
return siteApp.reload();
})
.then(() => {
throw err;
});
}
});
}
},
download: {
headers: {
disposition: {
type: 'yaml',
value: 'Attachment; filename="routes.yaml"'
}
},
response: {
format: 'plain'
},
permissions: {
method: 'browse'
},
query() {
const routesPath = path.join(config.getContentPath('settings'), 'routes.yaml');
return fs.readFile(routesPath, 'utf-8')
.catch((err) => {
if (err.code === 'ENOENT') {
return Promise.resolve([]);
}
if (common.errors.utils.isIgnitionError(err)) {
throw err;
}
throw new common.errors.NotFoundError({
err: err
});
});
}
}
};

View file

@ -11,8 +11,8 @@ const nonePublicAuth = (apiConfig, frame) => {
let permissionIdentifier = frame.options.id;
if (apiConfig.permissionIdentifier) {
permissionIdentifier = apiConfig.permissionIdentifier(frame);
if (apiConfig.identifier) {
permissionIdentifier = apiConfig.identifier(frame);
}
const unsafeAttrObject = apiConfig.unsafeAttrs && _.has(frame, `data.[${apiConfig.docName}][0]`) ? _.pick(frame.data[apiConfig.docName][0], apiConfig.unsafeAttrs) : {};

View file

@ -5,5 +5,9 @@ module.exports = {
get posts() {
return require('./posts');
},
get settings() {
return require('./settings');
}
};

View file

@ -0,0 +1,17 @@
const _ = require('lodash');
module.exports = {
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)) {
frame.data = {settings: [{key: frame.data, value: frame.options}]};
}
// prepare data
frame.data.settings.forEach((setting) => {
if (!_.isString(setting.value)) {
setting.value = JSON.stringify(setting.value);
}
});
}
};

View file

@ -17,5 +17,9 @@ module.exports = {
get posts() {
return require('./posts');
},
get settings() {
return require('./settings');
}
};

View file

@ -0,0 +1,51 @@
const _ = require('lodash');
const _private = {};
/**
* ### Settings Filter
* Filters an object based on a given filter object
* @private
* @param {Object} settings
* @param {String} filter
* @returns {*}
*/
_private.settingsFilter = (settings, filter) => {
return _.fromPairs(_.toPairs(settings).filter((setting) => {
if (filter) {
return _.some(filter.split(','), (f) => {
return setting[1].type === f;
});
}
return true;
}));
};
module.exports = {
browse(models, apiConfig, frame) {
let filteredSettings = _.values(_private.settingsFilter(models, frame.options.type));
frame.response = {
settings: filteredSettings,
meta: {}
};
if (frame.options.type) {
frame.response.meta.filters = {
type: frame.options.type
};
}
},
read() {
this.browse(...arguments);
},
edit(models, apiConfig, frame) {
const settingsKeyedJSON = _.keyBy(_.invokeMap(models, 'toJSON'), 'key');
this.browse(settingsKeyedJSON, apiConfig, frame);
},
download(bytes, apiConfig, frame) {
frame.response = bytes;
}
};

View file

@ -1,5 +1,9 @@
module.exports = {
get posts() {
return require('./posts');
},
get settings() {
return require('./settings');
}
};

View file

@ -0,0 +1,54 @@
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') {
return Promise.reject(new common.errors.NotFoundError({
message: common.i18n.t('errors.errors.resourceNotFound')
}));
}
},
edit(apiConfig, frame) {
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') {
// @NOTE: active theme has to be changed via theme endpoints
errors.push(
new common.errors.BadRequestError({
message: common.i18n.t('errors.api.settings.activeThemeSetViaAPI.error'),
help: common.i18n.t('errors.api.settings.activeThemeSetViaAPI.help')
})
);
} else if (settingFromCache.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})
}));
}
});
if (errors.length) {
return Promise.reject(errors[0]);
}
}
};

View file

@ -40,17 +40,17 @@ module.exports = function apiRoutes() {
], api.http(api.schedules.publishPost));
// ## Settings
router.get('/settings/routes/yaml', mw.authAdminAPI, api.http(api.settings.download));
router.get('/settings/routes/yaml', mw.authAdminAPI, apiv2.http(apiv2.settings.download));
router.post('/settings/routes/yaml',
mw.authAdminAPI,
upload.single('routes'),
shared.middlewares.validation.upload({type: 'routes'}),
api.http(api.settings.upload)
apiv2.http(apiv2.settings.upload)
);
router.get('/settings', mw.authAdminAPI, api.http(api.settings.browse));
router.get('/settings/:key', mw.authAdminAPI, api.http(api.settings.read));
router.put('/settings', mw.authAdminAPI, api.http(api.settings.edit));
router.get('/settings', mw.authAdminAPI, apiv2.http(apiv2.settings.browse));
router.get('/settings/:key', mw.authAdminAPI, apiv2.http(apiv2.settings.read));
router.put('/settings', mw.authAdminAPI, apiv2.http(apiv2.settings.edit));
// ## Users
router.get('/users', mw.authAdminAPI, api.http(api.users.browse));

View file

@ -0,0 +1,261 @@
const should = require('should');
const _ = require('lodash');
const supertest = require('supertest');
const os = require('os');
const fs = require('fs-extra');
const config = require('../../../../../../core/server/config');
const testUtils = require('../../../../utils');
const localUtils = require('./utils');
const ghost = testUtils.startGhost;
let request;
describe('Settings API V2', function () {
let ghostServer;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request);
});
});
after(function () {
return ghostServer.stop();
});
it('browse', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse);
testUtils.API.checkResponse(jsonResponse, 'settings');
JSON.parse(_.find(jsonResponse.settings, {key: 'unsplash'}).value).isActive.should.eql(true);
JSON.parse(_.find(jsonResponse.settings, {key: 'amp'}).value).should.eql(true);
should.not.exist(_.find(jsonResponse.settings, {key: 'permalinks'}));
testUtils.API.isISO8601(jsonResponse.settings[0].created_at).should.be.true();
jsonResponse.settings[0].created_at.should.be.an.instanceof(String);
should.not.exist(_.find(jsonResponse.settings, function (setting) {
return setting.type === 'core';
}));
done();
});
});
it('read', function (done) {
request.get(localUtils.API.getApiQuery('settings/title/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
testUtils.API.checkResponseValue(jsonResponse.settings[0], ['id', 'key', 'value', 'type', 'created_at', 'created_by', 'updated_at', 'updated_by']);
jsonResponse.settings[0].key.should.eql('title');
testUtils.API.isISO8601(jsonResponse.settings[0].created_at).should.be.true();
done();
});
});
it('read core setting', function () {
return request
.get(localUtils.API.getApiQuery('settings/db_hash/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403);
});
it('can\'t read permalinks', function (done) {
request.get(localUtils.API.getApiQuery('settings/permalinks/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
it('can\'t read non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/testsetting/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], ['message', 'errorType']);
done();
});
});
it('can edit settings', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
changedValue = [],
settingToChange = {
settings: [
{key: 'title', value: changedValue}
]
};
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
var putBody = res.body;
res.headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody);
putBody.settings[0].value.should.eql(JSON.stringify(changedValue));
testUtils.API.checkResponse(putBody, 'settings');
done();
});
});
});
it('can\'t edit permalinks', function (done) {
const settingToChange = {
settings: [{key: 'permalinks', value: '/:primary_author/:slug/'}]
};
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
it('can\'t edit non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
newValue = 'new value';
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
jsonResponse.settings = [{key: 'testvalue', value: newValue}];
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(jsonResponse)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], ['message', 'errorType']);
done();
});
});
});
it('can download routes.yaml', ()=> {
return request.get(localUtils.API.getApiQuery('settings/routes/yaml/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/yaml')
.expect(200)
.then((res)=> {
res.headers['content-disposition'].should.eql('Attachment; filename="routes.yaml"');
res.headers['content-type'].should.eql('application/yaml; charset=utf-8');
res.headers['content-length'].should.eql('138');
});
});
it('can upload routes.yaml', ()=> {
const newRoutesYamlPath = `${os.tmpdir()}/routes.yaml`;
return fs.writeFile(newRoutesYamlPath, 'routes:\ncollections:\ntaxonomies:\n')
.then(()=> {
return request
.post(localUtils.API.getApiQuery('settings/routes/yaml/'))
.set('Origin', config.get('url'))
.attach('routes', newRoutesYamlPath)
.expect('Content-Type', /application\/json/)
.expect(200);
})
.then((res)=> {
res.headers['x-cache-invalidate'].should.eql('/*');
})
.finally(()=> {
return ghostServer.stop();
});
});
});