Co-authored-by: Kevin Heis <heiskr@users.noreply.github.com> Co-authored-by: Hector Alfaro <hectorsector@github.com>
126 lines
4.7 KiB
TypeScript
126 lines
4.7 KiB
TypeScript
import { ExtendedRequestWithPageInfo } from '../types'
|
|
import type { NextFunction, Response } from 'express'
|
|
|
|
import { ExtendedRequest, Page } from '#src/types.js'
|
|
import { isArchivedVersionByPath } from '@/archives/lib/is-archived-version'
|
|
import getRedirect from '@/redirects/lib/get-redirect.js'
|
|
import { getVersionStringFromPath, getLangFromPath } from '#src/frame/lib/path-utils.js'
|
|
import nonEnterpriseDefaultVersion from '#src/versions/lib/non-enterprise-default-version.js'
|
|
|
|
// 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.js` 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) {
|
|
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()
|
|
}
|