diff --git a/assets/misuzu.js/csrf.js b/assets/misuzu.js/csrf.js
new file mode 100644
index 0000000..7df0610
--- /dev/null
+++ b/assets/misuzu.js/csrf.js
@@ -0,0 +1,24 @@
+#include utility.js
+
+const MszCSRF = (() => {
+ let elem;
+ const getElement = () => {
+ if(elem === undefined)
+ elem = $q('meta[name="csrf-token"]');
+ return elem;
+ };
+
+ return {
+ get token() {
+ return getElement()?.content ?? '';
+ },
+ set token(token) {
+ if(typeof token !== 'string')
+ throw 'token must be a string';
+
+ const elem = getElement();
+ if(elem instanceof HTMLMetaElement)
+ elem.content = token;
+ },
+ };
+})();
diff --git a/assets/misuzu.js/csrfp.js b/assets/misuzu.js/csrfp.js
deleted file mode 100644
index 86b0991..0000000
--- a/assets/misuzu.js/csrfp.js
+++ /dev/null
@@ -1,40 +0,0 @@
-#include utility.js
-
-const MszCSRFP = (() => {
- let elem;
- const getElement = () => {
- if(elem === undefined)
- elem = $q('meta[name="csrfp-token"]');
- return elem;
- };
-
- const getToken = () => {
- const elem = getElement();
- return typeof elem.content === 'string' ? elem.content : '';
- };
-
- const setToken = token => {
- if(typeof token !== 'string')
- throw 'token must be a string';
-
- const elem = getElement();
- if(typeof elem.content === 'string')
- elem.content = token;
- };
-
- return {
- getToken: getToken,
- setToken: setToken,
- setFromHeaders: result => {
- if(typeof result.headers !== 'function')
- throw 'result.headers is not a function';
-
- const headers = result.headers();
- if(!(headers instanceof Map))
- throw 'result of result.headers does not return a map';
-
- if(headers.has('x-csrfp-token'))
- setToken(headers.get('x-csrfp-token'));
- },
- };
-})();
diff --git a/assets/misuzu.js/ext/uiharu.js b/assets/misuzu.js/ext/uiharu.js
index 6c0299d..b288a8d 100644
--- a/assets/misuzu.js/ext/uiharu.js
+++ b/assets/misuzu.js/ext/uiharu.js
@@ -1,4 +1,4 @@
-#include utility.js
+#include xhr.js
const MszUiharu = function(apiUrl) {
const maxBatchSize = 4;
diff --git a/assets/misuzu.js/forum/editor.jsx b/assets/misuzu.js/forum/editor.jsx
index 1e8432b..c54c5a2 100644
--- a/assets/misuzu.js/forum/editor.jsx
+++ b/assets/misuzu.js/forum/editor.jsx
@@ -1,6 +1,7 @@
#include msgbox.jsx
#include parsing.js
#include utility.js
+#include xhr.js
#include ext/eeprom.js
let MszForumEditorAllowClose = false;
diff --git a/assets/misuzu.js/main.js b/assets/misuzu.js/main.js
index ba1774b..883b976 100644
--- a/assets/misuzu.js/main.js
+++ b/assets/misuzu.js/main.js
@@ -1,4 +1,5 @@
#include utility.js
+#include xhr.js
#include embed/embed.js
#include events/christmas2019.js
#include events/events.js
diff --git a/assets/misuzu.js/messages/messages.js b/assets/misuzu.js/messages/messages.js
index c43084a..74ff985 100644
--- a/assets/misuzu.js/messages/messages.js
+++ b/assets/misuzu.js/messages/messages.js
@@ -1,6 +1,6 @@
-#include csrfp.js
#include msgbox.jsx
#include utility.js
+#include xhr.js
#include messages/actbtn.js
#include messages/list.js
#include messages/recipient.js
@@ -33,7 +33,6 @@ const MszMessages = () => {
const msgsCreate = async (title, text, parser, draft, recipient, replyTo) => {
const formData = new FormData;
- formData.append('_csrfp', MszCSRFP.getToken());
formData.append('title', title);
formData.append('body', text);
formData.append('parser', parser);
@@ -41,10 +40,7 @@ const MszMessages = () => {
formData.append('recipient', recipient);
formData.append('reply', replyTo);
- const result = await $x.post('/messages/create', { type: 'json' }, formData);
-
- MszCSRFP.setFromHeaders(result);
-
+ const result = await $x.post('/messages/create', { type: 'json', csrf: true }, formData);
const body = result.body();
if(body.error !== undefined)
throw body.error;
@@ -54,16 +50,12 @@ const MszMessages = () => {
const msgsUpdate = async (messageId, title, text, parser, draft) => {
const formData = new FormData;
- formData.append('_csrfp', MszCSRFP.getToken());
formData.append('title', title);
formData.append('body', text);
formData.append('parser', parser);
formData.append('draft', draft);
- const result = await $x.post(`/messages/${encodeURIComponent(messageId)}`, { type: 'json' }, formData);
-
- MszCSRFP.setFromHeaders(result);
-
+ const result = await $x.post(`/messages/${encodeURIComponent(messageId)}`, { type: 'json', csrf: true }, formData);
const body = result.body();
if(body.error !== undefined)
throw body.error;
@@ -72,14 +64,10 @@ const MszMessages = () => {
};
const msgsMark = async (msgs, state) => {
- const result = await $x.post('/messages/mark', { type: 'json' }, {
- _csrfp: MszCSRFP.getToken(),
+ const result = await $x.post('/messages/mark', { type: 'json', csrf: true }, {
type: state,
messages: msgs.map(extractMsgIds).join(','),
});
-
- MszCSRFP.setFromHeaders(result);
-
const body = result.body();
if(body.error !== undefined)
throw body.error;
@@ -88,13 +76,9 @@ const MszMessages = () => {
};
const msgsDelete = async msgs => {
- const result = await $x.post('/messages/delete', { type: 'json' }, {
- _csrfp: MszCSRFP.getToken(),
+ const result = await $x.post('/messages/delete', { type: 'json', csrf: true }, {
messages: msgs.map(extractMsgIds).join(','),
});
-
- MszCSRFP.setFromHeaders(result);
-
const body = result.body();
if(body.error !== undefined)
throw body.error;
@@ -103,13 +87,9 @@ const MszMessages = () => {
};
const msgsRestore = async msgs => {
- const result = await $x.post('/messages/restore', { type: 'json' }, {
- _csrfp: MszCSRFP.getToken(),
+ const result = await $x.post('/messages/restore', { type: 'json', csrf: true }, {
messages: msgs.map(extractMsgIds).join(','),
});
-
- MszCSRFP.setFromHeaders(result);
-
const body = result.body();
if(body.error !== undefined)
throw body.error;
@@ -118,13 +98,10 @@ const MszMessages = () => {
};
const msgsNuke = async msgs => {
- const result = await $x.post('/messages/nuke', { type: 'json' }, {
- _csrfp: MszCSRFP.getToken(),
+ const result = await $x.post('/messages/nuke', { type: 'json', csrf: true }, {
messages: msgs.map(extractMsgIds).join(','),
});
- MszCSRFP.setFromHeaders(result);
-
const body = result.body();
if(body.error !== undefined)
throw body.error;
diff --git a/assets/misuzu.js/messages/recipient.js b/assets/misuzu.js/messages/recipient.js
index 22374ad..ea0ca20 100644
--- a/assets/misuzu.js/messages/recipient.js
+++ b/assets/misuzu.js/messages/recipient.js
@@ -1,5 +1,4 @@
-#include csrfp.js
-#include utility.js
+#include xhr.js
const MszMessagesRecipient = function(element) {
if(!(element instanceof Element))
@@ -10,13 +9,9 @@ const MszMessagesRecipient = function(element) {
let updateHandler = undefined;
const update = async () => {
- const result = await $x.post(element.dataset.msgLookup, { type: 'json' }, {
- _csrfp: MszCSRFP.getToken(),
+ const result = await $x.post(element.dataset.msgLookup, { type: 'json', csrf: true }, {
name: nameInput.value,
});
-
- MszCSRFP.setFromHeaders(result);
-
const body = result.body();
if(updateHandler !== undefined)
diff --git a/assets/misuzu.js/utility.js b/assets/misuzu.js/utility.js
index 71c0037..cb845f2 100644
--- a/assets/misuzu.js/utility.js
+++ b/assets/misuzu.js/utility.js
@@ -162,114 +162,6 @@ const $as = function(array) {
}
};
-const $x = (function() {
- const send = function(method, url, options, body) {
- if(options === undefined)
- options = {};
- else if(typeof options !== 'object')
- throw 'options must be undefined or an object';
-
- const xhr = new XMLHttpRequest;
- const requestHeaders = new Map;
-
- if('headers' in options && typeof options.headers === 'object')
- for(const name in options.headers)
- if(options.headers.hasOwnProperty(name))
- requestHeaders.set(name.toLowerCase(), options.headers[name]);
-
- if(typeof options.download === 'function') {
- xhr.onloadstart = ev => options.download(ev);
- xhr.onprogress = ev => options.download(ev);
- xhr.onloadend = ev => options.download(ev);
- }
-
- if(typeof options.upload === 'function') {
- xhr.upload.onloadstart = ev => options.upload(ev);
- xhr.upload.onprogress = ev => options.upload(ev);
- xhr.upload.onloadend = ev => options.upload(ev);
- }
-
- if(options.authed)
- xhr.withCredentials = true;
-
- if(typeof options.timeout === 'number')
- xhr.timeout = options.timeout;
-
- if(typeof options.type === 'string')
- xhr.responseType = options.type;
-
- if(typeof options.abort === 'function')
- options.abort(() => xhr.abort());
-
- if(typeof options.xhr === 'function')
- options.xhr(() => xhr);
-
- if(typeof body === 'object') {
- if(body instanceof URLSearchParams) {
- requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
- } else if(body instanceof FormData) {
- // content-type is implicitly set
- } else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) {
- if(!requestHeaders.has('content-type'))
- requestHeaders.set('content-type', 'application/octet-stream');
- } else if(!requestHeaders.has('content-type')) {
- const bodyParts = [];
- for(const name in body)
- if(body.hasOwnProperty(name))
- bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name]));
- body = bodyParts.join('&');
- requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
- }
- }
-
- return new Promise((resolve, reject) => {
- let responseHeaders = undefined;
-
- xhr.onload = ev => resolve({
- status: xhr.status,
- body: () => xhr.response,
- text: () => xhr.responseText,
- headers: () => {
- if(responseHeaders !== undefined)
- return responseHeaders;
-
- responseHeaders = new Map;
-
- const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
- for(const name in raw)
- if(raw.hasOwnProperty(name)) {
- const parts = raw[name].split(': ');
- responseHeaders.set(parts.shift(), parts.join(': '));
- }
-
- return responseHeaders;
- },
- xhr: xhr,
- ev: ev,
- });
-
- xhr.onerror = ev => reject({
- xhr: xhr,
- ev: ev,
- });
-
- xhr.open(method, url);
- for(const [name, value] of requestHeaders)
- xhr.setRequestHeader(name, value);
- xhr.send(body);
- });
- };
-
- return {
- send: send,
- get: (url, options, body) => send('GET', url, options, body),
- post: (url, options, body) => send('POST', url, options, body),
- delete: (url, options, body) => send('DELETE', url, options, body),
- patch: (url, options, body) => send('PATCH', url, options, body),
- put: (url, options, body) => send('PUT', url, options, body),
- };
-})();
-
const $insertTags = function(target, tagOpen, tagClose) {
tagOpen = tagOpen || '';
tagClose = tagClose || '';
diff --git a/assets/misuzu.js/xhr.js b/assets/misuzu.js/xhr.js
new file mode 100644
index 0000000..f59d020
--- /dev/null
+++ b/assets/misuzu.js/xhr.js
@@ -0,0 +1,116 @@
+#include csrf.js
+
+const $x = (function() {
+ const send = function(method, url, options, body) {
+ if(options === undefined)
+ options = {};
+ else if(typeof options !== 'object')
+ throw 'options must be undefined or an object';
+
+ Object.freeze(options);
+
+ const xhr = new XMLHttpRequest;
+ const requestHeaders = new Map;
+
+ if('headers' in options && typeof options.headers === 'object')
+ for(const name in options.headers)
+ if(options.headers.hasOwnProperty(name))
+ requestHeaders.set(name.toLowerCase(), options.headers[name]);
+
+ if(options.csrf)
+ requestHeaders.set('x-csrf-token', MszCSRF.token);
+
+ if(typeof options.download === 'function') {
+ xhr.onloadstart = ev => options.download(ev);
+ xhr.onprogress = ev => options.download(ev);
+ xhr.onloadend = ev => options.download(ev);
+ }
+
+ if(typeof options.upload === 'function') {
+ xhr.upload.onloadstart = ev => options.upload(ev);
+ xhr.upload.onprogress = ev => options.upload(ev);
+ xhr.upload.onloadend = ev => options.upload(ev);
+ }
+
+ if(options.authed)
+ xhr.withCredentials = true;
+
+ if(typeof options.timeout === 'number')
+ xhr.timeout = options.timeout;
+
+ if(typeof options.type === 'string')
+ xhr.responseType = options.type;
+
+ if(typeof options.abort === 'function')
+ options.abort(() => xhr.abort());
+
+ if(typeof options.xhr === 'function')
+ options.xhr(() => xhr);
+
+ if(typeof body === 'object') {
+ if(body instanceof URLSearchParams) {
+ requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
+ } else if(body instanceof FormData) {
+ // content-type is implicitly set
+ } else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) {
+ if(!requestHeaders.has('content-type'))
+ requestHeaders.set('content-type', 'application/octet-stream');
+ } else if(!requestHeaders.has('content-type')) {
+ const bodyParts = [];
+ for(const name in body)
+ if(body.hasOwnProperty(name))
+ bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name]));
+ body = bodyParts.join('&');
+ requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
+ }
+ }
+
+ return new Promise((resolve, reject) => {
+ xhr.onload = ev => {
+ const headers = (headersString => {
+ const headers = new Map;
+
+ const raw = headersString.trim().split(/[\r\n]+/);
+ for(const name in raw)
+ if(raw.hasOwnProperty(name)) {
+ const parts = raw[name].split(': ');
+ headers.set(parts.shift(), parts.join(': '));
+ }
+
+ return headers;
+ })(xhr.getAllResponseHeaders());
+
+ if(options.csrf && headers.has('x-csrf-token'))
+ MszCSRF.token = headers.get('x-csrf-token');
+
+ resolve({
+ status: xhr.status,
+ body: () => xhr.response,
+ text: () => xhr.responseText,
+ headers: () => headers,
+ xhr: xhr,
+ ev: ev,
+ });
+ };
+
+ xhr.onerror = ev => reject({
+ xhr: xhr,
+ ev: ev,
+ });
+
+ xhr.open(method, url);
+ for(const [name, value] of requestHeaders)
+ xhr.setRequestHeader(name, value);
+ xhr.send(body);
+ });
+ };
+
+ return {
+ send: send,
+ get: (url, options, body) => send('GET', url, options, body),
+ post: (url, options, body) => send('POST', url, options, body),
+ delete: (url, options, body) => send('DELETE', url, options, body),
+ patch: (url, options, body) => send('PATCH', url, options, body),
+ put: (url, options, body) => send('PUT', url, options, body),
+ };
+})();
diff --git a/src/Hanyuu/HanyuuRpcHandler.php b/src/Hanyuu/HanyuuRpcHandler.php
index 7942bc5..e735732 100644
--- a/src/Hanyuu/HanyuuRpcHandler.php
+++ b/src/Hanyuu/HanyuuRpcHandler.php
@@ -160,8 +160,8 @@ final class HanyuuRpcHandler implements RpcHandler {
if($userInfo !== $userInfoReal) {
$response['guise'] = $extractUserInfo($userInfoReal);
- $csrfp = CSRF::create($sessionInfo->token);
- $response['guise']['revert_url'] = $baseUrl . $this->urls->format('auth-revert', ['csrf' => $csrfp->createToken()]);
+ $csrf = CSRF::create($sessionInfo->token);
+ $response['guise']['revert_url'] = $baseUrl . $this->urls->format('auth-revert', ['csrf' => $csrf->createToken()]);
}
return self::createPayload('auth:check:success', $response);
diff --git a/src/Messages/MessagesRoutes.php b/src/Messages/MessagesRoutes.php
index e90630b..8cb1d80 100644
--- a/src/Messages/MessagesRoutes.php
+++ b/src/Messages/MessagesRoutes.php
@@ -60,7 +60,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
if(!($content instanceof FormHttpContent))
return 400;
- if(!$content->hasParam('_csrfp') || !CSRF::validate((string)$content->getParam('_csrfp')))
+ if(!CSRF::validate($request->getHeaderLine('x-csrf-token')))
return [
'error' => [
'name' => 'msgs:verify',
@@ -68,7 +68,7 @@ class MessagesRoutes implements RouteHandler, UrlSource {
],
];
- $response->setHeader('X-CSRFP-Token', CSRF::token());
+ $response->setHeader('X-CSRF-Token', CSRF::token());
}
}
diff --git a/templates/master.twig b/templates/master.twig
index 72ffa4b..f55d129 100644
--- a/templates/master.twig
+++ b/templates/master.twig
@@ -4,7 +4,7 @@
{% include '_layout/meta.twig' %}
-
+
{% if site_background is defined %}