forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidation.ts
More file actions
171 lines (145 loc) · 6.2 KB
/
Copy pathvalidation.ts
File metadata and controls
171 lines (145 loc) · 6.2 KB
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
import { ExtendedRequestWithPageInfo } from '../types'
import type { NextFunction, Response } from 'express'
import { ExtendedRequest, Page } from '@/types'
import { isArchivedVersionByPath } from '@/archives/lib/is-archived-version'
import getRedirect from '@/redirects/lib/get-redirect'
import { getVersionStringFromPath, getLangFromPath } from '@/frame/lib/path-utils'
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version'
import { allVersions } from '@/versions/lib/all-versions'
// validates the path for pagelist endpoint
// specifically, defaults to `/en/free-pro-team@latest` when those values are missing
// when they're provided, checks and cleans them up so we don't just lookup bad lang codes or versions
export const pagelistValidationMiddleware = (
req: ExtendedRequest,
res: Response,
next: NextFunction,
) => {
// get version from path, fallback to default version if it can't be resolved
const versionFromPath = getVersionStringFromPath(req.path) || nonEnterpriseDefaultVersion
// in the rare case that this failed, probably won't be reached
if (!versionFromPath)
return res.status(400).json({ error: `Couldn't get version from the given path.` })
// get the language from path, fallback to english if it can't be resolved
const langFromPath = getLangFromPath(req.path) || 'en'
// in the rare case that the language fallback failed
if (!langFromPath)
return res.status(400).json({
error: `Couldn't get language from the from the given path.`,
})
// set the version and language in the context, we'll use it later
req.context!.currentVersion = versionFromPath
req.context!.currentLanguage = langFromPath
return next()
}
export const pathValidationMiddleware = (
req: ExtendedRequestWithPageInfo,
res: Response,
next: NextFunction,
) => {
const pathname = req.query.pathname as string | string[] | undefined
if (!pathname) {
return res.status(400).json({ error: `No 'pathname' query` })
}
if (Array.isArray(pathname)) {
return res.status(400).json({ error: "Multiple 'pathname' keys" })
}
if (!pathname.trim()) {
return res.status(400).json({ error: `'pathname' query empty` })
}
if (!pathname.startsWith('/')) {
return res.status(400).json({ error: `'pathname' has to start with /` })
}
if (/\s/.test(pathname)) {
return res.status(400).json({ error: `'pathname' cannot contain whitespace` })
}
// req.pageinfo.page will be defined later or it will throw
req.pageinfo = { pathname, page: {} as Page }
return next()
}
export const pageValidationMiddleware = (
req: ExtendedRequestWithPageInfo,
res: Response,
next: NextFunction,
) => {
let { pathname } = req.pageinfo
// We can't use the `findPage` middleware utility function because we
// need to know when the pathname is a redirect.
// This is important so that the final `pathname` value
// matches the page's permalinks.
// This is important when rendering a page because of translations,
// if it needs to do a fallback, it needs to know the correct
// equivalent English page.
if (!req.context || !req.context.pages || !req.context.redirects)
throw new Error('request not yet contextualized')
const redirectsContext = { pages: req.context.pages, redirects: req.context.redirects }
// Similar to how the `handle-redirects.ts` middleware works, let's first
// check if the URL is just having a trailing slash.
while (pathname.endsWith('/') && pathname.length > 1) {
pathname = pathname.slice(0, -1)
}
// E.g. a request for `/` is handled as a redirect outside the
// getRedirect() function.
if (pathname === '/') {
pathname = `/${req.context.currentLanguage}`
}
// Initialize archived property to avoid it being undefined
req.pageinfo.archived = { isArchived: false }
if (!(pathname in req.context.pages)) {
// If a pathname is not a known page, it might *either* be a redirect,
// or an archived enterprise version, or both.
// That's why it's import to not bother looking at the redirects
// if the pathname is an archived enterprise version.
// This mimics how our middleware work and their order.
req.pageinfo.archived = isArchivedVersionByPath(pathname)
if (!req.pageinfo.archived.isArchived) {
const redirect = getRedirect(pathname, redirectsContext)
if (redirect) {
req.pageinfo.redirectedFrom = pathname
pathname = redirect
}
}
}
// Remember this might yield undefined if the pathname is not a page
req.pageinfo.page = req.context.pages[pathname]
if (!req.pageinfo.page && !req.pageinfo.archived.isArchived) {
return res.status(404).json({ error: `No page found for '${pathname}'` })
}
// The pathname might have changed if it was a redirect
req.pageinfo.pathname = pathname
return next()
}
export const apiVersionValidationMiddleware = (
req: ExtendedRequestWithPageInfo,
res: Response,
next: NextFunction,
) => {
const apiVersion = req.query.apiVersion as string | string[] | undefined
// If no apiVersion is provided, continue (it will default to latest)
if (!apiVersion) {
return next()
}
// Validate apiVersion is a single string, not an array
if (Array.isArray(apiVersion)) {
return res.status(400).json({ error: "Multiple 'apiVersion' keys" })
}
// Get the version from the pathname query parameter
const pathname = req.pageinfo?.pathname || (req.query.pathname as string)
if (!pathname) {
// This should not happen as pathValidationMiddleware runs first
throw new Error('pathname not available for apiVersion validation')
}
// Extract version from the pathname
const currentVersion = getVersionStringFromPath(pathname) || nonEnterpriseDefaultVersion
const versionInfo = allVersions[currentVersion]
if (!versionInfo) {
return res.status(400).json({ error: `Invalid version '${currentVersion}'` })
}
const validApiVersions = versionInfo.apiVersions || []
// If this version has API versioning, validate the provided version
if (validApiVersions.length > 0 && !validApiVersions.includes(apiVersion)) {
return res.status(400).json({
error: `Invalid apiVersion '${apiVersion}' for ${currentVersion}. Valid API versions are: ${validApiVersions.join(', ')}`,
})
}
return next()
}