summaryrefslogtreecommitdiff
path: root/site/index.html
blob: 6141163092ce99f508d972d27a23a519526cd89f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Error</title>
        <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
        <main id="root">
    </body>
    <script type="module">

     import init, { age_encrypt, age_decrypt } from "./rage_wasm.js";
     import _, { setLang, tryBrowserDefaultLang } from "./i18n.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 = "", c = null) {
         let p = document.createElement(kind);
         p.innerText = text;
         if (c !== null) {
             p.classList.add(c);
         }
         return p;
     }

     function setErrormsg (element, msg=null) {
         element.hidden = msg === null;
         if (!element.hidden)
             element.innerText = msg;
         return element;
     }

     /// 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, label) {
         let buttons = options.map((option) =>
             mkOption(kind, option, qid)
         );
         let fieldset = appendChildren (
             mkElement("fieldset"), [
             mkElement("legend", label)
         ].concat(buttons.map((button) => {
             let li = mkElement("li");
             appendChildren(li, button);
             return li;
         })));
         let errormsg = setErrormsg(
             mkElement("p", "", "error"),
             null
         );
         let div = appendChildren(
             mkElement("div"),
             [fieldset, errormsg]
         )
         // 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);

             // a weird attempt at pattern matching in js, misusing
             // an array where one field must always be null as something
             // that would otherwise be a sum type
             let [ret, error] =
                 kind === "radio"
                 ? (selected.length != 0
                    ? [selected[0], null]
                    : [null, _("Have to select an option")])
                 : kind === "checkbox" ? [selected, null]
                 : [null, "PartialityError: encountered unknown type of list space"];
             setErrormsg (errormsg, error);
             return ret;
         }
         return [div, getAnswer];
     }

     // for freeform text answers (TODO)
     function mkFreeform (placeholder, text) {
         let input = mkElement("textarea");
         let label = mkElement("label", text);
         input.id = text;
         label.htmlFor = text;
         let div = appendChildren(mkElement("div"), [
             label,
             input
         ])
         input.placeholder = placeholder;
         return [
             div,
             () => input.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 mkQuestion(question) {
         let space = question.space;
         if (space === "YesOrNo") {
             return mkListSpace("radio", ["yes", "no"], question.name, question.question);
         } if ("Single" in space) {
             return mkListSpace("radio", space.Single, question.name, question.question);
         } if ("Multiple" in space) {
             return mkListSpace("checkbox", space.Multiple, question.name, question.question);
         } if ("Freeform" in space) {
             return mkFreeform(space.Freeform, question.question);
         }
         console.err("PartialityError: encountered unknown AnswerSpace type!");
     }

     // makes a survey from a given json config object
     function mkSurvey (survey) {
         document.title = survey.title;
         setLang(survey.lang);
         document.getElementsByTagName("html")[0].lang = survey.lang;
         // 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] =
                 mkQuestion(question);
             appendChildren(section, [
                 mkElement("h2", question.name),
                 answerspace
             ]);
             root.appendChild(section);
             return whichselection;
         });

         let footer = mkElement("section");
         let submit = mkElement("button", _("submit"));
         let errormsg = setErrormsg (
             mkElement("p", "", "error"),
             null
         );
         appendChildren(footer, [
             mkElement("hr"),
             submit,
             errormsg
         ]);
         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());

             if (answers.filter((c) => c == null).length === 0) {
                 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);
                     if (response.status === 200) {
                         console.log(response.text())
                         window.location.href = "thanks.html";
                     }
                     setErrormsg(errormsg,
                         _("Error: post returned error") + "\n"
                         + response.status + ": " + response.statusText
                     );
                 }).catch(error => {
                     console.log(error);
                     setErrormsg(errormsg, _("Error: could not post"));
                 });

             } else {
                 setErrormsg(errormsg, _("Error: answers invalid"));
             }
         }
         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 passphrase")
         );
         passphrase.value = "";
         appendChildren(div, [
             mkElement("h1", _("Passphrase")),
             appendChildren(mkElement("p"),[label]),
             passphrase,
             button
         ].concat(
             secondTry ? [mkElement("p",_("passphrase incorrect"),"error")] : []
         ));
         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);
             }
         };
     }

     function mkReadError (msg) {
         appendChildren(root, [
             mkElement("h1", "Error"),
             mkElement("p", msg, "error"),
             appendChildren(
                 mkElement("p",_("attempted path:"),"error"),
                 [mkElement("tt"," "+surveyUrl,"error")]
             )
         ]);
     }

     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) {
                         if (string.lastIndexOf("age-encryption.org/v1") === 0) {
                             console.log ("survey appears to be encrypted");
                             askPassphrase(bytearray);
                         } else {
                             mkReadError (_("Error: unknown format"));
                         }
                     }
                     /// 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). This is
                     /// out here only to avoid catching errors from within mkSurvey.
                     if (survey !== null) {
                         mkSurvey(survey);
                     }
                 // couldn't load survey json, show error message
                 } else {
                     mkReadError (_("Error: survey doesn't exist"));
                 }
             }
         }
     }

     // attempt to set the site's language to the browsers's preference
     tryBrowserDefaultLang();
     if (surveyUrl === "") {
         mkReadError (_("Error: nothing here"));
     } else {
         main ()
     }

    </script>
</html>