From 75745c2b3bfe4eecf298f017acdef3d040a7170d Mon Sep 17 00:00:00 2001 From: Austin Burdine Date: Tue, 19 May 2015 20:00:27 -0600 Subject: [PATCH] refactor permalink verification in single post controller closes #4322 - removes verifying "sections" of permalinks in favor of checking the url returned with the post - fixes unit tests to define post.url in mock post requests --- core/server/controllers/frontend.js | 81 +++++----------- core/test/unit/frontend_spec.js | 144 +++++++++++++++------------- 2 files changed, 101 insertions(+), 124 deletions(-) diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js index 7af96a89a8..3a6ed32ea3 100644 --- a/core/server/controllers/frontend.js +++ b/core/server/controllers/frontend.js @@ -4,23 +4,19 @@ /*global require, module */ -var moment = require('moment'), - rss = require('../data/xml/rss'), - _ = require('lodash'), - Promise = require('bluebird'), +var _ = require('lodash'), api = require('../api'), - config = require('../config'), - filters = require('../filters'), - template = require('../helpers/template'), - errors = require('../errors'), - routeMatch = require('path-match')(), + rss = require('../data/xml/rss'), path = require('path'), + config = require('../config'), + errors = require('../errors'), + filters = require('../filters'), + Promise = require('bluebird'), + template = require('../helpers/template'), + routeMatch = require('path-match')(), frontendControllers, - staticPostPermalink; - -// Cache static post permalink regex -staticPostPermalink = routeMatch('/:slug/:edit?'); + staticPostPermalink = routeMatch('/:slug/:edit?'); function getPostPage(options) { return api.settings.read('postsPerPage').then(function (response) { @@ -277,21 +273,21 @@ frontendControllers = { }, single: function (req, res, next) { - var path = req.path, + var postPath = req.path, params, usingStaticPermalink = false; api.settings.read('permalinks').then(function (response) { - var permalink = response.settings[0], + var permalink = response.settings[0].value, editFormat, postLookup, match; - editFormat = permalink.value[permalink.value.length - 1] === '/' ? ':edit?' : '/:edit?'; + editFormat = permalink.substr(permalink.length - 1) === '/' ? ':edit?' : '/:edit?'; // Convert saved permalink into a path-match function - permalink = routeMatch(permalink.value + editFormat); - match = permalink(path); + permalink = routeMatch(permalink + editFormat); + match = permalink(postPath); // Check if the path matches the permalink structure. // @@ -299,7 +295,7 @@ frontendControllers = { // need to verify it's not a static post, // and test against that permalink structure. if (match === false) { - match = staticPostPermalink(path); + match = staticPostPermalink(postPath); // If there are still no matches then return. if (match === false) { // Reject promise chain with type 'NotFound' @@ -320,8 +316,7 @@ frontendControllers = { return api.posts.read(postLookup); }).then(function (result) { var post = result.posts[0], - slugDate = [], - slugFormat = []; + postUrl = (params.edit) ? postPath.replace(params.edit + '/', '') : postPath; if (!post) { return next(); @@ -352,49 +347,17 @@ frontendControllers = { if (post.page) { return render(); } - return next(); } - // If there is an author parameter in the slug, check that the - // post is actually written by the given author\ - if (params.author) { - if (post.author.slug === params.author) { - return render(); - } + // Check if the url provided with the post object matches req.path + // If it does, render the post + // If not, return 404 + if (post.url && post.url === postUrl) { + return render(); + } else { return next(); } - - // If there is any date based parameter in the slug - // we will check it against the post published date - // to verify it's correct. - if (params.year || params.month || params.day) { - if (params.year) { - slugDate.push(params.year); - slugFormat.push('YYYY'); - } - - if (params.month) { - slugDate.push(params.month); - slugFormat.push('MM'); - } - - if (params.day) { - slugDate.push(params.day); - slugFormat.push('DD'); - } - - slugDate = slugDate.join('/'); - slugFormat = slugFormat.join('/'); - - if (slugDate === moment(post.published_at).format(slugFormat)) { - return render(); - } - - return next(); - } - - return render(); }).catch(function (err) { // If we've thrown an error message // of type: 'NotFound' then we found diff --git a/core/test/unit/frontend_spec.js b/core/test/unit/frontend_spec.js index cd6a91cde9..dc263b943c 100644 --- a/core/test/unit/frontend_spec.js +++ b/core/test/unit/frontend_spec.js @@ -534,7 +534,8 @@ describe('Frontend Controller', function () { name: 'Test User', slug: 'test', email: 'test@ghost.org' - } + }, + url: '/test-static-page/' }] }, { posts: [{ @@ -566,7 +567,8 @@ describe('Frontend Controller', function () { name: 'Test User', slug: 'test', email: 'test@ghost.org' - } + }, + url: '/about/' }] }]; @@ -620,7 +622,7 @@ describe('Frontend Controller', function () { it('it will render custom page template if it exists', function (done) { var req = { - path: '/' + mockPosts[2].posts[0].slug, + path: '/' + mockPosts[2].posts[0].slug + '/', route: { path: '*' }, @@ -634,6 +636,7 @@ describe('Frontend Controller', function () { done(); } }; + mockPosts[2].posts[0].url = req.path; frontend.single(req, res, failTest(done)); }); }); @@ -647,9 +650,9 @@ describe('Frontend Controller', function () { })); }); - it('will render static page via /:slug', function (done) { + it('will render static page via /:slug/', function (done) { var req = { - path: '/' + mockPosts[0].posts[0].slug, + path: '/' + mockPosts[0].posts[0].slug + '/', route: { path: '*' }, @@ -669,7 +672,7 @@ describe('Frontend Controller', function () { it('will NOT render static page via /YYY/MM/DD/:slug', function (done) { var req = { - path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug].join('/') + path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug].join('/') + '/' }, res = { locals: {}, @@ -684,7 +687,7 @@ describe('Frontend Controller', function () { it('will NOT render static page via /:author/:slug', function (done) { var req = { - path: '/' + ['test', mockPosts[0].posts[0].slug].join('/') + path: '/' + ['test', mockPosts[0].posts[0].slug].join('/') + '/' }, res = { locals: {}, @@ -699,7 +702,7 @@ describe('Frontend Controller', function () { it('will redirect static page to admin edit page via /:slug/edit', function (done) { var req = { - path: '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') + path: '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -716,7 +719,7 @@ describe('Frontend Controller', function () { it('will NOT redirect static page to admin edit page via /YYYY/MM/DD/:slug/edit', function (done) { var req = { - path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug, 'edit'].join('/') + path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -733,7 +736,7 @@ describe('Frontend Controller', function () { it('will NOT redirect static page to admin edit page via /:author/:slug/edit', function (done) { var req = { - path: '/' + ['test', mockPosts[0].posts[0].slug, 'edit'].join('/') + path: '/' + ['test', mockPosts[0].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -760,7 +763,7 @@ describe('Frontend Controller', function () { it('will render static page via /:slug', function (done) { var req = { - path: '/' + mockPosts[0].posts[0].slug, + path: '/' + mockPosts[0].posts[0].slug + '/', route: { path: '*' }, @@ -780,7 +783,7 @@ describe('Frontend Controller', function () { it('will NOT render static page via /YYYY/MM/DD/:slug', function (done) { var req = { - path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug].join('/') + path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug].join('/') + '/' }, res = { render: sinon.spy() @@ -794,7 +797,7 @@ describe('Frontend Controller', function () { it('will redirect static page to admin edit page via /:slug/edit', function (done) { var req = { - path: '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') + path: '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -811,7 +814,7 @@ describe('Frontend Controller', function () { it('will NOT redirect static page to admin edit page via /YYYY/MM/DD/:slug/edit', function (done) { var req = { - path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug, 'edit'].join('/') + path: '/' + ['2012/12/30', mockPosts[0].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -833,14 +836,16 @@ describe('Frontend Controller', function () { beforeEach(function () { apiSettingsStub.withArgs('permalinks').returns(Promise.resolve({ settings: [{ - value: '/:slug' + value: '/:slug/' }] })); + + mockPosts[1].posts[0].url = '/' + mockPosts[1].posts[0].slug + '/'; }); - it('will render post via /:slug', function (done) { + it('will render post via /:slug/', function (done) { var req = { - path: '/' + mockPosts[1].posts[0].slug, + path: '/' + mockPosts[1].posts[0].slug + '/', route: { path: '*' }, @@ -861,7 +866,7 @@ describe('Frontend Controller', function () { it('will NOT render post via /YYYY/MM/DD/:slug', function (done) { var req = { - path: '/' + ['2012/12/30', mockPosts[1].posts[0].slug].join('/') + path: '/' + ['2012/12/30', mockPosts[1].posts[0].slug].join('/') + '/' }, res = { locals: {}, @@ -876,7 +881,7 @@ describe('Frontend Controller', function () { it('will NOT render post via /:author/:slug', function (done) { var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug].join('/') + path: '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/' }, res = { locals: {}, @@ -892,7 +897,7 @@ describe('Frontend Controller', function () { // Handle Edit append it('will redirect post to admin edit page via /:slug/edit', function (done) { var req = { - path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -909,7 +914,7 @@ describe('Frontend Controller', function () { it('will NOT redirect post to admin edit page via /YYYY/MM/DD/:slug/edit', function (done) { var req = { - path: '/' + ['2012/12/30', mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + ['2012/12/30', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -926,7 +931,7 @@ describe('Frontend Controller', function () { it('will NOT redirect post to admin edit page via /:author/:slug/edit', function (done) { var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -946,15 +951,18 @@ describe('Frontend Controller', function () { beforeEach(function () { apiSettingsStub.withArgs('permalinks').returns(Promise.resolve({ settings: [{ - value: '/:year/:month/:day/:slug' + value: '/:year/:month/:day/:slug/' }] })); + + var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'); + mockPosts[1].posts[0].url = '/' + date + '/' + mockPosts[1].posts[0].slug + '/'; }); - it('will render post via /YYYY/MM/DD/:slug', function (done) { + it('will render post via /YYYY/MM/DD/:slug/', function (done) { var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'), req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/'), + path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/', route: { path: '*' }, @@ -973,10 +981,10 @@ describe('Frontend Controller', function () { frontend.single(req, res, failTest(done)); }); - it('will NOT render post via /YYYY/MM/DD/:slug with non-matching date in url', function (done) { + it('will NOT render post via /YYYY/MM/DD/:slug/ with non-matching date in url', function (done) { var date = moment(mockPosts[1].published_at).subtract(1, 'days').format('YYYY/MM/DD'), req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/' }, res = { locals: {}, @@ -989,9 +997,9 @@ describe('Frontend Controller', function () { }); }); - it('will NOT render post via /:slug', function (done) { + it('will NOT render post via /:slug/', function (done) { var req = { - path: '/' + mockPosts[1].posts[0].slug + path: '/' + mockPosts[1].posts[0].slug + '/' }, res = { locals: {}, @@ -1004,9 +1012,9 @@ describe('Frontend Controller', function () { }); }); - it('will NOT render post via /:author/:slug', function (done) { + it('will NOT render post via /:author/:slug/', function (done) { var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug].join('/') + path: '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/' }, res = { locals: {}, @@ -1020,10 +1028,10 @@ describe('Frontend Controller', function () { }); // Handle Edit append - it('will redirect post to admin edit page via /YYYY/MM/DD/:slug/edit', function (done) { + it('will redirect post to admin edit page via /YYYY/MM/DD/:slug/edit/', function (done) { var dateFormat = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'), req = { - path: '/' + [dateFormat, mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + [dateFormat, mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -1038,9 +1046,9 @@ describe('Frontend Controller', function () { frontend.single(req, res, failTest(done)); }); - it('will NOT redirect post to admin edit page via /:slug/edit', function (done) { + it('will NOT redirect post to admin edit page via /:slug/edit/', function (done) { var req = { - path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -1055,9 +1063,9 @@ describe('Frontend Controller', function () { }); }); - it('will NOT redirect post to admin edit page via /:author/:slug/edit', function (done) { + it('will NOT redirect post to admin edit page via /:author/:slug/edit/', function (done) { var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -1077,14 +1085,17 @@ describe('Frontend Controller', function () { beforeEach(function () { apiSettingsStub.withArgs('permalinks').returns(Promise.resolve({ settings: [{ - value: '/:author/:slug' + value: '/:author/:slug/' }] })); + + // set post url to permalink-defined url + mockPosts[1].posts[0].url = '/test/' + mockPosts[1].posts[0].slug + '/'; }); - it('will render post via /:author/:slug', function (done) { + it('will render post via /:author/:slug/', function (done) { var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug].join('/'), + path: '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/', route: { path: '*' }, @@ -1103,10 +1114,10 @@ describe('Frontend Controller', function () { frontend.single(req, res, failTest(done)); }); - it('will NOT render post via /YYYY/MM/DD/:slug', function (done) { + it('will NOT render post via /YYYY/MM/DD/:slug/', function (done) { var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'), req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/' }, res = { locals: {}, @@ -1119,9 +1130,9 @@ describe('Frontend Controller', function () { }); }); - it('will NOT render post via /:author/:slug when author does not match post author', function (done) { + it('will NOT render post via /:author/:slug/ when author does not match post author', function (done) { var req = { - path: '/' + ['test-2', mockPosts[1].posts[0].slug].join('/') + path: '/' + ['test-2', mockPosts[1].posts[0].slug].join('/') + '/' }, res = { locals: {}, @@ -1134,9 +1145,9 @@ describe('Frontend Controller', function () { }); }); - it('will NOT render post via /:slug', function (done) { + it('will NOT render post via /:slug/', function (done) { var req = { - path: '/' + mockPosts[1].posts[0].slug + path: '/' + mockPosts[1].posts[0].slug + '/' }, res = { locals: {}, @@ -1150,9 +1161,9 @@ describe('Frontend Controller', function () { }); // Handle Edit append - it('will redirect post to admin edit page via /:author/:slug/edit', function (done) { + it('will redirect post to admin edit page via /:author/:slug/edit/', function (done) { var req = { - path: '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + ['test', mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -1167,10 +1178,10 @@ describe('Frontend Controller', function () { frontend.single(req, res, failTest(done)); }); - it('will NOT redirect post to admin edit page via /YYYY/MM/DD/:slug/edit', function (done) { + it('will NOT redirect post to admin edit page via /YYYY/MM/DD/:slug/edit/', function (done) { var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'), req = { - path: '/' + [date, mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + [date, mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -1185,9 +1196,9 @@ describe('Frontend Controller', function () { }); }); - it('will NOT redirect post to admin edit page /:slug/edit', function (done) { + it('will NOT redirect post to admin edit page /:slug/edit/', function (done) { var req = { - path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -1207,15 +1218,18 @@ describe('Frontend Controller', function () { beforeEach(function () { apiSettingsStub.withArgs('permalinks').returns(Promise.resolve({ settings: [{ - value: '/:year/:slug' + value: '/:year/:slug/' }] })); + + var date = moment(mockPosts[1].posts[0].published_at).format('YYYY'); + mockPosts[1].posts[0].url = '/' + date + '/' + mockPosts[1].posts[0].slug + '/'; }); - it('will render post via /:year/:slug', function (done) { + it('will render post via /:year/:slug/', function (done) { var date = moment(mockPosts[1].posts[0].published_at).format('YYYY'), req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/'), + path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/', route: { path: '*' }, @@ -1234,10 +1248,10 @@ describe('Frontend Controller', function () { frontend.single(req, res, failTest(done)); }); - it('will NOT render post via /YYYY/MM/DD/:slug', function (done) { + it('will NOT render post via /YYYY/MM/DD/:slug/', function (done) { var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD'), req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/' }, res = { locals: {}, @@ -1250,10 +1264,10 @@ describe('Frontend Controller', function () { }); }); - it('will NOT render post via /:year/slug when year does not match post year', function (done) { + it('will NOT render post via /:year/slug/ when year does not match post year', function (done) { var date = moment(mockPosts[1].posts[0].published_at).subtract(1, 'years').format('YYYY'), req = { - path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + path: '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/' }, res = { locals: {}, @@ -1266,9 +1280,9 @@ describe('Frontend Controller', function () { }); }); - it('will NOT render post via /:slug', function (done) { + it('will NOT render post via /:slug/', function (done) { var req = { - path: '/' + mockPosts[1].posts[0].slug + path: '/' + mockPosts[1].posts[0].slug + '/' }, res = { locals: {}, @@ -1282,10 +1296,10 @@ describe('Frontend Controller', function () { }); // Handle Edit append - it('will redirect post to admin edit page via /:year/:slug/edit', function (done) { + it('will redirect post to admin edit page via /:year/:slug/edit/', function (done) { var date = moment(mockPosts[1].posts[0].published_at).format('YYYY'), req = { - path: '/' + [date, mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + [date, mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {}, @@ -1300,9 +1314,9 @@ describe('Frontend Controller', function () { frontend.single(req, res, failTest(done)); }); - it('will NOT redirect post to admin edit page /:slug/edit', function (done) { + it('will NOT redirect post to admin edit page /:slug/edit/', function (done) { var req = { - path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + '/' }, res = { locals: {},