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 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 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/main.css">
<link rel="stylesheet" href="/static/css/index.css">
<script type="text/javascript"> <script type="text/javascript">
const MAX_POLL_OPTIONS = "{{ MAX_POLL_OPTIONS }}"; const MAX_POLL_OPTIONS = "{{ MAX_POLL_OPTIONS }}";
const MAX_CHARACTER_LENGTH = "{{ MAX_CHARACTER_LENGTH }}"; const MAX_CHARACTER_LENGTH = "{{ MAX_CHARACTER_LENGTH }}";
@ -33,6 +34,7 @@
{{ FORM_OPTION_DIVS }} {{ FORM_OPTION_DIVS }}
</section> </section>
<section class="poll-footer"> <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"> <select id="dupe-check" name="dupe-check">
<option value="ip" {{ FORM_DUPECHECK_IP }}>IP-based duplication checking</option> <option value="ip" {{ FORM_DUPECHECK_IP }}>IP-based duplication checking</option>
<option value="cookie" {{ FORM_DUPECHECK_COOKIE }}>Cookie-based duplication checking</option> <option value="cookie" {{ FORM_DUPECHECK_COOKIE }}>Cookie-based duplication checking</option>
@ -44,7 +46,8 @@
<span>Multiple Choice</span> <span>Multiple Choice</span>
</label> </label>
<br> <br>
<input type="submit" name="submit" value="Create poll" id="submit-button"> <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> </section>
</form> </form>
</section> </section>

View file

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

View file

@ -2,7 +2,10 @@
(() => { (() => {
const inputList = []; const inputList = [];
const pollOptionsAnchor = document.querySelector(".poll-options"); 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() { function createPollOptionInput() {
if (inputList.length >= parseInt(MAX_POLL_OPTIONS)) return; if (inputList.length >= parseInt(MAX_POLL_OPTIONS)) return;
const optionEl = document.createElement("div"); const optionEl = document.createElement("div");
@ -15,18 +18,29 @@
optionEl.appendChild(input); optionEl.appendChild(input);
input.addEventListener("keyup", () => { 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); inputList.push(input);
inputCounter.textContent = inputList.length;
return optionEl; return optionEl;
} }
pollOptionsAnchor.querySelectorAll(".poll-option input").forEach(el => { pollOptionsAnchor.querySelectorAll(".poll-option input").forEach(el => {
el.addEventListener("keyup", () => { 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); inputList.push(el);
}); });
inputCounter.textContent = inputList.length;
if (extendMessageDiv) extendMessageDiv.style.display = "block";
if (addOptionButton) addOptionButton.remove();
return createPollOptionInput; return createPollOptionInput;
})(); })();

View file

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