mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Migrated redirects controller to API v2 (#10053)
refs #9866 - Migrated redirect routes to use new redirect controller
This commit is contained in:
parent
d3194c95f4
commit
4177548a84
7 changed files with 363 additions and 25 deletions
|
@ -23,6 +23,10 @@ module.exports = {
|
|||
return shared.pipeline(require('./pages'), localUtils);
|
||||
},
|
||||
|
||||
get redirects() {
|
||||
return shared.pipeline(require('./redirects'), localUtils);
|
||||
},
|
||||
|
||||
get roles() {
|
||||
return shared.pipeline(require('./roles'), localUtils);
|
||||
},
|
||||
|
|
98
core/server/api/v2/redirects.js
Normal file
98
core/server/api/v2/redirects.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const Promise = require('bluebird');
|
||||
const moment = require('moment-timezone');
|
||||
const config = require('../../config');
|
||||
const common = require('../../lib/common');
|
||||
const validation = require('../../data/validation');
|
||||
const web = require('../../web');
|
||||
|
||||
const _private = {};
|
||||
|
||||
_private.readRedirectsFile = (customRedirectsPath) => {
|
||||
const redirectsPath = customRedirectsPath || path.join(config.getContentPath('data'), 'redirects.json');
|
||||
|
||||
return fs.readFile(redirectsPath, 'utf-8')
|
||||
.then((content) => {
|
||||
try {
|
||||
content = JSON.parse(content);
|
||||
} catch (err) {
|
||||
throw new common.errors.BadRequestError({
|
||||
message: common.i18n.t('errors.general.jsonParse', {context: err.message})
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
if (common.errors.utils.isIgnitionError(err)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new common.errors.NotFoundError({
|
||||
err: err
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
docName: 'redirects',
|
||||
|
||||
download: {
|
||||
headers: {
|
||||
disposition: {
|
||||
type: 'file',
|
||||
value: 'redirects.json'
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query() {
|
||||
return _private.readRedirectsFile();
|
||||
}
|
||||
},
|
||||
|
||||
upload: {
|
||||
permissions: true,
|
||||
headers: {
|
||||
cacheInvalidate: true
|
||||
},
|
||||
query(frame) {
|
||||
const redirectsPath = path.join(config.getContentPath('data'), 'redirects.json');
|
||||
const backupRedirectsPath = path.join(config.getContentPath('data'), `redirects-${moment().format('YYYY-MM-DD-HH-mm-ss')}.json`);
|
||||
|
||||
return fs.pathExists(redirectsPath)
|
||||
.then((exists) => {
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fs.pathExists(backupRedirectsPath)
|
||||
.then((exists) => {
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fs.unlink(backupRedirectsPath);
|
||||
})
|
||||
.then(() => {
|
||||
return fs.move(redirectsPath, backupRedirectsPath);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return _private.readRedirectsFile(frame.file.path)
|
||||
.then((content) => {
|
||||
validation.validateRedirects(content);
|
||||
return fs.writeFile(redirectsPath, JSON.stringify(content), 'utf-8');
|
||||
})
|
||||
.then(() => {
|
||||
// CASE: trigger that redirects are getting re-registered
|
||||
web.shared.middlewares.customRedirects.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
|
@ -15,6 +15,10 @@ module.exports = {
|
|||
return require('./pages');
|
||||
},
|
||||
|
||||
get redirects() {
|
||||
return require('./redirects');
|
||||
},
|
||||
|
||||
get roles() {
|
||||
return require('./roles');
|
||||
},
|
||||
|
|
5
core/server/api/v2/utils/serializers/output/redirects.js
Normal file
5
core/server/api/v2/utils/serializers/output/redirects.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
download(response, apiConfig, frame) {
|
||||
frame.response = response;
|
||||
}
|
||||
};
|
|
@ -214,12 +214,12 @@ module.exports = function apiRoutes() {
|
|||
router.del('/invites/:id', mw.authAdminApi, apiv2.http(apiv2.invites.destroy));
|
||||
|
||||
// ## Redirects (JSON based)
|
||||
router.get('/redirects/json', mw.authAdminApi, api.http(api.redirects.download));
|
||||
router.get('/redirects/json', mw.authAdminApi, apiv2.http(apiv2.redirects.download));
|
||||
router.post('/redirects/json',
|
||||
mw.authAdminApi,
|
||||
upload.single('redirects'),
|
||||
shared.middlewares.validation.upload({type: 'redirects'}),
|
||||
api.http(api.redirects.upload)
|
||||
apiv2.http(apiv2.redirects.upload)
|
||||
);
|
||||
|
||||
// ## Webhooks (RESTHooks)
|
||||
|
|
|
@ -10,30 +10,25 @@ var should = require('should'),
|
|||
ghost = testUtils.startGhost,
|
||||
request, accesstoken;
|
||||
|
||||
should.equal(true, true);
|
||||
|
||||
describe('Redirects API', function () {
|
||||
var ghostServer;
|
||||
let originalContentPath;
|
||||
|
||||
before(function () {
|
||||
return ghost({redirectsFile: true})
|
||||
.then(function (_ghostServer) {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return localUtils.doAuth(request, 'client:trusted-domain');
|
||||
})
|
||||
.then(function (token) {
|
||||
accesstoken = token;
|
||||
|
||||
originalContentPath = configUtils.config.get('paths:contentPath');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Download', function () {
|
||||
let originalContentPath;
|
||||
|
||||
before(function () {
|
||||
return ghost({redirectsFile: true})
|
||||
.then(function (_ghostServer) {
|
||||
ghostServer = _ghostServer;
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return localUtils.doAuth(request, 'client:trusted-domain');
|
||||
})
|
||||
.then(function (token) {
|
||||
accesstoken = token;
|
||||
|
||||
originalContentPath = configUtils.config.get('paths:contentPath');
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
configUtils.config.set('paths:contentPath', originalContentPath);
|
||||
});
|
||||
|
@ -80,6 +75,8 @@ describe('Redirects API', function () {
|
|||
res.headers['content-type'].should.eql('application/json; charset=utf-8');
|
||||
res.headers['content-length'].should.eql('648');
|
||||
|
||||
res.body.should.not.be.empty();
|
||||
res.body.length.should.equal(11);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -151,8 +148,7 @@ describe('Redirects API', function () {
|
|||
describe('Ensure re-registering redirects works', function () {
|
||||
var startGhost = function (options) {
|
||||
return ghost(options)
|
||||
.then(function (_ghostServer) {
|
||||
ghostServer = _ghostServer;
|
||||
.then(function () {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
|
|
231
core/test/functional/api/v2/admin/redirects_spec.js
Normal file
231
core/test/functional/api/v2/admin/redirects_spec.js
Normal file
|
@ -0,0 +1,231 @@
|
|||
const should = require('should');
|
||||
const supertest = require('supertest');
|
||||
const fs = require('fs-extra');
|
||||
const Promise = require('bluebird');
|
||||
const path = require('path');
|
||||
const testUtils = require('../../../../utils');
|
||||
const localUtils = require('./utils');
|
||||
const configUtils = require('../../../../utils/configUtils');
|
||||
const config = require('../../../../../../core/server/config');
|
||||
|
||||
const ghost = testUtils.startGhost;
|
||||
let request;
|
||||
|
||||
describe('Redirects API', () => {
|
||||
let originalContentPath;
|
||||
|
||||
before(() => {
|
||||
return ghost({redirectsFile: true})
|
||||
.then(() => {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(() => {
|
||||
return localUtils.doAuth(request, 'client:trusted-domain');
|
||||
})
|
||||
.then(() => {
|
||||
originalContentPath = configUtils.config.get('paths:contentPath');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Download', () => {
|
||||
afterEach(() => {
|
||||
configUtils.config.set('paths:contentPath', originalContentPath);
|
||||
});
|
||||
|
||||
it('file does not exist', () => {
|
||||
// Just set any content folder, which does not contain a redirects file.
|
||||
configUtils.set('paths:contentPath', path.join(__dirname, '../../../utils/fixtures/data'));
|
||||
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery('redirects/json/?client_id=ghost-admin&client_secret=not_available'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.headers['content-disposition'].should.eql('Attachment; filename="redirects.json"');
|
||||
res.headers['content-type'].should.eql('application/json; charset=utf-8');
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
|
||||
// API returns an empty file with the correct file structure (empty [])
|
||||
res.headers['content-length'].should.eql('2');
|
||||
});
|
||||
});
|
||||
|
||||
it('file exists', () => {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery('redirects/json/?client_id=ghost-admin&client_secret=not_available'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect('Content-Disposition', 'Attachment; filename="redirects.json"')
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
res.headers['content-disposition'].should.eql('Attachment; filename="redirects.json"');
|
||||
res.headers['content-type'].should.eql('application/json; charset=utf-8');
|
||||
res.headers['content-length'].should.eql('648');
|
||||
|
||||
res.body.should.not.be.empty();
|
||||
res.body.length.should.equal(11);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Upload', () => {
|
||||
describe('Error cases', () => {
|
||||
it('syntax error', () => {
|
||||
fs.writeFileSync(path.join(config.get('paths:contentPath'), 'redirects.json'), 'something');
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery('redirects/json/?client_id=ghost-admin&client_secret=not_available'))
|
||||
.set('Origin', config.get('url'))
|
||||
.attach('redirects', path.join(config.get('paths:contentPath'), 'redirects.json'))
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('wrong format: no array', () => {
|
||||
fs.writeFileSync(path.join(config.get('paths:contentPath'), 'redirects.json'), JSON.stringify({
|
||||
from: 'c',
|
||||
to: 'd'
|
||||
}));
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery('redirects/json/?client_id=ghost-admin&client_secret=not_available'))
|
||||
.set('Origin', config.get('url'))
|
||||
.attach('redirects', path.join(config.get('paths:contentPath'), 'redirects.json'))
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect(422);
|
||||
});
|
||||
|
||||
it('wrong format: no from/to', () => {
|
||||
fs.writeFileSync(path.join(config.get('paths:contentPath'), 'redirects.json'), JSON.stringify([{to: 'd'}]));
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery('redirects/json/?client_id=ghost-admin&client_secret=not_available'))
|
||||
.set('Origin', config.get('url'))
|
||||
.attach('redirects', path.join(config.get('paths:contentPath'), 'redirects.json'))
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect(422);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ensure re-registering redirects works', () => {
|
||||
const startGhost = (options) => {
|
||||
return ghost(options)
|
||||
.then(() => {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(() => {
|
||||
return localUtils.doAuth(request, 'client:trusted-domain');
|
||||
});
|
||||
};
|
||||
|
||||
it('no redirects file exists', () => {
|
||||
return startGhost({redirectsFile: false, forceStart: true})
|
||||
.then(() => {
|
||||
return request
|
||||
.get('/my-old-blog-post/')
|
||||
.expect(404);
|
||||
})
|
||||
.then(() => {
|
||||
// Provide a redirects file in the root directory of the content test folder
|
||||
fs.writeFileSync(path.join(config.get('paths:contentPath'), 'redirects-init.json'), JSON.stringify([{
|
||||
from: 'k',
|
||||
to: 'l'
|
||||
}]));
|
||||
})
|
||||
.then(() => {
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery('redirects/json/?client_id=ghost-admin&client_secret=not_available'))
|
||||
.set('Origin', config.get('url'))
|
||||
.attach('redirects', path.join(config.get('paths:contentPath'), 'redirects-init.json'))
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect(200);
|
||||
})
|
||||
.then((res) => {
|
||||
res.headers['x-cache-invalidate'].should.eql('/*');
|
||||
|
||||
return request
|
||||
.get('/k/')
|
||||
.expect(302);
|
||||
})
|
||||
.then((response) => {
|
||||
response.headers.location.should.eql('/l');
|
||||
|
||||
const dataFiles = fs.readdirSync(config.getContentPath('data'));
|
||||
dataFiles.join(',').match(/(redirects)/g).length.should.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('override', () => {
|
||||
return startGhost({forceStart: true})
|
||||
.then(() => {
|
||||
return request
|
||||
.get('/my-old-blog-post/')
|
||||
.expect(301);
|
||||
})
|
||||
.then((response) => {
|
||||
response.headers.location.should.eql('/revamped-url/');
|
||||
})
|
||||
.then(() => {
|
||||
// Provide a second redirects file in the root directory of the content test folder
|
||||
fs.writeFileSync(path.join(config.get('paths:contentPath'), 'redirects.json'), JSON.stringify([{
|
||||
from: 'c',
|
||||
to: 'd'
|
||||
}]));
|
||||
})
|
||||
.then(() => {
|
||||
// Override redirects file
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery('redirects/json/?client_id=ghost-admin&client_secret=not_available'))
|
||||
.set('Origin', config.get('url'))
|
||||
.attach('redirects', path.join(config.get('paths:contentPath'), 'redirects.json'))
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect(200);
|
||||
})
|
||||
.then((res) => {
|
||||
res.headers['x-cache-invalidate'].should.eql('/*');
|
||||
|
||||
return request
|
||||
.get('/my-old-blog-post/')
|
||||
.expect(404);
|
||||
})
|
||||
.then(() => {
|
||||
return request
|
||||
.get('/c/')
|
||||
.expect(302);
|
||||
})
|
||||
.then((response) => {
|
||||
response.headers.location.should.eql('/d');
|
||||
|
||||
// check backup of redirects files
|
||||
const dataFiles = fs.readdirSync(config.getContentPath('data'));
|
||||
dataFiles.join(',').match(/(redirects)/g).length.should.eql(2);
|
||||
|
||||
// Provide another redirects file in the root directory of the content test folder
|
||||
fs.writeFileSync(path.join(config.get('paths:contentPath'), 'redirects-something.json'), JSON.stringify([{
|
||||
from: 'e',
|
||||
to: 'b'
|
||||
}]));
|
||||
})
|
||||
.then(() => {
|
||||
// the backup is in the format HH:mm:ss, we have to wait minimum a second
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 1100);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Override redirects file again and ensure the backup file works twice
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery('redirects/json/?client_id=ghost-admin&client_secret=not_available'))
|
||||
.set('Origin', config.get('url'))
|
||||
.attach('redirects', path.join(config.get('paths:contentPath'), 'redirects-something.json'))
|
||||
.expect('Content-Type', /application\/json/)
|
||||
.expect(200);
|
||||
})
|
||||
.then(() => {
|
||||
const dataFiles = fs.readdirSync(config.getContentPath('data'));
|
||||
dataFiles.join(',').match(/(redirects)/g).length.should.eql(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue