tvote.c - vote - simple cgi voting system for web and gopher
(HTM) git clone git://src.adamsgaard.dk/vote
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
tvote.c (9791B)
---
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/types.h>
6 #include <sys/stat.h>
7 #include <err.h>
8 #include <fcntl.h>
9 #include <limits.h>
10 #include <fts.h>
11
12 #include "util.h"
13
14 #define LEN(s) (sizeof(s) / sizeof(s[0]))
15 #define POLLS_DIR "polls"
16
17 static char fname[PATH_MAX];
18 static char poll[1024];
19 static char create[2];
20 static char question[4096];
21 static char options[4096];
22 static char choice[16];
23
24 void
25 http_status(int statuscode)
26 {
27 switch(statuscode) {
28 case 307:
29 printf("Status: 307 Temporary Redirect\r\n");
30 printf("Location: /vote?poll=%s&choice=0\r\n\r\n", poll);
31 break;
32 case 401:
33 printf("Status: 401 Bad Request\r\n\r\n");
34 break;
35 case 404:
36 printf("Status: 404 Not Found\r\n\r\n");
37 break;
38 case 500:
39 printf("Status: 500 Internal Server Error\r\n\r\n");
40 break;
41 default:
42 printf("Status: 500 Internal Server Error\r\n\r\n");
43 err(1, "unknown status code %d\n", statuscode);
44 }
45 }
46
47 char *
48 pollfile(const char *poll_name, const char *postfix)
49 {
50 char buf[PATH_MAX];
51 int r;
52
53 strlcpy(buf, poll_name, sizeof(buf));
54 escapechars(buf);
55 r = snprintf(fname, sizeof(fname), "%s/%s%s",
56 POLLS_DIR, buf, postfix);
57 if (r < 0 || (size_t)r >= sizeof(fname)) {
58 http_status(500);
59 err(1, "show_poll: snprintf fname %s/%s%s",
60 POLLS_DIR, buf, postfix);
61 }
62
63 return fname;
64 }
65
66 void
67 print_html_head(void)
68 {
69 printf("Content-type: text/html; charset=utf-8\r\n\r\n");
70 puts("<!DOCTYPE html>\n"
71 "<html>\n"
72 "<head>\n"
73 " <style type=\"text/css\">\n"
74 " body {\n"
75 " margin: 1em auto;\n"
76 " max-width: 40em;\n"
77 " padding: 0 .62em;\n"
78 " font: 16px/1.62 sans-serif;\n"
79 " line-height: 1.3;\n"
80 " }\n"
81 " h1, h2, h3 {\n"
82 " line-height: 1.2;\n"
83 " }\n"
84 " a {\n"
85 " color: #000;\n"
86 " }\n"
87 " td.choice {\n"
88 " padding: .5em;\n"
89 " }\n"
90 " table.create, td.input, input.name,\n"
91 " textarea.question, textarea.options {\n"
92 " box-sizing: border-box;\n"
93 " width: 100%;\n"
94 " }\n"
95 " </style>\n"
96 "</head>\n"
97 "<body>\n");
98 }
99
100 void
101 print_html_foot(void)
102 {
103 printf("</body>\n"
104 "</html>\n");
105 }
106
107 int
108 print_poll_line(char *line, size_t *i, int intable, int vote)
109 {
110 size_t c;
111
112 if (sscanf(line, "%ld\t%4095[^\n]", &c, options) == 2) {
113 if (!intable) {
114 puts("</p>");
115 if (vote) {
116 puts("<form method=\"get\" action=\"\">");
117 printf("<input type=\"hidden\" name=\"poll\" "
118 "value=\"%s\" />\n", poll);
119 }
120 puts("<table>");
121 }
122 if (vote) {
123 (*i)++;
124 printf("\t<tr><td class=\"choice\">");
125 printf("<input type=\"radio\" "
126 "id=\"o%ld\" name=\"choice\" value=\"%ld\" />",
127 *i, *i);
128 printf("</td><td><label for=\"o%ld\">%s</label></td></tr>\n",
129 *i, options);
130 } else
131 printf("\t<tr><td class=\"choice\">%ld</td><td>%s</td></tr>\n",
132 c, options);
133 return 1;
134 } else {
135 printf("%s<br/>\n", line);
136 return 0;
137 }
138 }
139
140 void
141 print_poll_file(FILE *fp, int vote)
142 {
143 char *line = NULL;
144 size_t linesize = 0, lineno = 0, i = 0;
145 ssize_t linelen;
146 int intable = 0;
147
148 while ((linelen = getline(&line, &linesize, fp)) != -1) {
149 lineno++;
150 if (line[linelen - 1] == '\n')
151 line[--linelen] = '\0';
152
153 if (lineno == 1) {
154 printf("<h2>");
155 fwrite(line, linelen, 1, stdout);
156 printf("</h2>\n<p>");
157 } else {
158 intable = print_poll_line(line, &i, intable, vote);
159 }
160 }
161 free(line);
162 if (ferror(fp)) {
163 http_status(500);
164 err(1, "print_poll_file: getline");
165 }
166
167 puts("</table>");
168 if (vote) {
169 puts("<input type=\"submit\" value=\"Submit\" "
170 "class=\"button\"/>");
171 puts("</form>");
172 /* printf("<p><a href=\"?poll=%s&choice=0\">"
173 "Show results</a></p>\n", poll); */
174 }
175 }
176
177 int
178 create_poll_file(const char *name, const char *question, const char *options)
179 {
180 FILE *fp;
181 struct stat sb;
182 size_t col;
183 char *fname;
184
185 if (!*name || !*question || !*options) {
186 http_status(401);
187 puts("<p><b>Error: Could not create poll</b></p>");
188 puts("<ul>");
189 if (!*name)
190 puts("<li>Poll name is missing</li>");
191 if (!*question)
192 puts("<li>Poll question is missing</li>");
193 if (!*options)
194 puts("<li>Poll options are missing</li>");
195 puts("</ul>");
196 return -1;
197 }
198
199 fname = pollfile(name, "");
200 if (stat(fname, &sb) == 0) {
201 http_status(401);
202 printf("<p>Poll '%s' already exists</p>", name);
203 return -1;
204 } else {
205 if (!(fp = fopen(fname, "w"))) {
206 http_status(404);
207 exit(1);
208 } else {
209 fputs(question, fp);
210 fputc('\n', fp);
211
212 for (col = 0; *options; (void)*options++) {
213 if (++col == 1 && *options != '\n' && *options != '\r')
214 fputs("0\t", fp);
215
216 switch(*options) {
217 case '\t':
218 fputc(' ', fp);
219 break;
220 case '\r':
221 break;
222 case '\n':
223 if (col < 3) {
224 col = 0;
225 break;
226 }
227 col = 0;
228 default:
229 fputc(*options, fp);
230 break;
231 }
232 }
233 fputc('\n', fp);
234 fclose(fp);
235 }
236 }
237 return 0;
238 }
239
240 void
241 show_poll(const char *poll, int vote)
242 {
243 FILE *fp;
244
245 if (pledge("stdio rpath", NULL) == -1) {
246 http_status(500);
247 err(1, "show_poll: pledge");
248 }
249
250 if (!(fp = fopen(pollfile(poll, ""), "r"))) {
251 http_status(404);
252 exit(1);
253 } else {
254 print_poll_file(fp, vote);
255 fclose(fp);
256 }
257 }
258
259 void
260 list_polls(void)
261 {
262 FTS *ftsp;
263 FTSENT *p;
264 int fts_options = FTS_COMFOLLOW | FTS_LOGICAL | FTS_NOCHDIR;
265 char *paths[] = { (char*)POLLS_DIR, NULL };
266
267 if (pledge("stdio rpath", NULL) == -1) {
268 http_status(500);
269 err(1, "list_polls: pledge");
270 }
271
272 if ((ftsp = fts_open(paths, fts_options, NULL)) == NULL) {
273 http_status(500);
274 err(1, "list_polls: fts_open");
275 }
276
277 puts("<h2>Poll listing</h2>");
278 puts("<ul>");
279
280 while ((p = fts_read(ftsp)) != NULL) {
281 switch (p->fts_info) {
282 case FTS_F:
283 printf("<li><a href=\"?poll=%s\">%s</a></li>\n",
284 p->fts_path + LEN(POLLS_DIR),
285 p->fts_path + LEN(POLLS_DIR));
286 break;
287 default:
288 break;
289 }
290 }
291 fts_close(ftsp);
292 puts("</ul>");
293 }
294
295 void
296 increment_option(char *poll, size_t n)
297 {
298 static char fname_tmp[PATH_MAX];
299 FILE *fp, *fp_tmp;
300 size_t v, lineno = 0;
301 char *line = NULL, *fname = NULL;
302 size_t linesize = 0;
303 ssize_t linelen;
304 struct stat sb;
305
306 fname = pollfile(poll, "_lock");
307 strlcpy(fname_tmp, fname, sizeof(fname_tmp));
308 while (stat(fname_tmp, &sb) == 0)
309 usleep(100);
310 if (!(fp_tmp = fopen(fname_tmp, "w"))) {
311 http_status(500);
312 err(1, "increment_option: fopen fp_tmp");
313 }
314
315 fname = pollfile(poll, "");
316 if (!(fp = fopen(fname, "r"))) {
317 http_status(404);
318 err(1, "increment_option: fopen fp");
319 }
320
321 while ((linelen = getline(&line, &linesize, fp)) != -1) {
322 if (sscanf(line, "%ld\t%4095[^\n]", &v, options) != 2)
323 fputs(line, fp_tmp);
324 else {
325 if (++lineno == n)
326 v++;
327 fprintf(fp_tmp, "%zu\t%s\n", v, options);
328 }
329 }
330
331 free(line);
332 if (ferror(fp) || ferror(fp_tmp)) {
333 http_status(500);
334 err(1, "increment_option: getline");
335 }
336 fclose(fp);
337 fclose(fp_tmp);
338
339 if (rename(fname_tmp, fname) != 0) {
340 http_status(500);
341 err(1, "increment_option: rename");
342 }
343 }
344
345 void
346 print_poll_create_form(void)
347 {
348 puts("<h2>Create new poll</h2>");
349 puts("<form method=\"get\" action=\"\">\n"
350 "<input type=\"hidden\" name=\"create\" value=\"1\" />\n"
351 "<table class=\"create\" width=\"100%\" border=\"0\" "
352 "cellpadding=\"0\" cellspacing=\"0\">\n"
353 "<tr>\n"
354 " <td width=\"100%\" class=\"input\">\n"
355 " <input type=\"text\" name=\"poll\" "
356 "placeholder=\"Name\" class=\"name\" required />\n"
357 " </td>\n"
358 "</tr>\n"
359 "<tr>\n"
360 " <td width=\"100%\" class=\"input\">\n"
361 " <textarea rows=\"2\" name=\"question\" "
362 "placeholder=\"Question (first line is header)\" "
363 "class=\"question\" required></textarea>\n"
364 " </td>\n"
365 "</tr>\n"
366 "<tr>\n"
367 " <td width=\"100%\" class=\"input\">\n"
368 " <textarea rows=\"5\" name=\"options\" "
369 "placeholder=\"Options (one per line)\" class=\"options\" required></textarea>\n"
370 " </td>\n"
371 "</tr>\n"
372 "<tr>\n"
373 " <td nowrap class=\"nowrap\">\n"
374 " <input type=\"submit\" value=\"Create\" class=\"button\"/>\n"
375 " </td>\n"
376 "</tr>\n"
377 "</table>\n"
378 "</form>\n");
379 }
380
381 void
382 parse_query(void)
383 {
384 char *query, *p;
385
386 if (!(query = getenv("QUERY_STRING")))
387 return;
388
389 if ((p = getparam(query, "create"))) {
390 if (decodeparam(create, sizeof(create), p) == -1) {
391 http_status(401);
392 exit(1);
393 }
394 }
395
396 if ((p = getparam(query, "poll"))) {
397 if (decodeparam(poll, sizeof(poll), p) == -1) {
398 http_status(401);
399 exit(1);
400 }
401 }
402
403 if ((p = getparam(query, "question"))) {
404 if (decodeparam(question, sizeof(question), p) == -1) {
405 http_status(401);
406 exit(1);
407 }
408 }
409
410 if ((p = getparam(query, "options"))) {
411 if (decodeparam(options, sizeof(options), p) == -1) {
412 http_status(401);
413 exit(1);
414 }
415 }
416
417 if ((p = getparam(query, "choice"))) {
418 if (decodeparam(choice, sizeof(choice), p) == -1) {
419 http_status(401);
420 exit(1);
421 }
422 }
423 }
424
425 int
426 main(void)
427 {
428 size_t c;
429 const char *errstr;
430 struct stat sb;
431
432 if (unveil(POLLS_DIR, "rwc") == -1) {
433 http_status(500);
434 err(1, "unveil");
435 }
436
437 if (pledge("stdio cpath rpath wpath", NULL) == -1) {
438 http_status(500);
439 err(1, "pledge");
440 }
441
442 if (stat(POLLS_DIR, &sb) == -1) {
443 if (mkdir(POLLS_DIR, 0755) == -1) {
444 http_status(500);
445 err(1, "mkdir '%s' failed", POLLS_DIR);
446 }
447 }
448
449 parse_query();
450
451 if (*create) {
452 if (create_poll_file(poll, question, options) == 0) {
453 print_html_head();
454 show_poll(poll, 0);
455 print_html_foot();
456 }
457 } else if (*poll) {
458 if (*choice) {
459 c = strtonum(choice, 0, 256, &errstr);
460 if (errstr != NULL)
461 errx(1, "could not parse choice: %s, %s", errstr, choice);
462 if (c > 0) {
463 increment_option(poll, c);
464 http_status(307);
465 } else {
466 print_html_head();
467 show_poll(poll, 0);
468 print_html_foot();
469 }
470 } else {
471 print_html_head();
472 show_poll(poll, 1);
473 print_html_foot();
474 }
475 } else {
476 print_html_head();
477 list_polls();
478 print_poll_create_form();
479 print_html_foot();
480 }
481
482 return 0;
483 }