7e556d84de
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
158 lines
4.3 KiB
JavaScript
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;
|