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 }