lunch-poll-legacy

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

mittag.c (8352B)


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