1
0
mirror of synced 2025-12-23 03:44:00 -05:00
Files
docs/middleware/redirects/handle-redirects.js
Peter Bengtsson 07c8fc3c2a Decouple redirects from language (#24597)
* experimenting with redirects

* cleanup developer.json

* wip

* clean console.log

* progress

* some progress

* progress

* much progress

* debugging tests

* hacky progress

* ditch latest -> number redirects

* minor

* hacky progress

* lots of progress

* some small fixes

* fix rendering tests

* small fixes

* progress

* undo debugging

* better

* routing tests OK

* more cleaning

* unit tests

* undoing lineending edit

* undoing temporary debugging

* don't ever set this.redirects on Page

* cope with archived version redirects

* adding code comments on the major if statements

* address all feedback

* update README about redirects

* delete invalid test

* fix feedback
2022-02-14 20:19:10 +00:00

137 lines
5.1 KiB
JavaScript

import patterns from '../../lib/patterns.js'
import { URL } from 'url'
import languages, { pathLanguagePrefixed } from '../../lib/languages.js'
import getRedirect from '../../lib/get-redirect.js'
import { cacheControlFactory } from '../cache-control.js'
const cacheControl = cacheControlFactory(60 * 60 * 24) // one day
const noCacheControl = cacheControlFactory(0)
export default function handleRedirects(req, res, next) {
// never redirect assets
if (patterns.assetPaths.test(req.path)) return next()
// blanket redirects for languageless homepage
if (req.path === '/') {
let language = 'en'
// if set, redirect to user's preferred language translation or else English
if (
req.context.userLanguage &&
languages[req.context.userLanguage] &&
!languages[req.context.userLanguage].wip
) {
language = req.context.userLanguage
}
// Undo the cookie setting that CSRF sets.
res.removeHeader('set-cookie')
noCacheControl(res)
return res.redirect(302, `/${language}`)
}
// begin redirect handling
let redirect = req.path
let queryParams = req._parsedUrl.query
// update old-style query params (#9467)
if ('q' in req.query) {
const newQueryParams = new URLSearchParams(queryParams)
newQueryParams.set('query', newQueryParams.get('q'))
newQueryParams.delete('q')
return res.redirect(301, `${req.path}?${newQueryParams.toString()}`)
}
// have to do this now because searchPath replacement changes the path as well as the query params
if (queryParams) {
queryParams = '?' + queryParams
redirect = (redirect + queryParams).replace(patterns.searchPath, '$1')
}
// remove query params temporarily so we can find the path in the redirects object
let redirectWithoutQueryParams = removeQueryParams(redirect)
const redirectTo = getRedirect(redirectWithoutQueryParams, req.context)
redirectWithoutQueryParams = redirectTo || redirectWithoutQueryParams
redirect = queryParams ? redirectWithoutQueryParams + queryParams : redirectWithoutQueryParams
if (!redirectTo && !pathLanguagePrefixed(req.path)) {
// No redirect necessary, but perhaps it's to a known page, and the URL
// currently doesn't have a language prefix, then we need to add
// the language prefix.
// We can't always force on the language prefix because some URLs
// aren't pages. They're other middleware endpoints such as
// `/healthz` which should never redirect.
// But for example, a `/authentication/connecting-to-github-with-ssh`
// needs to become `/en/authentication/connecting-to-github-with-ssh`
const possibleRedirectTo = `/en${req.path}`
if (possibleRedirectTo in req.context.pages) {
// As of Jan 2022 we always redirect to `/en` if the URL doesn't
// specify a language. ...except for the root home page (`/`).
// It's unfortunate but that's how it currently works.
// It's tracked in #1145
// Perhaps a more ideal solution would be to do something similar to
// the code above for `req.path === '/'` where we look at the user
// agent for a header and/or cookie.
// Note, it's important to use `req.url` here and not `req.path`
// because the full URL can contain query strings.
// E.g. `/foo?json=breadcrumbs`
redirect = `/en${req.url}`
}
}
// do not redirect a path to itself
// req._parsedUrl.path includes query params whereas req.path does not
if (redirect === req._parsedUrl.path) {
return next()
}
// do not redirect if the redirected page can't be found
if (!req.context.pages[removeQueryParams(redirect)]) {
// display error on the page in development, but not in production
// include final full redirect path in the message
if (process.env.NODE_ENV !== 'production' && req.context) {
req.context.redirectNotFound = redirect
}
return next()
}
// Undo the cookie setting that CSRF sets.
res.removeHeader('set-cookie')
// do the redirect if the from-URL already had a language in it
if (pathLanguagePrefixed(req.path)) {
cacheControl(res)
}
const permanent = usePermanentRedirect(req)
return res.redirect(permanent ? 301 : 302, redirect)
}
function usePermanentRedirect(req) {
// If the redirect was to essentially swap `enterprise-server@latest`
// for `enterprise-server@3.x` then, we definitely don't want to
// do a permanent redirect.
// When this is the case, we don't want a permanent redirect because
// it could overzealously cache in the users' browser which could
// be bad when whatever "latest" means changes.
if (req.path.includes('/enterprise-server@latest')) return false
// If the redirect involved injecting a language prefix, then don't
// permanently redirect because that could overly cache in users'
// browsers if we some day want to make the language redirect
// depend on a cookie or 'Accept-Language' header.
if (pathLanguagePrefixed(req.path)) return true
// The default is to *not* do a permanent redirect.
return false
}
function removeQueryParams(redirect) {
return new URL(redirect, 'https://docs.github.com').pathname
}