lunch-poll-legacy

Lunch Poll Legacy -- poll colleagues where to have lunch
Log | Files | Refs | README | LICENSE

mittag.c (8474B)


      1 /**
      2  * Lunch Poll Legacy
      3  * Copyright 2024 - 2026 Matthias Balk
      4  */
      5 
      6 #include <errno.h>
      7 #include <regex.h>
      8 #include <stdio.h>
      9 #include <stdlib.h>
     10 #include <string.h>
     11 #include <time.h>
     12 
     13 #include <sqlite3.h>
     14 
     15 #include "http.h"
     16 #include "date-utils.h"
     17 #include "utils.h"
     18 #include "mittag.h"
     19 #include "config.h"
     20 
     21 
     22 static sqlite3 *db;
     23 static char _header_printed = 0;
     24 
     25 
     26 static int get_votes_callback(void *unused,
     27                               int num_cols,
     28                               char **col_values,
     29                               char **col_names)
     30 {
     31   /* TODO: direct output is not always beautiful, think about alternatives */
     32   if (!_header_printed) {
     33     _header_printed = 1;
     34     header_status(200);
     35     header("X-OSSE", PROG_VERSION);
     36     header("Content-Type", "text/html; charset=utf-8");
     37     header_end();
     38 
     39     puts("<!DOCTYPE html>");
     40     puts("<html lang=\"de\"><head><meta charset=\"utf-8\">");
     41     puts("<meta name=\"robots\" content=\"noindex\">");
     42     puts("<meta name=\"viewport\" content=\"width=device-width, "
     43          "initial-scale=1.0\">");
     44     puts("<style> <!--");
     45     puts("table { tr:nth-child(odd) { background-color: #eee; } }");
     46     puts("--> </style>");
     47     puts("<title>Alle mampfen Mamba. Mampfred auch.</title></head>");
     48     puts("<body><form action=\"/mittag.cgi/votes\" method=\"POST\"><table>");
     49   }
     50 
     51   if (strlen(col_values[2] /* r_url */)) {
     52     printf("<tr><td><input type=\"checkbox\" name=\"restaurant_id\" "
     53            "value=\"%s\"></td><td>%s</td><td>%s</td><td><a href=\"%s\" "
     54            "target=\"_blank\">%s</a></td></tr>\n",
     55            col_values[4],   /* r_id      */
     56            col_values[3],   /* num_votes */
     57            col_values[0],   /* voters    */
     58            col_values[2],   /* r_url     */
     59            col_values[1]);  /* r_name    */
     60   }
     61   else {
     62     printf("<tr><td><input type=\"checkbox\" name=\"restaurant_id\" "
     63            "value=\"%s\"></td><td>%s</td><td>%s</td><td>%s</td></tr>\n",
     64            col_values[4],   /* r_id      */
     65            col_values[3],   /* num_votes */
     66            col_values[0],   /* voters    */
     67            col_values[1]);  /* r_name    */
     68   }
     69 
     70   return 0;
     71 }
     72 
     73 static void get_votes(void)
     74 {
     75   char date[MAX_LEN_DATE + 1] = { 0 };
     76 
     77   char *query_string = getenv("QUERY_STRING");
     78   if (query_string != NULL) {
     79     char* params[MAX_POST_SIZE];
     80     split_and_decode_form_params(query_string, params);
     81 
     82     for (int idx = 0; idx < MAX_PARAMS_COUNT && params[idx] != NULL; idx++) {
     83       if (strstr(params[idx], "date=") == params[idx]) {
     84         strlcpy(date,
     85                 strchr(params[idx], '=') + 1,
     86                 (MAX_LEN_DATE + 1) * sizeof(char));
     87         break;
     88       }
     89     }
     90   }
     91 
     92   if (*date == 0) {
     93     time_t t = time(NULL);
     94     struct tm *tm = gmtime(&t);
     95     strftime(date, (MAX_LEN_DATE + 1) * sizeof(char), "%Y-%m-%d", tm);
     96   }
     97 
     98   char buffer[350];
     99   snprintf(buffer, 350 * sizeof(char),
    100            "SELECT "
    101               "CASE WHEN GROUP_CONCAT(v.voter) IS NOT NULL THEN "
    102                  "GROUP_CONCAT(v.voter, ', ') ELSE '' END AS 'voters', "
    103               "r.name, "
    104               "r.url, "
    105               "COUNT(v.id) AS 'num_votes', "
    106               "r.id "
    107             "FROM "
    108               "restaurant r "
    109             "LEFT JOIN vote v "
    110               "ON v.restaurant_id = r.id AND "
    111                  "v.date = '%s' "
    112             "WHERE "
    113               "r.days_open & %d <> 0 "
    114             "GROUP BY "
    115               "r.id "
    116             "ORDER BY "
    117               "num_votes DESC, "
    118               "r.category ASC, "
    119               "r.id ASC;",
    120            date, get_day_of_week(date));
    121   char *zErrMsg = NULL;
    122   int rc = sqlite3_exec(db, buffer, get_votes_callback, 0, &zErrMsg);
    123   if (rc != SQLITE_OK) {
    124     _header_printed = 1;
    125     header_status(500);
    126     header_end();
    127 
    128     fprintf(stderr, "SQL error: %s\n", zErrMsg);
    129     sqlite3_free(zErrMsg);
    130     return;
    131   }
    132 
    133   if (!_header_printed) {
    134     header_status(404);
    135     header_end();
    136     return;
    137   }
    138 
    139   puts("</table>Name: <input type=\"text\" name=\"name\">");
    140   puts("<input type=\"submit\" value=\"Abstimmen\"></form>");
    141   printf("<p>%s %s -- %s</p>\n",
    142          PROG_NAME, PROG_VERSION, COPYRIGHT);
    143   if (SOURCE_CODE_DOWNLOAD_URL) {
    144     printf("<p><a href=\"%s\" target=\"_blank\">Source Code</a></p>\n",
    145            SOURCE_CODE_DOWNLOAD_URL);
    146   }
    147   puts("</body></html>");
    148 }
    149 
    150 static void post_votes(void)
    151 {
    152   char *content_type = getenv("CONTENT_TYPE");
    153   if (content_type == NULL ||
    154       strcmp("application/x-www-form-urlencoded", content_type) != 0) {
    155     header_status(415);
    156     header_end();
    157     return;
    158   }
    159 
    160   char buffer[MAX_POST_SIZE] = { 0 };
    161   fread(buffer, sizeof(*buffer), MAX_POST_SIZE - 1, stdin);
    162   if (ferror(stdin)) {
    163     const char* err = strerror(errno);
    164     fputs(err, stderr);
    165     err_exit(err);
    166   }
    167   if (!feof(stdin)) {
    168     fprintf(stderr,
    169             "error: posted data exceeds maximum supported size of %lu bytes\n",
    170             MAX_POST_SIZE * sizeof(*buffer));
    171     err_exit("error: posted data exceeds maximum supported size");
    172   }
    173 
    174   char* params[MAX_POST_SIZE];
    175   split_and_decode_form_params(buffer, params);
    176 
    177   char *name = NULL;
    178   for (int idx = 0; params[idx] != NULL; idx++) {
    179     if (strstr(params[idx], "name=") == params[idx]) {
    180       name = strchr(params[idx], '=') + 1;
    181       break;
    182     }
    183   }
    184   /* TODO: trim beginning and trailing whitespaces from name:
    185    *       'w3m' adds a trailing newline... */
    186 
    187   if (name != NULL) name = trim(name);
    188 
    189   if (name == NULL ||
    190       *name == '\0' ||
    191       strlen(name) > MAX_LEN_NAME) {
    192     header_status(400);
    193     header("Content-Type", "text/plain; charset=utf-8");
    194     header_end();
    195     puts("required field 'name' is missing or too long");
    196     return;
    197   }
    198 
    199   char date[MAX_LEN_DATE + 1] = { 0 };
    200   time_t t = time(NULL);
    201   struct tm *tm = gmtime(&t);
    202   strftime(date, (MAX_LEN_DATE + 1) * sizeof(char), "%Y-%m-%d", tm);
    203 
    204 
    205   char stmnt[48 + MAX_LEN_NAME + MAX_LEN_DATE];
    206   snprintf(stmnt, (48 + MAX_LEN_NAME + MAX_LEN_DATE) * sizeof(char),
    207            "DELETE FROM vote WHERE voter = '%s' AND date = '%s'",
    208            name, date);
    209 
    210   char *zErrMsg = NULL;
    211   int rc = sqlite3_exec(db, stmnt, NULL, 0, &zErrMsg);
    212   if (rc != SQLITE_OK) {
    213     _header_printed = 1;
    214     header_status(500);
    215     header_end();
    216 
    217     fprintf(stderr, "SQL error: %s\n", zErrMsg);
    218     sqlite3_free(zErrMsg);
    219     return;
    220   }
    221 
    222   for (int idx = 0; params[idx] != NULL; idx++) {
    223     if (strstr(params[idx], "restaurant_id=") == params[idx]) {
    224       char *id = strchr(params[idx], '=') + 1;
    225 
    226       char stmnt[64 + MAX_LEN_NAME + MAX_LEN_DATE + MAX_LEN_RESTAURANT_ID];
    227       snprintf(stmnt,
    228                (64 + MAX_LEN_NAME + MAX_LEN_DATE + MAX_LEN_RESTAURANT_ID)
    229                    * sizeof(char),
    230                "INSERT INTO vote (voter, date, restaurant_id) "
    231                  "VALUES ('%s', '%s', %s)",
    232                name, date, id);
    233 
    234       char *zErrMsg = NULL;
    235       int rc = sqlite3_exec(db, stmnt, NULL, 0, &zErrMsg);
    236       if (rc != SQLITE_OK) {
    237         _header_printed = 1;
    238         header_status(500);
    239         header_end();
    240 
    241         fprintf(stderr, "SQL error: %s\n", zErrMsg);
    242         sqlite3_free(zErrMsg);
    243         return;
    244       }
    245     }
    246   }
    247 
    248   header_status(303);
    249   header("Location", "/mittag.cgi/votes/");
    250   header_end();
    251 }
    252 
    253 static void open_database(void)
    254 {
    255   /* directory which contains the database must be writeable! */
    256   /* TODO: pledge?
    257      TODO: OPEN-Flag dependent on GET (READ ONLY) / POST (WRITE ONLY) */
    258   if (sqlite3_open_v2("var/mittag.db", &db, SQLITE_OPEN_READWRITE, NULL)
    259       != SQLITE_OK)
    260   {
    261     const char* err = sqlite3_errmsg(db);
    262     fputs(err, stderr);
    263     sqlite3_close(db);
    264     err_exit("sqlite3_open error");
    265   }
    266 }
    267 
    268 static void close_database(void)
    269 {
    270   sqlite3_close(db);
    271 }
    272 
    273 int main(int argc, char **argv)
    274 {
    275   Route *r;
    276   char path_found = 0;
    277   char *path = getenv("PATH_INFO") ? getenv("PATH_INFO") : "/";
    278   for (r = routes; r < routes + LEN(routes); r++) {
    279     regex_t preg;
    280     if (regcomp(&preg, r->path, REG_EXTENDED | REG_NOSUB) != 0)
    281     {
    282       err_exit("regcomp");
    283     }
    284     path_found = regexec(&preg, path, 0, NULL, 0) != REG_NOMATCH;
    285     regfree(&preg);
    286     if (path_found && getenv("REQUEST_METHOD")) {
    287       if (strcmp(getenv("REQUEST_METHOD"), r->method) == 0) {
    288         open_database();
    289         /* TODO: pass args */
    290         r->func();
    291         close_database();
    292         return EXIT_SUCCESS;
    293       }
    294     }
    295   }
    296 
    297   !path_found ? header_status(404) : header_status(405);
    298   header_end();
    299   return EXIT_SUCCESS;
    300 }