Add non-JS way of adding options

A new button has been added (which gets automatically removed by JS)
that lets a user add a new option.
Also, an XSS exploit has been fixed.
This commit is contained in:
Wolvan 2022-01-11 21:00:44 +01:00
parent 071a35814b
commit cb0ec9dfa1
4 changed files with 37 additions and 14 deletions

View file

@ -8,6 +8,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/index.css">
<script type="text/javascript">
const MAX_POLL_OPTIONS = "{{ MAX_POLL_OPTIONS }}";
const MAX_CHARACTER_LENGTH = "{{ MAX_CHARACTER_LENGTH }}";
@ -33,6 +34,7 @@
{{ FORM_OPTION_DIVS }}
</section>
<section class="poll-footer">
<div id="extend-message">Used <span id="options-counter">0</span>/{{ MAX_POLL_OPTIONS }} options.<br>New boxes appear automatically.</div>
<select id="dupe-check" name="dupe-check">
<option value="ip" {{ FORM_DUPECHECK_IP }}>IP-based duplication checking</option>
<option value="cookie" {{ FORM_DUPECHECK_COOKIE }}>Cookie-based duplication checking</option>
@ -45,6 +47,7 @@
</label>
<br>
<input type="submit" name="submit" value="Create poll" id="submit-button">
<input type="submit" name="submit" value="Add options" formaction="#" formmethod="get" id="add-options-button">
</section>
</form>
</section>

View file

@ -0,0 +1,4 @@
#extend-message {
padding-bottom: 5px;
display: none;
}

View file

@ -2,6 +2,9 @@
(() => {
const inputList = [];
const pollOptionsAnchor = document.querySelector(".poll-options");
const extendMessageDiv = document.querySelector("#extend-message");
const inputCounter = document.querySelector("#options-counter");
const addOptionButton = document.querySelector("#add-options-button");
function createPollOptionInput() {
if (inputList.length >= parseInt(MAX_POLL_OPTIONS)) return;
@ -15,18 +18,29 @@
optionEl.appendChild(input);
input.addEventListener("keyup", () => {
if (inputList.every(el => el.value) && pollOptionsAnchor) pollOptionsAnchor.appendChild(createPollOptionInput());
if (inputList.every(el => el.value) && pollOptionsAnchor) {
pollOptionsAnchor.appendChild(createPollOptionInput());
}
});
inputList.push(input);
inputCounter.textContent = inputList.length;
return optionEl;
}
pollOptionsAnchor.querySelectorAll(".poll-option input").forEach(el => {
el.addEventListener("keyup", () => {
if (inputList.every(el => el.value) && pollOptionsAnchor) pollOptionsAnchor.appendChild(createPollOptionInput());
if (inputList.every(el => el.value) && pollOptionsAnchor) {
pollOptionsAnchor.appendChild(createPollOptionInput());
}
});
inputList.push(el);
});
inputCounter.textContent = inputList.length;
if (extendMessageDiv) extendMessageDiv.style.display = "block";
if (addOptionButton) addOptionButton.remove();
return createPollOptionInput;
})();

View file

@ -173,10 +173,12 @@ export default function init(router: Router): void {
});
router.get("/", (req, res) => {
const options = (typeof req.query.options === "string" ? req.query.options.split("\uFFFE") : [])
.filter(i => i)
.concat(Array(3).fill(""))
.slice(0, 3);
const options = (typeof req.query.options === "string" ? req.query.options.split("\uFFFE") : Array.isArray(req.query["poll-option"]) ? req.query["poll-option"] as string[] : [])
.filter(i => i);
if (options.length < 3)
for (let i = options.length; i < 2; ++i) options.push("");
if (options.length < MAX_POLL_OPTIONS) options.push("");
const pollOptionDivs = options.map(option => `
<div class="poll-option">
<input type="text" name="poll-option" maxlength="${MAX_CHARACTER_LENGTH}" placeholder="Enter your option here" value="${xss(option)}">
@ -185,13 +187,13 @@ export default function init(router: Router): void {
displayPage(req, res, "index.html", {
"BACKEND_BASE_PATH": (program.opts().backendBaseUrl || ""),
"FORM_SUBMISSION_ERROR": req.query.error,
"FORM_SUBMISSION_ERROR": xss(req.query.error + ""),
"FORM_SUBMISSION_ERROR_SHOWN_CLASS": req.query.error ? "error-visible" : "",
"FORM_TITLE": req.query.title || "",
"FORM_DUPECHECK_IP": req.query.dupecheck === "ip" ? "selected" : "",
"FORM_DUPECHECK_COOKIE": req.query.dupecheck === "cookie" ? "selected" : "",
"FORM_DUPECHECK_NONE": req.query.dupecheck === "none" ? "selected" : "",
"FORM_MULTI_SELECT": req.query.multiselect === "true" ? "checked" : "",
"FORM_TITLE": xss((req.query.title || req.query["poll-title"] || "") + ""),
"FORM_DUPECHECK_IP": req.query.dupecheck === "ip" || req.query["dupe-check"] === "ip" ? "selected" : "",
"FORM_DUPECHECK_COOKIE": req.query.dupecheck === "cookie" || req.query["dupe-check"] === "cookie" ? "selected" : "",
"FORM_DUPECHECK_NONE": req.query.dupecheck === "none" || req.query["dupe-check"] === "none" ? "selected" : "",
"FORM_MULTI_SELECT": req.query.multiselect === "true" || req.query["multi-select"] === "on" ? "checked" : "",
"FORM_CAPTCHA": req.query.captcha === "true" ? "checked" : "",
"FORM_OPTION_DIVS": pollOptionDivs,
"MAX_POLL_OPTIONS": MAX_POLL_OPTIONS,