2021-07-21 18:12:33 +02:00
import type { IncomingMessage } from 'http'
2021-11-09 02:28:39 +01:00
import type { Key } from 'next/dist/compiled/path-to-regexp'
import type { NextParsedUrlQuery } from '../../../../server/request-meta'
import type { Params } from '../../../../server/router'
2021-07-21 18:12:33 +02:00
import type { RouteHas } from '../../../../lib/load-custom-routes'
2021-11-09 02:28:39 +01:00
import { compile , pathToRegexp } from 'next/dist/compiled/path-to-regexp'
2022-01-03 18:41:50 +01:00
import { escapeStringRegexp } from '../../escape-regexp'
2021-11-09 02:28:39 +01:00
import { parseUrl } from './parse-url'
2022-01-14 22:01:35 +01:00
import { BaseNextRequest } from '../../../../server/base-http'
2021-03-24 17:50:16 +01:00
export function matchHas (
2022-01-14 22:01:35 +01:00
req : BaseNextRequest | IncomingMessage ,
2021-03-24 17:50:16 +01:00
has : RouteHas [ ] ,
query : Params
) : false | Params {
const params : Params = { }
2021-06-17 17:52:11 +02:00
2021-03-24 17:50:16 +01:00
const allMatch = has . every ( ( hasItem ) = > {
let value : undefined | string
let key = hasItem . key
switch ( hasItem . type ) {
case 'header' : {
key = key ! . toLowerCase ( )
value = req . headers [ key ] as string
break
}
case 'cookie' : {
value = ( req as any ) . cookies [ hasItem . key ]
break
}
case 'query' : {
value = query [ key ! ]
break
}
case 'host' : {
const { host } = req ? . headers || { }
// remove port from host if present
const hostname = host ? . split ( ':' ) [ 0 ] . toLowerCase ( )
value = hostname
break
}
default : {
break
}
}
if ( ! hasItem . value && value ) {
params [ getSafeParamName ( key ! ) ] = value
return true
} else if ( value ) {
const matcher = new RegExp ( ` ^ ${ hasItem . value } $ ` )
2021-09-20 03:32:30 +02:00
const matches = Array . isArray ( value )
? value . slice ( - 1 ) [ 0 ] . match ( matcher )
: value . match ( matcher )
2021-03-24 17:50:16 +01:00
if ( matches ) {
2021-09-20 03:32:30 +02:00
if ( Array . isArray ( matches ) ) {
if ( matches . groups ) {
Object . keys ( matches . groups ) . forEach ( ( groupKey ) = > {
params [ groupKey ] = matches . groups ! [ groupKey ]
} )
} else if ( hasItem . type === 'host' && matches [ 0 ] ) {
params . host = matches [ 0 ]
}
2021-03-24 17:50:16 +01:00
}
return true
}
}
return false
} )
if ( allMatch ) {
return params
}
return false
}
2020-11-07 05:30:14 +01:00
export function compileNonPath ( value : string , params : Params ) : string {
if ( ! value . includes ( ':' ) ) {
return value
}
for ( const key of Object . keys ( params ) ) {
if ( value . includes ( ` : ${ key } ` ) ) {
value = value
. replace (
new RegExp ( ` : ${ key } \\ * ` , 'g' ) ,
` : ${ key } --ESCAPED_PARAM_ASTERISKS `
)
. replace (
new RegExp ( ` : ${ key } \\ ? ` , 'g' ) ,
` : ${ key } --ESCAPED_PARAM_QUESTION `
)
. replace ( new RegExp ( ` : ${ key } \\ + ` , 'g' ) , ` : ${ key } --ESCAPED_PARAM_PLUS ` )
. replace (
new RegExp ( ` : ${ key } (?! \\ w) ` , 'g' ) ,
` --ESCAPED_PARAM_COLON ${ key } `
)
}
}
value = value
. replace ( /(:|\*|\?|\+|\(|\)|\{|\})/g , '\\$1' )
. replace ( /--ESCAPED_PARAM_PLUS/g , '+' )
. replace ( /--ESCAPED_PARAM_COLON/g , ':' )
. replace ( /--ESCAPED_PARAM_QUESTION/g , '?' )
. replace ( /--ESCAPED_PARAM_ASTERISKS/g , '*' )
// the value needs to start with a forward-slash to be compiled
// correctly
2021-11-09 02:28:39 +01:00
return compile ( ` / ${ value } ` , { validate : false } ) ( params ) . substr ( 1 )
2020-11-07 05:30:14 +01:00
}
2021-11-09 02:28:39 +01:00
export function prepareDestination ( args : {
2020-12-04 11:14:55 +01:00
appendParamsToQuery : boolean
2021-11-09 02:28:39 +01:00
destination : string
params : Params
query : NextParsedUrlQuery
} ) {
const query = Object . assign ( { } , args . query )
2020-10-29 18:48:54 +01:00
delete query . __nextLocale
2020-11-11 03:09:45 +01:00
delete query . __nextDefaultLocale
2020-10-29 18:48:54 +01:00
2021-11-09 02:28:39 +01:00
let escapedDestination = args . destination
2021-09-14 22:13:46 +02:00
2021-11-09 02:28:39 +01:00
for ( const param of Object . keys ( { . . . args . params , . . . query } ) ) {
2021-09-14 22:13:46 +02:00
escapedDestination = escapeSegment ( escapedDestination , param )
}
const parsedDestination = parseUrl ( escapedDestination )
2020-08-13 14:39:36 +02:00
const destQuery = parsedDestination . query
2021-09-14 22:13:46 +02:00
const destPath = unescapeSegments (
` ${ parsedDestination . pathname ! } ${ parsedDestination . hash || '' } `
)
const destHostname = unescapeSegments ( parsedDestination . hostname || '' )
2021-11-09 02:28:39 +01:00
const destPathParamKeys : Key [ ] = [ ]
const destHostnameParamKeys : Key [ ] = [ ]
pathToRegexp ( destPath , destPathParamKeys )
pathToRegexp ( destHostname , destHostnameParamKeys )
2020-08-13 14:39:36 +02:00
2021-09-14 22:13:46 +02:00
const destParams : ( string | number ) [ ] = [ ]
2020-08-14 20:51:58 +02:00
2021-09-14 22:13:46 +02:00
destPathParamKeys . forEach ( ( key ) = > destParams . push ( key . name ) )
destHostnameParamKeys . forEach ( ( key ) = > destParams . push ( key . name ) )
2021-11-09 02:28:39 +01:00
const destPathCompiler = compile (
2020-08-14 20:51:58 +02:00
destPath ,
2020-08-13 14:39:36 +02:00
// we don't validate while compiling the destination since we should
// have already validated before we got to this point and validating
// breaks compiling destinations with named pattern params from the source
// e.g. /something:hello(.*) -> /another/:hello is broken with validation
// since compile validation is meant for reversing and not for inserting
// params from a separate path-regex into another
{ validate : false }
)
2021-11-09 02:28:39 +01:00
const destHostnameCompiler = compile ( destHostname , { validate : false } )
2020-08-13 14:39:36 +02:00
// update any params in query values
for ( const [ key , strOrArray ] of Object . entries ( destQuery ) ) {
2021-07-06 23:20:53 +02:00
// the value needs to start with a forward-slash to be compiled
// correctly
if ( Array . isArray ( strOrArray ) ) {
2021-09-14 22:13:46 +02:00
destQuery [ key ] = strOrArray . map ( ( value ) = >
2021-11-09 02:28:39 +01:00
compileNonPath ( unescapeSegments ( value ) , args . params )
2021-09-14 22:13:46 +02:00
)
2021-07-06 23:20:53 +02:00
} else {
2021-11-09 02:28:39 +01:00
destQuery [ key ] = compileNonPath ( unescapeSegments ( strOrArray ) , args . params )
2020-08-13 14:39:36 +02:00
}
}
// add path params to query if it's not a redirect and not
2020-08-14 20:51:58 +02:00
// already defined in destination query or path
2021-11-11 21:11:50 +01:00
let paramKeys = Object . keys ( args . params ) . filter (
( name ) = > name !== 'nextInternalLocale'
)
2020-08-14 20:51:58 +02:00
if (
2021-11-09 02:28:39 +01:00
args . appendParamsToQuery &&
2021-09-14 22:13:46 +02:00
! paramKeys . some ( ( key ) = > destParams . includes ( key ) )
2020-08-14 20:51:58 +02:00
) {
for ( const key of paramKeys ) {
if ( ! ( key in destQuery ) ) {
2021-11-09 02:28:39 +01:00
destQuery [ key ] = args . params [ key ]
2020-08-13 14:39:36 +02:00
}
}
}
2021-11-09 02:28:39 +01:00
let newUrl
2020-08-13 14:39:36 +02:00
try {
2021-11-09 02:28:39 +01:00
newUrl = destPathCompiler ( args . params )
2020-08-13 14:39:36 +02:00
const [ pathname , hash ] = newUrl . split ( '#' )
2021-11-09 02:28:39 +01:00
parsedDestination . hostname = destHostnameCompiler ( args . params )
2020-08-13 14:39:36 +02:00
parsedDestination . pathname = pathname
parsedDestination . hash = ` ${ hash ? '#' : '' } ${ hash || '' } `
2020-11-14 04:35:42 +01:00
delete ( parsedDestination as any ) . search
2021-11-09 02:28:39 +01:00
} catch ( err : any ) {
if ( err . message . match ( /Expected .*? to not repeat, but got an array/ ) ) {
2020-08-13 14:39:36 +02:00
throw new Error (
2021-03-29 10:25:00 +02:00
` To use a multi-match in the destination you must add \` * \` at the end of the param name to signify it should repeat. https://nextjs.org/docs/messages/invalid-multi-match `
2020-08-13 14:39:36 +02:00
)
}
throw err
}
// Query merge order lowest priority to highest
// 1. initial URL query values
// 2. path segment values
// 3. destination specified query values
parsedDestination . query = {
. . . query ,
. . . parsedDestination . query ,
}
return {
newUrl ,
parsedDestination ,
}
}
2021-11-09 02:28:39 +01:00
/ * *
* Ensure only a - zA - Z are used for param names for proper interpolating
* with path - to - regexp
* /
function getSafeParamName ( paramName : string ) {
let newParamName = ''
for ( let i = 0 ; i < paramName . length ; i ++ ) {
const charCode = paramName . charCodeAt ( i )
if (
( charCode > 64 && charCode < 91 ) || // A-Z
( charCode > 96 && charCode < 123 ) // a-z
) {
newParamName += paramName [ i ]
}
}
return newParamName
}
function escapeSegment ( str : string , segmentName : string ) {
return str . replace (
2021-11-30 20:15:13 +01:00
new RegExp ( ` : ${ escapeStringRegexp ( segmentName ) } ` , 'g' ) ,
2021-11-09 02:28:39 +01:00
` __ESC_COLON_ ${ segmentName } `
)
}
function unescapeSegments ( str : string ) {
return str . replace ( /__ESC_COLON_/gi , ':' )
}