mirror of
https://gitlab.com/octospacc/octospacc.gitlab.io
synced 2025-06-05 21:59:15 +02:00
Update WuppiMini
This commit is contained in:
@@ -11,7 +11,7 @@ pages:
|
|||||||
cd ./src/WuppiMini
|
cd ./src/WuppiMini
|
||||||
npm install
|
npm install
|
||||||
node ./index.js html
|
node ./index.js html
|
||||||
cp ./index.js ./index.html ../../public/WuppiMini/
|
cp ./index.js ./index.html ./node_modules/SpaccDotWeb/SpaccDotWeb.Server.js ../../public/WuppiMini/
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
|
1
src/WuppiMini/.gitignore
vendored
1
src/WuppiMini/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/index.html
|
/index.html
|
||||||
|
/SpaccDotWeb.Server.js
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
// NodeJS requirements, for server deployment:
|
// search "Copyright" in this file for licensing info
|
||||||
// `npm install mime-types parse-multipart-data`
|
|
||||||
|
|
||||||
// configuration
|
// configuration
|
||||||
const appName = 'WuppìMini';
|
const appName = 'WuppìMini';
|
||||||
@@ -7,7 +6,7 @@ const serverPort = 8135;
|
|||||||
const detailedLogging = true;
|
const detailedLogging = true;
|
||||||
const serverLanUpstreams = false;
|
const serverLanUpstreams = false;
|
||||||
const serverPlaintextUpstreams = false;
|
const serverPlaintextUpstreams = false;
|
||||||
let resFiles = ['package.json', 'package-lock.json'];
|
let resFiles = [ 'package.json', 'package-lock.json' ];
|
||||||
const appTerms = `
|
const appTerms = `
|
||||||
<p >(These terms apply to the server-hosted version of the app only.)
|
<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/>This service is offered for free, in the hope that it can be useful, but without any warranty.
|
||||||
@@ -18,18 +17,21 @@ const appTerms = `
|
|||||||
<br/>By continuing with the usage of this site, you declare to understand and agree to these terms.
|
<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> to host it yourself, find another instance, or use the <a href="/info#h-versions">local, client-side version</a>.
|
<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> to host it yourself, find another instance, or use the <a href="/info#h-versions">local, client-side version</a>.
|
||||||
</p>`;
|
</p>`;
|
||||||
const suggestedTags = ['fromWuppiMini'];
|
const suggestedTags = [ 'fromWuppiMini' ];
|
||||||
const corsProxies = ['corsproxy.io', 'corsproxy.org'];
|
const corsProxies = [ 'corsproxy.io', 'corsproxy.org' ];
|
||||||
|
|
||||||
let fs, path, mime, multipart, crypto;
|
const SpaccDotWebServer = require('SpaccDotWeb/SpaccDotWeb.Server.js');
|
||||||
let isEnvServer = (typeof(window) === 'undefined');
|
let crypto;
|
||||||
let isEnvBrowser = !isEnvServer;
|
let isEnvServer = SpaccDotWebServer.envIsNode;
|
||||||
|
let isEnvBrowser = SpaccDotWebServer.envIsBrowser;
|
||||||
const httpCodes = { success: [200,201] };
|
const httpCodes = { success: [200,201] };
|
||||||
const strings = {
|
const strings = {
|
||||||
csrfErrorHtml: `<p class="notice error">Authorization token mismatch. Please try resubmitting.</p>`,
|
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>`,
|
upstreamDisallowedHtml: `<p class="notice error">Upstream destination is not allowed from backend.</p>`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const appPager = (content, title) => `${title ? `<h2>${title}</h2>` : ''}${content}`;
|
||||||
|
|
||||||
const newHtmlPage = (content, title) => `<!DOCTYPE html><html><head>
|
const newHtmlPage = (content, title) => `<!DOCTYPE html><html><head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
@@ -85,7 +87,7 @@ div#app {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
}
|
}
|
||||||
div#transitioner {
|
div#transition {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -108,15 +110,15 @@ p.notice.error {
|
|||||||
border-color: #de0000;
|
border-color: #de0000;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head><body>
|
</head><body><!--
|
||||||
${isEnvBrowser ? `<div id="transitioner"></div>` : ''}
|
-->${isEnvBrowser ? `<div id="transition"></div>` : ''}<!--
|
||||||
<div id="header">
|
--><div id="header"><!--
|
||||||
<h1><a href="/">${appName}</a></h1><!--
|
--><h1><a href="/">${appName}</a></h1><!--
|
||||||
--><a href="/info">Info</a><!--
|
--><a href="/info">Info</a><!--
|
||||||
--><a href="/settings">Settings</a>
|
--><a href="/settings">Settings</a><!--
|
||||||
</div>
|
--></div><!--
|
||||||
<div id="app">${content}</div>
|
--><div id="app">${appPager(content, title)}</div><!--
|
||||||
</body></html>`;
|
--></body></html>`;
|
||||||
|
|
||||||
const A = (href) => `<a href="${href}">${href}</a>`
|
const A = (href) => `<a href="${href}">${href}</a>`
|
||||||
|
|
||||||
@@ -135,62 +137,6 @@ const checkUpstreamAllowed = (url) => {
|
|||||||
return true;
|
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
|
// 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) => {
|
const makeFormCsrf = (accountString) => {
|
||||||
@@ -208,72 +154,10 @@ const genCsrfToken = (accountString, time) => (isEnvServer && time && crypto.scr
|
|||||||
|
|
||||||
const matchCsrfToken = (bodyParams, accountString) => (isEnvServer ? bodyParams.formToken === genCsrfToken(accountString, bodyParams.formTime) : true);
|
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:///)
|
|
||||||
const clientCookieApi = (isEnvBrowser && (document.cookie || (!document.cookie && (document.cookie = '_=_') && document.cookie) ? (set) => (set ? (document.cookie = set) : document.cookie) : (set) => {
|
|
||||||
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 corsProxyIfNeed = (need) => (isEnvBrowser /*&& need*/ ? `https://${corsProxies[~~(Math.random() * corsProxies.length)]}?` : '');
|
||||||
|
|
||||||
const handleRequest = async (req, res={}) => {
|
/*const handleRequest = async (req, res={}) => {
|
||||||
|
// TODO warn if the browser has cookies disabled when running on server side
|
||||||
// to check if we can save cookies:
|
// to check if we can save cookies:
|
||||||
// first check if any cookie is saved, if it is then we assume to be good
|
// 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 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
|
||||||
@@ -283,59 +167,59 @@ const handleRequest = async (req, res={}) => {
|
|||||||
//if (!getCookie(req)) {
|
//if (!getCookie(req)) {
|
||||||
// return redirectTo('/thecookieflagthingy', res);
|
// 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') {
|
if (req.method === 'HEAD') {
|
||||||
req.method = 'GET';
|
req.method = 'GET';
|
||||||
} else if (req.method === 'POST') {
|
};
|
||||||
try {
|
};*/
|
||||||
if (isEnvServer) {
|
|
||||||
req.body = Buffer.alloc(0);
|
// todo handle optional options field(s)
|
||||||
req.on('data', (data) => {
|
const accountDataFromString = (accountString) => {
|
||||||
req.body = Buffer.concat([req.body, data]);
|
const tokens = accountString.split(',');
|
||||||
if (req.body.length > 4e6) { // 4 MB upload limit is (fair enough?)
|
return { instance: tokens[0], username: tokens[1], password: tokens.slice(2).join(',') };
|
||||||
req.connection.destroy();
|
}
|
||||||
}
|
|
||||||
})
|
const makeFragmentLoggedIn = (accountString) => {
|
||||||
await new Promise((resolve) => req.on('end', () => resolve()));
|
const accountData = accountDataFromString(accountString);
|
||||||
}
|
return `<p>Logged in as <i>${accountData.username} @ ${A(accountData.instance)}</i>.</p>`;
|
||||||
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()) {
|
const main = () => {
|
||||||
bodyParams[key] = value;
|
if (SpaccDotWebServer.envIsNode && process.argv[2] !== 'html') {
|
||||||
}
|
resFiles = [__filename.split(require('path').sep).slice(-1)[0], ...resFiles];
|
||||||
} 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());
|
const server = SpaccDotWebServer.setup({
|
||||||
}
|
appName: appName,
|
||||||
} else if (isEnvBrowser && ['application/x-www-form-urlencoded', 'multipart/form-data'].includes(contentMime)) {
|
staticPrefix: '/res/',
|
||||||
for (const [key, value] of req.body) {
|
staticFiles: resFiles,
|
||||||
bodyParams[key] = value;
|
appPager: appPager,
|
||||||
bodyParams[key].filename = bodyParams[key].name;
|
htmlPager: newHtmlPage,
|
||||||
}
|
});
|
||||||
}
|
|
||||||
} catch(err) {
|
if (SpaccDotWebServer.envIsNode && process.argv[2] === 'html') {
|
||||||
console.log(err);
|
server.writeStaticHtml();
|
||||||
req.connection?.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!section)
|
|
||||||
{
|
|
||||||
if (getCookie(req, 'account')) {
|
|
||||||
return redirectTo('/compose', res);
|
|
||||||
} else {
|
} else {
|
||||||
return redirectTo('/info', res);
|
if (SpaccDotWebServer.envIsNode) {
|
||||||
}
|
crypto = require('crypto');
|
||||||
}
|
console.log('Running Server...');
|
||||||
else if (section === 'info' && req.method === 'GET')
|
};
|
||||||
{
|
server.initServer({
|
||||||
res.statusCode = 200;
|
port: serverPort,
|
||||||
return setPageContent(res, `
|
address: '0.0.0.0',
|
||||||
${!getCookie(req, 'account') ? `<p>You must login first. Go to <a href="/settings">Settings</a> to continue.</p>` : ''}
|
maxBodyUploadSize: 4e6, // 4 MB
|
||||||
|
endpoints: [ endpointRoot, endpointInfo, endpointCompose, endpointSettings, endpointCatch ],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpointRoot = [ (ctx) => (!ctx.urlSections[0]), (ctx) => ctx.redirectTo(ctx.getCookie('account') ? '/compose' : '/info') ];
|
||||||
|
|
||||||
|
const endpointCatch = [ (ctx) => true, (ctx) => ctx.renderPage('', (ctx.response.statusCode = 404)) ];
|
||||||
|
|
||||||
|
const endpointInfo = [ (ctx) => (ctx.urlSections[0] === 'info' && ctx.request.method === 'GET'), (ctx) => {
|
||||||
|
ctx.response.statusCode = 200;
|
||||||
|
ctx.renderPage(`
|
||||||
|
${!ctx.getCookie('account') ? `<p>You must login first. Go to <a href="/settings">Settings</a> to continue.</p>` : ''}
|
||||||
<h3>About</h3>
|
<h3>About</h3>
|
||||||
<p>
|
<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).
|
${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).
|
||||||
@@ -378,6 +262,11 @@ const handleRequest = async (req, res={}) => {
|
|||||||
</p>
|
</p>
|
||||||
${isEnvServer ? `<h3>Terms of Use and Privacy Policy</h3>${appTerms}` : ''}
|
${isEnvServer ? `<h3>Terms of Use and Privacy Policy</h3>${appTerms}` : ''}
|
||||||
<h3>Changelog</h3>
|
<h3>Changelog</h3>
|
||||||
|
<h4>2024-02-24</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Allow uploading posts as published or draft, via 2 distinct buttons.</li>
|
||||||
|
<li>Migrated fancy portable-server-codebase to <a href="https://gitlab.com/SpaccInc/SpaccDotWeb">SpaccDotWeb</a> for code reuse and slimming down of the application core.</li>
|
||||||
|
</ul>
|
||||||
<h4>2024-02-12</h3>
|
<h4>2024-02-12</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>First working client-side version of the current app, without backend server (still a bit buggy).</li>
|
<li>First working client-side version of the current app, without backend server (still a bit buggy).</li>
|
||||||
@@ -386,7 +275,7 @@ const handleRequest = async (req, res={}) => {
|
|||||||
<h4>2024-02-10</h4>
|
<h4>2024-02-10</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Add "remember me" login option.</li>
|
<li>Add "remember me" login option.</li>
|
||||||
<!--<li>Add "suggested tags" publishing option, will automatically add this list of tags to the post: [${suggestedTags}].</li>-->
|
<li>Add "suggested tags" publishing option, will automatically add this list of tags to the post: [${suggestedTags}].</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h4>2024-02-09</h4>
|
<h4>2024-02-09</h4>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -396,47 +285,48 @@ const handleRequest = async (req, res={}) => {
|
|||||||
<li>Tested on New and Old 3DS.</li>
|
<li>Tested on New and Old 3DS.</li>
|
||||||
</ul>
|
</ul>
|
||||||
`);
|
`);
|
||||||
}
|
} ];
|
||||||
else if (section === 'compose' && ['GET', 'POST'].includes(req.method))
|
|
||||||
{
|
const endpointCompose = [ (ctx) => (ctx.urlSections[0] === 'compose' && ['GET', 'POST'].includes(ctx.request.method)), async (ctx) => {
|
||||||
let noticeHtml = '';
|
let noticeHtml = '';
|
||||||
const accountString = getCookie(req, 'account');
|
const accountString = ctx.getCookie('account');
|
||||||
if (!accountString) {
|
if (!accountString) {
|
||||||
return redirectTo('/', res);
|
return ctx.redirectTo('/');
|
||||||
}
|
}
|
||||||
res.statusCode = 200;
|
ctx.response.statusCode = 200;
|
||||||
if (req.method === 'POST' && bodyParams.publish) {
|
const postUploadStatus = ((ctx.bodyParameters?.publish && 'publish') || (ctx.bodyParameters?.draft && 'draft'));
|
||||||
if (!matchCsrfToken(bodyParams, accountString)) {
|
if (ctx.request.method === 'POST' && postUploadStatus) {
|
||||||
res.statusCode = 401;
|
if (!matchCsrfToken(ctx.bodyParameters, accountString)) {
|
||||||
|
ctx.response.statusCode = 401;
|
||||||
noticeHtml = strings.csrfErrorHtml;
|
noticeHtml = strings.csrfErrorHtml;
|
||||||
}
|
}
|
||||||
const isThereAnyFile = ((bodyParams.file?.data?.length || bodyParams.file?.size) > 0);
|
const isThereAnyFile = ((ctx.bodyParameters.file?.data?.length || ctx.bodyParameters.file?.size) > 0);
|
||||||
if (!bodyParams.text?.trim() && !isThereAnyFile) {
|
if (!ctx.bodyParameters.text?.trim() && !isThereAnyFile) {
|
||||||
res.statusCode = 500;
|
ctx.response.statusCode = 500;
|
||||||
noticeHtml = `<p class="notice error">Post content is empty. Please write some text or upload a media.</p>`;
|
noticeHtml = `<p class="notice error">Post content is empty. Please write some text or upload a media.</p>`;
|
||||||
}
|
}
|
||||||
const account = accountDataFromString(accountString);
|
const account = accountDataFromString(accountString);
|
||||||
if (!checkUpstreamAllowed(account.instance)) {
|
if (!checkUpstreamAllowed(account.instance)) {
|
||||||
res.statusCode = 500;
|
ctx.response.statusCode = 500;
|
||||||
noticeHtml = strings.upstreamDisallowedHtml;
|
noticeHtml = strings.upstreamDisallowedHtml;
|
||||||
}
|
}
|
||||||
let mediaData;
|
let mediaData;
|
||||||
try {
|
try {
|
||||||
// there is a media to upload first
|
// there is a media to upload first
|
||||||
if (httpCodes.success.includes(res.statusCode) && isThereAnyFile) {
|
if (httpCodes.success.includes(ctx.response.statusCode) && isThereAnyFile) {
|
||||||
const mediaReq = await fetch(`${corsProxyIfNeed(account.cors)}${account.instance}/wp-json/wp/v2/media`, { headers: {
|
const mediaReq = await fetch(`${corsProxyIfNeed(account.cors)}${account.instance}/wp-json/wp/v2/media`, { headers: {
|
||||||
Authorization: `Basic ${btoa(account.username + ':' + account.password)}`,
|
Authorization: `Basic ${btoa(account.username + ':' + account.password)}`,
|
||||||
"Content-Type": bodyParams.file.type,
|
"Content-Type": ctx.bodyParameters.file.type,
|
||||||
"Content-Disposition": `attachment; filename=${bodyParams.file.filename}`,
|
"Content-Disposition": `attachment; filename=${ctx.bodyParameters.file.filename}`,
|
||||||
}, method: "POST", body: (bodyParams.file.data || bodyParams.file) });
|
}, method: "POST", body: (ctx.bodyParameters.file.data || ctx.bodyParameters.file) });
|
||||||
mediaData = await mediaReq.json();
|
mediaData = await mediaReq.json();
|
||||||
if (!httpCodes.success.includes(mediaReq.status)) {
|
if (!httpCodes.success.includes(mediaReq.status)) {
|
||||||
noticeHtml = `<p class="notice error">Upstream server responded with error ${res.statusCode = mediaReq.status}: ${JSON.stringify(mediaData)}</p>`;
|
noticeHtml = `<p class="notice error">Upstream server responded with error ${ctx.response.statusCode = mediaReq.status}: ${JSON.stringify(mediaData)}</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// upload actual post if nothing has errored before
|
// upload actual post if nothing has errored before
|
||||||
if (httpCodes.success.includes(res.statusCode)) {
|
if (httpCodes.success.includes(ctx.response.statusCode)) {
|
||||||
const tagsHtml = (bodyParams.tags === 'on' ? `
|
const tagsHtml = (ctx.bodyParameters.tags === 'on' ? `
|
||||||
<!-- wp:paragraph -->
|
<!-- wp:paragraph -->
|
||||||
<p> #${suggestedTags.join(' #')} </p>
|
<p> #${suggestedTags.join(' #')} </p>
|
||||||
<!-- /wp:paragraph -->
|
<!-- /wp:paragraph -->
|
||||||
@@ -450,17 +340,17 @@ const handleRequest = async (req, res={}) => {
|
|||||||
Authorization: `Basic ${btoa(account.username + ':' + account.password)}`,
|
Authorization: `Basic ${btoa(account.username + ':' + account.password)}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}, method: "POST", body: JSON.stringify({
|
}, method: "POST", body: JSON.stringify({
|
||||||
status: "publish",
|
status: postUploadStatus,
|
||||||
featured_media: mediaData?.id,
|
featured_media: mediaData?.id,
|
||||||
title: bodyParams.title,
|
title: ctx.bodyParameters.title,
|
||||||
content: (bodyParams.html === 'on' ? `${bodyParams.text}${tagsHtml}${figureHtml}` : `
|
content: (ctx.bodyParameters.html === 'on' ? `${ctx.bodyParameters.text}${tagsHtml}${figureHtml}` : `
|
||||||
${bodyParams.text?.trim() ? `
|
${ctx.bodyParameters.text?.trim() ? `
|
||||||
<!-- wp:paragraph -->
|
<!-- wp:paragraph -->
|
||||||
<p>${bodyParams.text.replaceAll('\r\n', '<br/>')}</p>
|
<p>${ctx.bodyParameters.text.replaceAll('\r\n', '<br/>')}</p>
|
||||||
<!-- /wp:paragraph -->
|
<!-- /wp:paragraph -->
|
||||||
` : ''}
|
` : ''}
|
||||||
${tagsHtml}
|
${tagsHtml}
|
||||||
${bodyParams.text?.trim() && mediaData?.id && mediaData?.source_url ? `
|
${ctx.bodyParameters.text?.trim() && mediaData?.id && mediaData?.source_url ? `
|
||||||
<!-- wp:paragraph -->
|
<!-- wp:paragraph -->
|
||||||
<p></p>
|
<p></p>
|
||||||
<!-- /wp:paragraph -->
|
<!-- /wp:paragraph -->
|
||||||
@@ -469,75 +359,74 @@ ${figureHtml}`.trim()),
|
|||||||
}) });
|
}) });
|
||||||
const postData = await postReq.json();
|
const postData = await postReq.json();
|
||||||
if (httpCodes.success.includes(postReq.status)) {
|
if (httpCodes.success.includes(postReq.status)) {
|
||||||
noticeHtml = `<p class="notice success">Post uploaded! ${A(postData.link)}</p>`;
|
noticeHtml = `<p class="notice success">${postUploadStatus === 'publish' ? 'Post published' : ''}${postUploadStatus === 'draft' ? 'Draft uploaded' : ''}! ${A(postData.link)}</p>`;
|
||||||
} else {
|
} else {
|
||||||
noticeHtml = `<p class="notice error">Upstream server responded with error ${res.statusCode = postReq.status}: ${JSON.stringify(postData)}</p>`;
|
noticeHtml = `<p class="notice error">Upstream server responded with error ${ctx.response.statusCode = postReq.status}: ${JSON.stringify(postData)}</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
res.statusCode = 500;
|
ctx.response.statusCode = 500;
|
||||||
// display only generic error from server-side, for security
|
// 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>`;
|
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
|
// 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}
|
ctx.renderPage(`${noticeHtml}
|
||||||
${makeFragmentLoggedIn(accountString)}
|
${makeFragmentLoggedIn(accountString)}
|
||||||
<form method="POST" enctype="multipart/form-data">${makeFormCsrf(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="text" name="title" placeholder="Post Title" value="${ctx.bodyParameters.title && ctx.response.statusCode !== 200 ? ctx.bodyParameters.title : ''}"/>
|
||||||
<input type="file" accept="image/jpeg,image/gif,image/png,image/webp,image/bmp" name="file"/>
|
<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 : ''}</textarea>
|
<textarea name="text" rows="10" placeholder="What's on your mind?">${ctx.bodyParameters.text && ctx.response.statusCode !== 200 ? ctx.bodyParameters.text : ''}</textarea>
|
||||||
<label><input type="checkbox" name="html" ${bodyParams.html === 'on' && res.statusCode !== 200 ? 'checked="true"' : ''}/> Raw HTML mode</label>
|
<!-- TODO: fix the turn off-on on submit of these checkboxes... -->
|
||||||
<label><input type="checkbox" name="tags" ${req.method === 'GET' || (bodyParams.tags === 'on' && res.statusCode !== 200) ? 'checked="true"' : ''}/> Include suggested tags</label>
|
<label><input type="checkbox" name="html" ${ctx.bodyParameters.html === 'on' && ctx.response.statusCode !== 200 ? 'checked="true"' : ''}/> Raw HTML mode</label>
|
||||||
<!--<input type="submit" name="draft" value="Save Draft"/>-->
|
<label><input type="checkbox" name="tags" ${ctx.request.method === 'GET' || (ctx.bodyParameters.tags === 'on' && ctx.response.statusCode !== 200) ? 'checked="true"' : ''}/> Include suggested tags</label>
|
||||||
|
<input type="submit" name="draft" value="Upload Draft"/>
|
||||||
<input type="submit" name="publish" value="Publish!"/>
|
<input type="submit" name="publish" value="Publish!"/>
|
||||||
</form>
|
</form>
|
||||||
`, 'Compose Post');
|
`, 'Compose Post');
|
||||||
}
|
} ];
|
||||||
else if (section === 'settings' && ['GET', 'POST'].includes(req.method))
|
|
||||||
{
|
const endpointSettings = [ (ctx) => (ctx.urlSections[0] === 'settings' && ['GET', 'POST'].includes(ctx.request.method)), async (ctx) => {
|
||||||
let noticeHtml = '';
|
let noticeHtml = '';
|
||||||
const accountString = getCookie(req, 'account');
|
const accountString = ctx.getCookie('account');
|
||||||
res.statusCode = 200;
|
ctx.response.statusCode = 200;
|
||||||
if (req.method === 'POST') {
|
if (ctx.request.method === 'POST') {
|
||||||
if (accountString && !matchCsrfToken(bodyParams, accountString)) {
|
if (accountString && !matchCsrfToken(ctx.bodyParameters, accountString)) {
|
||||||
res.statusCode = 401;
|
ctx.response.statusCode = 401;
|
||||||
noticeHtml = strings.csrfErrorHtml;
|
noticeHtml = strings.csrfErrorHtml;
|
||||||
}
|
}
|
||||||
if (res.statusCode === 200 && bodyParams.login) {
|
if (ctx.response.statusCode === 200 && ctx.bodyParameters.login) {
|
||||||
bodyParams.instance = bodyParams.instance.trim();
|
ctx.bodyParameters.instance = ctx.bodyParameters.instance.trim();
|
||||||
if (!checkUpstreamAllowed(bodyParams.instance)) {
|
if (!checkUpstreamAllowed(ctx.bodyParameters.instance)) {
|
||||||
res.statusCode = 500;
|
ctx.response.statusCode = 500;
|
||||||
noticeHtml = strings.upstreamDisallowedHtml;
|
noticeHtml = strings.upstreamDisallowedHtml;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const upstreamReq = await fetch(`${corsProxyIfNeed(bodyParams.cors === 'on')}${bodyParams.instance}/wp-json/wp/v2/users?context=edit`, { headers: {
|
const upstreamReq = await fetch(`${corsProxyIfNeed(ctx.bodyParameters.cors === 'on')}${ctx.bodyParameters.instance}/wp-json/wp/v2/users?context=edit`, { headers: {
|
||||||
Authorization: `Basic ${btoa(bodyParams.username + ':' + bodyParams.password)}`,
|
Authorization: `Basic ${btoa(ctx.bodyParameters.username + ':' + ctx.bodyParameters.password)}`,
|
||||||
} });
|
} });
|
||||||
const upstreamData = await upstreamReq.json();
|
const upstreamData = await upstreamReq.json();
|
||||||
if (upstreamReq.status === 200) {
|
if (upstreamReq.status === 200) {
|
||||||
let cookieFlags = (bodyParams.remember === 'on' ? `; max-age=${365*24*60*60}` : '');
|
let cookieFlags = (ctx.bodyParameters.remember === 'on' ? `; max-age=${365*24*60*60}` : '');
|
||||||
setCookies(res, [
|
ctx.setCookie(`account=${ctx.bodyParameters.instance},${ctx.bodyParameters.username},${ctx.bodyParameters.password}${cookieFlags}`); // TODO: add cookie renewal procedure
|
||||||
`account=${bodyParams.instance},${bodyParams.username},${bodyParams.password}${cookieFlags}`
|
return ctx.redirectTo('/');
|
||||||
]); // TODO: add cookie renewal procedure
|
|
||||||
return redirectTo('/', res);
|
|
||||||
} else {
|
} else {
|
||||||
res.statusCode = upstreamReq.status;
|
ctx.response.statusCode = upstreamReq.status;
|
||||||
noticeHtml = `<p class="notice error">Upstream server responded with error ${upstreamReq.status}: ${JSON.stringify(upstreamData)}</p>`;
|
noticeHtml = `<p class="notice error">Upstream server responded with error ${upstreamReq.status}: ${JSON.stringify(upstreamData)}</p>`;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
res.statusCode = 500;
|
ctx.response.statusCode = 500;
|
||||||
// display only generic error from server-side, for security
|
// 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>`;
|
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) {
|
} else if (ctx.response.statusCode === 200 && ctx.bodyParameters.logout) {
|
||||||
setCookies(res, [`account=`]);
|
ctx.setCookie(`account=`);
|
||||||
return redirectTo('/', res);
|
return ctx.redirectTo('/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return setPageContent(res, `${noticeHtml}
|
ctx.renderPage(`${noticeHtml}
|
||||||
${accountString ? `
|
${accountString ? `
|
||||||
<h3>Current Account</h3>
|
<h3>Current Account</h3>
|
||||||
${makeFragmentLoggedIn(accountString)}
|
${makeFragmentLoggedIn(accountString)}
|
||||||
@@ -553,11 +442,11 @@ ${figureHtml}`.trim()),
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<label><i>Note: For WordPress.org you must use an "application password" (<code>/wp-admin/profile.php#application-passwords-section</code>)</i></label>
|
<label><i>Note: For WordPress.org you must use an "application password" (<code>/wp-admin/profile.php#application-passwords-section</code>)</i></label>
|
||||||
<input type="url" name="instance" placeholder="Site/Instance URL" value="${bodyParams.instance || ''}" required="true"/>
|
<input type="url" name="instance" placeholder="Site/Instance URL" value="${ctx.bodyParameters.instance || ''}" required="true"/>
|
||||||
<input type="text" name="username" placeholder="Username" value="${bodyParams.username || ''}" required="true"/>
|
<input type="text" name="username" placeholder="Username" value="${ctx.bodyParameters.username || ''}" required="true"/>
|
||||||
<input type="password" name="password" placeholder="Password" value="${bodyParams.password || ''}" required="true"/>
|
<input type="password" name="password" placeholder="Password" value="${ctx.bodyParameters.password || ''}" required="true"/>
|
||||||
<!--${isEnvBrowser ? `<label><input type="checkbox" name="cors" ${bodyParams.cors === 'on' && res.statusCode !== 200 ? 'checked="true"' : ''}/> Site disallows CORS, use proxy</label>` : ''}-->
|
<!--${isEnvBrowser ? `<label><input type="checkbox" name="cors" ${ctx.bodyParameters.cors === 'on' && ctx.response.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</label>
|
<label><input type="checkbox" name="remember" ${ctx.request.method === 'POST' && ctx.bodyParameters.remember !== 'on' ? '' : 'checked="true"'}/> Remember me</label>
|
||||||
<input type="submit" name="login" value="Login and Save"/>
|
<input type="submit" name="login" value="Login and Save"/>
|
||||||
</form>` : ''}
|
</form>` : ''}
|
||||||
<!--${true ? `
|
<!--${true ? `
|
||||||
@@ -571,62 +460,6 @@ ${figureHtml}`.trim()),
|
|||||||
</form>
|
</form>
|
||||||
` : ''}-->
|
` : ''}-->
|
||||||
`, 'Settings');
|
`, '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)
|
main();
|
||||||
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}": "${''}"`)} }; </script> -->
|
|
||||||
<script src="${scriptName}"></script>
|
|
||||||
`));
|
|
||||||
} 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();
|
|
||||||
}
|
|
||||||
|
11
src/WuppiMini/package-lock.json
generated
11
src/WuppiMini/package-lock.json
generated
@@ -6,7 +6,8 @@
|
|||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"parse-multipart-data": "^1.5.0"
|
"parse-multipart-data": "^1.5.0",
|
||||||
|
"SpaccDotWeb": "gitlab:SpaccInc/SpaccDotWeb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
@@ -32,6 +33,14 @@
|
|||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-multipart-data/-/parse-multipart-data-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-multipart-data/-/parse-multipart-data-1.5.0.tgz",
|
||||||
"integrity": "sha512-ck5zaMF0ydjGfejNMnlo5YU2oJ+pT+80Jb1y4ybanT27j+zbVP/jkYmCrUGsEln0Ox/hZmuvgy8Ra7AxbXP2Mw=="
|
"integrity": "sha512-ck5zaMF0ydjGfejNMnlo5YU2oJ+pT+80Jb1y4ybanT27j+zbVP/jkYmCrUGsEln0Ox/hZmuvgy8Ra7AxbXP2Mw=="
|
||||||
|
},
|
||||||
|
"node_modules/SpaccDotWeb": {
|
||||||
|
"version": "indev",
|
||||||
|
"resolved": "git+ssh://git@gitlab.com/SpaccInc/SpaccDotWeb.git#618842883d76136ef78d439addee571c68862dde",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"parse-multipart-data": "^1.5.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"parse-multipart-data": "^1.5.0"
|
"parse-multipart-data": "^1.5.0",
|
||||||
|
"SpaccDotWeb": "gitlab:SpaccInc/SpaccDotWeb"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user