diff --git a/core/server/config/url.js b/core/server/config/url.js index a34257c3dc..29afb1496d 100644 --- a/core/server/config/url.js +++ b/core/server/config/url.js @@ -61,8 +61,9 @@ function urlPathForPost(post, permalinks) { year: function () { return moment(post.published_at).format('YYYY'); }, month: function () { return moment(post.published_at).format('MM'); }, day: function () { return moment(post.published_at).format('DD'); }, - slug: function () { return post.slug; }, - id: function () { return post.id; } + author: function () { return post.author.slug; }, + slug: function () { return post.slug; }, + id: function () { return post.id; } }; if (post.page) { diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js index 0ecb89ddf1..f36891b537 100644 --- a/core/server/controllers/frontend.js +++ b/core/server/controllers/frontend.js @@ -379,6 +379,15 @@ frontendControllers = { 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(); + } + return next(); + } + // If there is any date based paramter in the slug // we will check it against the post published date // to verify it's correct. diff --git a/core/test/unit/frontend_spec.js b/core/test/unit/frontend_spec.js index 3c7c95ca7c..7037df18f6 100644 --- a/core/test/unit/frontend_spec.js +++ b/core/test/unit/frontend_spec.js @@ -503,6 +503,7 @@ describe('Frontend Controller', function () { author: { id: 1, name: 'Test User', + slug: 'test', email: 'test@ghost.org' } }] @@ -518,6 +519,7 @@ describe('Frontend Controller', function () { author: { id: 1, name: 'Test User', + slug: 'test', email: 'test@ghost.org' } }] @@ -533,6 +535,7 @@ describe('Frontend Controller', function () { author: { id: 1, name: 'Test User', + slug: 'test', email: 'test@ghost.org' } }] @@ -647,6 +650,21 @@ 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('/') + }, + res = { + locals: {}, + render: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + done(); + }); + }); + it('will redirect static page to admin edit page via /:slug/edit', function (done) { var req = { path: '/' + [mockPosts[0].posts[0].slug, 'edit'].join('/') @@ -680,6 +698,23 @@ describe('Frontend Controller', function () { done(); }); }); + + 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('/') + }, + res = { + locals: {}, + render: sinon.spy(), + redirect: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + res.redirect.called.should.be.false; + done(); + }); + }); }); describe('permalink set to date', function () { @@ -808,6 +843,21 @@ describe('Frontend Controller', function () { }); }); + it('will NOT render post via /:author/:slug', function (done) { + var req = { + path: '/' + ['test', mockPosts[1].posts[0].slug].join('/') + }, + res = { + locals: {}, + render: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + done(); + }); + }); + // Handle Edit append it('will redirect post to admin edit page via /:slug/edit', function (done) { var req = { @@ -842,6 +892,23 @@ describe('Frontend Controller', 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('/') + }, + res = { + locals: {}, + render: sinon.spy(), + redirect: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + res.redirect.called.should.be.false; + done(); + }); + }); }); describe('permalink set to date', function () { @@ -907,6 +974,21 @@ describe('Frontend Controller', function () { }); }); + it('will NOT render post via /:author/:slug', function (done) { + var req = { + path: '/' + ['test', mockPosts[1].posts[0].slug].join('/') + }, + res = { + locals: {}, + render: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + done(); + }); + }); + // Handle Edit append 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'), @@ -942,6 +1024,154 @@ describe('Frontend Controller', 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('/') + }, + res = { + locals: {}, + render: sinon.spy(), + redirect: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + res.redirect.called.should.be.false; + done(); + }); + }); + }); + + describe('permalink set to author', function () { + beforeEach(function () { + apiSettingsStub.withArgs('permalinks').returns(Promise.resolve({ + settings: [{ + value: '/:author/:slug' + }] + })); + }); + + it('will render post via /:author/:slug', function (done) { + var req = { + path: '/' + ['test', mockPosts[1].posts[0].slug].join('/'), + route: { + path: '*' + }, + params: {} + }, + res = { + locals: {}, + render: function (view, context) { + assert.equal(view, 'post'); + assert(context.post, 'Context object has post attribute'); + assert.equal(context.post, mockPosts[1].posts[0]); + assert.equal(context.post.author.email, undefined); + done(); + } + }; + + frontend.single(req, res, failTest(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('/') + }, + res = { + locals: {}, + render: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + 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('/') + }, + res = { + locals: {}, + render: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + done(); + }); + }); + + it('will NOT render post via /:slug', function (done) { + var req = { + path: '/' + mockPosts[1].posts[0].slug + }, + res = { + locals: {}, + render: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + done(); + }); + }); + + // Handle Edit append + 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('/') + }, + res = { + locals: {}, + render: sinon.spy(), + redirect: function (arg) { + res.render.called.should.be.false; + arg.should.eql(adminEditPagePath + mockPosts[1].posts[0].id + '/'); + done(); + } + }; + + frontend.single(req, res, failTest(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('/') + }, + res = { + locals: {}, + render: sinon.spy(), + redirect: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + res.redirect.called.should.be.false; + done(); + }); + }); + + it('will NOT redirect post to admin edit page /:slug/edit', function (done) { + var req = { + path: '/' + [mockPosts[1].posts[0].slug, 'edit'].join('/') + }, + res = { + locals: {}, + render: sinon.spy(), + redirect: sinon.spy() + }; + + frontend.single(req, res, function () { + res.render.called.should.be.false; + res.redirect.called.should.be.false; + done(); + }); + }); }); describe('permalink set to custom format', function () {