diff --git a/core/server/services/routing/ParentRouter.js b/core/server/services/routing/ParentRouter.js index a9c511795c..c10ac4c4da 100644 --- a/core/server/services/routing/ParentRouter.js +++ b/core/server/services/routing/ParentRouter.js @@ -10,6 +10,7 @@ const debug = require('ghost-ignition').debug('services:routing:ParentRouter'), EventEmitter = require('events').EventEmitter, express = require('express'), _ = require('lodash'), + url = require('url'), setPrototypeOf = require('setprototypeof'), security = require('../../lib/security'), urlService = require('../url'), @@ -47,6 +48,49 @@ class ParentRouter extends EventEmitter { this._router = GhostRouter({mergeParams: true, parent: this}); } + _getSiteRouter(req) { + let siteRouter = null; + + req.app._router.stack.every((router) => { + if (router.name === 'SiteRouter') { + siteRouter = router; + return false; + } + + return true; + }); + + return siteRouter; + } + + _respectDominantRouter(req, res, next, slug) { + let siteRouter = this._getSiteRouter(req); + let targetRoute = null; + + siteRouter.handle.stack.every((router) => { + if (router.handle.parent && router.handle.parent.isRedirectEnabled && router.handle.parent.isRedirectEnabled(this.getType(), slug)) { + targetRoute = router.handle.parent.getRoute(); + return false; + } + + return true; + }); + + if (targetRoute) { + debug('_respectDominantRouter'); + + const matchPath = this.permalinks.getValue().replace(':slug', '[a-zA-Z0-9-_]+'); + const toAppend = req.url.replace(new RegExp(matchPath), ''); + + return urlService.utils.redirect301(res, url.format({ + pathname: urlService.utils.createUrl(urlService.utils.urlJoin(targetRoute, toAppend), false, false, true), + search: url.parse(req.originalUrl).search + })); + } + + next(); + } + mountRouter(path, router) { if (arguments.length === 1) { router = path; diff --git a/core/test/unit/services/routing/ParentRouter_spec.js b/core/test/unit/services/routing/ParentRouter_spec.js index 9fba2d60b2..7aea51e496 100644 --- a/core/test/unit/services/routing/ParentRouter_spec.js +++ b/core/test/unit/services/routing/ParentRouter_spec.js @@ -1,7 +1,9 @@ const should = require('should'), sinon = require('sinon'), + configUtils = require('../../../utils/configUtils'), settingsCache = require('../../../../server/services/settings/cache'), common = require('../../../../server/lib/common'), + urlService = require('../../../../server/services/url'), ParentRouter = require('../../../../server/services/routing/ParentRouter'), sandbox = sinon.sandbox.create(); @@ -14,7 +16,15 @@ describe('UNIT - services/routing/ParentRouter', function () { sandbox.stub(common.events, 'emit'); sandbox.stub(common.events, 'on'); + sandbox.stub(urlService.utils, 'redirect301'); + req = sandbox.stub(); + req.app = { + _router: { + stack: [] + } + }; + res = sandbox.stub(); next = sandbox.stub(); @@ -23,7 +33,228 @@ describe('UNIT - services/routing/ParentRouter', function () { afterEach(function () { sandbox.restore(); + configUtils.restore(); }); + + describe('fn: _getSiteRouter', function () { + it('find site router', function () { + const parentRouter = new ParentRouter(); + + req.app = { + _router: { + stack: [{ + name: 'SiteRouter' + }] + } + }; + + should.exist(parentRouter._getSiteRouter(req)); + }); + }); + + describe('fn: _respectDominantRouter', function () { + it('redirect', function () { + const parentRouter = new ParentRouter(); + parentRouter.getType = sandbox.stub().returns('tags'); + parentRouter.permalinks = { + getValue: sandbox.stub().returns('/tag/:slug/') + }; + + req.url = '/tag/bacon/'; + req.originalUrl = '/tag/bacon/'; + + req.app._router.stack = [{ + name: 'SiteRouter', + handle: { + stack: [{ + name: 'StaticRoutesRouter', + handle: { + parent: { + isRedirectEnabled: sandbox.stub().returns(true), + getRoute: sandbox.stub().returns('/channel/') + } + } + }] + } + }]; + + parentRouter._respectDominantRouter(req, res, next, 'bacon'); + next.called.should.eql(false); + urlService.utils.redirect301.withArgs(res, '/channel/').calledOnce.should.be.true(); + }); + + it('redirect with query params', function () { + const parentRouter = new ParentRouter('tag', '/tag/:slug/'); + parentRouter.getType = sandbox.stub().returns('tags'); + parentRouter.permalinks = { + getValue: sandbox.stub().returns('/tag/:slug/') + }; + + req.url = '/tag/bacon/'; + req.originalUrl = '/tag/bacon/?a=b'; + + req.app._router.stack = [{ + name: 'SiteRouter', + handle: { + stack: [{ + name: 'StaticRoutesRouter', + handle: { + parent: { + isRedirectEnabled: sandbox.stub().returns(true), + getRoute: sandbox.stub().returns('/channel/') + } + } + }] + } + }]; + + parentRouter._respectDominantRouter(req, res, next, 'bacon'); + next.called.should.eql(false); + urlService.utils.redirect301.withArgs(res, '/channel/?a=b').calledOnce.should.be.true(); + }); + + it('redirect rss', function () { + const parentRouter = new ParentRouter('tag', '/tag/:slug/'); + parentRouter.getType = sandbox.stub().returns('tags'); + parentRouter.permalinks = { + getValue: sandbox.stub().returns('/tag/:slug/') + }; + + req.url = '/tag/bacon/rss/'; + req.originalUrl = '/tag/bacon/rss/'; + + req.app._router.stack = [{ + name: 'SiteRouter', + handle: { + stack: [{ + name: 'StaticRoutesRouter', + handle: { + parent: { + isRedirectEnabled: sandbox.stub().returns(true), + getRoute: sandbox.stub().returns('/channel/') + } + } + }] + } + }]; + + parentRouter._respectDominantRouter(req, res, next, 'bacon'); + next.called.should.eql(false); + urlService.utils.redirect301.withArgs(res, '/channel/rss/').calledOnce.should.be.true(); + }); + + it('redirect pagination', function () { + const parentRouter = new ParentRouter('tag', '/tag/:slug/'); + parentRouter.getType = sandbox.stub().returns('tags'); + parentRouter.permalinks = { + getValue: sandbox.stub().returns('/tag/:slug/') + }; + + req.url = '/tag/bacon/page/2/'; + req.originalUrl = '/tag/bacon/page/2/'; + + req.app._router.stack = [{ + name: 'SiteRouter', + handle: { + stack: [{ + name: 'StaticRoutesRouter', + handle: { + parent: { + isRedirectEnabled: sandbox.stub().returns(true), + getRoute: sandbox.stub().returns('/channel/') + } + } + }] + } + }]; + + parentRouter._respectDominantRouter(req, res, next, 'bacon'); + next.called.should.eql(false); + urlService.utils.redirect301.withArgs(res, '/channel/page/2/').calledOnce.should.be.true(); + }); + + it('redirect correctly with subdirectory', function () { + configUtils.set('url', 'http://localhost:7777/blog/'); + + const parentRouter = new ParentRouter('tag', '/tag/:slug/'); + parentRouter.getType = sandbox.stub().returns('tags'); + parentRouter.permalinks = { + getValue: sandbox.stub().returns('/tag/:slug/') + }; + + req.url = '/tag/bacon/'; + req.originalUrl = '/blog/tag/bacon/'; + + req.app._router.stack = [{ + name: 'SiteRouter', + handle: { + stack: [{ + name: 'StaticRoutesRouter', + handle: { + parent: { + isRedirectEnabled: sandbox.stub().returns(true), + getRoute: sandbox.stub().returns('/channel/') + } + } + }] + } + }]; + + parentRouter._respectDominantRouter(req, res, next, 'bacon'); + next.called.should.eql(false); + urlService.utils.redirect301.withArgs(res, '/blog/channel/').calledOnce.should.be.true(); + }); + + it('no redirect: different data key', function () { + const parentRouter = new ParentRouter('tag', '/tag/:slug/'); + parentRouter.getType = sandbox.stub().returns('tags'); + parentRouter.permalinks = { + getValue: sandbox.stub().returns('/tag/:slug/') + }; + + req.app._router.stack = [{ + name: 'SiteRouter', + handle: { + stack: [{ + name: 'StaticRoutesRouter', + handle: { + parent: { + isRedirectEnabled: sandbox.stub().returns(false), + getRoute: sandbox.stub().returns('/channel/') + } + } + }] + } + }]; + + parentRouter._respectDominantRouter(req, res, next, 'bacon'); + next.called.should.eql(true); + urlService.utils.redirect301.called.should.be.false(); + }); + + it('no redirect: no channel defined', function () { + const parentRouter = new ParentRouter('tag', '/tag/:slug/'); + parentRouter.getType = sandbox.stub().returns('tags'); + parentRouter.permalinks = { + getValue: sandbox.stub().returns('/tag/:slug/') + }; + + req.app._router.stack = [{ + name: 'SiteRouter', + handle: { + stack: [{ + name: 'StaticPagesRouter', + handle: {} + }] + } + }]; + + parentRouter._respectDominantRouter(req, res, next, 'bacon'); + next.called.should.eql(true); + urlService.utils.redirect301.called.should.be.false(); + }); + }); + describe('fn: isRedirectEnabled', function () { it('no data key defined', function () { const parentRouter = new ParentRouter();