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 }