<html> <head> <title>test</title> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body> <div id="root"> </body> <script type="module"> import init, { age_encrypt, age_decrypt } from "./rage_wasm.js"; /// the basic idea here is to have functions which construct parts /// of the DOM that renders the survey to a user. These functions /// are all free of side effects (as far as possible, i.e. apart /// from creating DOM elements), and each returns the root element /// it just created + a callback function which will be used later /// to check which values the user entered in the corresponding /// parts of the survey. /// In other words, this misuses lots of lambdas to get a kind of /// polymorphism which would otherwise be hard to get in javascript. let root = document.getElementById("root"); let surveyUrl = window.location.hash.slice(1); // appends multiple children to an element. returns the root // element to be composable function appendChildren (root, children) { for (var child of children) { root.appendChild (child); } return root; } function mkElement (kind, text = "") { let p = document.createElement(kind); p.innerText = text; return p; } /// creates an option with a label next to it. /// mainly for list-like things, but also the password field function mkOption (kind, option="", qid="") { let i = document.createElement("input"); i.type = kind; i.name = qid; i.value = option; i.id = option; let l = mkElement("label", option); l.htmlFor = option; return [i,l]; } // for all kinds of multiple-choise like answer spaces function mkListSpace(kind, options, qid) { let ul = mkElement("ul") let buttons = options.map((option) => mkOption(kind, option, qid) ); appendChildren (ul, buttons.map((button) => { let li = mkElement("li"); appendChildren(li, button); return li; })); // return the answer chosen by the user. This function uses // null to indicate that a choice was invalid, i.e. the form // is not ready for submission yet. let getAnswer = () => { let selected = buttons .filter((b) => b[0].checked) .map((b) => b[0].value); if (kind === "radio") return selected.length == 0 ? null : selected[0]; if (kind === "checkbox") return selected; console.err("PartialityError: encountered unknown list type!") } return [ul, getAnswer]; } // for freeform text answers (TODO) function mkFreeform (placeholder) { let text = mkElement("textarea"); text.placeholder = placeholder; return [ text, () => text.value ]; } // essentially a case - of statement over the answerspace type. // since there is no validation of the config json, it may turn // out to be a partial function in practice. function mkAnswerSpace(space, qid) { if (space === "YesOrNo") { return mkListSpace("radio", ["yes", "no"], qid); } if ("Single" in space) { return mkListSpace("radio", space.Single, qid); } if ("Multiple" in space) { return mkListSpace("checkbox", space.Multiple, qid); } if ("Freeform" in space) { return mkFreeform(space.Freeform); } console.err("PartialityError: encountered unknown AnswerSpace type!"); } // makes a survey from a given json config object function mkSurvey (survey) { document.title = survey.title; // make the header with general information let header = document.createElement("div"); appendChildren(header, [ mkElement("h1", survey.title), mkElement("p", survey.description) ]); root.appendChild(header); // map question configs to their callbacks. note that // this iterator uses a function with side-effects, which // append the created objects to the DOM survey root. let callbacks = survey.questions.map ((question) => { let section = document.createElement("section"); let [answerspace, whichselection] = mkAnswerSpace(question.space, question.name); appendChildren(section, [ mkElement("h2", question.name), mkElement("p", question.question), //mkAnswerSpace(question.space, question.name) answerspace ]); root.appendChild(section); return whichselection; }); let footer = mkElement("section"); let submit = mkElement("button", "Submit"); appendChildren(footer, [ mkElement("hr"), submit ]); submit.onclick = () => { // the callback over the complete survey just maps all // other callbacks to their values, i.e. calls them all let answers = callbacks.map((c) => c()); console.log("answers given by user:", answers); // encrypt things let byteArray = age_encrypt(JSON.stringify(answers), survey.pubkey); let blobData = new Blob( [byteArray], { type: 'application/octet-stream' } ); fetch("/upload", { method: "POST", body: blobData, headers: { "X-Survey": survey.title, "Content-Type": "text/age" } }).then(response => console.log(response.text())) } root.appendChild(footer); return () => { return callbacks.map ((c) => c()) }; } /// displays a passphrase prompt until the use enters a passphrase /// which age can use for successful decryption function askPassphrase (ciphertext, secondTry=false) { document.title = "Enter Passphrase"; let div = mkElement("div"); let button = mkElement("button", "decrypt"); let [passphrase,label] = mkOption( "password", "please enter a passphrase to access the survey:" ); passphrase.value = ""; appendChildren(div, [ mkElement("h1", "Passphrase"), appendChildren(mkElement("p"),[label]), passphrase, button ].concat( secondTry ? [mkElement("p","passphrase was incorrect!")] : [] )); root.appendChild(div); button.onclick = () => { console.log("trying passphrase ..."); let decrypted = age_decrypt ( ciphertext, passphrase.value ); /// if we can't decrypt, the passphrase was wrong if (decrypted === undefined) { div.remove(); askPassphrase (ciphertext, true); } else { let survey = JSON.parse(decrypted); div.remove(); mkSurvey(survey); } }; } async function main () { // initialise the web assembly parts of this await init(); const Http = new XMLHttpRequest(); const url= surveyUrl; Http.open("GET", url); Http.responseType = "arraybuffer"; Http.send(); Http.onreadystatechange = (e) => { if (Http.readyState == 4) { if (Http.status == 200) { let bytearray = new Uint8Array (Http.response); let string = String.fromCharCode.apply(null, bytearray); let survey = null; try { survey = JSON.parse(string); } catch (e) { console.log ("survey appears to be encrypted"); askPassphrase(bytearray); } /// if the survey was unencrypted, start it here. If it /// was encrypted, we need to wait for user action via /// a js callback (handled in askPassphrase). if (survey !== null) { mkSurvey(survey); } // couldn't load survey json, show error message } else { appendChildren(root, [ mkElement("h1", "Error"), mkElement("p", "Could not load this survey; are you sure that it exists?"), appendChildren( mkElement("p","attempted path: "), [mkElement("tt", surveyUrl)] ) ]); } } } } main () </script> </html>