2024-02-13 00:26:47 +01:00
// NodeJS requirements, for server deployment:
// `npm install mime-types parse-multipart-data`
// configuration
const appName = 'WuppìMini' ;
const serverPort = 8135 ;
const detailedLogging = true ;
const serverLanUpstreams = false ;
const serverPlaintextUpstreams = false ;
let resFiles = [ 'package.json' , 'package-lock.json' ] ;
const appTerms = `
< p > ( These terms apply to the server - hosted version of the app only . )
< br / > This service is offered for free , in the hope that it can be useful , but without any warranty .
< br / > For the service to be able to publish your posts , your content is transmitted to our server , which then forwards it to the server of the service you specified at the time of login , operating on your behalf with the credentials you provided .
< br / > Usage of the service might be automatically monitored , and the metadata generated by you might be archived for analytics , debugging , or legal reasons , for as long as we see fit . For every web request , this could include : your IP address , your user agent , the time of request , the requested URL . On any request to an upstream server , this could include : the requested URL on the upstream server , your username hash . On request for posting content , this could include : the hash of each text field ' s content of your post , the metadata of your uploaded files ( filename hash , content hash , content length , mime type ) . Your content itself , and all normal data , is never stored .
< br / > You are forbidden from using the service in any way that is damaging to the service itself or our infrastructure , or that is illegal in the jurisdiction this server is hosted in ( Italy , Europe ) .
< br / > We reserve the right to ban you from using the service at any time , for any reason , and without any explanation or prior warning .
< br / > By continuing with the usage of this site , you declare to understand and agree to these terms .
< br / > If you don ' t agree with these terms , discontinue usage of this site immediately , and instead < a href = "/info#h-floss" > get the source code < / a > t o h o s t i t y o u r s e l f , f i n d a n o t h e r i n s t a n c e , o r u s e t h e < a h r e f = " / i n f o # h - v e r s i o n s " > l o c a l , c l i e n t - s i d e v e r s i o n < / a > .
< / p > ` ;
const suggestedTags = [ 'fromWuppiMini' ] ;
const corsProxies = [ 'corsproxy.io' , 'corsproxy.org' ] ;
let fs , path , mime , multipart , crypto ;
let isEnvServer = ( typeof ( window ) === 'undefined' ) ;
let isEnvBrowser = ! isEnvServer ;
const httpCodes = { success : [ 200 , 201 ] } ;
const strings = {
csrfErrorHtml : ` <p class="notice error">Authorization token mismatch. Please try resubmitting.</p> ` ,
upstreamDisallowedHtml : ` <p class="notice error">Upstream destination is not allowed from backend.</p> ` ,
} ;
const newHtmlPage = ( content , title ) => ` <!DOCTYPE html><html><head>
< meta charset = "utf-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" / >
< title > $ { title ? ` ${ title } — ` : '' } $ { appName } < / t i t l e >
< style >
* {
box - sizing : border - box ;
font - family : sans - serif ;
}
code {
font - family : revert ;
}
body {
margin : 0 ;
padding - bottom : 8 px ;
color : # 000 ;
background : # eee url ( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPAAAADwBAMAAADMe/ShAAAAHlBMVEX29vb09PTy8vL19fXz8/Px8fHw8PDv7+/u7u739/dHdNvrAAACSElEQVR4AezYNX7DMBiGcafcKTRlC0hdy7T1Z7iAcS5YOkEl38DeQlNOW24YX63fsxr+Ztm2RmvjMiu2bWbOXPmjtW2A7S8YKlfMCK4lKPzeMoJvYhROHYIJno9gggkmmGCCCSaYYIIJJphgggkmGP/PZQZf4XDHCK5JFFYtE7hXkwWYap0awEMPh8NHA7jPNQprv2EAH5zh8MsTDo9vY/xGxuCeLXFYsVMY7nNZwCm/AcM3scZhnTooPBwfafBYP4JwiZvBfgWDh7Xxkc71DmXjY916hOAbLrMxK7ZuTOfKdxC4ZMf6z1Ui2iEh/2Sdssru8L4dy+zXFVGbuVtntyPxK+cqZRc7wr1Ll0v95/qser1DVZv/yVr5bv10Bczq+ML4Rl/als/ww4WfJptbwuACMbgwhaUVfkvgt6LU1icpdkAAAADAEEz/1EKshHOBgMAnwZ7gNgG9DEwybTKpyZiLRCT6ItqUCJuIYqKoosaJlEsMJBki+ZOElwRfkpqSuElcS9Qnd4LcGN3enRUACMRQDFSDJRzg3wIKuNnNzygYA83rGbx9g1cwGAwGg8FgMBgMBoPBYDAYDAaDwWAwGAwGg8FgMBgMBoPB/bFgfx7ZH4TWJ7DB0W9w5hwcdgen7MHxfp8r9IFGkKQEEU6WHWWhVZaWZTFdlg9mwWSdiAZRbJABB+FzkHoHcXuQ82cDBtlkQzVSkc1yZEMk2fRKNjZTzetkg0LlhJK1KjAYDAaDwWAwGAwGg8FgcAD3L/H6J4DLS/j67eEOl9GYoKbAV2YAAAAASUVORK5CYII=' ) ;
background - size : 10 px ;
}
a {
color : # 777 ;
}
h1 {
display : inline ;
}
h1 > a {
color : # 000 ;
}
form > input , form > select , form > textarea , form > label {
display : block ;
margin : 8 px 0 px ;
width : 100 % ;
}
form > input [ type = "text" ] , form > input [ type = "url" ] , form > input [ type = "password" ] , form > input [ type = "submit" ] , /*form > input[type="button"],*/ form > select , textarea {
min - height : 2 em ;
}
textarea {
font - size : medium ;
}
div # header {
margin : 0 ;
padding : 8 px ;
border - bottom : 4 px solid # 5 ac800 ;
background : # fff ;
}
div # header > * {
display : inline ;
padding : 0 px 8 px ;
}
div # app {
width : 90 % ;
margin - left : auto ;
margin - right : auto ;
padding : 0 px 8 px ;
}
div # transitioner {
width : 100 vw ;
height : 100 vh ;
position : absolute ;
top : 0 ;
left : 0 ;
background : black ;
opacity : 0.25 ;
}
p . notice {
background : white ;
padding : 1 em ;
border - width : 4 px ;
border - left - width : 1 em ;
border - style : solid ;
}
p . notice . success {
border - color : # 5 ac800 ;
}
p . notice . error {
border - color : # de0000 ;
}
< / s t y l e >
< / h e a d > < b o d y >
$ { isEnvBrowser ? ` <div id="transitioner"></div> ` : '' }
< div id = "header" >
< h1 > < a href = "/" > $ { appName } < / a > < / h 1 > < ! - -
-- > < a href = "/info" > Info < / a > < ! - -
-- > < a href = "/settings" > Settings < / a >
< / d i v >
< div id = "app" > $ { content } < / d i v >
< / b o d y > < / h t m l > ` ;
const A = ( href ) => ` <a href=" ${ href } "> ${ href } </a> `
const Log = ( type , msg ) => ( ( type !== 'D' || detailedLogging ) && console . log ( ` ${ type } : ${ msg } ` ) ) ;
const checkUpstreamAllowed = ( url ) => {
const [ protocol , ... rest ] = url . split ( '://' ) ;
const domain = rest [ 0 ] . split ( '/' ) [ 0 ] . trim ( ) ;
if ( isEnvServer && (
( ! serverLanUpstreams && ( domain === 'localhost' || ! isNaN ( domain . replaceAll ( '.' , '' ) . replaceAll ( ':' , '' ) ) ) )
||
( ! serverPlaintextUpstreams && protocol . toLowerCase ( ) !== 'https' )
) ) {
return false ;
}
return true ;
}
const redirectTo = ( url , res ) => {
if ( isEnvServer ) {
res . statusCode = 302 ;
res . setHeader ( 'Location' , url ) ;
res . end ( ) ;
} else if ( isEnvBrowser ) {
location . hash = url ;
}
} ;
const setPageContent = ( res , content , title ) => {
const titleHtml = ( title ? ` <h2> ${ title } </h2> ` : '' ) ;
if ( isEnvServer ) {
res . setHeader ( 'Content-Type' , 'text/html; charset=utf-8' ) ;
res . end ( newHtmlPage ( ( titleHtml + content ) , title ) ) ;
} else if ( isEnvBrowser ) {
if ( title ) {
document . title = title ;
}
document . querySelector ( 'div#app' ) . innerHTML = ( titleHtml + content ) ;
//for (const srcElem of document.querySelectorAll('[src^="/res/"]')) {
// srcElem.src = resFilesData[srcElem.getAttribute('src')];
//}
//for (const linkElem of document.querySelectorAll('link[rel="stylesheet"][href^="/res/"]')) {
// linkElem.href = resFilesData[linkElem.getAttribute('href')];
//}
for ( const aElem of document . querySelectorAll ( 'a[href^="/"]' ) ) {
aElem . href = ` # ${ aElem . getAttribute ( 'href' ) } ` ;
}
for ( const formElem of document . querySelectorAll ( 'form' ) ) {
const submitElem = formElem . querySelector ( 'input[type="submit"]' ) ;
formElem . onsubmit = ( event ) => {
event . preventDefault ( ) ;
const formData = ( new FormData ( formElem ) ) ;
formData . append ( submitElem . getAttribute ( 'name' ) , ( submitElem . value || 'Submit' ) ) ;
handleRequest ( {
method : ( formElem . getAttribute ( 'method' ) || 'GET' ) ,
url : ( formElem . getAttribute ( 'action' ) || location . hash . slice ( 1 ) ) , // + '?' + (new URLSearchParams(new FormData(formElem))),
headers : {
"content-type" : ( formElem . getAttribute ( 'enctype' ) || "application/x-www-form-urlencoded" ) ,
} ,
body : formData ,
//body: `${(new URLSearchParams(new FormData(formElem))).toString()}&${submitElem.name}=${submitElem.value || 'Submit'}`,
} )
} ;
//submitElem.type = 'button';
//submitElem.value = (submitElem.getAttribute('value') || 'Submit');
//submitElem.onclick = () => handleRequest({
// method: (formElem.getAttribute('method') || 'GET'),
// url: (formElem.getAttribute('action') || location.hash.slice(1)) + '?' + (new URLSearchParams(new FormData(formElem))),
//});
}
document . querySelector ( 'div#transitioner' ) . style . display = 'none' ;
}
} ;
// the below anti-CSRF routines do useful work only on the server, that kind of attack is not possible with the client app
const makeFormCsrf = ( accountString ) => {
if ( ! isEnvServer ) {
return '' ;
}
const time = Date . now ( ) . toString ( ) ;
return ( accountString ? `
< input type = "hidden" name = "formTime" value = "${time}" / >
< input type = "hidden" name = "formToken" value = "${genCsrfToken(accountString, time)}" / >
` : '');
} ;
const genCsrfToken = ( accountString , time ) => ( isEnvServer && time && crypto . scryptSync ( accountString , time , 32 ) . toString ( 'base64' ) ) ;
const matchCsrfToken = ( bodyParams , accountString ) => ( isEnvServer ? bodyParams . formToken === genCsrfToken ( accountString , bodyParams . formTime ) : true ) ;
// try to use the built-in cookie API, fallback to a Storage-based wrapper in case it fails (for example on file:///)
2024-02-13 00:38:27 +01:00
const clientCookieApi = ( isEnvBrowser && ( document . cookie || ( ! document . cookie && ( document . cookie = '_=_' ) && document . cookie ) ? ( set ) => ( set ? ( document . cookie = set ) : document . cookie ) : ( set ) => {
2024-02-13 00:26:47 +01:00
if ( set ) {
let api = sessionStorage ;
const tokens = set . split ( ';' ) ;
const [ 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 ;
}
}
api . setItem ( ` ${ appName } / ${ key } ` , rest . join ( '=' ) ) ;
} else /*(get)*/ {
let items = '' ;
for ( const item of Object . entries ( { ... localStorage , ... sessionStorage } ) ) {
items += ( item . join ( '=' ) + ';' ) . slice ( appName . length + 1 ) ;
}
return items . slice ( 0 , - 1 ) ;
}
} ) ) ;
const getCookie = ( req , keyQuery ) => {
if ( isEnvServer ) {
if ( keyQuery ) {
// get a specific cookie
for ( const cookie of ( req . headers ? . cookie ? . split ( ';' ) || [ ] ) ) {
const [ key , ... rest ] = cookie . split ( '=' ) ;
if ( key === keyQuery ) {
return rest . join ( '=' ) ;
}
}
} else {
// get all cookies
return req . headers ? . cookie ? . join ( ';' ) ;
}
} else if ( isEnvBrowser ) {
const cookies = clientCookieApi ( ) ;
if ( keyQuery ) {
for ( const cookie of cookies . split ( ';' ) ) {
const [ key , ... rest ] = cookie . split ( '=' ) ;
if ( key === keyQuery ) {
return rest . join ( '=' ) ;
}
}
} else {
return cookies ;
}
}
}
// TODO warn if the browser has cookies disabled when running on server side
const setCookies = ( res , list ) => {
if ( isEnvServer ) {
res . setHeader ( 'Set-Cookie' , list ) ;
} else if ( isEnvBrowser ) {
for ( const cookie of list ) {
clientCookieApi ( cookie ) ;
}
}
}
const corsProxyIfNeed = ( need ) => ( isEnvBrowser /*&& need*/ ? ` https:// ${ corsProxies [ ~ ~ ( Math . random ( ) * corsProxies . length ) ] } ? ` : '' ) ;
const handleRequest = async ( req , res = { } ) => {
// to check if we can save cookies:
// first check if any cookie is saved, if it is then we assume to be good
// if none is present, redirect to another endpoint that should set a "flag cookie" and redirect to a second one that checks if the flag is present
// if the check is successful we return to where we were before, otherwise we show a cookie warning
//if (!getCookie(req, '_')) { // flag
//}
//if (!getCookie(req)) {
// return redirectTo('/thecookieflagthingy', res);
//}
if ( isEnvBrowser && document . querySelector ( 'div#transitioner' ) ) {
document . querySelector ( 'div#transitioner' ) . style . display = 'block' ;
}
const section = req . url . split ( '/' ) [ 1 ] . split ( '?' ) [ 0 ] . toLowerCase ( ) ;
const urlParams = new URLSearchParams ( req . url . split ( '?' ) [ 1 ] ) ;
const bodyParams = { } ;
if ( req . method === 'HEAD' ) {
req . method = 'GET' ;
} else if ( req . method === 'POST' ) {
try {
if ( isEnvServer ) {
req . body = Buffer . alloc ( 0 ) ;
req . on ( 'data' , ( data ) => {
req . body = Buffer . concat ( [ req . body , data ] ) ;
if ( req . body . length > 4e6 ) { // 4 MB upload limit is (fair enough?)
req . connection . destroy ( ) ;
}
} )
await new Promise ( ( resolve ) => req . on ( 'end' , ( ) => resolve ( ) ) ) ;
}
const contentMime = req . headers [ 'content-type' ] . split ( ';' ) [ 0 ] ;
if ( isEnvServer && contentMime === 'application/x-www-form-urlencoded' ) {
for ( const [ key , value ] of ( new URLSearchParams ( req . body . toString ( ) ) ) . entries ( ) ) {
bodyParams [ key ] = value ;
}
} else if ( isEnvServer && contentMime === 'multipart/form-data' ) {
for ( const param of multipart . parse ( req . body , req . headers [ 'content-type' ] . split ( ';' ) [ 1 ] . split ( 'boundary=' ) [ 1 ] ) ) {
bodyParams [ param . name ] = ( param . type && param . filename !== undefined ? param : param . data . toString ( ) ) ;
}
} else if ( isEnvBrowser && [ 'application/x-www-form-urlencoded' , 'multipart/form-data' ] . includes ( contentMime ) ) {
for ( const [ key , value ] of req . body ) {
bodyParams [ key ] = value ;
bodyParams [ key ] . filename = bodyParams [ key ] . name ;
}
}
} catch ( err ) {
console . log ( err ) ;
req . connection ? . destroy ( ) ;
}
}
if ( ! section )
{
if ( getCookie ( req , 'account' ) ) {
return redirectTo ( '/compose' , res ) ;
} else {
return redirectTo ( '/info' , res ) ;
}
}
else if ( section === 'info' && req . method === 'GET' )
{
res . statusCode = 200 ;
return setPageContent ( res , `
$ { ! getCookie ( req , 'account' ) ? ` <p>You must login first. Go to <a href="/settings">Settings</a> to continue.</p> ` : '' }
< h3 > About < / h 3 >
< p >
$ { appName } ( temporary name ? ) is a minimalist , basic HTML - based frontend , designed for quickly and efficiently publishing to social media and content management services ( note that only WordPress is currently supported ) .
< br / >
Mainly aimed at old systems that might not support modern web - apps , the server - hosted version of this application works without any client - side scripts , and should be optionally reachable via unencrypted HTTP .
< br / >
About practical use cases , you ask ? I made this to upload game posts from my 3 DS , and possibly microblog with my Kindle ! ( See an example : < a href = "https://octospacc.altervista.org/2024/02/09/test-wuppimini/" > this post < / a > w a s p u b l i s h e d f r o m m y n 3 D S . )
< br / > < br / >
Check out all my other web endeavors at $ { A ( 'https://hub.octt.eu.org' ) } , or join my Matrix space to chat or if you need help : $ { A ( 'https://matrix.to/#/#Spacc:matrix.org' ) } .
< / p >
< h3 id = "h-versions" > Versions < / h 3 >
< p >
This app uses a novel approach behind the scenes to be able to run in one of either two modes , while reusing a single codebase : a classical server - side - rendered application , which works well on very limited systems but requires connection with a dedicated backend server that runs it , or a modern client - side single - page - application , relying on many modern web technologies , but working without an hosting server . Occasional bugs or update delays aside , the two essentially have feature parity and the same interface , but can be useful in different situations . Use whatever you prefer in each possible situation .
< / p >
< ul >
< li > Server - hosted version : $ { A ( 'https://wuppimini.octt.eu.org/' ) } . < / l i >
< li > Client - side version : $ { A ( 'https://hub.octt.eu.org/WuppiMini/' ) } . < / l i >
< / u l >
< h3 id = "h-floss" > Open - Source , Licensing , Disclaimers < / h 3 >
< p >
Copyright ( C ) 2024 OctoSpacc
< br / >
This program is free software : you can redistribute it and / or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation , either version 3 of the
License , or ( at your option ) any later version .
< br / >
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU Affero General Public License for more details .
< br / >
You should have received a copy of the GNU Affero General Public License
along with this program . If not , see $ { A ( 'https://www.gnu.org/licenses/' ) } .
< / p >
< p >
$ { isEnvServer ? ` You can obtain the full source code and assets by downloading the following files:
$ { resFiles . map ( ( file ) => ` • <a href="/res/ ${ file } "> ${ file } </a> ` ) . join ( '' ) } .
` : 'To get the original, unminified source code, visit this same page on the server-side version (refer to the Versions section above).'}
< / p >
$ { isEnvServer ? ` <h3>Terms of Use and Privacy Policy</h3> ${ appTerms } ` : '' }
< h3 > Changelog < / h 3 >
< h4 > 2024 - 02 - 12 < / h 3 >
< ul >
< li > First working client - side version of the current app , without backend server ( still a bit buggy ) . < / l i >
< li > Fixed suggested tags handling not working and making the post error out , by instead simply writing them in the post body < / l i >
< / u l >
< h4 > 2024 - 02 - 10 < / h 4 >
< ul >
< li > Add "remember me" login option . < / l i >
<!-- < li > Add "suggested tags" publishing option , will automatically add this list of tags to the post : [ $ { suggestedTags } ] . < / l i > - - >
< / u l >
< h4 > 2024 - 02 - 09 < / h 4 >
< ul >
< li > First working version , with an UI reminiscent of [ that dead social network that rhymes with Meterse ] , and Info , Settings , and Composition pages ! < / l i >
< li > Allow logging in with a WordPress . org profile , and creating new posts , including uploading images . < / l i >
< li > Add licensing and proper source code listing . < / l i >
< li > Tested on New and Old 3 DS . < / l i >
< / u l >
` );
}
else if ( section === 'compose' && [ 'GET' , 'POST' ] . includes ( req . method ) )
{
let noticeHtml = '' ;
const accountString = getCookie ( req , 'account' ) ;
if ( ! accountString ) {
return redirectTo ( '/' , res ) ;
}
res . statusCode = 200 ;
if ( req . method === 'POST' && bodyParams . publish ) {
if ( ! matchCsrfToken ( bodyParams , accountString ) ) {
res . statusCode = 401 ;
noticeHtml = strings . csrfErrorHtml ;
}
const isThereAnyFile = ( ( bodyParams . file ? . data ? . length || bodyParams . file ? . size ) > 0 ) ;
if ( ! bodyParams . text ? . trim ( ) && ! isThereAnyFile ) {
res . statusCode = 500 ;
noticeHtml = ` <p class="notice error">Post content is empty. Please write some text or upload a media.</p> ` ;
}
const account = accountDataFromString ( accountString ) ;
if ( ! checkUpstreamAllowed ( account . instance ) ) {
res . statusCode = 500 ;
noticeHtml = strings . upstreamDisallowedHtml ;
}
let mediaData ;
try {
// there is a media to upload first
if ( httpCodes . success . includes ( res . statusCode ) && isThereAnyFile ) {
const mediaReq = await fetch ( ` ${ corsProxyIfNeed ( account . cors ) } ${ account . instance } /wp-json/wp/v2/media ` , { headers : {
Authorization : ` Basic ${ btoa ( account . username + ':' + account . password ) } ` ,
"Content-Type" : bodyParams . file . type ,
"Content-Disposition" : ` attachment; filename= ${ bodyParams . file . filename } ` ,
} , method : "POST" , body : ( bodyParams . file . data || bodyParams . file ) } ) ;
mediaData = await mediaReq . json ( ) ;
if ( ! httpCodes . success . includes ( mediaReq . status ) ) {
noticeHtml = ` <p class="notice error">Upstream server responded with error ${ res . statusCode = mediaReq . status } : ${ JSON . stringify ( mediaData ) } </p> ` ;
}
}
// upload actual post if nothing has errored before
if ( httpCodes . success . includes ( res . statusCode ) ) {
const tagsHtml = ( bodyParams . tags === 'on' ? `
<!-- wp : paragraph -- >
< p > # $ { suggestedTags . join ( ' #' ) } < / p >
<!-- / w p : p a r a g r a p h - - >
` : '');
const figureHtml = ` ${ mediaData ? . id && mediaData ? . source _url ? `
<!-- wp : image { "id" : $ { mediaData . id } , "sizeSlug" : "large" } -- >
< figure class = "wp-block-image size-large" > < img src = "${mediaData.source_url}" class = "wp-image-${mediaData.id}" / > < / f i g u r e >
<!-- / w p : i m a g e - - >
` : ''} ` ;
const postReq = await fetch ( ` ${ corsProxyIfNeed ( account . cors ) } ${ account . instance } /wp-json/wp/v2/posts ` , { headers : {
Authorization : ` Basic ${ btoa ( account . username + ':' + account . password ) } ` ,
"Content-Type" : "application/json" ,
} , method : "POST" , body : JSON . stringify ( {
status : "publish" ,
featured _media : mediaData ? . id ,
title : bodyParams . title ,
content : ( bodyParams . html === 'on' ? ` ${ bodyParams . text } ${ tagsHtml } ${ figureHtml } ` : `
$ { bodyParams . text ? . trim ( ) ? `
<!-- wp : paragraph -- >
< p > $ { bodyParams . text . replaceAll ( '\r\n' , '<br/>' ) } < / p >
<!-- / w p : p a r a g r a p h - - >
` : ''}
$ { tagsHtml }
$ { bodyParams . text ? . trim ( ) && mediaData ? . id && mediaData ? . source _url ? `
<!-- wp : paragraph -- >
< p > < / p >
<!-- / w p : p a r a g r a p h - - >
` : ''}
$ { figureHtml } ` .trim()),
} ) } ) ;
const postData = await postReq . json ( ) ;
if ( httpCodes . success . includes ( postReq . status ) ) {
noticeHtml = ` <p class="notice success">Post uploaded! ${ A ( postData . link ) } </p> ` ;
} else {
noticeHtml = ` <p class="notice error">Upstream server responded with error ${ res . statusCode = postReq . status } : ${ JSON . stringify ( postData ) } </p> ` ;
}
}
} catch ( err ) {
console . log ( err ) ;
res . statusCode = 500 ;
// display only generic error from server-side, for security
noticeHtml = ` <p class="notice error"> ${ isEnvServer ? 'Some unknown error just happened. Please check that your data is correct, and try again.' : err } </p> ` ;
}
// TODO handle media upload success but post fail, either delete the remote media or find a way to reuse it when the user probably retries posting
}
return setPageContent ( res , ` ${ noticeHtml }
$ { makeFragmentLoggedIn ( accountString ) }
< form method = "POST" enctype = "multipart/form-data" > $ { makeFormCsrf ( accountString ) }
< input type = "text" name = "title" placeholder = "Post Title" value = "${bodyParams.title && res.statusCode !== 200 ? bodyParams.title : ''}" / >
< input type = "file" accept = "image/jpeg,image/gif,image/png,image/webp,image/bmp" name = "file" / >
< textarea name = "text" rows = "10" placeholder = "What's on your mind?" > $ { bodyParams . text && res . statusCode !== 200 ? bodyParams . text : '' } < / t e x t a r e a >
< label > < input type = "checkbox" name = "html" $ { bodyParams . html === 'on' && res . statusCode !== 200 ? 'checked="true"' : '' } / > Raw HTML mode < / l a b e l >
< label > < input type = "checkbox" name = "tags" $ { req . method === 'GET' || ( bodyParams . tags === 'on' && res . statusCode !== 200 ) ? 'checked="true"' : '' } / > Include suggested tags < / l a b e l >
<!-- < input type = "submit" name = "draft" value = "Save Draft" / > -- >
< input type = "submit" name = "publish" value = "Publish!" / >
< / f o r m >
` , 'Compose Post');
}
else if ( section === 'settings' && [ 'GET' , 'POST' ] . includes ( req . method ) )
{
let noticeHtml = '' ;
const accountString = getCookie ( req , 'account' ) ;
res . statusCode = 200 ;
if ( req . method === 'POST' ) {
if ( accountString && ! matchCsrfToken ( bodyParams , accountString ) ) {
res . statusCode = 401 ;
noticeHtml = strings . csrfErrorHtml ;
}
if ( res . statusCode === 200 && bodyParams . login ) {
bodyParams . instance = bodyParams . instance . trim ( ) ;
if ( ! checkUpstreamAllowed ( bodyParams . instance ) ) {
res . statusCode = 500 ;
noticeHtml = strings . upstreamDisallowedHtml ;
}
try {
const upstreamReq = await fetch ( ` ${ corsProxyIfNeed ( bodyParams . cors === 'on' ) } ${ bodyParams . instance } /wp-json/wp/v2/users?context=edit ` , { headers : {
Authorization : ` Basic ${ btoa ( bodyParams . username + ':' + bodyParams . password ) } ` ,
} } ) ;
const upstreamData = await upstreamReq . json ( ) ;
if ( upstreamReq . status === 200 ) {
let cookieFlags = ( bodyParams . remember === 'on' ? ` ; max-age= ${ 365 * 24 * 60 * 60 } ` : '' ) ;
setCookies ( res , [
` account= ${ bodyParams . instance } , ${ bodyParams . username } , ${ bodyParams . password } ${ cookieFlags } `
] ) ; // TODO: add cookie renewal procedure
return redirectTo ( '/' , res ) ;
} else {
res . statusCode = upstreamReq . status ;
noticeHtml = ` <p class="notice error">Upstream server responded with error ${ upstreamReq . status } : ${ JSON . stringify ( upstreamData ) } </p> ` ;
}
} catch ( err ) {
console . log ( err ) ;
res . statusCode = 500 ;
// display only generic error from server-side, for security
noticeHtml = ` <p class="notice error"> ${ isEnvServer ? 'Some unknown error just happened. Please check that your data is correct, and try again.' : err } </p> ` ;
}
} else if ( res . statusCode === 200 && bodyParams . logout ) {
setCookies ( res , [ ` account= ` ] ) ;
return redirectTo ( '/' , res ) ;
}
}
return setPageContent ( res , ` ${ noticeHtml }
$ { accountString ? `
< h3 > Current Account < / h 3 >
$ { makeFragmentLoggedIn ( accountString ) }
< form method = "POST" > $ { makeFormCsrf ( accountString ) }
< input type = "submit" name = "logout" value = "Logout" / >
< / f o r m >
` : '<p>You must login first.</p>'}
$ { ! accountString ? ` <h3><!--Add New Account-->Login</h3>
< form method = "POST" > $ { makeFormCsrf ( accountString ) }
< select name = "backend" >
< option value = "wp.org" >
WordPress . org ( Community / Self - hosted )
< / o p t i o n >
< / s e l e c t >
< label > < i > Note : For WordPress . org you must use an "application password" ( < code > / w p - a d m i n / p r o f i l e . p h p # a p p l i c a t i o n - p a s s w o r d s - s e c t i o n < / c o d e > ) < / i > < / l a b e l >
< input type = "url" name = "instance" placeholder = "Site/Instance URL" value = "${bodyParams.instance || ''}" required = "true" / >
< input type = "text" name = "username" placeholder = "Username" value = "${bodyParams.username || ''}" required = "true" / >
< input type = "password" name = "password" placeholder = "Password" value = "${bodyParams.password || ''}" required = "true" / >
<!-- $ { isEnvBrowser ? ` <label><input type="checkbox" name="cors" ${ bodyParams . cors === 'on' && res . statusCode !== 200 ? 'checked="true"' : '' } /> Site disallows CORS, use proxy</label> ` : '' } -- >
< label > < input type = "checkbox" name = "remember" $ { req . method === 'POST' && bodyParams . remember !== 'on' ? '' : 'checked="true"' } / > Remember me < / l a b e l >
< input type = "submit" name = "login" value = "Login and Save" / >
< / f o r m > ` : ' ' }
<!-- $ { true ? `
< h3 > Select and Manage Accounts < / h 3 >
< form method = "POST" > $ { makeFormCsrf ( accountString ) }
< ul >
< li >
< input type = "submit" name = "select" value = "username@url" / >
< / l i >
< / u l >
< / f o r m >
` : ''}-->
` , 'Settings');
}
else if ( section === 'res' && req . method === 'GET' && isEnvServer )
{
// serve static files if it exists and is allowed
const resPath = req . url . split ( '/res/' ) . slice ( 1 ) . join ( '/res/' ) ;
const filePath = ( _ _dirname + path . sep + resPath ) ;
if ( resFiles . includes ( resPath ) && fs . existsSync ( filePath ) ) {
res . setHeader ( 'Content-Type' , mime . lookup ( filePath ) ) ;
return res . end ( fs . readFileSync ( filePath ) ) ;
} else {
return setPageContent ( res , '' , ( res . statusCode = 404 ) ) ;
}
}
else
{
return setPageContent ( res , '' , ( res . statusCode = 404 ) ) ;
}
return setPageContent ( res , '' , ( res . statusCode = 500 ) ) ;
} ;
// todo handle optional options field(s)
const accountDataFromString = ( accountString ) => {
const tokens = accountString . split ( ',' ) ;
return { instance : tokens [ 0 ] , username : tokens [ 1 ] , password : tokens . slice ( 2 ) . join ( ',' ) } ;
}
const makeFragmentLoggedIn = ( accountString ) => {
const accountData = accountDataFromString ( accountString ) ;
return ` <p>Logged in as <i> ${ accountData . username } @ ${ A ( accountData . instance ) } </i>.</p> ` ;
}
if ( isEnvServer ) {
fs = require ( 'fs' ) ;
path = require ( 'path' ) ;
mime = require ( 'mime-types' ) ;
multipart = require ( 'parse-multipart-data' ) ;
crypto = require ( "crypto" ) ;
const scriptName = _ _filename . split ( path . sep ) . slice ( - 1 ) [ 0 ] ;
if ( process . argv [ 2 ] === 'html' ) {
// dump the base html for static usage
isEnvBrowser = true ;
fs . writeFileSync ( ` ${ _ _dirname } ${ path . sep } ${ scriptName . split ( '.' ) . slice ( 0 , - 1 ) . join ( '.' ) } .html ` , newHtmlPage ( `
<!-- < script > window . resFilesData = { $ { resFiles . map ( ( file ) => ` " ${ file } ": " ${ '' } " ` ) } } ; < / s c r i p t > - - >
< script src = "${scriptName}" > < / s c r i p t >
` ));
} else {
console . log ( 'Running Server...' )
resFiles = [ scriptName , ... resFiles ] ;
require ( 'http' ) . createServer ( handleRequest ) . listen ( serverPort , '0.0.0.0' ) ;
}
} else if ( isEnvBrowser ) {
location . hash || = '/' ;
const navigatePage = ( ) => handleRequest ( { url : location . hash . slice ( 1 ) , method : "GET" } ) ;
window . onhashchange = ( ) => {
location . hash || = '/' ;
navigatePage ( ) ;
} ;
navigatePage ( ) ;
}