2024-02-24 20:26:34 +01:00
/ * T O D O :
* built - in logging
2024-07-14 21:16:59 +02:00
* relative URL root for redirects and internal functions ? ( or useless since HTML must be custom anyways ? )
2024-07-12 18:43:09 +02:00
* utility functions for rewriting url query parameters ?
2024-07-15 00:47:19 +02:00
* fix client hash navigation to prevent no - going - back issue ( possible ? )
2024-07-13 22:15:53 +02:00
* finish polishing the cookie API
2024-07-14 21:16:59 +02:00
* implement some nodejs ` fs ` functions for client - side
2024-02-24 20:26:34 +01:00
* other things listed in the file
* /
2024-02-23 01:38:08 +01:00
( ( ) => {
2024-02-24 20:26:34 +01:00
const envIsNode = ( typeof module === 'object' && typeof module . exports === 'object' ) ;
const envIsBrowser = ( typeof window !== 'undefined' && typeof window . document !== 'undefined' ) ;
const allOpts = { } ;
let fs , path , mime , multipart ;
2024-07-13 22:15:53 +02:00
// 2 years is a good default duration for a system cookie
// they say Firefox limits to that; Chromium forces expiry after 400 days
// (https://http.dev/set-cookie#max-age)
const cookieMaxAge2Years = ( 2 * 365 * 24 * 60 * 60 ) ;
2024-02-24 20:26:34 +01:00
const setup = ( globalOptions = { } ) => {
allOpts . global = globalOptions ;
2024-07-12 18:43:09 +02:00
//allOpts.global.appName ||= 'Untitled SpaccDotWeb App';
2024-07-14 21:16:59 +02:00
//allOpts.global.appRoot ||= ''; //TODO
2024-02-24 20:26:34 +01:00
allOpts . global . staticPrefix || = '/static/' ;
2024-07-14 21:16:59 +02:00
//allOpts.global.staticRoot ||= ''; //TODO
2024-02-24 20:26:34 +01:00
allOpts . global . staticFiles || = [ ] ;
allOpts . global . linkStyles || = [ ] ;
2024-07-14 21:16:59 +02:00
//allOpts.global.linkRuntimeScripts ||= []; //TODO
allOpts . global . linkClientScripts || = [ ] ;
for ( const item of [ ... allOpts . global . linkStyles , ... allOpts . global . linkClientScripts ] ) {
2024-02-24 20:26:34 +01:00
const itemLow = item . toLowerCase ( ) ;
if ( ! ( itemLow . startsWith ( 'http://' ) || itemLow . startsWith ( 'https://' ) || itemLow . startsWith ( '/' ) ) ) {
allOpts . global . staticFiles . push ( item ) ;
2024-07-08 19:43:55 +02:00
}
}
2024-07-15 00:47:19 +02:00
allOpts . global . pageTitler || = ( title , opts = { } , context ) => ` ${ title || '' } ${ title && allOpts . global . appName ? ' — ' : '' } ${ allOpts . global . appName || '' } ` ,
allOpts . global . appPager || = ( content , title , opts = { } , context ) => content ,
allOpts . global . htmlPager || = ( content , title , opts = { } , context ) => ` <!DOCTYPE html><html><head><!--
2024-02-24 20:26:34 +01:00
-- > < meta charset = "utf-8" / > <!--
-- > < meta name = "viewport" content = "width=device-width, initial-scale=1.0" / > <!--
2024-07-15 00:47:19 +02:00
-- > < title > $ { ( opts . pageTitler || allOpts . global . pageTitler ) ( title , opts , context ) } < / t i t l e > < ! - -
2024-07-14 21:16:59 +02:00
-- > $ { allOpts . global . linkStyles . map ( ( path ) => makeHtmlStyleFragment ( path , opts . selfContained ) ) . join ( '' ) } <!--
-- > $ { allOpts . global . linkClientScripts . map ( ( path ) => makeHtmlScriptFragment ( path , opts . selfContained ) ) . join ( '' ) } <!--
2024-02-24 20:26:34 +01:00
-- > < / h e a d > < b o d y > < ! - -
-- > < div id = "transition" > < / d i v > < ! - -
2024-07-15 00:47:19 +02:00
-- > < div id = "app" > $ { ( opts . appPager || allOpts . global . appPager ) ( content , title , opts , context ) } < / d i v > < ! - -
2024-02-24 20:26:34 +01:00
-- > < / b o d y > < / h t m l > ` ;
const result = { } ;
result . initServer = ( serverOptions = { } ) => initServer ( serverOptions ) ;
if ( envIsNode ) {
result . writeStaticHtml = writeStaticHtml ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
return result ;
} ;
2024-02-23 01:38:08 +01:00
2024-02-24 20:26:34 +01:00
const initServer = ( serverOptions ) => {
allOpts . server = serverOptions ;
allOpts . server . defaultResponse || = { code : 500 , headers : { } } ;
allOpts . server . endpointsFalltrough || = false ;
2024-07-13 22:15:53 +02:00
allOpts . server . metaCookie || = 'spaccdotweb-meta' ;
2024-02-24 20:26:34 +01:00
if ( envIsNode ) {
allOpts . server . port || = 3000 ;
allOpts . server . address || = '127.0.0.1' ;
allOpts . server . maxBodyUploadSize = ( parseInt ( allOpts . server . maxBodyUploadSize ) || undefined ) ;
2024-07-12 18:43:09 +02:00
allOpts . server . handleHttpHead || = true ;
2024-02-24 20:26:34 +01:00
require ( 'http' ) . createServer ( handleRequest ) . listen ( allOpts . server . port , allOpts . server . address ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
if ( envIsBrowser ) {
allOpts . server . appElement || = 'div#app' ;
allOpts . server . transitionElement || = 'div#transition' ;
window . addEventListener ( 'hashchange' , ( ) => {
window . location . hash || = '/' ;
2024-07-15 00:47:19 +02:00
navigateClientPage ( ) ;
2024-02-24 20:26:34 +01:00
} ) ;
2024-07-15 00:47:19 +02:00
navigateClientPage ( ) ;
2024-07-08 19:43:55 +02:00
}
return allOpts . server ;
2024-02-24 20:26:34 +01:00
} ;
2024-02-23 01:38:08 +01:00
2024-07-15 00:47:19 +02:00
const navigateClientPage = ( forceUrl ) => ( ( ! forceUrl || ( window . location . hash === forceUrl ) )
&& handleRequest ( { url : window . location . hash . slice ( 1 ) , method : 'GET' } ) ) ;
2024-07-14 21:16:59 +02:00
const writeStaticHtml = ( selfContained = false ) => {
const appFilePath = process . mainModule . filename ;
const htmlFilePath = ( appFilePath . split ( '.' ) . slice ( 0 , - 1 ) . join ( '.' ) + '.html' ) ;
// path.relative seems to always append an extra '../', so we must slice it
const libraryPath = path . relative ( appFilePath , _ _filename ) . split ( path . sep ) . slice ( 1 ) . join ( path . sep ) ;
const libraryFolder = libraryPath . split ( path . sep ) . slice ( 0 , - 1 ) . join ( path . sep ) ;
2024-07-15 00:47:19 +02:00
const context = { envIsNode : false , envIsBrowser : true } ;
2024-07-14 21:16:59 +02:00
fs . writeFileSync ( htmlFilePath , allOpts . global . htmlPager ( `
$ { makeHtmlScriptFragment ( libraryPath , selfContained ) }
$ { makeHtmlScriptFragment ( ( ( libraryFolder && ( libraryFolder + '/' ) ) + 'SpaccDotWeb.Alt.js' ) , selfContained ) }
< $ { 'script' } >
2024-07-12 18:43:09 +02:00
window . require = ( ) => {
window . require = async ( src , type ) => {
await SpaccDotWeb . RequireScript ( ( src . startsWith ( './' ) ? src : ( 'node_modules/' + src ) ) , type ) ;
} ;
return window . SpaccDotWebServer ;
} ;
2024-07-14 21:16:59 +02:00
window . SpaccDotWebServer . staticFilesData = { $ { selfContained ? allOpts . global . staticFiles . map ( ( file ) => {
// TODO check if these paths are correct or must still be fixed
const filePath = ( appFilePath . split ( path . sep ) . slice ( 0 , - 1 ) . join ( path . sep ) + path . sep + file ) ;
2024-07-12 18:43:09 +02:00
return ` " ${ file } ":"data: ${ mime . lookup ( filePath ) } ;base64, ${ fs . readFileSync ( filePath ) . toString ( 'base64' ) } " ` ;
2024-07-14 21:16:59 +02:00
} ) . join ( ) : '' } } ;
< / $ { ' s c r i p t ' } >
$ { makeHtmlScriptFragment ( path . basename ( appFilePath ) , selfContained ) }
2024-07-15 00:47:19 +02:00
` , null, { selfContained, context }, context));
2024-07-14 21:16:59 +02:00
return htmlFilePath ;
2024-02-24 20:26:34 +01:00
} ;
2024-02-23 01:38:08 +01:00
2024-07-14 21:16:59 +02:00
const makeHtmlStyleFragment = ( path , getContent ) => {
const data = getFilePathContent ( path , getContent ) ;
return ( data [ 1 ] ? ` <style> ${ data [ 1 ] } </style> ` : ` <link rel="stylesheet" href=" ${ data [ 0 ] } "/> ` ) ;
} ;
const makeHtmlScriptFragment = ( path , getContent ) => {
const data = getFilePathContent ( path , getContent ) ;
return ` < ${ 'script' } ${ data [ 1 ] ? ` > ${ data [ 1 ] } ` : ` src=" ${ data [ 0 ] } "> ` } </ ${ 'script' } > ` ;
} ;
const getFilePathContent = ( path , getContent ) => ( [
( allOpts . global . staticFiles . includes ( path ) ? ( allOpts . global . staticPrefix + path ) : ( './' + path ) ) ,
( getContent && fs . existsSync ( path ) && fs . readFileSync ( path ) ) ,
] ) ;
2024-02-24 20:26:34 +01:00
const handleRequest = async ( request , response = { } ) => {
// build request context and handle special tasks
let result = allOpts . server . defaultResponse ;
const context = {
2024-07-15 00:47:19 +02:00
envIsNode , envIsBrowser ,
request , response ,
2024-07-13 22:15:53 +02:00
cookieString : ( envIsNode ? ( request . headers . cookie || '' ) : envIsBrowser ? clientCookieApi ( ) : '' ) ,
clientLanguages : parseclientLanguages ( request ) ,
2024-07-12 18:43:09 +02:00
urlPath : request . url . slice ( 1 ) . toLowerCase ( ) . split ( '?' ) [ 0 ] ,
urlQuery : request . url . split ( '?' ) ? . slice ( 1 ) ? . join ( '?' ) ,
2024-07-08 19:43:55 +02:00
bodyParameters : ( [ 'POST' , 'PUT' , 'PATCH' ] . includes ( request . method ) && await parseBodyParams ( request ) ) , // TODO which other methods need body?
//storageApi: (key,value, opts) => storageApi(key, value, opts),
2024-02-24 20:26:34 +01:00
redirectTo : ( url ) => redirectTo ( response , url ) ,
} ;
2024-07-15 00:47:19 +02:00
context . renderPage = ( content , title , opts ) => renderPage ( response , content , title , { ... opts , context } , context ) ;
2024-07-12 18:43:09 +02:00
context . urlSections = context . urlPath . split ( '/' ) ;
context . urlParameters = Object . fromEntries ( new URLSearchParams ( context . urlQuery ) ) ;
2024-07-13 22:15:53 +02:00
context . cookieData = parseCookieString ( context . cookieString ) ;
context . getCookie = ( cookieName ) => ( cookieName ? context . cookieData [ cookieName ] ? . value : context . cookieString ) ;
context . setCookie = ( cookie , cookieValue , cookieFlags ) => ( context . cookieString = setCookie ( context . cookieData , response , cookie , cookieValue , cookieFlags ) ) ;
if ( allOpts . server . metaCookie ) {
context . renewCookie = ( cookieName ) => renewCookie ( context . getCookie , context . setCookie , cookieName ) ;
}
2024-07-12 18:43:09 +02:00
setClientTransition ( true ) ;
const handlingHttpHead = ( allOpts . server . handleHttpHead && request . method === 'HEAD' )
if ( handlingHttpHead ) {
request . method = 'GET' ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
// serve static files
if ( envIsNode && request . method === 'GET' && request . url . toLowerCase ( ) . startsWith ( allOpts . global . staticPrefix ) ) {
const resPath = request . url . split ( allOpts . global . staticPrefix ) . slice ( 1 ) . join ( allOpts . global . staticPrefix ) ;
const filePath = ( process . mainModule . path + path . sep + resPath ) ; // TODO i think we need to read this another way if the module is in a different directory from the importing program
2024-07-08 19:43:55 +02:00
if ( allOpts . global . staticFiles . includes ( resPath ) && fs . existsSync ( filePath ) ) {
2024-02-24 20:26:34 +01:00
result = { code : 200 , headers : { 'content-type' : mime . lookup ( filePath ) } , body : fs . readFileSync ( filePath ) } ;
2024-02-23 01:38:08 +01:00
} else {
2024-02-24 20:26:34 +01:00
result = { code : 404 } ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
} else {
// handle custom endpoints
for ( const [ check , procedure ] of allOpts . server . endpoints ) {
2024-07-08 19:43:55 +02:00
if ( await requestCheckFunction ( check , context ) ) {
2024-02-24 20:26:34 +01:00
result = await procedure ( context ) ;
if ( ! allOpts . server . endpointsFalltrough ) {
break ;
2024-07-08 19:43:55 +02:00
}
}
}
}
2024-02-24 20:26:34 +01:00
// finalize a normal response
if ( result ) {
response . statusCode = result . code ;
for ( const name in result . headers ) {
response . setHeader ( name , result . headers [ name ] ) ;
2024-07-08 19:43:55 +02:00
}
2024-07-12 18:43:09 +02:00
response . end ( ! handlingHttpHead && result . body ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
} ;
2024-02-23 01:38:08 +01:00
2024-07-08 19:43:55 +02:00
const requestCheckFunction = ( check , context ) => {
if ( typeof check == 'function' ) {
return check ( context ) ;
} else if ( typeof check == 'string' ) {
let [ method , ... urlSections ] = check . trim ( ) . split ( ' ' ) ;
urlSections = urlSections . join ( ' ' ) . trim ( ) . split ( '/' ) . slice ( 1 , - 1 ) ;
const methodCheck = ( method === '*' || method . split ( '|' ) . includes ( context . request . method ) ) ;
let urlCheck = true ;
for ( const sectionIndex in urlSections ) {
const urlSection = urlSections [ sectionIndex ] ;
const checkSection = context . urlSections [ sectionIndex ] ;
if ( ! [ '' , '*' , checkSection ] . includes ( urlSection ) ) {
urlCheck = false ;
break ;
}
}
return ( methodCheck && urlCheck ) ;
}
} ;
2024-07-15 00:47:19 +02:00
const renderPage = ( response , content , title , opts = { } , context ) => {
2024-02-24 20:26:34 +01:00
// TODO titles and things
2024-07-12 18:43:09 +02:00
// TODO status code could need to be different in different situations and so should be set accordingly? (but we don't handle it here?)
2024-02-24 20:26:34 +01:00
if ( envIsNode ) {
response . setHeader ( 'content-type' , 'text/html; charset=utf-8' ) ;
2024-07-15 00:47:19 +02:00
response . end ( ( opts . htmlPager || allOpts . global . htmlPager ) ( content , title , opts , context ) ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
if ( envIsBrowser ) {
2024-07-15 00:47:19 +02:00
document . title = ( opts . pageTitler || allOpts . global . pageTitler ) ( title , opts , context ) ;
document . querySelector ( allOpts . server . appElement ) . innerHTML = ( ( opts . appPager || allOpts . global . appPager ) ( content , title , opts , context ) ) ;
2024-02-24 20:26:34 +01:00
for ( const srcElem of document . querySelectorAll ( ` [src^=" ${ allOpts . global . staticPrefix } "] ` ) ) {
2024-07-14 21:16:59 +02:00
var fileUrl = makeStaticClientFileUrl ( srcElem . getAttribute ( 'src' ) ) ;
if ( srcElem . tagName === 'SCRIPT' ) {
// script elements die immediately after being first set up,
// so we must re-create them to have them run with correct uri
srcElem . remove ( ) ;
document . head . appendChild ( Object . assign ( document . createElement ( 'script' ) , { src : fileUrl } ) ) ;
} else {
srcElem . src = fileUrl ;
}
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
for ( const linkElem of document . querySelectorAll ( ` link[rel="stylesheet"][href^=" ${ allOpts . global . staticPrefix } "] ` ) ) {
2024-07-14 21:16:59 +02:00
linkElem . href = makeStaticClientFileUrl ( linkElem . getAttribute ( 'href' ) ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
for ( const aElem of document . querySelectorAll ( 'a[href^="/"]' ) ) {
2024-07-15 00:47:19 +02:00
var url = ( '#' + aElem . getAttribute ( 'href' ) ) ;
aElem . href = url ;
// force navigation to page if the url is equal to current (refresh)
aElem . addEventListener ( 'click' , ( ) => {
navigateClientPage ( url ) ;
} ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
for ( const formElem of document . querySelectorAll ( 'form' ) ) {
formElem . onsubmit = ( event ) => {
event . preventDefault ( ) ;
const formData = ( new FormData ( formElem ) ) ;
formData . append ( event . submitter . getAttribute ( 'name' ) , ( event . submitter . value || 'Submit' ) ) ;
handleRequest ( {
method : ( formElem . getAttribute ( 'method' ) || 'GET' ) ,
url : ( formElem . getAttribute ( 'action' ) || location . hash . slice ( 1 ) ) ,
headers : { 'content-type' : ( formElem . getAttribute ( 'enctype' ) || "application/x-www-form-urlencoded" ) } ,
body : formData ,
2024-02-23 01:38:08 +01:00
} ) ;
} ;
2024-07-08 19:43:55 +02:00
}
2024-07-12 18:43:09 +02:00
setClientTransition ( false ) ;
2024-02-23 01:38:08 +01:00
} ;
2024-02-24 20:26:34 +01:00
} ;
2024-02-23 01:38:08 +01:00
2024-07-14 21:16:59 +02:00
const makeStaticClientFileUrl = ( url ) => {
url = url . slice ( allOpts . global . staticPrefix . length ) ;
return ( window . SpaccDotWebServer . staticFilesData [ url ] || ( './' + url ) ) ;
} ;
2024-07-12 18:43:09 +02:00
const setClientTransition = ( status ) => {
let transitionElement ;
if ( envIsBrowser && ( transitionElement = document . querySelector ( allOpts . server . transitionElement ) ) ) {
transitionElement . hidden = ! status ;
transitionElement . style . display = ( status ? 'block' : 'none' ) ;
}
}
2024-02-24 20:26:34 +01:00
const redirectTo = ( response , url ) => {
if ( envIsNode ) {
response . statusCode = 302 ;
response . setHeader ( 'location' , url ) ;
response . end ( ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
if ( envIsBrowser ) {
2024-07-15 00:47:19 +02:00
navigateClientPage ( '#' + ( location . hash = url ) ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
} ;
2024-02-23 01:38:08 +01:00
2024-02-24 20:26:34 +01:00
const parseBodyParams = async ( request ) => {
try {
let params = { } ;
2024-02-23 01:38:08 +01:00
if ( envIsNode ) {
2024-02-24 20:26:34 +01:00
request . body = Buffer . alloc ( 0 ) ;
request . on ( 'data' , ( data ) => {
request . body = Buffer . concat ( [ request . body , data ] ) ;
if ( request . body . length > allOpts . server . maxBodyUploadSize ) {
request . connection ? . destroy ( ) ;
// TODO handle this more gracefully? maybe an error callback or something?
2024-07-12 18:43:09 +02:00
}
2024-02-24 20:26:34 +01:00
} ) ;
await new Promise ( ( resolve ) => request . on ( 'end' , ( ) => resolve ( ) ) ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
const [ contentMime , contentEnc ] = request . headers [ 'content-type' ] . split ( ';' ) ;
if ( envIsNode && contentMime === 'application/x-www-form-urlencoded' ) {
for ( const [ key , value ] of ( new URLSearchParams ( request . body . toString ( ) ) ) . entries ( ) ) {
2024-07-12 18:43:09 +02:00
if ( key in params ) {
params [ key ] = [ ] . concat ( params [ key ] , value ) ;
} else {
params [ key ] = value ;
}
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
} else if ( envIsNode && contentMime === 'multipart/form-data' ) {
for ( const param of multipart . parse ( request . body , contentEnc . split ( 'boundary=' ) [ 1 ] ) ) {
params [ param . name ] = ( param . type && param . filename !== undefined ? param : param . data . toString ( ) ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
} else if ( envIsBrowser && [ 'application/x-www-form-urlencoded' , 'multipart/form-data' ] . includes ( contentMime ) ) {
for ( const [ key , value ] of request . body ) {
params [ key ] = value ;
params [ key ] . filename = params [ key ] . name ;
2024-07-08 19:43:55 +02:00
}
}
2024-02-24 20:26:34 +01:00
return params ;
} catch ( err ) {
console . log ( err ) ;
request . connection ? . destroy ( ) ;
2024-07-12 18:43:09 +02:00
}
2024-02-24 20:26:34 +01:00
} ;
2024-02-23 01:38:08 +01:00
2024-07-13 22:15:53 +02:00
const makeCookieString = ( cookieData ) => {
let cookieString = '' ;
for ( const cookieName in cookieData ) {
cookieString += ` ; ${ cookieName } = ${ cookieData [ cookieName ] . value } ` ;
2024-07-08 19:43:55 +02:00
}
2024-07-13 22:15:53 +02:00
return cookieString . slice ( 2 ) ;
}
const parseCookieString = ( cookieString ) => {
const cookieData = { } ;
if ( ! cookieString ) {
return cookieData ;
}
for ( const cookie of cookieString . split ( '; ' ) ) {
const [ key , ... rest ] = cookie . split ( '=' ) ;
cookieData [ key ] = { value : rest . join ( '=' ) } ;
}
if ( allOpts . server . metaCookie ) {
2024-07-14 21:16:59 +02:00
const metaData = parseMetaCookie ( cookieData [ allOpts . server . metaCookie ] ? . value ) ;
2024-07-13 22:15:53 +02:00
for ( const cookieName in cookieData ) {
cookieData [ cookieName ] = { ... cookieData [ cookieName ] , ... metaData [ cookieName ] } ;
2024-07-08 19:43:55 +02:00
}
}
2024-07-13 22:15:53 +02:00
return cookieData ;
2024-02-24 20:26:34 +01:00
} ;
2024-02-23 01:38:08 +01:00
2024-07-13 22:15:53 +02:00
const setCookie = ( cookieData , response , cookie , cookieValue , cookieFlags ) => {
const cookieSet = [ ] ;
const setCookie = ( cookie , cookieValue , cookieFlags ) => {
// TODO implement setCookie with standard raw format (current) and optional javascript object format
let cookieString , cookieName ;
if ( cookieValue === undefined ) {
cookieString = cookie ;
[ cookieName , ... cookieValue ] = cookieString . split ( '; ' ) [ 0 ] . split ( '=' ) ;
cookieValue = cookieValue . join ( '=' ) ;
} else {
cookieName = cookie ;
}
// Expires is deprecated, but old browsers don't support Max-Age
// (https://mrcoles.com/blog/cookies-max-age-vs-expires)
// so, we set Expires when it is missing but Max-Age is present
let expires = false ;
let maxAge ;
for ( let flag of cookieString . split ( '; ' ) . slice ( 1 ) ) {
flag = flag . toLowerCase ( ) ;
if ( ! expires && flag . startsWith ( 'max-age=' ) ) {
maxAge = flag . split ( '=' ) [ 1 ] ;
} else if ( flag . startsWith ( 'expires=' ) ) {
expires = true ;
}
}
if ( ! expires && maxAge ) {
cookieString += ` ; expires= ${ ( new Date ( Date . now ( ) + ( maxAge * 1000 ) ) ) . toUTCString ( ) } ` ;
}
if ( envIsNode ) {
cookieSet . push ( cookieString ) ;
} else if ( envIsBrowser ) {
clientCookieApi ( cookieString ) ;
}
// TODO update cookie flags inside cookieData, this just updates the value
// because of this (?) also the value of the metaCookie is not updated in the request
cookieData [ cookieName ] || = { } ;
cookieData [ cookieName ] . value = cookieValue ;
cookieString = makeCookieString ( cookieData ) ;
return cookieString ;
} ;
const result = setCookie ( cookie , cookieValue , cookieFlags ) ;
if ( allOpts . server . metaCookie ) {
const [ cookieBody , ... cookieFlags ] = cookie . split ( '; ' ) ;
const cookieName = cookieBody . split ( '=' ) [ 0 ] ;
const metaData = parseMetaCookie ( cookieData [ allOpts . server . metaCookie ] ? . value ) ;
if ( cookieFlags . length ) {
metaData [ cookieName ] = parseMetaCookie ( ` ${ cookieName } & ${ cookieFlags . join ( '&' ) } ` ) [ cookieName ] ;
} else {
delete metaData [ cookieName ] ;
}
setCookie ( ` ${ allOpts . server . metaCookie } = ${ makeMetaCookie ( metaData ) } ; max-age= ${ cookieMaxAge2Years } ` ) ;
}
2024-02-23 01:38:08 +01:00
if ( envIsNode ) {
2024-07-13 22:15:53 +02:00
response . setHeader ( 'set-cookie' , cookieSet ) ;
}
return result ;
} ;
// TODO handle renewal of all cookies at the same time if no name specified?
const renewCookie = ( getCookie , setCookie , cookieName ) => {
const metaData = parseMetaCookie ( getCookie ( allOpts . server . metaCookie ) ) ;
const cookieFlags = makeMetaCookie ( { [ cookieName ] : metaData [ cookieName ] } ) . split ( '|' ) [ 0 ] . slice ( cookieName . length ) . replaceAll ( '&' , '; ' ) ;
setCookie ( ` ${ cookieName } = ${ getCookie ( cookieName ) } ${ cookieFlags } ` ) ;
setCookie ( ` ${ allOpts . server . metaCookie } = ${ makeMetaCookie ( metaData ) } ; max-age= ${ cookieMaxAge2Years } ` ) ;
} ;
// below we use pipe ('|') to split cookies and amperstand ('&') for fields,
// no problem since as of 2024 no standard cookie flag has that in the body
const makeMetaCookie = ( metaData ) => {
let metaString = '' ;
metaData [ allOpts . server . metaCookie ] || = { } ;
metaData [ allOpts . server . metaCookie ] [ 'set-date' ] = ( new Date ( ) ) . toUTCString ( ) ;
for ( const [ name , flags ] of Object . entries ( metaData ) ) {
metaString += ` | ${ name } & ` ;
for ( const [ key , value ] of Object . entries ( flags ) ) {
// by standard, boolean cookie flags are named without any value if true, omitted if false
metaString += ( value === true ? ` ${ key } & ` : ` ${ key } = ${ value } & ` ) ;
}
metaString = metaString . slice ( 0 , - 1 ) ;
}
return metaString . slice ( 1 ) ;
} ;
const parseMetaCookie = ( metaString ) => {
const metaCookie = { } ;
if ( ! metaString ) {
return metaCookie ;
}
for ( const cookie of metaString . split ( '|' ) ) {
const [ name , ... fields ] = cookie . split ( '&' ) ;
metaCookie [ name ] = { } ;
for ( const field of fields ) {
if ( ! field ) {
continue ;
}
const [ key , ... tokens ] = field . split ( '=' ) ;
metaCookie [ name ] [ key ] = ( tokens . join ( '=' ) || true ) ;
}
2024-07-08 19:43:55 +02:00
}
2024-07-13 22:15:53 +02:00
return metaCookie ;
2024-02-24 20:26:34 +01:00
} ;
// try to use the built-in cookie API, fallback to a Storage-based wrapper in case it fails (for example on file:///)
2024-07-12 18:43:09 +02:00
const clientCookieApi = ( envIsBrowser && ( document . cookie || ( ! document . cookie && ( document . cookie = '_=_' ) && document . cookie )
? ( set ) => ( set ? ( document . cookie = set ) : document . cookie )
: ( set ) => {
2024-02-24 20:26:34 +01:00
const gid = allOpts . global . appName ; // TODO: introduce a conf field that is specifically a GID for less potential problems?
// also, TODO: what to do when no app name or any id is set?
if ( set ) {
let api = sessionStorage ;
const tokens = set . split ( ';' ) ;
let [ key , ... rest ] = tokens [ 0 ] . split ( '=' ) ;
for ( let token of tokens ) {
token = token . trim ( ) ;
if ( [ 'expires' , 'max-age' ] . includes ( token . split ( '=' ) [ 0 ] . toLowerCase ( ) ) ) {
api = localStorage ;
break ;
2024-07-08 19:43:55 +02:00
}
}
2024-02-24 20:26:34 +01:00
key = ` ${ gid } / ${ key } ` ;
const value = rest . join ( '=' ) ;
if ( value ) {
api . setItem ( key , value ) ;
} else {
sessionStorage . removeItem ( key ) ;
localStorage . removeItem ( key ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
} else /*(get)*/ {
let items = '' ;
for ( const item of Object . entries ( { ... localStorage , ... sessionStorage } ) ) {
if ( item [ 0 ] . startsWith ( ` ${ gid } / ` ) ) {
items += ( item . join ( '=' ) + ';' ) . slice ( gid . length + 1 ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
}
return items . slice ( 0 , - 1 ) ;
2024-07-08 19:43:55 +02:00
}
2024-02-24 20:26:34 +01:00
} ) ) ;
2024-07-12 18:43:09 +02:00
const parseclientLanguages = ( request ) => {
if ( envIsNode ) {
const languages = [ ] ;
const languageTokens = request . headers [ 'accept-language' ] ? . split ( ',' ) ;
if ( languageTokens ) {
for ( const language of languageTokens ) {
languages . push ( language . split ( ';' ) [ 0 ] . trim ( ) ) ;
}
}
return languages ;
} else if ( envIsBrowser ) {
return ( navigator . languages || [ navigator . language /* || navigator.userLanguage */ ] ) ;
}
} ;
2024-07-14 21:16:59 +02:00
const exportObj = { envIsNode , envIsBrowser , setup , makeHtmlStyleFragment , makeHtmlScriptFragment } ;
2024-02-24 20:26:34 +01:00
if ( envIsNode ) {
fs = require ( 'fs' ) ;
path = require ( 'path' ) ;
mime = require ( 'mime-types' ) ;
multipart = require ( 'parse-multipart-data' ) ;
module . exports = exportObj ;
2024-07-14 21:16:59 +02:00
}
2024-02-24 20:26:34 +01:00
if ( envIsBrowser ) {
window . SpaccDotWebServer = exportObj ;
2024-07-14 21:16:59 +02:00
}
2024-02-24 20:26:34 +01:00
2024-02-23 01:38:08 +01:00
} ) ( ) ;