diff --git a/core/client b/core/client new file mode 160000 index 0000000000..39622c4e28 --- /dev/null +++ b/core/client @@ -0,0 +1 @@ +Subproject commit 39622c4e284554d0ac1f6dbe3de90a6e6943a6ed diff --git a/core/server/config/url.js b/core/server/config/url.js index ccc1b08580..73f7d97235 100644 --- a/core/server/config/url.js +++ b/core/server/config/url.js @@ -1,7 +1,7 @@ // Contains all path information to be used throughout // the codebase. -var moment = require('moment'), +var moment = require('moment-timezone'), _ = require('lodash'), ghostConfig = '', // @TODO: unify this with routes.apiBaseUrl @@ -95,18 +95,20 @@ function createUrl(urlPath, absolute, secure) { return urlJoin(base, urlPath); } -// ## urlPathForPost -// Always sync -// Creates the url path for a post, given a post and a permalink -// Parameters: -// - post - a json object representing a post +/** + * creates the url path for a post based on blog timezone and permalink pattern + * + * @param {JSON} post + * @returns {string} + */ function urlPathForPost(post) { var output = '', permalinks = ghostConfig.theme.permalinks, + publishedAtMoment = moment.tz(post.published_at, ghostConfig.theme.timezone), tags = { - 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'); }, + year: function () { return publishedAtMoment.format('YYYY'); }, + month: function () { return publishedAtMoment.format('MM'); }, + day: function () { return publishedAtMoment.format('DD'); }, author: function () { return post.author.slug; }, slug: function () { return post.slug; }, id: function () { return post.id; } diff --git a/core/server/controllers/frontend/index.js b/core/server/controllers/frontend/index.js index 87f5cd42a5..ffffd9a8a4 100644 --- a/core/server/controllers/frontend/index.js +++ b/core/server/controllers/frontend/index.js @@ -66,11 +66,21 @@ frontendControllers = { return next(); } - // If we're ready to render the page but the last param is 'edit' then we'll send you to the edit page. + // CASE: we only support /:slug format for pages + if (post.page && post.url !== req.path) { + return next(); + } + + // CASE: last param is of url is /edit, redirect to admin if (lookup.isEditURL) { return res.redirect(config.paths.subdir + '/ghost/editor/' + post.id + '/'); } + // CASE: permalink is not valid anymore, we redirect him permanently to the correct one + if (post.url !== req.path) { + return res.redirect(301, post.url); + } + setRequestIsSecure(req, post); filters.doFilter('prePostsRender', post, res.locals) diff --git a/core/server/controllers/frontend/post-lookup.js b/core/server/controllers/frontend/post-lookup.js index 788c4188ee..c9925fc161 100644 --- a/core/server/controllers/frontend/post-lookup.js +++ b/core/server/controllers/frontend/post-lookup.js @@ -16,28 +16,32 @@ function postLookup(postUrl) { postPermalink = config.theme.permalinks, pagePermalink = '/:slug/', isEditURL = false, - matchFunc, + matchFuncPost, + matchFuncPage, + postParams, + pageParams, params; // Convert saved permalink into a path-match function - matchFunc = routeMatch(getEditFormat(postPermalink)); - params = matchFunc(postPath); + matchFuncPost = routeMatch(getEditFormat(postPermalink)); + postParams = matchFuncPost(postPath); // Check if the path matches the permalink structure. // If there are no matches found, test to see if this is a page instead - if (params === false) { - matchFunc = routeMatch(getEditFormat(pagePermalink)); - params = matchFunc(postPath); + if (postParams === false) { + matchFuncPage = routeMatch(getEditFormat(pagePermalink)); + pageParams = matchFuncPage(postPath); } // If there are still no matches then return empty. - if (params === false) { + if (pageParams === false) { return Promise.resolve(); } + params = postParams || pageParams; + // If params contains edit, and it is equal to 'edit' this is an edit URL if (params.edit && params.edit.toLowerCase() === 'edit') { - postPath = postPath.replace(params.edit + '/', ''); isEditURL = true; } else if (params.edit !== undefined) { // Unknown string in URL, return empty @@ -53,9 +57,12 @@ function postLookup(postUrl) { return api.posts.read(params).then(function then(result) { var post = result.posts[0]; - // If there is no post, or the post has no URL, or it isn't a match for our original lookup, return empty - // This also catches the case where we use the pagePermalink but the post is not a page - if (!post || !post.url || post.url !== postPath) { + if (!post) { + return Promise.resolve(); + } + + // CASE: we originally couldn't match the post based on date permalink and we tried to check if its a page + if (!post.page && !postParams) { return Promise.resolve(); } diff --git a/core/test/functional/routes/frontend_spec.js b/core/test/functional/routes/frontend_spec.js index 221bdefec1..4ed2b209f3 100644 --- a/core/test/functional/routes/frontend_spec.js +++ b/core/test/functional/routes/frontend_spec.js @@ -516,7 +516,6 @@ describe('Frontend Routing', function () { }); it('should load a post with date permalink', function (done) { - // get today's date var date = moment().format('YYYY/MM/DD'); request.get('/' + date + '/welcome-to-ghost/') @@ -525,6 +524,20 @@ describe('Frontend Routing', function () { .end(doEnd(done)); }); + it('expect redirect because of wrong/old permalink prefix', function (done) { + var date = moment().format('YYYY/MM/DD'); + + request.get('/2016/04/01/welcome-to-ghost/') + .expect('Content-Type', /html/) + .end(function (err, res) { + res.status.should.eql(301); + request.get('/' + date + '/welcome-to-ghost/') + .expect(200) + .expect('Content-Type', /html/) + .end(doEnd(done)); + }); + }); + it('should serve RSS with date permalink', function (done) { request.get('/rss/') .expect('Content-Type', 'text/xml; charset=utf-8') diff --git a/core/test/unit/config_spec.js b/core/test/unit/config_spec.js index 107cc94cb0..3e96c16301 100644 --- a/core/test/unit/config_spec.js +++ b/core/test/unit/config_spec.js @@ -384,8 +384,8 @@ describe('Config', function () { }); describe('urlPathForPost', function () { - it('should output correct url for post', function () { - configUtils.set({theme: {permalinks: '/:slug/'}}); + it('permalink is /:slug/', function () { + configUtils.set({theme: {permalinks: '/:slug/', timezone: 'Europe/Dublin'}}); var testData = testUtils.DataGenerator.Content.posts[2], postLink = '/short-and-sweet/'; @@ -393,20 +393,17 @@ describe('Config', function () { config.urlPathForPost(testData).should.equal(postLink); }); - it('should output correct url for post with date permalink', function () { - configUtils.set({theme: {permalinks: '/:year/:month/:day/:slug/'}}); + it('permalink is /:year/:month/:day/:slug, blog timezone is Los Angeles', function () { + configUtils.set({theme: {permalinks: '/:year/:month/:day/:slug/', timezone: 'America/Los_Angeles'}}); var testData = testUtils.DataGenerator.Content.posts[2], - today = testData.published_at, - dd = ('0' + today.getDate()).slice(-2), - mm = ('0' + (today.getMonth() + 1)).slice(-2), - yyyy = today.getFullYear(), - postLink = '/' + yyyy + '/' + mm + '/' + dd + '/short-and-sweet/'; + postLink = '/2016/05/17/short-and-sweet/'; + testData.published_at = new Date('2016-05-18T06:30:00.000Z'); config.urlPathForPost(testData).should.equal(postLink); }); - it('should output correct url for page with date permalink', function () { - configUtils.set({theme: {permalinks: '/:year/:month/:day/:slug/'}}); + it('post is page, no permalink usage allowed at all', function () { + configUtils.set({theme: {permalinks: '/:year/:month/:day/:slug/', timezone: 'America/Los_Angeles'}}); var testData = testUtils.DataGenerator.Content.posts[5], postLink = '/static-page-test/'; @@ -414,16 +411,23 @@ describe('Config', function () { config.urlPathForPost(testData).should.equal(postLink); }); - it('should output correct url for post with complex permalink', function () { - configUtils.set({theme: {permalinks: '/:year/:id/:author/'}}); + it('permalink is /:year/:id:/:author', function () { + configUtils.set({theme: {permalinks: '/:year/:id/:author/', timezone: 'America/Los_Angeles'}}); - var testData = _.extend( - {}, testUtils.DataGenerator.Content.posts[2], {id: 3}, {author: {slug: 'joe-bloggs'}} - ), - today = testData.published_at, - yyyy = today.getFullYear(), - postLink = '/' + yyyy + '/3/joe-bloggs/'; + var testData = _.merge(testUtils.DataGenerator.Content.posts[2], {id: 3}, {author: {slug: 'joe-blog'}}), + postLink = '/2015/3/joe-blog/'; + testData.published_at = new Date('2016-01-01T00:00:00.000Z'); + config.urlPathForPost(testData).should.equal(postLink); + }); + + it('permalink is /:year/:id:/:author', function () { + configUtils.set({theme: {permalinks: '/:year/:id/:author/', timezone: 'Europe/Berlin'}}); + + var testData = _.merge(testUtils.DataGenerator.Content.posts[2], {id: 3}, {author: {slug: 'joe-blog'}}), + postLink = '/2016/3/joe-blog/'; + + testData.published_at = new Date('2016-01-01T00:00:00.000Z'); config.urlPathForPost(testData).should.equal(postLink); }); }); diff --git a/core/test/unit/controllers/frontend/index_spec.js b/core/test/unit/controllers/frontend/index_spec.js index 1ec42f48ae..9e05fbf779 100644 --- a/core/test/unit/controllers/frontend/index_spec.js +++ b/core/test/unit/controllers/frontend/index_spec.js @@ -426,16 +426,6 @@ 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) { - var date = moment(mockPosts[1].published_at).subtract(1, 'days').format('YYYY/MM/DD'); - req.path = '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/'; - - frontend.single(req, res, function () { - res.render.called.should.be.false(); - done(); - }); - }); - it('will NOT render post via /:slug/', function (done) { req.path = '/' + mockPosts[1].posts[0].slug + '/';