2024-07-18 21:35:17 +03:00
import log from "../services/log.js" ;
import fileService from "./api/files.js" ;
import scriptService from "../services/script.js" ;
import cls from "../services/cls.js" ;
import sql from "../services/sql.js" ;
import becca from "../becca/becca.js" ;
2025-01-09 18:36:24 +02:00
import type { Request , Response , Router } from "express" ;
2025-06-17 19:37:40 +00:00
import { safeExtractMessageAndStackFromError , normalizeCustomHandlerPattern } from "../services/utils.js" ;
2024-04-07 14:05:50 +03:00
function handleRequest ( req : Request , res : Response ) {
2019-03-24 22:41:53 +01:00
2025-04-09 00:17:30 +02:00
// handle path from "*path" route wildcard
// in express v4, you could just add
// req.params.path + req.params[0], but with v5
// we get a split array that we have to join ourselves again
// @TriliumNextTODO: remove typecasting once express types are fixed
// they currently only treat req.params as string, while in reality
// it can also be a string[], when using wildcards
const splitPath = req . params . path as unknown as string [ ] ;
//const path = splitPath.map(segment => encodeURIComponent(segment)).join("/")
// naively join the "decoded" paths using a slash
// this is to mimick handleRequest behaviour
// as with the previous express v4.
// @TriliumNextTODO: using something like =>
// splitPath.map(segment => encodeURIComponent(segment)).join("/")
// might be safer
const path = splitPath . join ( "/" )
2019-03-24 22:41:53 +01:00
2024-04-07 14:05:50 +03:00
const attributeIds = sql . getColumn < string > ( "SELECT attributeId FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')" ) ;
2021-05-02 19:59:16 +02:00
2025-01-09 18:07:02 +02:00
const attrs = attributeIds . map ( ( attrId ) = > becca . getAttribute ( attrId ) ) ;
2019-01-27 15:47:40 +01:00
2020-06-22 23:13:53 +02:00
for ( const attr of attrs ) {
2024-04-07 14:05:50 +03:00
if ( ! attr ? . value . trim ( ) ) {
2020-11-06 21:52:57 +01:00
continue ;
}
2025-06-17 19:37:40 +00:00
// Get normalized patterns to handle both trailing slash cases
const patterns = normalizeCustomHandlerPattern ( attr . value ) ;
2025-06-18 20:46:11 +00:00
let match : RegExpMatchArray | null = null ;
2019-01-27 12:28:20 +01:00
2025-06-18 20:46:11 +00:00
try {
// Try each pattern until we find a match
for ( const pattern of patterns ) {
2025-06-17 19:37:40 +00:00
const regex = new RegExp ( ` ^ ${ pattern } $ ` ) ;
match = path . match ( regex ) ;
if ( match ) {
break ; // Found a match, exit pattern loop
}
}
2025-06-18 20:46:11 +00:00
} catch ( e : unknown ) {
const [ errMessage , errStack ] = safeExtractMessageAndStackFromError ( e ) ;
log . error ( ` Testing path for label ' ${ attr . attributeId } ', regex ' ${ attr . value } ' failed with error: ${ errMessage } , stack: ${ errStack } ` ) ;
continue ;
2020-06-22 23:13:53 +02:00
}
if ( ! match ) {
continue ;
}
2019-01-27 15:47:40 +01:00
2025-01-09 18:07:02 +02:00
if ( attr . name === "customRequestHandler" ) {
2020-06-24 22:08:31 +02:00
const note = attr . getNote ( ) ;
2019-01-27 12:28:20 +01:00
2023-05-03 10:23:20 +02:00
log . info ( ` Handling custom request ' ${ path } ' with note ' ${ note . noteId } ' ` ) ;
2019-01-27 12:28:20 +01:00
try {
2020-06-24 22:08:31 +02:00
scriptService . executeNote ( note , {
2020-06-22 23:13:53 +02:00
pathParams : match.slice ( 1 ) ,
req ,
res
} ) ;
2025-03-08 00:22:12 +01:00
} catch ( e : unknown ) {
2025-03-08 17:07:25 +01:00
const [ errMessage , errStack ] = safeExtractMessageAndStackFromError ( e ) ;
2025-03-08 00:22:12 +01:00
log . error ( ` Custom handler ' ${ note . noteId } ' failed with: ${ errMessage } , ${ errStack } ` ) ;
res . setHeader ( "Content-Type" , "text/plain" ) . status ( 500 ) . send ( errMessage ) ;
2019-01-27 16:37:18 +01:00
}
2025-01-09 18:07:02 +02:00
} else if ( attr . name === "customResourceProvider" ) {
2023-05-03 10:23:20 +02:00
fileService . downloadNoteInt ( attr . noteId , res ) ;
2025-01-09 18:07:02 +02:00
} else {
2023-05-03 10:23:20 +02:00
throw new Error ( ` Unrecognized attribute name ' ${ attr . name } ' ` ) ;
2020-06-22 23:13:53 +02:00
}
2019-01-27 16:37:18 +01:00
2023-05-05 23:41:11 +02:00
return ; // only the first handler is executed
2020-06-22 23:13:53 +02:00
}
2019-01-27 16:37:18 +01:00
2023-05-03 10:23:20 +02:00
const message = ` No handler matched for custom ' ${ path } ' request. ` ;
2019-02-03 11:15:32 +01:00
2020-06-22 23:13:53 +02:00
log . info ( message ) ;
2025-01-09 18:07:02 +02:00
res . setHeader ( "Content-Type" , "text/plain" ) . status ( 404 ) . send ( message ) ;
2020-06-22 23:13:53 +02:00
}
2019-01-27 16:37:18 +01:00
2024-04-07 14:05:50 +03:00
function register ( router : Router ) {
2020-06-22 23:13:53 +02:00
// explicitly no CSRF middleware since it's meant to allow integration from external services
2019-01-27 12:28:20 +01:00
2025-04-09 00:17:30 +02:00
router . all ( "/custom/*path" , ( req : Request , res : Response , _next ) = > {
2020-06-22 23:13:53 +02:00
cls . namespace . bindEmitter ( req ) ;
cls . namespace . bindEmitter ( res ) ;
2019-01-27 15:47:40 +01:00
2020-06-22 23:13:53 +02:00
cls . init ( ( ) = > handleRequest ( req , res ) ) ;
2019-01-27 12:28:20 +01:00
} ) ;
}
2024-07-18 21:42:44 +03:00
export default {
2019-01-27 12:28:20 +01:00
register
2020-06-20 12:31:38 +02:00
} ;