Ghost/ghost/express-dynamic-redirects/lib/DynamicRedirectManager.js
Simon Backx 7e556d84de Fixed adding same redirect multiple times throws an error on removal (#379)
refs https://ghost.slack.com/archives/C02G9E68C/p1647599592576139

When you add a redirect multiple times, and remove it afterwards, an error is thrown:
`Cannot destructure property 'fromRegex' of 'this.redirects[redirectId]' as it is undefined.` 
This was caused by `redirectIds` that contained the same id multiple times.

* Added a test for adding a redirect multiple times and removing it once
* Fixed adding same redirect multiple times throws an error on removal
2022-03-24 09:54:19 +01:00

158 lines
4.3 KiB
JavaScript

const express = require('express');
const {parse: parseURL, format: formatURL} = require('url');
const {parse: parseQuerystring, stringify: formatQuerystring} = require('querystring');
class DynamicRedirectManager {
/**
* @param {object} config
* @param {number} config.permanentMaxAge
* @param {function} config.getSubdirectoryURL
*/
constructor({permanentMaxAge, getSubdirectoryURL}) {
/** @private */
this.permanentMaxAge = permanentMaxAge;
this.getSubdirectoryURL = getSubdirectoryURL;
/** @private */
this.router = express.Router();
/** @private @type {string[]} */
this.redirectIds = [];
/** @private @type {Object.<string, {fromRegex: RegExp, to: string, options: {permanent: boolean}}>} */
this.redirects = {};
this.handleRequest = this.handleRequest.bind(this);
}
/**
* @private
* @param {string} string
* @returns {RegExp}
*/
buildRegex(string) {
let flags = '';
if (string.startsWith('/') && string.endsWith('/i')) {
string = string.slice(1, -2);
flags += 'i';
}
if (string.endsWith('/')) {
string = string.slice(0, -1);
}
if (!string.endsWith('$')) {
string += '/?$';
}
return new RegExp(string, flags);
}
/**
* @private
* @param {string} redirectId
* @returns {void}
*/
setupRedirect(redirectId) {
const {fromRegex, to, options: {permanent}} = this.redirects[redirectId];
this.router.get(fromRegex, (req, res) => {
const maxAge = permanent ? this.permanentMaxAge : 0;
const toURL = parseURL(to);
const toURLParams = parseQuerystring(toURL.query);
const currentURL = parseURL(req.url);
const currentURLParams = parseQuerystring(currentURL.query);
const params = Object.assign({}, currentURLParams, toURLParams);
const search = formatQuerystring(params);
toURL.pathname = currentURL.pathname.replace(fromRegex, toURL.pathname);
toURL.search = search !== '' ? `?${search}` : null;
/**
* Only if the url is internal should we prepend the Ghost subdirectory
* @see https://github.com/TryGhost/Ghost/issues/10776
*/
if (!toURL.hostname) {
toURL.pathname = this.getSubdirectoryURL(toURL.pathname);
}
res.set({
'Cache-Control': `public, max-age=${maxAge}`
});
res.redirect(permanent ? 301 : 302, formatURL(toURL));
});
}
/**
* @param {string} from
* @param {string} to
* @param {object} [options]
* @param {boolean} [options.permanent]
*
* @returns {string} The redirect ID
*/
addRedirect(from, to, options = {}) {
try {
const fromRegex = this.buildRegex(from);
const redirectId = from;
if (!this.redirectIds.includes(redirectId)) {
this.redirectIds.push(redirectId);
}
this.redirects[redirectId] = {
fromRegex,
to,
options
};
this.setupRedirect(redirectId);
return redirectId;
} catch (error) {
if (error.message.match(/Invalid regular expression/gi)) {
return null;
}
throw error;
}
}
/**
* @param {string} redirectId
* @returns {void}
*/
removeRedirect(redirectId) {
this.redirectIds.splice(this.redirectIds.indexOf(redirectId), 1);
delete this.redirects[redirectId];
this.router = express.Router();
this.redirectIds.forEach(id => this.setupRedirect(id));
return;
}
/**
* @returns {void}
*/
removeAllRedirects() {
this.redirectIds = [];
this.redirects = {};
this.router = express.Router();
}
/**
* @param {express.Request} req
* @param {express.Response} res
* @param {express.NextFunction} next
*
* @returns {void}
*/
handleRequest(req, res, next) {
this.router(req, res, next);
}
}
module.exports = DynamicRedirectManager;