summaryrefslogtreecommitdiff
path: root/site/index.html
blob: 5771e9453a5b9d07a2bfdd5108e9004b3638d762 (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
<html>
    <head>
        <meta charset="UTF-8">
        <title>test</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";

     /// 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;
     }


     /// 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 ul = mkElement("fieldset");
         let buttons = options.map((option) =>
             mkOption(kind, option, qid)
         );
         appendChildren (ul, [
             mkElement("legend", label)
         ].concat(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, 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;
         // 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 = mkElement("p", "", "error");
         appendChildren(footer, [
             mkElement("hr"),
             submit,
             errormsg
         ]);
         errormsg.hidden = true;
         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";
                     }
                     errormsg.innerText = "POST request returned error code:\n"
                                        + response.status + ": " + response.statusText;
                     errormsg.hidden = false;
                 }).catch(error => {
                     console.log(error);
                     errormsg.innerText = "Error: The http POST request did not succeed.";
                     errormsg.hidden = false;
                 });

             } else {
                 errormsg.innerText = "Cannot submit: not all required questions were filled in!";
                 errormsg.hidden = false;
             }
         }
         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!","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 ("Could not load this survey; it appears to be in a wrong or 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 ("Could not load this survey; are you sure that it exists?");
                 }
             }
         }
     }

     if (surveyUrl === "") {
         mkReadError ("There's nothing here. Maybe try appending something after the url?\n\nAlternatively, check to see if you copied the whole url.");
     } else {
         main ()
     }

    </script>
</html>