forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
handle-redirects.js
200 lines (173 loc) · 7.84 KB
/
handle-redirects.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import patterns from '../../lib/patterns.js'
import { URL } from 'url'
import { pathLanguagePrefixed } from '../../lib/languages.js'
import { deprecatedWithFunctionalRedirects } from '../../lib/enterprise-server-releases.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()
// Any double-slashes in the URL should be removed first
if (req.path.includes('//')) {
return res.redirect(301, req.path.replace(/\/\//g, '/'))
}
// blanket redirects for languageless homepage
if (req.path === '/') {
const language = getLanguage(req)
noCacheControl(res)
return res.redirect(302, `/${language}`)
}
// Don't try to redirect if the URL is `/search` which is the XHR
// endpoint. It should not become `/en/search`.
// It's unfortunate and looks a bit needlessly complicated. But
// it comes from the legacy that the JSON API endpoint was and needs to
// continue to be `/search` when it would have been more neat if it
// was something like `/api/search`.
// If someone types in `/search?query=foo` manually, they'll get JSON.
// Maybe sometime in 2023 we remove `/search` as an endpoint for the
// JSON.
if (req.path === '/search') return next()
// begin redirect handling
let redirect = req.path
let queryParams = req._parsedUrl.query
// If process.env.ELASTICSEARCH_URL isn't set, you can't go to the
// dedicated search results page.
// If that's the case, use the "old redirect" where all it does is
// "correcting" the old query string 'q' to 'query'.
if (!process.env.ELASTICSEARCH_URL && 'q' in req.query && !('query' in req.query)) {
// update old-style query params (#9467)
const newQueryParams = new URLSearchParams(queryParams)
newQueryParams.set('query', newQueryParams.get('q'))
newQueryParams.delete('q')
return res.redirect(301, `${req.path}?${newQueryParams.toString()}`)
}
// If process.env.ELASTICSEARCH_URL is set, the dedicated search
// result page is ready. If that's the case, we can redirect to
// `/$locale/search?query=...` from `/foo/bar?query=...` or from
// (the old style) `/foo/bar/?q=...`
if (
process.env.ELASTICSEARCH_URL &&
('q' in req.query ||
('query' in req.query &&
!(req.path.endsWith('/search') || req.path.startsWith('/api/search'))))
) {
// If you had the old legacy format of /some/uri?q=stuff
// it needs to redirect to /en/search?query=stuff or
// /some/uri?query=stuff depending on if ELASTICSEARCH_URL has been
// set up.
// If you have the new format of /some/uri?query=stuff it too needs
// to redirect to /en/search?query=stuff
// ...or /en/{version}/search?query=stuff
const language = getLanguage(req)
const sp = new URLSearchParams(req.query)
if (sp.has('q') && !sp.has('query')) {
sp.set('query', sp.get('q'))
sp.delete('q')
}
let redirectTo = `/${language}`
const { currentVersion } = req.context
if (currentVersion !== 'free-pro-team@latest') {
redirectTo += `/${currentVersion}`
// The `req.context.currentVersion` is just the portion of the URL
// pathname. It could be that the currentVersion is something
// like `enterprise` which needs to be redirected to its new name.
redirectTo = getRedirect(redirectTo, req.context)
}
redirectTo += `/search?${sp.toString()}`
return res.redirect(301, redirectTo)
}
// have to do this now because searchPath replacement changes the path as well as the query params
if (queryParams) {
queryParams = '?' + queryParams
}
// 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 || isDeprecatedVersion(req.path)) {
const language = getLanguage(req)
// 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 = `/${language}${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)] || isDeprecatedVersion(req.path)) &&
!redirect.includes('://')
) {
// 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()
}
// do the redirect if the from-URL already had a language in it
if (pathLanguagePrefixed(req.path) || redirect.includes('://')) {
cacheControl(res)
} else {
noCacheControl(res)
}
const permanent = redirect.includes('://') || usePermanentRedirect(req)
return res.redirect(permanent ? 301 : 302, redirect)
}
function getLanguage(req, default_ = 'en') {
// req.context.userLanguage, if it truthy, is always a valid supported
// language. It's whatever was in the user's request but filtered
// based on non-WIP languages in lib/languages.js
return req.context.userLanguage || default_
}
function usePermanentRedirect(req) {
// If the redirect was to essentially swap `enterprise-server@latest`
// for `[email protected]` 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
}
function isDeprecatedVersion(path) {
// When we rewrote how redirects work, from a lookup model to a
// functional model, the enterprise-server releases that got
// deprecated since then fall between the cracks. Especially
// for custom NextJS page-like pages like /admin/release-notes
// These URLs don't come from any remaining .json lookup file
// and they're not active pages either (e.g. req.context.pages)
const split = path.split('/')
for (const version of deprecatedWithFunctionalRedirects) {
if (split.includes(`enterprise-server@${version}`)) {
return true
}
}
return false
}